Samouczek: tworzenie asystenta sztucznej inteligencji za pomocą wywoływania narzędzi

Utwórz asystenta sztucznej inteligencji, który wykracza poza konwersację — może wywoływać funkcje w celu wykonywania akcji. Asystent decyduje, kiedy jest potrzebna funkcja, wykonuje ją i przekaże wynik z powrotem. Wszystkie elementy są uruchamiane lokalnie za pomocą lokalnego zestawu SDK usługi Foundry.

W tym poradniku nauczysz się, jak:

  • Konfigurowanie projektu i instalowanie lokalnego zestawu SDK rozwiązania Foundry
  • Zdefiniuj narzędzia, które asystent może wywołać
  • Wysyłanie komunikatu wyzwalającego użycie narzędzia
  • Wykonywanie narzędzia i zwracanie wyników do modelu
  • Obsługa pełnej pętli wywołań narzędzi
  • Uprzątnij zasoby

Wymagania wstępne

  • Komputer Windows, macOS lub Linux z co najmniej 8 GB pamięci RAM.

Repozytorium przykładów

Pełny kod przykładowy do tego artykułu można znaleźć w repozytorium GitHub przykładów platformy Foundry. Aby sklonować repozytorium i przejść do przykładu, użyj następującego polecenia:

git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/csharp/foundry-local/tutorial-tool-calling

Instalowanie pakietów

dotnet add package Microsoft.AI.Foundry.Local
dotnet add package OpenAI

Przykłady języka C# w repozytorium GitHub to wstępnie skonfigurowane projekty. Jeśli tworzysz od podstaw, zapoznaj się z dokumentacją zestawu SDK lokalnego rozwiązania Foundry , aby uzyskać więcej informacji na temat sposobu konfigurowania projektu w języku C# przy użyciu rozwiązania Foundry Local.

Definiowanie narzędzi

Wywołanie narzędzia umożliwia modelowi żądanie, że kod uruchamia funkcję i zwraca wynik. Dostępne narzędzia są definiowane jako lista schematów JSON opisujących nazwy, przeznaczenie i parametry każdej funkcji.

  1. Otwórz Program.cs i dodaj następujące definicje narzędzi:

    // --- Tool definitions and implementations ---
    var toolDefinitions = new[]
    {
        new
        {
            Name = "get_weather",
            Description = "Get the current weather for a location",
            Schema =
                """
                {
                  "type": "object",
                  "properties": {
                    "location": { "type": "string", "description": "The city or location" },
                    "unit": {
                      "type": "string",
                      "description": "Temperature unit",
                      "enum": ["celsius", "fahrenheit"]
                    }
                  },
                  "required": ["location"]
                }
                """
        },
        new
        {
            Name = "calculate",
            Description = "Perform a math calculation",
            Schema =
                """
                {
                  "type": "object",
                  "properties": {
                    "expression": { "type": "string", "description": "The math expression to evaluate" }
                  },
                  "required": ["expression"]
                }
                """
        }
    };
    
    string ExecuteTool(string functionName, JsonElement arguments)
    {
        switch (functionName)
        {
            case "get_weather":
                var location = arguments.GetProperty("location").GetString() ?? "unknown";
                var unit = arguments.TryGetProperty("unit", out var u)
                    ? u.GetString() ?? "celsius"
                    : "celsius";
                var temp = unit == "celsius" ? 22 : 72;
                return JsonSerializer.Serialize(new
                {
                    location,
                    temperature = temp,
                    unit,
                    condition = "Sunny"
                });
    
            case "calculate":
                var expression = arguments.GetProperty("expression").GetString() ?? "";
                try
                {
                    var result = new System.Data.DataTable().Compute(expression, null);
                    return JsonSerializer.Serialize(new { expression, result = result?.ToString() });
                }
                catch (Exception ex)
                {
                    return JsonSerializer.Serialize(new { error = ex.Message });
                }
    
            default:
                return JsonSerializer.Serialize(new { error = $"Unknown function: {functionName}" });
        }
    }
    

    Każda definicja narzędzia zawiera element name, description który pomaga modelowi zdecydować, kiedy go używać, oraz schemat opisujący parameters oczekiwane dane wejściowe.

  2. Dodaj metody języka C#, które implementują każde narzędzie:

    // --- Tool definitions and implementations ---
    var toolDefinitions = new[]
    {
        new
        {
            Name = "get_weather",
            Description = "Get the current weather for a location",
            Schema =
                """
                {
                  "type": "object",
                  "properties": {
                    "location": { "type": "string", "description": "The city or location" },
                    "unit": {
                      "type": "string",
                      "description": "Temperature unit",
                      "enum": ["celsius", "fahrenheit"]
                    }
                  },
                  "required": ["location"]
                }
                """
        },
        new
        {
            Name = "calculate",
            Description = "Perform a math calculation",
            Schema =
                """
                {
                  "type": "object",
                  "properties": {
                    "expression": { "type": "string", "description": "The math expression to evaluate" }
                  },
                  "required": ["expression"]
                }
                """
        }
    };
    
    string ExecuteTool(string functionName, JsonElement arguments)
    {
        switch (functionName)
        {
            case "get_weather":
                var location = arguments.GetProperty("location").GetString() ?? "unknown";
                var unit = arguments.TryGetProperty("unit", out var u)
                    ? u.GetString() ?? "celsius"
                    : "celsius";
                var temp = unit == "celsius" ? 22 : 72;
                return JsonSerializer.Serialize(new
                {
                    location,
                    temperature = temp,
                    unit,
                    condition = "Sunny"
                });
    
            case "calculate":
                var expression = arguments.GetProperty("expression").GetString() ?? "";
                try
                {
                    var result = new System.Data.DataTable().Compute(expression, null);
                    return JsonSerializer.Serialize(new { expression, result = result?.ToString() });
                }
                catch (Exception ex)
                {
                    return JsonSerializer.Serialize(new { error = ex.Message });
                }
    
            default:
                return JsonSerializer.Serialize(new { error = $"Unknown function: {functionName}" });
        }
    }
    

    Model nie uruchamia tych funkcji bezpośrednio. Zwraca żądanie wywołania narzędzia z nazwą funkcji i argumentami, a kod wykonuje funkcję.

Wysyłanie komunikatu wyzwalającego użycie narzędzia

Zainicjuj lokalny zestaw SDK usługi Foundry, załaduj model i wyślij komunikat, na który model może odpowiedzieć, wywołując narzędzie.

// --- Main application ---
var config = new Configuration
{
    AppName = "foundry_local_samples",
    LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information
};

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<Program>();

await FoundryLocalManager.CreateAsync(config, logger);
var mgr = FoundryLocalManager.Instance;

var currentEp = "";
await mgr.DownloadAndRegisterEpsAsync((epName, percent) =>
{
    if (epName != currentEp)
    {
        if (currentEp != "") Console.WriteLine();
        currentEp = epName;
    }
    Console.Write($"\r  {epName.PadRight(30)}  {percent,6:F1}%");
});
if (currentEp != "") Console.WriteLine();

var catalog = await mgr.GetCatalogAsync();
var model = await catalog.GetModelAsync("qwen2.5-0.5b")
    ?? throw new Exception("Model not found");

await model.DownloadAsync(progress =>
{
    Console.Write($"\rDownloading model: {progress:F2}%");
    if (progress >= 100f) Console.WriteLine();
});

await model.LoadAsync();
Console.WriteLine("Model loaded and ready.");

using var chatSession = new ChatSession(model);
chatSession.SetOptions(new RequestOptions { ToolChoice = ToolChoice.Auto });
foreach (var tool in toolDefinitions)
{
    chatSession.AddToolDefinition(tool.Name, tool.Description, tool.Schema);
}

Gdy model ustali, że potrzebne jest narzędzie, odpowiedź zawiera ToolCalls zamiast zwykłego komunikatu tekstowego. W następnym kroku pokazano, jak wykrywać i obsługiwać te wywołania.

Wykonywanie narzędzia i zwracanie wyników

Gdy model odpowie za pomocą wywołania narzędzia, wyodrębnisz nazwę i argumenty funkcji, uruchom funkcję i wyślij wynik z powrotem.

Console.WriteLine("\nTool-calling assistant ready! Type 'quit' to exit.\n");

while (true)
{
    Console.Write("You: ");
    var userInput = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userInput) ||
        userInput.Equals("quit", StringComparison.OrdinalIgnoreCase) ||
        userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
    {
        break;
    }

    using var request = new Request();
    if (chatSession.TurnCount == 0)
    {
        request.AddItem(MessageItem.System(
            "You are a helpful assistant with access to tools. Use them when needed."));
    }
    request.AddItem(MessageItem.User(userInput));

    using var response = await chatSession.ProcessRequestAsync(request, ct);
    var toolCalls = response.OfType<ToolCallItem>()
        .Select(call => (call.CallId, call.Name, call.Arguments))
        .ToList();

    if (toolCalls.Count > 0)
    {
        using var toolRequest = new Request();
        foreach (var toolCall in toolCalls)
        {
            using var argumentsDocument = JsonDocument.Parse(toolCall.Arguments);
            var arguments = argumentsDocument.RootElement;
            Console.WriteLine($"  Tool call: {toolCall.Name}({arguments})");
            var result = ExecuteTool(toolCall.Name, arguments);
            toolRequest.AddItem(new ToolResultItem(toolCall.CallId, result));
        }

        using var finalResponse = await chatSession.ProcessRequestAsync(toolRequest, ct);
        var answer = GetResponseText(finalResponse);
        Console.WriteLine($"Assistant: {answer}\n");
    }
    else
    {
        Console.WriteLine($"Assistant: {GetResponseText(response)}\n");
    }
}

chatSession.Dispose();
await model.UnloadAsync();
Console.WriteLine("Model unloaded. Goodbye!");

Kluczowe kroki w pętli wywoływania narzędzi to:

  1. Wykrywanie wywołań narzędzi — sprawdź response.Choices[0].Message.ToolCalls.
  2. Wykonaj funkcję — przeanalizuj argumenty i wywołaj funkcję lokalną.
  3. Zwróć wynik — dodaj komunikat z rolą tool i pasującym ToolCallId elementem.
  4. Uzyskaj ostateczną odpowiedź — model używa wyniku narzędzia do generowania naturalnej odpowiedzi.

Obsługa pełnej pętli wywołań narzędzi

Oto kompletna aplikacja, która łączy definicje narzędzi, inicjowanie zestawu SDK i pętlę wywoływania narzędzia w jeden plik możliwy do uruchomienia.

Zastąp zawartość Program.cs następującym kompletnym kodem:

using System.Text.Json;
using Microsoft.AI.Foundry.Local;
using Microsoft.Extensions.Logging;

CancellationToken ct = CancellationToken.None;

// --- Tool definitions and implementations ---
var toolDefinitions = new[]
{
    new
    {
        Name = "get_weather",
        Description = "Get the current weather for a location",
        Schema =
            """
            {
              "type": "object",
              "properties": {
                "location": { "type": "string", "description": "The city or location" },
                "unit": {
                  "type": "string",
                  "description": "Temperature unit",
                  "enum": ["celsius", "fahrenheit"]
                }
              },
              "required": ["location"]
            }
            """
    },
    new
    {
        Name = "calculate",
        Description = "Perform a math calculation",
        Schema =
            """
            {
              "type": "object",
              "properties": {
                "expression": { "type": "string", "description": "The math expression to evaluate" }
              },
              "required": ["expression"]
            }
            """
    }
};

string ExecuteTool(string functionName, JsonElement arguments)
{
    switch (functionName)
    {
        case "get_weather":
            var location = arguments.GetProperty("location").GetString() ?? "unknown";
            var unit = arguments.TryGetProperty("unit", out var u)
                ? u.GetString() ?? "celsius"
                : "celsius";
            var temp = unit == "celsius" ? 22 : 72;
            return JsonSerializer.Serialize(new
            {
                location,
                temperature = temp,
                unit,
                condition = "Sunny"
            });

        case "calculate":
            var expression = arguments.GetProperty("expression").GetString() ?? "";
            try
            {
                var result = new System.Data.DataTable().Compute(expression, null);
                return JsonSerializer.Serialize(new { expression, result = result?.ToString() });
            }
            catch (Exception ex)
            {
                return JsonSerializer.Serialize(new { error = ex.Message });
            }

        default:
            return JsonSerializer.Serialize(new { error = $"Unknown function: {functionName}" });
    }
}

// --- Main application ---
var config = new Configuration
{
    AppName = "foundry_local_samples",
    LogLevel = Microsoft.AI.Foundry.Local.LogLevel.Information
};

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<Program>();

await FoundryLocalManager.CreateAsync(config, logger);
var mgr = FoundryLocalManager.Instance;

var currentEp = "";
await mgr.DownloadAndRegisterEpsAsync((epName, percent) =>
{
    if (epName != currentEp)
    {
        if (currentEp != "") Console.WriteLine();
        currentEp = epName;
    }
    Console.Write($"\r  {epName.PadRight(30)}  {percent,6:F1}%");
});
if (currentEp != "") Console.WriteLine();

var catalog = await mgr.GetCatalogAsync();
var model = await catalog.GetModelAsync("qwen2.5-0.5b")
    ?? throw new Exception("Model not found");

await model.DownloadAsync(progress =>
{
    Console.Write($"\rDownloading model: {progress:F2}%");
    if (progress >= 100f) Console.WriteLine();
});

await model.LoadAsync();
Console.WriteLine("Model loaded and ready.");

using var chatSession = new ChatSession(model);
chatSession.SetOptions(new RequestOptions { ToolChoice = ToolChoice.Auto });
foreach (var tool in toolDefinitions)
{
    chatSession.AddToolDefinition(tool.Name, tool.Description, tool.Schema);
}

Console.WriteLine("\nTool-calling assistant ready! Type 'quit' to exit.\n");

while (true)
{
    Console.Write("You: ");
    var userInput = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userInput) ||
        userInput.Equals("quit", StringComparison.OrdinalIgnoreCase) ||
        userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
    {
        break;
    }

    using var request = new Request();
    if (chatSession.TurnCount == 0)
    {
        request.AddItem(MessageItem.System(
            "You are a helpful assistant with access to tools. Use them when needed."));
    }
    request.AddItem(MessageItem.User(userInput));

    using var response = await chatSession.ProcessRequestAsync(request, ct);
    var toolCalls = response.OfType<ToolCallItem>()
        .Select(call => (call.CallId, call.Name, call.Arguments))
        .ToList();

    if (toolCalls.Count > 0)
    {
        using var toolRequest = new Request();
        foreach (var toolCall in toolCalls)
        {
            using var argumentsDocument = JsonDocument.Parse(toolCall.Arguments);
            var arguments = argumentsDocument.RootElement;
            Console.WriteLine($"  Tool call: {toolCall.Name}({arguments})");
            var result = ExecuteTool(toolCall.Name, arguments);
            toolRequest.AddItem(new ToolResultItem(toolCall.CallId, result));
        }

        using var finalResponse = await chatSession.ProcessRequestAsync(toolRequest, ct);
        var answer = GetResponseText(finalResponse);
        Console.WriteLine($"Assistant: {answer}\n");
    }
    else
    {
        Console.WriteLine($"Assistant: {GetResponseText(response)}\n");
    }
}

chatSession.Dispose();
await model.UnloadAsync();
Console.WriteLine("Model unloaded. Goodbye!");

Uruchom asystenta wywoływania narzędzi:

dotnet run

Zobaczysz dane wyjściowe podobne do:

Downloading model: 100.00%
Model loaded and ready.

Tool-calling assistant ready! Type 'quit' to exit.

You: What's the weather like today?
  Tool call: get_weather({"location":"current location"})
Assistant: The weather today is sunny with a temperature of 22°C.

You: What is 245 * 38?
  Tool call: calculate({"expression":"245 * 38"})
Assistant: 245 multiplied by 38 equals 9,310.

You: quit
Model unloaded. Goodbye!

Model decyduje, kiedy wywołać narzędzie na podstawie komunikatu użytkownika. W przypadku pytania o pogodę wywołuje get_weathermetodę , w przypadku matematyki wywołuje calculatemetodę , a w przypadku pytań ogólnych odpowiada bezpośrednio bez żadnych wywołań narzędzi.

Repozytorium przykładów

Kompletny przykładowy kod dla tego artykułu jest dostępny w repozytorium foundry-samples GitHub. Aby sklonować repozytorium i przejść do przykładowego użycia:

git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/javascript/foundry-local/tutorial-tool-calling

Instalowanie pakietów

npm install foundry-local-sdk openai

Definiowanie narzędzi

Wywołanie narzędzia umożliwia modelowi żądanie, że kod uruchamia funkcję i zwraca wynik. Dostępne narzędzia są definiowane jako lista schematów JSON opisujących nazwy, przeznaczenie i parametry każdej funkcji.

  1. Utwórz plik o nazwie index.js.

  2. Dodaj następujące definicje narzędzi:

    // --- Tool definitions ---
    const tools = [
        {
            name: 'get_weather',
            description: 'Get the current weather for a location',
            jsonSchema: JSON.stringify({
                type: 'object',
                properties: {
                    location: {
                        type: 'string',
                        description: 'The city or location'
                    },
                    unit: {
                        type: 'string',
                        enum: ['celsius', 'fahrenheit'],
                        description: 'Temperature unit'
                    }
                },
                required: ['location']
            })
        },
        {
            name: 'calculate',
            description: 'Perform a math calculation',
            jsonSchema: JSON.stringify({
                type: 'object',
                properties: {
                    expression: {
                        type: 'string',
                        description: 'The math expression to evaluate'
                    }
                },
                required: ['expression']
            })
        }
    ];
    
    // --- Tool implementations ---
    function getWeather(location, unit = 'celsius') {
        return {
            location,
            temperature: unit === 'celsius' ? 22 : 72,
            unit,
            condition: 'Sunny'
        };
    }
    
    function calculate(expression) {
        // Input is validated against a strict allowlist of numeric/math characters,
        // making this safe from code injection in this tutorial context.
        const allowed = /^[0-9+\-*/(). ]+$/;
        if (!allowed.test(expression)) {
            return { error: 'Invalid expression' };
        }
        try {
            const result = Function(
                `"use strict"; return (${expression})`
            )();
            return { expression, result };
        } catch (err) {
            return { error: err.message };
        }
    }
    
    const toolFunctions = {
        get_weather: (args) => getWeather(args.location, args.unit),
        calculate: (args) => calculate(args.expression)
    };
    

    Każda definicja narzędzia zawiera element name, description który pomaga modelowi zdecydować, kiedy go używać, oraz schemat opisujący parameters oczekiwane dane wejściowe.

  3. Dodaj funkcje języka JavaScript, które implementują każde narzędzie:

    // --- Tool definitions ---
    const tools = [
        {
            name: 'get_weather',
            description: 'Get the current weather for a location',
            jsonSchema: JSON.stringify({
                type: 'object',
                properties: {
                    location: {
                        type: 'string',
                        description: 'The city or location'
                    },
                    unit: {
                        type: 'string',
                        enum: ['celsius', 'fahrenheit'],
                        description: 'Temperature unit'
                    }
                },
                required: ['location']
            })
        },
        {
            name: 'calculate',
            description: 'Perform a math calculation',
            jsonSchema: JSON.stringify({
                type: 'object',
                properties: {
                    expression: {
                        type: 'string',
                        description: 'The math expression to evaluate'
                    }
                },
                required: ['expression']
            })
        }
    ];
    
    // --- Tool implementations ---
    function getWeather(location, unit = 'celsius') {
        return {
            location,
            temperature: unit === 'celsius' ? 22 : 72,
            unit,
            condition: 'Sunny'
        };
    }
    
    function calculate(expression) {
        // Input is validated against a strict allowlist of numeric/math characters,
        // making this safe from code injection in this tutorial context.
        const allowed = /^[0-9+\-*/(). ]+$/;
        if (!allowed.test(expression)) {
            return { error: 'Invalid expression' };
        }
        try {
            const result = Function(
                `"use strict"; return (${expression})`
            )();
            return { expression, result };
        } catch (err) {
            return { error: err.message };
        }
    }
    
    const toolFunctions = {
        get_weather: (args) => getWeather(args.location, args.unit),
        calculate: (args) => calculate(args.expression)
    };
    

    Model nie uruchamia tych funkcji bezpośrednio. Zwraca żądanie wywołania narzędzia z nazwą funkcji i argumentami, a kod wykonuje funkcję.

Wysyłanie komunikatu wyzwalającego użycie narzędzia

Zainicjuj lokalny zestaw SDK usługi Foundry, załaduj model i wyślij komunikat, na który model może odpowiedzieć, wywołując narzędzie.

// --- Main application ---
const manager = FoundryLocalManager.create({
    appName: 'foundry_local_samples',
    logLevel: 'info'
});

// Download and register all execution providers.
let currentEp = '';
await manager.downloadAndRegisterEps((epName, percent) => {
    if (epName !== currentEp) {
        if (currentEp !== '') process.stdout.write('\n');
        currentEp = epName;
    }
    process.stdout.write(`\r  ${epName.padEnd(30)}  ${percent.toFixed(1).padStart(5)}%`);
});
if (currentEp !== '') process.stdout.write('\n');

const model = await manager.catalog.getModel('qwen2.5-0.5b');

await model.download((progress) => {
    process.stdout.write(
        `\rDownloading model: ${progress.toFixed(2)}%`
    );
});
console.log('\nModel downloaded.');

await model.load();
console.log('Model loaded and ready.');

const chatSession = new ChatSession(model);
for (const tool of tools) {
    chatSession.addToolDefinition(tool);
}
let firstTurn = true;

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const askQuestion = (prompt) =>
    new Promise((resolve) => {
        if (rl.closed) return resolve('quit');
        const onClose = () => resolve('quit');
        rl.once('close', onClose);
        rl.question(prompt, (answer) => {
            rl.off('close', onClose);
            resolve(answer);
        });
    });

console.log(
    '\nTool-calling assistant ready! Type \'quit\' to exit.\n'
);

while (true) {
    const userInput = await askQuestion('You: ');
    if (
        userInput.trim().toLowerCase() === 'quit' ||
        userInput.trim().toLowerCase() === 'exit'
    ) {
        break;
    }

    const request = new Request().setOptions({ toolChoice: 'auto' });
    if (firstTurn) {
        request.addItem(Item.systemMessage(
            'You are a helpful assistant with access to tools. ' +
            'Use them when needed to answer questions accurately.'
        ));
        firstTurn = false;
    }
    request.addItem(Item.userMessage(userInput));

    const response = await chatSession.processRequest(request);
    const answer = await processToolCalls(
        response, chatSession
    );

    console.log(`Assistant: ${answer}\n`);
}

chatSession.dispose();
await model.unload();
console.log('Model unloaded. Goodbye!');
rl.close();

Gdy model ustali, że potrzebne jest narzędzie, odpowiedź zawiera tool_calls zamiast zwykłego komunikatu tekstowego. W następnym kroku pokazano, jak wykrywać i obsługiwać te wywołania.

Wykonywanie narzędzia i zwracanie wyników

Gdy model odpowie za pomocą wywołania narzędzia, wyodrębnisz nazwę i argumenty funkcji, uruchom funkcję i wyślij wynik z powrotem.

async function processToolCalls(response, chatSession) {
    let currentResponse = response;

    while (currentResponse.finishReason === 'toolCalls') {
        const toolResults = [];
        for (const toolCall of currentResponse.output.filter(
            (item) => item.type === 'toolCall'
        )) {
            const functionName = toolCall.name;
            const args = JSON.parse(toolCall.arguments);
            console.log(
                `  Tool call: ${functionName}` +
                `(${JSON.stringify(args)})`
            );

            const implementation = toolFunctions[functionName];
            const result = implementation
                ? implementation(args)
                : { error: `Unknown tool: ${functionName}` };
            toolResults.push(Item.toolResult(
                toolCall.callId,
                JSON.stringify(result)
            ));
        }
        if (toolResults.length === 0) {
            throw new Error('The model requested tools but returned no tool calls.');
        }

        const followUp = new Request()
            .setOptions({ toolChoice: 'auto' });
        for (const result of toolResults) {
            followUp.addItem(result);
        }
        currentResponse = await chatSession.processRequest(followUp);
    }

    return currentResponse.output
        .map((item) => item.type === 'message'
            ? item.content ?? item.parts?.map((part) => part.text ?? '').join('') ?? ''
            : item.type === 'text' ? item.text : '')
        .join('');
}

Kluczowe kroki w pętli wywoływania narzędzi to:

  1. Wykrywanie wywołań narzędzi — sprawdź response.choices[0]?.message?.tool_calls.
  2. Wykonaj funkcję — przeanalizuj argumenty i wywołaj funkcję lokalną.
  3. Zwróć wynik — dodaj komunikat z rolą tool i pasującym tool_call_id elementem.
  4. Uzyskaj ostateczną odpowiedź — model używa wyniku narzędzia do generowania naturalnej odpowiedzi.

Obsługa pełnej pętli wywołań narzędzi

Oto kompletna aplikacja, która łączy definicje narzędzi, inicjowanie zestawu SDK i pętlę wywoływania narzędzia w jeden plik możliwy do uruchomienia.

Utwórz plik o nazwie index.js i dodaj następujący kompletny kod:

import {
    ChatSession,
    FoundryLocalManager,
    Item,
    Request
} from 'foundry-local-sdk';
import * as readline from 'readline';

// --- Tool definitions ---
const tools = [
    {
        name: 'get_weather',
        description: 'Get the current weather for a location',
        jsonSchema: JSON.stringify({
            type: 'object',
            properties: {
                location: {
                    type: 'string',
                    description: 'The city or location'
                },
                unit: {
                    type: 'string',
                    enum: ['celsius', 'fahrenheit'],
                    description: 'Temperature unit'
                }
            },
            required: ['location']
        })
    },
    {
        name: 'calculate',
        description: 'Perform a math calculation',
        jsonSchema: JSON.stringify({
            type: 'object',
            properties: {
                expression: {
                    type: 'string',
                    description: 'The math expression to evaluate'
                }
            },
            required: ['expression']
        })
    }
];

// --- Tool implementations ---
function getWeather(location, unit = 'celsius') {
    return {
        location,
        temperature: unit === 'celsius' ? 22 : 72,
        unit,
        condition: 'Sunny'
    };
}

function calculate(expression) {
    // Input is validated against a strict allowlist of numeric/math characters,
    // making this safe from code injection in this tutorial context.
    const allowed = /^[0-9+\-*/(). ]+$/;
    if (!allowed.test(expression)) {
        return { error: 'Invalid expression' };
    }
    try {
        const result = Function(
            `"use strict"; return (${expression})`
        )();
        return { expression, result };
    } catch (err) {
        return { error: err.message };
    }
}

const toolFunctions = {
    get_weather: (args) => getWeather(args.location, args.unit),
    calculate: (args) => calculate(args.expression)
};

async function processToolCalls(response, chatSession) {
    let currentResponse = response;

    while (currentResponse.finishReason === 'toolCalls') {
        const toolResults = [];
        for (const toolCall of currentResponse.output.filter(
            (item) => item.type === 'toolCall'
        )) {
            const functionName = toolCall.name;
            const args = JSON.parse(toolCall.arguments);
            console.log(
                `  Tool call: ${functionName}` +
                `(${JSON.stringify(args)})`
            );

            const implementation = toolFunctions[functionName];
            const result = implementation
                ? implementation(args)
                : { error: `Unknown tool: ${functionName}` };
            toolResults.push(Item.toolResult(
                toolCall.callId,
                JSON.stringify(result)
            ));
        }
        if (toolResults.length === 0) {
            throw new Error('The model requested tools but returned no tool calls.');
        }

        const followUp = new Request()
            .setOptions({ toolChoice: 'auto' });
        for (const result of toolResults) {
            followUp.addItem(result);
        }
        currentResponse = await chatSession.processRequest(followUp);
    }

    return currentResponse.output
        .map((item) => item.type === 'message'
            ? item.content ?? item.parts?.map((part) => part.text ?? '').join('') ?? ''
            : item.type === 'text' ? item.text : '')
        .join('');
}

// --- Main application ---
const manager = FoundryLocalManager.create({
    appName: 'foundry_local_samples',
    logLevel: 'info'
});

// Download and register all execution providers.
let currentEp = '';
await manager.downloadAndRegisterEps((epName, percent) => {
    if (epName !== currentEp) {
        if (currentEp !== '') process.stdout.write('\n');
        currentEp = epName;
    }
    process.stdout.write(`\r  ${epName.padEnd(30)}  ${percent.toFixed(1).padStart(5)}%`);
});
if (currentEp !== '') process.stdout.write('\n');

const model = await manager.catalog.getModel('qwen2.5-0.5b');

await model.download((progress) => {
    process.stdout.write(
        `\rDownloading model: ${progress.toFixed(2)}%`
    );
});
console.log('\nModel downloaded.');

await model.load();
console.log('Model loaded and ready.');

const chatSession = new ChatSession(model);
for (const tool of tools) {
    chatSession.addToolDefinition(tool);
}
let firstTurn = true;

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const askQuestion = (prompt) =>
    new Promise((resolve) => {
        if (rl.closed) return resolve('quit');
        const onClose = () => resolve('quit');
        rl.once('close', onClose);
        rl.question(prompt, (answer) => {
            rl.off('close', onClose);
            resolve(answer);
        });
    });

console.log(
    '\nTool-calling assistant ready! Type \'quit\' to exit.\n'
);

while (true) {
    const userInput = await askQuestion('You: ');
    if (
        userInput.trim().toLowerCase() === 'quit' ||
        userInput.trim().toLowerCase() === 'exit'
    ) {
        break;
    }

    const request = new Request().setOptions({ toolChoice: 'auto' });
    if (firstTurn) {
        request.addItem(Item.systemMessage(
            'You are a helpful assistant with access to tools. ' +
            'Use them when needed to answer questions accurately.'
        ));
        firstTurn = false;
    }
    request.addItem(Item.userMessage(userInput));

    const response = await chatSession.processRequest(request);
    const answer = await processToolCalls(
        response, chatSession
    );

    console.log(`Assistant: ${answer}\n`);
}

chatSession.dispose();
await model.unload();
console.log('Model unloaded. Goodbye!');
rl.close();

Uruchom asystenta wywoływania narzędzi:

node index.js

Zobaczysz dane wyjściowe podobne do:

Downloading model: 100.00%
Model downloaded.
Model loaded and ready.

Tool-calling assistant ready! Type 'quit' to exit.

You: What's the weather like today?
  Tool call: get_weather({"location":"current location"})
Assistant: The weather today is sunny with a temperature of 22°C.

You: What is 245 * 38?
  Tool call: calculate({"expression":"245 * 38"})
Assistant: 245 multiplied by 38 equals 9,310.

You: quit
Model unloaded. Goodbye!

Model decyduje, kiedy wywołać narzędzie na podstawie komunikatu użytkownika. W przypadku pytania o pogodę wywołuje get_weathermetodę , w przypadku matematyki wywołuje calculatemetodę , a w przypadku pytań ogólnych odpowiada bezpośrednio bez żadnych wywołań narzędzi.

Repozytorium przykładów

Kompletny przykładowy kod dla tego artykułu jest dostępny w repozytorium foundry-samples GitHub. Aby sklonować repozytorium i przejść do przykładowego użycia:

git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/python/foundry-local/tutorial-tool-calling

Instalowanie pakietów

pip install foundry-local-sdk openai

Definiowanie narzędzi

Wywołanie narzędzia umożliwia modelowi żądanie, że kod uruchamia funkcję i zwraca wynik. Dostępne narzędzia są definiowane jako lista schematów JSON opisujących nazwy, przeznaczenie i parametry każdej funkcji.

Utwórz plik o nazwie main.py i dodaj następujące definicje narzędzi:

# --- Tool definitions ---
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city or location",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit",
                },
            },
            "required": ["location"],
        },
    },
    {
        "name": "calculate",
        "description": "Perform a math calculation",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "The math expression to evaluate",
                }
            },
            "required": ["expression"],
        },
    },
]


# --- Tool implementations ---
def get_weather(location, unit="celsius"):
    """Simulate a weather lookup."""
    return {
        "location": location,
        "temperature": 22 if unit == "celsius" else 72,
        "unit": unit,
        "condition": "Sunny",
    }


def calculate(expression):
    """Evaluate a math expression safely."""
    allowed = set("0123456789+-*/(). ")
    if not all(character in allowed for character in expression):
        return {"error": "Invalid expression"}
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return {"expression": expression, "result": result}
    except Exception as error:
        return {"error": str(error)}


tool_functions = {"get_weather": get_weather, "calculate": calculate}

Każda definicja narzędzia zawiera element name, description który pomaga modelowi zdecydować, kiedy go używać, oraz schemat opisujący parameters oczekiwane dane wejściowe.

Następnie dodaj funkcje Python, które implementują każde narzędzie:

# --- Tool definitions ---
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city or location",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit",
                },
            },
            "required": ["location"],
        },
    },
    {
        "name": "calculate",
        "description": "Perform a math calculation",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "The math expression to evaluate",
                }
            },
            "required": ["expression"],
        },
    },
]


# --- Tool implementations ---
def get_weather(location, unit="celsius"):
    """Simulate a weather lookup."""
    return {
        "location": location,
        "temperature": 22 if unit == "celsius" else 72,
        "unit": unit,
        "condition": "Sunny",
    }


def calculate(expression):
    """Evaluate a math expression safely."""
    allowed = set("0123456789+-*/(). ")
    if not all(character in allowed for character in expression):
        return {"error": "Invalid expression"}
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return {"expression": expression, "result": result}
    except Exception as error:
        return {"error": str(error)}


tool_functions = {"get_weather": get_weather, "calculate": calculate}

Model nie uruchamia tych funkcji bezpośrednio. Zwraca żądanie wywołania narzędzia z nazwą funkcji i argumentami, a kod wykonuje funkcję.

Wysyłanie komunikatu wyzwalającego użycie narzędzia

Zainicjuj lokalny zestaw SDK usługi Foundry, załaduj model i wyślij komunikat, na który model może odpowiedzieć, wywołując narzędzie.

def main():
    # Initialize the Foundry Local SDK
    config = Configuration(app_name="foundry_local_samples")
    FoundryLocalManager.initialize(config)
    manager = FoundryLocalManager.instance

    # Discover and register execution providers that are not already available.
    missing_eps = [ep.name for ep in manager.discover_eps() if not ep.is_registered]
    if missing_eps:
        manager.download_and_register_eps(missing_eps)

    # Select and load a tool-capable model.
    model = manager.catalog.get_model("qwen2.5-0.5b")
    if model is None:
        raise RuntimeError("Model 'qwen2.5-0.5b' was not found in the catalog.")
    if not model.is_cached:
        model.download(
            lambda progress: print(
                f"\rDownloading model: {progress:.2f}%", end="", flush=True
            )
        )
        print()
    model.load()
    print("Model loaded and ready.")

    try:
        with ChatSession(model) as session:
            session.set_options(
                RequestOptions(
                    search=SearchOptions(temperature=0.0, max_output_tokens=512)
                )
            )
            for tool in tools:
                session.add_tool_definition(
                    tool["name"],
                    tool["description"],
                    json.dumps(tool["parameters"]),
                )

            system_prompt = (
                "You are a helpful assistant with access to tools. "
                "Use them when needed to answer questions accurately."
            )
            first_turn = True

            print("\nTool-calling assistant ready! Type 'quit' to exit.\n")

            while True:
                user_input = input("You: ")
                if user_input.strip().lower() in ("quit", "exit"):
                    break

                with Request() as request:
                    if first_turn:
                        request.add_item(MessageItem.system(system_prompt))
                        first_turn = False
                    request.add_item(MessageItem.user(user_input))

                    with session.process_request(request) as response:
                        answer, pending_calls = response_content(response)
                    answer = process_tool_calls(session, answer, pending_calls)

                print(f"Assistant: {answer}\n")
    finally:
        # Clean up
        if model.is_loaded:
            model.unload()
        manager.close()
        print("Model unloaded. Goodbye!")

Gdy model ustali, że potrzebne jest narzędzie, odpowiedź zawiera tool_calls zamiast zwykłego komunikatu tekstowego. W następnym kroku pokazano, jak wykrywać i obsługiwać te wywołania.

Wykonywanie narzędzia i zwracanie wyników

Gdy model odpowie za pomocą wywołania narzędzia, wyodrębnisz nazwę i argumenty funkcji, uruchom funkcję i wyślij wynik z powrotem.

def process_tool_calls(session, answer, pending_calls):
    """Handle tool calls until the stateful session produces a final answer."""
    while pending_calls:
        with Request() as follow_up:
            for call_id, function_name, arguments_json in pending_calls:
                arguments = json.loads(arguments_json)
                print(f"  Tool call: {function_name}({arguments})")
                result = tool_functions[function_name](**arguments)
                follow_up.add_item(
                    ToolResultItem(call_id=call_id, result=json.dumps(result))
                )

            with session.process_request(follow_up) as next_response:
                answer, pending_calls = response_content(next_response)

    return answer

Kluczowe kroki w pętli wywoływania narzędzi to:

  1. Wykrywanie wywołań narzędzi — sprawdź response.choices[0].message.tool_calls.
  2. Wykonaj funkcję — przeanalizuj argumenty i wywołaj funkcję lokalną.
  3. Zwróć wynik — dodaj komunikat z rolą tool i pasującym tool_call_id elementem.
  4. Uzyskaj ostateczną odpowiedź — model używa wyniku narzędzia do generowania naturalnej odpowiedzi.

Obsługa pełnej pętli wywołań narzędzi

Oto kompletna aplikacja, która łączy definicje narzędzi, inicjowanie zestawu SDK i pętlę wywoływania narzędzia w jeden plik możliwy do uruchomienia.

Utwórz plik o nazwie main.py i dodaj następujący kompletny kod:

import json

from foundry_local_sdk import (
    ChatSession,
    Configuration,
    FoundryLocalManager,
    MessageItem,
    Request,
    RequestOptions,
    SearchOptions,
    TextItem,
    ToolCallItem,
    ToolResultItem,
)



# --- Tool definitions ---
tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "The city or location",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit",
                },
            },
            "required": ["location"],
        },
    },
    {
        "name": "calculate",
        "description": "Perform a math calculation",
        "parameters": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "The math expression to evaluate",
                }
            },
            "required": ["expression"],
        },
    },
]


# --- Tool implementations ---
def get_weather(location, unit="celsius"):
    """Simulate a weather lookup."""
    return {
        "location": location,
        "temperature": 22 if unit == "celsius" else 72,
        "unit": unit,
        "condition": "Sunny",
    }


def calculate(expression):
    """Evaluate a math expression safely."""
    allowed = set("0123456789+-*/(). ")
    if not all(character in allowed for character in expression):
        return {"error": "Invalid expression"}
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return {"expression": expression, "result": result}
    except Exception as error:
        return {"error": str(error)}


tool_functions = {"get_weather": get_weather, "calculate": calculate}


def response_content(response):
    text_parts = []
    tool_calls = []
    for item in response:
        parts = item.parts if isinstance(item, MessageItem) else [item]
        for part in parts:
            if isinstance(part, TextItem):
                text_parts.append(part.text)
            elif isinstance(part, ToolCallItem):
                tool_calls.append((part.call_id, part.name, part.arguments))
    return "".join(text_parts), tool_calls


def process_tool_calls(session, answer, pending_calls):
    """Handle tool calls until the stateful session produces a final answer."""
    while pending_calls:
        with Request() as follow_up:
            for call_id, function_name, arguments_json in pending_calls:
                arguments = json.loads(arguments_json)
                print(f"  Tool call: {function_name}({arguments})")
                result = tool_functions[function_name](**arguments)
                follow_up.add_item(
                    ToolResultItem(call_id=call_id, result=json.dumps(result))
                )

            with session.process_request(follow_up) as next_response:
                answer, pending_calls = response_content(next_response)

    return answer




def main():
    # Initialize the Foundry Local SDK
    config = Configuration(app_name="foundry_local_samples")
    FoundryLocalManager.initialize(config)
    manager = FoundryLocalManager.instance

    # Discover and register execution providers that are not already available.
    missing_eps = [ep.name for ep in manager.discover_eps() if not ep.is_registered]
    if missing_eps:
        manager.download_and_register_eps(missing_eps)

    # Select and load a tool-capable model.
    model = manager.catalog.get_model("qwen2.5-0.5b")
    if model is None:
        raise RuntimeError("Model 'qwen2.5-0.5b' was not found in the catalog.")
    if not model.is_cached:
        model.download(
            lambda progress: print(
                f"\rDownloading model: {progress:.2f}%", end="", flush=True
            )
        )
        print()
    model.load()
    print("Model loaded and ready.")

    try:
        with ChatSession(model) as session:
            session.set_options(
                RequestOptions(
                    search=SearchOptions(temperature=0.0, max_output_tokens=512)
                )
            )
            for tool in tools:
                session.add_tool_definition(
                    tool["name"],
                    tool["description"],
                    json.dumps(tool["parameters"]),
                )

            system_prompt = (
                "You are a helpful assistant with access to tools. "
                "Use them when needed to answer questions accurately."
            )
            first_turn = True

            print("\nTool-calling assistant ready! Type 'quit' to exit.\n")

            while True:
                user_input = input("You: ")
                if user_input.strip().lower() in ("quit", "exit"):
                    break

                with Request() as request:
                    if first_turn:
                        request.add_item(MessageItem.system(system_prompt))
                        first_turn = False
                    request.add_item(MessageItem.user(user_input))

                    with session.process_request(request) as response:
                        answer, pending_calls = response_content(response)
                    answer = process_tool_calls(session, answer, pending_calls)

                print(f"Assistant: {answer}\n")
    finally:
        # Clean up
        if model.is_loaded:
            model.unload()
        manager.close()
        print("Model unloaded. Goodbye!")




if __name__ == "__main__":
    main()

Uruchom asystenta wywoływania narzędzi:

python main.py

Zobaczysz dane wyjściowe podobne do:

Downloading model: 100.00%
Model loaded and ready.

Tool-calling assistant ready! Type 'quit' to exit.

You: What's the weather like today?
  Tool call: get_weather({'location': 'current location'})
Assistant: The weather today is sunny with a temperature of 22°C.

You: What is 245 * 38?
  Tool call: calculate({'expression': '245 * 38'})
Assistant: 245 multiplied by 38 equals 9,310.

You: quit
Model unloaded. Goodbye!

Model decyduje, kiedy wywołać narzędzie na podstawie komunikatu użytkownika. W przypadku pytania o pogodę wywołuje get_weathermetodę , w przypadku matematyki wywołuje calculatemetodę , a w przypadku pytań ogólnych odpowiada bezpośrednio bez żadnych wywołań narzędzi.

Repozytorium przykładów

Kompletny przykładowy kod dla tego artykułu jest dostępny w repozytorium foundry-samples GitHub. Aby sklonować repozytorium i przejść do przykładowego użycia:

git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/rust/foundry-local/tutorial-tool-calling

Instalowanie pakietów

cargo add foundry-local-sdk
cargo add tokio --features full
cargo add tokio-stream anyhow

Definiowanie narzędzi

Wywołanie narzędzia umożliwia modelowi żądanie, że kod uruchamia funkcję i zwraca wynik. Dostępne narzędzia są definiowane jako lista schematów JSON opisujących nazwy, przeznaczenie i parametry każdej funkcji.

  1. serde_json Dodaj zależność do obsługi kodu JSON:

    cargo add serde_json
    
  2. Otwórz src/main.rs i dodaj następujące definicje narzędzi:

    let weather_schema = json!({
        "type": "object",
        "properties": {
            "location": {"type": "string", "description": "The city or location"},
            "unit": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "Temperature unit"
            }
        },
        "required": ["location"]
    });
    let calculator_schema = json!({
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate"
            }
        },
        "required": ["expression"]
    });
    

    Każda definicja narzędzia zawiera element name, description który pomaga modelowi zdecydować, kiedy go używać, oraz schemat opisujący parameters oczekiwane dane wejściowe.

  3. Dodaj funkcje Rust, które implementują każde narzędzie:

    let weather_schema = json!({
        "type": "object",
        "properties": {
            "location": {"type": "string", "description": "The city or location"},
            "unit": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "Temperature unit"
            }
        },
        "required": ["location"]
    });
    let calculator_schema = json!({
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate"
            }
        },
        "required": ["expression"]
    });
    

    Model nie uruchamia tych funkcji bezpośrednio. Zwraca żądanie wywołania narzędzia z nazwą funkcji i argumentami, a kod wykonuje funkcję.

Wysyłanie komunikatu wyzwalającego użycie narzędzia

Zainicjuj lokalny zestaw SDK usługi Foundry, załaduj model i wyślij komunikat, na który model może odpowiedzieć, wywołując narzędzie.

let manager = FoundryLocalManager::create(FoundryLocalConfig::new("foundry_local_samples"))?;

manager
    .download_and_register_eps_with_progress(None, {
        let mut current_ep = String::new();
        move |ep_name: &str, percent: f64| {
            if ep_name != current_ep {
                if !current_ep.is_empty() {
                    println!();
                }
                current_ep = ep_name.to_string();
            }
            print!("\r  {ep_name:<30}  {percent:5.1}%");
            io::stdout().flush().ok();
        }
    })
    .await?;
println!();

let model = manager.catalog().get_model("qwen2.5-0.5b").await?;
if !model.is_cached().await? {
    println!("Downloading model...");
    model
        .download(Some(|progress: f64| {
            print!("\r  {progress:.1}%");
            io::stdout().flush().ok();
        }))
        .await?;
    println!();
}

model.load().await?;
let session = ChatSession::new(&model).await?;
session
    .set_options(RequestOptions {
        search: SearchOptions {
            temperature: Some(0.7),
            max_output_tokens: Some(512),
            ..Default::default()
        },
        tool_choice: Some(ToolChoice::Auto),
        ..Default::default()
    })
    .await?;
session
    .add_tool_definition(
        ToolDefinition::new("get_weather", weather_schema.to_string())
            .with_description("Get the current weather for a location."),
    )
    .await?;
session
    .add_tool_definition(
        ToolDefinition::new("calculate", calculator_schema.to_string())
            .with_description("Perform a math calculation."),
    )
    .await?;

Gdy model ustali, że potrzebne jest narzędzie, odpowiedź zawiera tool_calls zamiast zwykłego komunikatu tekstowego. W następnym kroku pokazano, jak wykrywać i obsługiwać te wywołania.

Wykonywanie narzędzia i zwracanie wyników

Gdy model odpowie za pomocą wywołania narzędzia, wyodrębnisz nazwę i argumenty funkcji, uruchom funkcję i wyślij wynik z powrotem.

println!("\nTool-calling assistant ready! Type 'quit' to exit.\n");
let stdin = io::stdin();
loop {
    print!("You: ");
    io::stdout().flush()?;

    let mut input = String::new();
    stdin.lock().read_line(&mut input)?;
    let input = input.trim();
    if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
        break;
    }
    if input.is_empty() {
        continue;
    }

    let mut items = Vec::new();
    if session.turn_count() == 0 {
        items.push(Item::system_message(vec![Item::text(
            "You are a helpful assistant with access to tools. Use them when needed.",
        )]));
    }
    items.push(Item::user_message(vec![Item::text(input)]));

    loop {
        let response = session
            .process_request(Request::from_items(items).with_options(RequestOptions {
                search: SearchOptions {
                    temperature: Some(0.7),
                    max_output_tokens: Some(512),
                    ..Default::default()
                },
                tool_choice: Some(ToolChoice::Auto),
                ..Default::default()
            }))
            .await?;

        let mut tool_results = Vec::new();
        let mut answer = String::new();
        for item in &response.items {
            if let Some(call) = item.as_tool_call() {
                let arguments: Value =
                    serde_json::from_str(&call.arguments).unwrap_or_else(|_| json!({}));
                println!("  Tool call: {}({arguments})", call.name);
                let result = execute_tool(&call.name, &arguments);
                tool_results.push(Item::tool_result(call.call_id.clone(), result.to_string()));
            } else if let Some(message) = item.as_message() {
                answer.push_str(&message.text());
            } else if let Some(text) = item.as_text() {
                answer.push_str(text);
            }
        }

        if tool_results.is_empty() {
            println!("Assistant: {answer}\n");
            break;
        }
        items = tool_results;
    }
}

drop(session);
model.unload().await?;
println!("Model unloaded. Goodbye!");

Kluczowe kroki w pętli wywoływania narzędzi to:

  1. Wykrywanie wywołań narzędzi — sprawdź response.choices[0].message.tool_calls.
  2. Wykonaj funkcję — przeanalizuj argumenty i wywołaj funkcję lokalną.
  3. Zwróć wynik — dodaj komunikat z rolą tool i zgodnym identyfikatorem wywołania narzędzia.
  4. Uzyskaj ostateczną odpowiedź — model używa wyniku narzędzia do generowania naturalnej odpowiedzi.

Obsługa pełnej pętli wywołań narzędzi

Oto kompletna aplikacja, która łączy definicje narzędzi, inicjowanie zestawu SDK i pętlę wywoływania narzędzia w jeden plik możliwy do uruchomienia.

Zastąp zawartość src/main.rs następującym kompletnym kodem:

use foundry_local_sdk::{
    ChatSession, FoundryLocalConfig, FoundryLocalManager, Item, Request, RequestOptions,
    SearchOptions, ToolChoice, ToolDefinition,
};
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

fn execute_tool(name: &str, arguments: &Value) -> Value {
    match name {
        "get_weather" => {
            let location = arguments["location"].as_str().unwrap_or("unknown");
            let unit = arguments["unit"].as_str().unwrap_or("celsius");
            let temp = if unit == "celsius" { 22 } else { 72 };
            json!({
                "location": location,
                "temperature": temp,
                "unit": unit,
                "condition": "Sunny"
            })
        }
        "calculate" => {
            let expression = arguments["expression"].as_str().unwrap_or("");
            let is_valid = expression
                .chars()
                .all(|character| "0123456789+-*/(). ".contains(character));
            if !is_valid {
                return json!({"error": "Invalid expression"});
            }
            match eval_expression(expression) {
                Ok(result) => json!({"expression": expression, "result": result}),
                Err(error) => json!({"error": error}),
            }
        }
        _ => json!({"error": format!("Unknown function: {name}")}),
    }
}

fn eval_expression(expr: &str) -> Result<f64, String> {
    let expr = expr.replace(' ', "");
    let chars: Vec<char> = expr.chars().collect();
    let mut pos = 0;
    let result = parse_add(&chars, &mut pos)?;
    if pos < chars.len() {
        return Err("Unexpected character".to_string());
    }
    Ok(result)
}

fn parse_add(chars: &[char], pos: &mut usize) -> Result<f64, String> {
    let mut result = parse_mul(chars, pos)?;
    while *pos < chars.len() && (chars[*pos] == '+' || chars[*pos] == '-') {
        let op = chars[*pos];
        *pos += 1;
        let right = parse_mul(chars, pos)?;
        result = if op == '+' {
            result + right
        } else {
            result - right
        };
    }
    Ok(result)
}

fn parse_mul(chars: &[char], pos: &mut usize) -> Result<f64, String> {
    let mut result = parse_atom(chars, pos)?;
    while *pos < chars.len() && (chars[*pos] == '*' || chars[*pos] == '/') {
        let op = chars[*pos];
        *pos += 1;
        let right = parse_atom(chars, pos)?;
        result = if op == '*' {
            result * right
        } else {
            result / right
        };
    }
    Ok(result)
}

fn parse_atom(chars: &[char], pos: &mut usize) -> Result<f64, String> {
    if *pos < chars.len() && chars[*pos] == '(' {
        *pos += 1;
        let result = parse_add(chars, pos)?;
        if *pos < chars.len() && chars[*pos] == ')' {
            *pos += 1;
        }
        return Ok(result);
    }
    let start = *pos;
    while *pos < chars.len() && (chars[*pos].is_ascii_digit() || chars[*pos] == '.') {
        *pos += 1;
    }
    if start == *pos {
        return Err("Expected number".to_string());
    }
    chars[start..*pos]
        .iter()
        .collect::<String>()
        .parse::<f64>()
        .map_err(|error| error.to_string())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let weather_schema = json!({
        "type": "object",
        "properties": {
            "location": {"type": "string", "description": "The city or location"},
            "unit": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "Temperature unit"
            }
        },
        "required": ["location"]
    });
    let calculator_schema = json!({
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "The math expression to evaluate"
            }
        },
        "required": ["expression"]
    });

    let manager = FoundryLocalManager::create(FoundryLocalConfig::new("foundry_local_samples"))?;

    manager
        .download_and_register_eps_with_progress(None, {
            let mut current_ep = String::new();
            move |ep_name: &str, percent: f64| {
                if ep_name != current_ep {
                    if !current_ep.is_empty() {
                        println!();
                    }
                    current_ep = ep_name.to_string();
                }
                print!("\r  {ep_name:<30}  {percent:5.1}%");
                io::stdout().flush().ok();
            }
        })
        .await?;
    println!();

    let model = manager.catalog().get_model("qwen2.5-0.5b").await?;
    if !model.is_cached().await? {
        println!("Downloading model...");
        model
            .download(Some(|progress: f64| {
                print!("\r  {progress:.1}%");
                io::stdout().flush().ok();
            }))
            .await?;
        println!();
    }

    model.load().await?;
    let session = ChatSession::new(&model).await?;
    session
        .set_options(RequestOptions {
            search: SearchOptions {
                temperature: Some(0.7),
                max_output_tokens: Some(512),
                ..Default::default()
            },
            tool_choice: Some(ToolChoice::Auto),
            ..Default::default()
        })
        .await?;
    session
        .add_tool_definition(
            ToolDefinition::new("get_weather", weather_schema.to_string())
                .with_description("Get the current weather for a location."),
        )
        .await?;
    session
        .add_tool_definition(
            ToolDefinition::new("calculate", calculator_schema.to_string())
                .with_description("Perform a math calculation."),
        )
        .await?;

    println!("\nTool-calling assistant ready! Type 'quit' to exit.\n");
    let stdin = io::stdin();
    loop {
        print!("You: ");
        io::stdout().flush()?;

        let mut input = String::new();
        stdin.lock().read_line(&mut input)?;
        let input = input.trim();
        if input.eq_ignore_ascii_case("quit") || input.eq_ignore_ascii_case("exit") {
            break;
        }
        if input.is_empty() {
            continue;
        }

        let mut items = Vec::new();
        if session.turn_count() == 0 {
            items.push(Item::system_message(vec![Item::text(
                "You are a helpful assistant with access to tools. Use them when needed.",
            )]));
        }
        items.push(Item::user_message(vec![Item::text(input)]));

        loop {
            let response = session
                .process_request(Request::from_items(items).with_options(RequestOptions {
                    search: SearchOptions {
                        temperature: Some(0.7),
                        max_output_tokens: Some(512),
                        ..Default::default()
                    },
                    tool_choice: Some(ToolChoice::Auto),
                    ..Default::default()
                }))
                .await?;

            let mut tool_results = Vec::new();
            let mut answer = String::new();
            for item in &response.items {
                if let Some(call) = item.as_tool_call() {
                    let arguments: Value =
                        serde_json::from_str(&call.arguments).unwrap_or_else(|_| json!({}));
                    println!("  Tool call: {}({arguments})", call.name);
                    let result = execute_tool(&call.name, &arguments);
                    tool_results.push(Item::tool_result(call.call_id.clone(), result.to_string()));
                } else if let Some(message) = item.as_message() {
                    answer.push_str(&message.text());
                } else if let Some(text) = item.as_text() {
                    answer.push_str(text);
                }
            }

            if tool_results.is_empty() {
                println!("Assistant: {answer}\n");
                break;
            }
            items = tool_results;
        }
    }

    drop(session);
    model.unload().await?;
    println!("Model unloaded. Goodbye!");

    Ok(())
}

Uruchom asystenta wywoływania narzędzi:

cargo run

Zobaczysz dane wyjściowe podobne do:

Downloading model: 100.00%
Model loaded and ready.

Tool-calling assistant ready! Type 'quit' to exit.

You: What's the weather like today?
  Tool call: get_weather({"location":"current location"})
Assistant: The weather today is sunny with a temperature of 22°C.

You: What is 245 * 38?
  Tool call: calculate({"expression":"245 * 38"})
Assistant: 245 multiplied by 38 equals 9,310.

You: quit
Model unloaded. Goodbye!

Model decyduje, kiedy wywołać narzędzie na podstawie komunikatu użytkownika. W przypadku pytania o pogodę wywołuje get_weathermetodę , w przypadku matematyki wywołuje calculatemetodę , a w przypadku pytań ogólnych odpowiada bezpośrednio bez żadnych wywołań narzędzi.

Repozytorium przykładów

Sklonuj repozytorium GitHub z przykładami Foundry i przejdź do przykładu języka C++:

git clone https://github.com/microsoft-foundry/foundry-samples.git
cd foundry-samples/samples/cpp/foundry-local/tutorial-tool-calling-assistant

Instalowanie pakietów

Przykłady języka C++ wymagają:

Skonfiguruj i skompiluj przykład:

cmake -S . -B build
cmake --build build --config Release

Podczas konfiguracji przykład przywraca pakiet NuGet Microsoft.AI.Foundry.Local.Runtime w wersji 2.2.1 do build/_packages. Pakiet zawiera nagłówki języka C++ i biblioteki natywne dla Windows, Linux i macOS. Nie potrzebujesz oddzielnego pakietu WinML.

Konfiguracja narzędzia CMake wybiera zasoby środowiska uruchomieniowego dla systemu operacyjnego i architektury oraz kopiuje wymagane biblioteki natywne obok pliku wykonywalnego. W systemach macOS i Linux uruchom plik wykonywalny z build. W systemie Windows uruchom plik .exe z build.

Definiowanie narzędzi

Zdefiniuj dostępne narzędzia i funkcje lokalne, które je implementują:

constexpr const char* kTimeSchema = R"({
  "type": "object",
  "properties": {
    "timezone": {"type": "string", "description": "Requested timezone label"}
  },
  "required": ["timezone"]
})";

std::string GetLocalTime() {
  const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
  std::tm local_time{};
#ifdef _WIN32
  localtime_s(&local_time, &now);
#else
  localtime_r(&now, &local_time);
#endif
  std::ostringstream output;
  output << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S");
  return R"({"timezone":"local","time":")" + output.str() + R"("})";
}

Wysyłanie komunikatu wyzwalającego użycie narzędzia

Zainicjuj SDK, załaduj model i wyślij żądanie, które może używać narzędzia:

Configuration config("foundry_local_cpp_tool_assistant");
Manager manager(std::move(config));
auto model = manager.GetCatalog().GetModel("qwen2.5-0.5b");
if (!model) {
  throw std::runtime_error("Model 'qwen2.5-0.5b' was not found in the catalog.");
}
if (!model->IsCached()) {
  model->Download([](float progress) {
    std::cout << "\rDownloading model: " << static_cast<int>(progress) << "%" << std::flush;
    return 0;
  });
  std::cout << "\n";
}
model->Load();

Wykonywanie narzędzia i zwracanie wyników

Wykryj wywołanie narzędzia, wykonaj funkcję lokalną, zwróć jej wynik i poproś model o ostateczną odpowiedź:

Request request{
    SystemMessage("Use the available tool when current time is requested."),
    UserMessage("What time is it?"),
};
request.SetOptions({
    .search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
    .tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_REQUIRED,
});
Response response = session.ProcessRequest(request);

for (int iteration = 0; iteration < 8; ++iteration) {
  bool called_tool = false;
  for (const Item& item : response.GetItems()) {
    if (item.GetType() == FOUNDRY_LOCAL_ITEM_MESSAGE) {
      std::cout << "Assistant: ";
      PrintAssistant(item);
      continue;
    }
    if (item.GetType() != FOUNDRY_LOCAL_ITEM_TOOL_CALL) {
      continue;
    }

    const auto call = item.GetToolCall();
    if (call.name != "get_current_time") {
      throw std::runtime_error("The model requested an unknown tool.");
    }
    Request tool_result;
    tool_result.AddItem(Item::ToolResult(std::string(call.call_id), GetLocalTime()));
    tool_result.SetOptions({
        .search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
        .tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_NONE,
    });
    response = session.ProcessRequest(tool_result);
    called_tool = true;
    break;
  }
  if (!called_tool) {
    break;
  }
}

Kompletny kod

#include <foundry_local/foundry_local_cpp.h>

#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

using namespace foundry_local;

constexpr const char* kTimeSchema = R"({
  "type": "object",
  "properties": {
    "timezone": {"type": "string", "description": "Requested timezone label"}
  },
  "required": ["timezone"]
})";

std::string GetLocalTime() {
  const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
  std::tm local_time{};
#ifdef _WIN32
  localtime_s(&local_time, &now);
#else
  localtime_r(&now, &local_time);
#endif
  std::ostringstream output;
  output << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S");
  return R"({"timezone":"local","time":")" + output.str() + R"("})";
}

void PrintAssistant(const Item& message) {
  for (const Item& part : message.GetMessage().parts) {
    if (part.GetType() == FOUNDRY_LOCAL_ITEM_TEXT) {
      std::cout << part.GetText().text;
    }
  }
  std::cout << "\n";
}

int main() {
  try {
    Configuration config("foundry_local_cpp_tool_assistant");
    Manager manager(std::move(config));
    auto model = manager.GetCatalog().GetModel("qwen2.5-0.5b");
    if (!model) {
      throw std::runtime_error("Model 'qwen2.5-0.5b' was not found in the catalog.");
    }
    if (!model->IsCached()) {
      model->Download([](float progress) {
        std::cout << "\rDownloading model: " << static_cast<int>(progress) << "%" << std::flush;
        return 0;
      });
      std::cout << "\n";
    }
    model->Load();

    {
    ChatSession session(*model);
    session.AddToolDefinition(ToolDefinition{
        "get_current_time",
        "Get the current local date and time.",
        kTimeSchema,
    });

    Request request{
        SystemMessage("Use the available tool when current time is requested."),
        UserMessage("What time is it?"),
    };
    request.SetOptions({
        .search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
        .tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_REQUIRED,
    });
    Response response = session.ProcessRequest(request);

    for (int iteration = 0; iteration < 8; ++iteration) {
      bool called_tool = false;
      for (const Item& item : response.GetItems()) {
        if (item.GetType() == FOUNDRY_LOCAL_ITEM_MESSAGE) {
          std::cout << "Assistant: ";
          PrintAssistant(item);
          continue;
        }
        if (item.GetType() != FOUNDRY_LOCAL_ITEM_TOOL_CALL) {
          continue;
        }

        const auto call = item.GetToolCall();
        if (call.name != "get_current_time") {
          throw std::runtime_error("The model requested an unknown tool.");
        }
        Request tool_result;
        tool_result.AddItem(Item::ToolResult(std::string(call.call_id), GetLocalTime()));
        tool_result.SetOptions({
            .search = {.temperature = 0.0F, .max_output_tokens = 256, .seed = 42},
            .tool_choice = FOUNDRY_LOCAL_TOOL_CHOICE_NONE,
        });
        response = session.ProcessRequest(tool_result);
        called_tool = true;
        break;
      }
      if (!called_tool) {
        break;
      }
    }
    }

    model->Unload();
    manager.Shutdown();
  } catch (const Error& error) {
    std::cerr << "Foundry Local error [" << error.Code() << "]: " << error.what() << "\n";
    return 1;
  } catch (const std::exception& error) {
    std::cerr << "Error: " << error.what() << "\n";
    return 1;
  }

  return 0;
}

Uruchom asystenta:

./build/tutorial-tool-calling-assistant

Uprzątnij zasoby

Wagi modelu są przechowywane w Twojej lokalnej pamięci podręcznej po rozładowaniu modelu. Oznacza to, że przy następnym uruchomieniu aplikacji krok pobierania zostanie pominięty, a model ładuje się szybciej. Nie jest wymagane żadne dodatkowe czyszczenie, chyba że chcesz odzyskać miejsce na dysku.