Руководство. Создание помощника по искусственному интеллекту с помощью вызова инструментов

Создайте помощник по искусственному интеллекту, который выходит за рамки беседы— он может вызывать функции для выполнения действий. Помощник решает, когда требуется функция, выполняете ее и передаете результат обратно. Все выполняется локально с помощью локального пакета SDK для Foundry.

В этом руководстве вы узнаете, как:

  • Настройка проекта и установка локального SDK Foundry
  • Определение инструментов, которые помощник может использовать
  • Отправка сообщения, которое активирует использование средства
  • Выполните инструмент и передайте результаты модели
  • Обработка полного цикла вызова инструмента
  • Очистите ресурсы

Необходимые условия

  • Компьютер Windows, macOS или Linux с не менее 8 ГБ ОЗУ.

Репозиторий примеров

Полный пример кода для этой статьи можно найти в репозитории Foundry samples на GitHub. Чтобы клонировать репозиторий и перейти к примеру, используйте следующую команду:

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

Установка пакетов

Если вы разрабатываете или поставляете на Windows, выберите вкладку Windows. Пакет Windows интегрируется со средой выполнения Windows ML — обеспечивает тот же интерфейс API с более широким спектром аппаратного ускорения.

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

Примеры C# в репозитории GitHub предварительно настроены. Если вы создаете с нуля, ознакомьтесь со справочником по локальному пакету SDK Foundry для получения дополнительных сведений о настройке проекта C# с помощью Foundry Local.

Определение средств

Вызов функции позволяет модели запросить выполнение вашего кода и возврат результата. Доступные средства определяются как список схем JSON, описывающих имя, назначение и параметры каждой функции.

  1. Откройте Program.cs и добавьте следующие определения инструментов:

    // --- Tool definitions ---
    List<ToolDefinition> tools =
    [
        new ToolDefinition
        {
            Type = "function",
            Function = new FunctionDefinition()
            {
                Name = "get_weather",
                Description = "Get the current weather for a location",
                Parameters = new PropertyDefinition()
                {
                    Type = "object",
                    Properties = new Dictionary<string, PropertyDefinition>()
                    {
                        { "location", new PropertyDefinition() { Type = "string", Description = "The city or location" } },
                        { "unit", new PropertyDefinition() { Type = "string", Description = "Temperature unit (celsius or fahrenheit)" } }
                    },
                    Required = ["location"]
                }
            }
        },
        new ToolDefinition
        {
            Type = "function",
            Function = new FunctionDefinition()
            {
                Name = "calculate",
                Description = "Perform a math calculation",
                Parameters = new PropertyDefinition()
                {
                    Type = "object",
                    Properties = new Dictionary<string, PropertyDefinition>()
                    {
                        { "expression", new PropertyDefinition() { Type = "string", Description = "The math expression to evaluate" } }
                    },
                    Required = ["expression"]
                }
            }
        }
    ];
    
    // --- Tool implementations ---
    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}"
                });
        }
    }
    

    Каждое определение инструмента включает в name себя, description что помогает модели решать, когда его использовать, и parameters схему, описывающую ожидаемые входные данные.

  2. Добавьте методы C#, реализующие каждое средство:

    // --- Tool definitions ---
    List<ToolDefinition> tools =
    [
        new ToolDefinition
        {
            Type = "function",
            Function = new FunctionDefinition()
            {
                Name = "get_weather",
                Description = "Get the current weather for a location",
                Parameters = new PropertyDefinition()
                {
                    Type = "object",
                    Properties = new Dictionary<string, PropertyDefinition>()
                    {
                        { "location", new PropertyDefinition() { Type = "string", Description = "The city or location" } },
                        { "unit", new PropertyDefinition() { Type = "string", Description = "Temperature unit (celsius or fahrenheit)" } }
                    },
                    Required = ["location"]
                }
            }
        },
        new ToolDefinition
        {
            Type = "function",
            Function = new FunctionDefinition()
            {
                Name = "calculate",
                Description = "Perform a math calculation",
                Parameters = new PropertyDefinition()
                {
                    Type = "object",
                    Properties = new Dictionary<string, PropertyDefinition>()
                    {
                        { "expression", new PropertyDefinition() { Type = "string", Description = "The math expression to evaluate" } }
                    },
                    Required = ["expression"]
                }
            }
        }
    ];
    
    // --- Tool implementations ---
    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}"
                });
        }
    }
    

    Модель не выполняет эти функции напрямую. Он возвращает запрос вызова средства с именем и аргументами функции, а код выполняет функцию.

Отправка сообщения, которое активирует использование средства

Инициализировать локальный пакет SDK Foundry, загрузить модель и отправить сообщение, на которое модель может ответить, вызвав средство.

// --- 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;

// Download and register all execution providers.
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.");

var chatClient = await model.GetChatClientAsync();
chatClient.Settings.ToolChoice = ToolChoice.Auto;

var messages = new List<ChatMessage>
{
    new ChatMessage
    {
        Role = "system",
        Content = "You are a helpful assistant with access to tools. " +
                  "Use them when needed to answer questions accurately."
    }
};

Когда модель определяет, что требуется средство, ответ содержит ToolCalls вместо обычного текстового сообщения. На следующем шаге показано, как обнаруживать и обрабатывать эти вызовы.

Запустите средство и отобразите результаты

После того как модель отвечает вызовом средства, вы извлекаете имя и аргументы функции, запускаете функцию и отправляете результат обратно.

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;
    }

    messages.Add(new ChatMessage
    {
        Role = "user",
        Content = userInput
    });

    var response = await chatClient.CompleteChatAsync(
        messages, tools, ct
    );

    var choice = response.Choices[0].Message;

    if (choice.ToolCalls is { Count: > 0 })
    {
        messages.Add(choice);

        foreach (var toolCall in choice.ToolCalls)
        {
            var toolArgs = JsonDocument.Parse(
                toolCall.FunctionCall.Arguments
            ).RootElement;
            Console.WriteLine(
                $"  Tool call: {toolCall.FunctionCall.Name}({toolArgs})"
            );

            var result = ExecuteTool(
                toolCall.FunctionCall.Name, toolArgs
            );
            messages.Add(new ChatMessage
            {
                Role = "tool",
                ToolCallId = toolCall.Id,
                Content = result
            });
        }

        var finalResponse = await chatClient.CompleteChatAsync(
            messages, tools, ct
        );
        var answer = finalResponse.Choices[0].Message.Content ?? "";
        messages.Add(new ChatMessage
        {
            Role = "assistant",
            Content = answer
        });
        Console.WriteLine($"Assistant: {answer}\n");
    }
    else
    {
        var answer = choice.Content ?? "";
        messages.Add(new ChatMessage
        {
            Role = "assistant",
            Content = answer
        });
        Console.WriteLine($"Assistant: {answer}\n");
    }
}

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

Основные шаги в цикле вызова средства:

  1. Обнаружение вызовов инструментов — проверка response.Choices[0].Message.ToolCalls.
  2. Выполните функцию — синтаксический анализ аргументов и вызов локальной функции.
  3. Верните результат — добавьте сообщение с ролью tool и соответствием ToolCallId.
  4. Получение окончательного ответа — модель использует результат средства для создания естественного ответа.

Обработка полного цикла вызова инструмента

Ниже приведено полное приложение, которое объединяет определения инструментов, инициализацию пакета SDK и цикл вызова средства в один исполняемый файл.

Замените содержимое Program.cs следующим полным кодом:

using System.Text.Json;
using Microsoft.AI.Foundry.Local;
using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels;
using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels;
using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels;
using Microsoft.Extensions.Logging;

CancellationToken ct = CancellationToken.None;

// --- Tool definitions ---
List<ToolDefinition> tools =
[
    new ToolDefinition
    {
        Type = "function",
        Function = new FunctionDefinition()
        {
            Name = "get_weather",
            Description = "Get the current weather for a location",
            Parameters = new PropertyDefinition()
            {
                Type = "object",
                Properties = new Dictionary<string, PropertyDefinition>()
                {
                    { "location", new PropertyDefinition() { Type = "string", Description = "The city or location" } },
                    { "unit", new PropertyDefinition() { Type = "string", Description = "Temperature unit (celsius or fahrenheit)" } }
                },
                Required = ["location"]
            }
        }
    },
    new ToolDefinition
    {
        Type = "function",
        Function = new FunctionDefinition()
        {
            Name = "calculate",
            Description = "Perform a math calculation",
            Parameters = new PropertyDefinition()
            {
                Type = "object",
                Properties = new Dictionary<string, PropertyDefinition>()
                {
                    { "expression", new PropertyDefinition() { Type = "string", Description = "The math expression to evaluate" } }
                },
                Required = ["expression"]
            }
        }
    }
];

// --- Tool implementations ---
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;

// Download and register all execution providers.
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.");

var chatClient = await model.GetChatClientAsync();
chatClient.Settings.ToolChoice = ToolChoice.Auto;

var messages = new List<ChatMessage>
{
    new ChatMessage
    {
        Role = "system",
        Content = "You are a helpful assistant with access to tools. " +
                  "Use them when needed to answer questions accurately."
    }
};

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;
    }

    messages.Add(new ChatMessage
    {
        Role = "user",
        Content = userInput
    });

    var response = await chatClient.CompleteChatAsync(
        messages, tools, ct
    );

    var choice = response.Choices[0].Message;

    if (choice.ToolCalls is { Count: > 0 })
    {
        messages.Add(choice);

        foreach (var toolCall in choice.ToolCalls)
        {
            var toolArgs = JsonDocument.Parse(
                toolCall.FunctionCall.Arguments
            ).RootElement;
            Console.WriteLine(
                $"  Tool call: {toolCall.FunctionCall.Name}({toolArgs})"
            );

            var result = ExecuteTool(
                toolCall.FunctionCall.Name, toolArgs
            );
            messages.Add(new ChatMessage
            {
                Role = "tool",
                ToolCallId = toolCall.Id,
                Content = result
            });
        }

        var finalResponse = await chatClient.CompleteChatAsync(
            messages, tools, ct
        );
        var answer = finalResponse.Choices[0].Message.Content ?? "";
        messages.Add(new ChatMessage
        {
            Role = "assistant",
            Content = answer
        });
        Console.WriteLine($"Assistant: {answer}\n");
    }
    else
    {
        var answer = choice.Content ?? "";
        messages.Add(new ChatMessage
        {
            Role = "assistant",
            Content = answer
        });
        Console.WriteLine($"Assistant: {answer}\n");
    }
}

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

Запустите помощник по вызову инструментов:

dotnet run

Выходные данные похожи на следующие:

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!

Модель решает, когда следует вызывать средство на основе сообщения пользователя. Для вопроса о погоде он вызывает get_weather, для математики он вызывает calculate, и для общих вопросов он отвечает напрямую без вызовов инструментов.

Репозиторий примеров

Полный исходный код примера для этой статьи доступен в репозитории GitHub foundry-samples. Чтобы клонировать репозиторий и перейти к образцу, используйте:

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

Установка пакетов

Если вы разрабатываете или поставляете на Windows, выберите вкладку Windows. Пакет Windows интегрируется со средой выполнения Windows ML — обеспечивает тот же интерфейс API с более широким спектром аппаратного ускорения.

npm install foundry-local-sdk-winml openai

Определение средств

Вызов функции позволяет модели запросить выполнение вашего кода и возврат результата. Доступные средства определяются как список схем JSON, описывающих имя, назначение и параметры каждой функции.

  1. Создайте файл с именем index.js.

  2. Добавьте следующие определения инструментов:

    // --- Tool definitions ---
    const tools = [
        {
            type: 'function',
            function: {
                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']
                }
            }
        },
        {
            type: 'function',
            function: {
                name: 'calculate',
                description: 'Perform a math calculation',
                parameters: {
                    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)
    };
    

    Каждое определение инструмента включает в name себя, description что помогает модели решать, когда его использовать, и parameters схему, описывающую ожидаемые входные данные.

  3. Добавьте функции JavaScript, реализующие каждое средство:

    // --- Tool definitions ---
    const tools = [
        {
            type: 'function',
            function: {
                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']
                }
            }
        },
        {
            type: 'function',
            function: {
                name: 'calculate',
                description: 'Perform a math calculation',
                parameters: {
                    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)
    };
    

    Модель не выполняет эти функции напрямую. Он возвращает запрос вызова средства с именем и аргументами функции, а код выполняет функцию.

Отправка сообщения, которое активирует использование средства

Инициализировать локальный пакет SDK Foundry, загрузить модель и отправить сообщение, на которое модель может ответить, вызвав средство.

// --- 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 chatClient = model.createChatClient();

const messages = [
    {
        role: 'system',
        content:
            'You are a helpful assistant with access to tools. ' +
            'Use them when needed to answer questions accurately.'
    }
];

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;
    }

    messages.push({ role: 'user', content: userInput });

    const response = await chatClient.completeChat(
        messages, tools
    );
    const answer = await processToolCalls(
        messages, response, chatClient
    );

    messages.push({ role: 'assistant', content: answer });
    console.log(`Assistant: ${answer}\n`);
}

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

Когда модель определяет, что требуется средство, ответ содержит tool_calls вместо обычного текстового сообщения. На следующем шаге показано, как обнаруживать и обрабатывать эти вызовы.

Запустите средство и отобразите результаты

После того как модель отвечает вызовом средства, вы извлекаете имя и аргументы функции, запускаете функцию и отправляете результат обратно.

async function processToolCalls(messages, response, chatClient) {
    let choice = response.choices[0]?.message;

    while (choice?.tool_calls?.length > 0) {
        messages.push(choice);

        for (const toolCall of choice.tool_calls) {
            const functionName = toolCall.function.name;
            const args = JSON.parse(toolCall.function.arguments);
            console.log(
                `  Tool call: ${functionName}` +
                `(${JSON.stringify(args)})`
            );

            const result = toolFunctions[functionName](args);
            messages.push({
                role: 'tool',
                tool_call_id: toolCall.id,
                content: JSON.stringify(result)
            });
        }

        response = await chatClient.completeChat(
            messages, tools
        );
        choice = response.choices[0]?.message;
    }

    return choice?.content ?? '';
}

Основные шаги в цикле вызова средства:

  1. Обнаружение вызовов инструментов — проверка response.choices[0]?.message?.tool_calls.
  2. Выполните функцию — синтаксический анализ аргументов и вызов локальной функции.
  3. Верните результат — добавьте сообщение с ролью tool и соответствием tool_call_id.
  4. Получение окончательного ответа — модель использует результат средства для создания естественного ответа.

Обработка полного цикла вызова инструмента

Ниже приведено полное приложение, которое объединяет определения инструментов, инициализацию пакета SDK и цикл вызова средства в один исполняемый файл.

Создайте файл с именем index.js и добавьте следующий полный код:

import { FoundryLocalManager } from 'foundry-local-sdk';
import * as readline from 'readline';

// --- Tool definitions ---
const tools = [
    {
        type: 'function',
        function: {
            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']
            }
        }
    },
    {
        type: 'function',
        function: {
            name: 'calculate',
            description: 'Perform a math calculation',
            parameters: {
                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(messages, response, chatClient) {
    let choice = response.choices[0]?.message;

    while (choice?.tool_calls?.length > 0) {
        messages.push(choice);

        for (const toolCall of choice.tool_calls) {
            const functionName = toolCall.function.name;
            const args = JSON.parse(toolCall.function.arguments);
            console.log(
                `  Tool call: ${functionName}` +
                `(${JSON.stringify(args)})`
            );

            const result = toolFunctions[functionName](args);
            messages.push({
                role: 'tool',
                tool_call_id: toolCall.id,
                content: JSON.stringify(result)
            });
        }

        response = await chatClient.completeChat(
            messages, tools
        );
        choice = response.choices[0]?.message;
    }

    return choice?.content ?? '';
}

// --- 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 chatClient = model.createChatClient();

const messages = [
    {
        role: 'system',
        content:
            'You are a helpful assistant with access to tools. ' +
            'Use them when needed to answer questions accurately.'
    }
];

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;
    }

    messages.push({ role: 'user', content: userInput });

    const response = await chatClient.completeChat(
        messages, tools
    );
    const answer = await processToolCalls(
        messages, response, chatClient
    );

    messages.push({ role: 'assistant', content: answer });
    console.log(`Assistant: ${answer}\n`);
}

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

Запустите помощник по вызову инструментов:

node index.js

Выходные данные похожи на следующие:

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!

Модель решает, когда следует вызывать средство на основе сообщения пользователя. Для вопроса о погоде он вызывает get_weather, для математики он вызывает calculate, и для общих вопросов он отвечает напрямую без вызовов инструментов.

Репозиторий примеров

Полный исходный код примера для этой статьи доступен в репозитории GitHub foundry-samples. Чтобы клонировать репозиторий и перейти к образцу, используйте:

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

Установка пакетов

Если вы разрабатываете или поставляете на Windows, выберите вкладку Windows. Пакет Windows интегрируется со средой выполнения Windows ML — обеспечивает тот же интерфейс API с более широким спектром аппаратного ускорения.

pip install foundry-local-sdk-winml openai

Определение средств

Вызов функции позволяет модели запросить выполнение вашего кода и возврат результата. Доступные средства определяются как список схем JSON, описывающих имя, назначение и параметры каждой функции.

Создайте файл с именем main.py и добавьте следующие определения инструментов:

# --- Tool definitions ---
tools = [
    {
        "type": "function",
        "function": {
            "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"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "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(c in allowed for c in expression):
        return {"error": "Invalid expression"}
    try:
        result = eval(expression)
        return {"expression": expression, "result": result}
    except Exception as e:
        return {"error": str(e)}


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

Каждое определение инструмента включает в name себя, description что помогает модели решать, когда его использовать, и parameters схему, описывающую ожидаемые входные данные.

Затем добавьте функции Python, реализующие каждое средство:

# --- Tool definitions ---
tools = [
    {
        "type": "function",
        "function": {
            "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"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "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(c in allowed for c in expression):
        return {"error": "Invalid expression"}
    try:
        result = eval(expression)
        return {"expression": expression, "result": result}
    except Exception as e:
        return {"error": str(e)}


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

Модель не выполняет эти функции напрямую. Он возвращает запрос вызова средства с именем и аргументами функции, а код выполняет функцию.

Отправка сообщения, которое активирует использование средства

Инициализировать локальный пакет SDK Foundry, загрузить модель и отправить сообщение, на которое модель может ответить, вызвав средство.

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

    # Download and register all execution providers.
    current_ep = ""

    def ep_progress(ep_name: str, percent: float):
        nonlocal current_ep
        if ep_name != current_ep:
            if current_ep:
                print()
            current_ep = ep_name
        print(f"\r  {ep_name:<30}  {percent:5.1f}%", end="", flush=True)

    manager.download_and_register_eps(progress_callback=ep_progress)
    if current_ep:
        print()

    # Select and load a model
    model = manager.catalog.get_model("qwen2.5-0.5b")
    model.download(
        lambda progress: print(
            f"\rDownloading model: {progress:.2f}%", end="", flush=True
        )
    )
    print()
    model.load()
    print("Model loaded and ready.")

    # Get a chat client
    client = model.get_chat_client()

    # Conversation with a system prompt
    messages = [
        {
            "role": "system",
            "content": "You are a helpful assistant with access to tools. "
            "Use them when needed to answer questions accurately.",
        }
    ]

    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

        messages.append({"role": "user", "content": user_input})

        response = client.complete_chat(messages, tools=tools)
        answer = process_tool_calls(messages, response, client)

        messages.append({"role": "assistant", "content": answer})
        print(f"Assistant: {answer}\n")

    # Clean up
    model.unload()
    print("Model unloaded. Goodbye!")

Когда модель определяет, что требуется средство, ответ содержит tool_calls вместо обычного текстового сообщения. На следующем шаге показано, как обнаруживать и обрабатывать эти вызовы.

Запустите средство и отобразите результаты

После того как модель отвечает вызовом средства, вы извлекаете имя и аргументы функции, запускаете функцию и отправляете результат обратно.

def process_tool_calls(messages, response, client):
    """Handle tool calls in a loop until the model produces a final answer."""
    choice = response.choices[0].message

    while choice.tool_calls:
        # Convert the assistant message to a dict for the SDK
        assistant_msg = {
            "role": "assistant",
            "content": choice.content,
            "tool_calls": [
                {
                    "id": tc.id,
                    "type": tc.type,
                    "function": {
                        "name": tc.function.name,
                        "arguments": tc.function.arguments,
                    },
                }
                for tc in choice.tool_calls
            ],
        }
        messages.append(assistant_msg)

        for tool_call in choice.tool_calls:
            function_name = tool_call.function.name
            arguments = json.loads(tool_call.function.arguments)
            print(f"  Tool call: {function_name}({arguments})")

            # Execute the function and add the result
            func = tool_functions[function_name]
            result = func(**arguments)
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(result),
                }
            )

        # Send the updated conversation back
        response = client.complete_chat(messages, tools=tools)
        choice = response.choices[0].message

    return choice.content

Основные шаги в цикле вызова средства:

  1. Обнаружение вызовов инструментов — проверка response.choices[0].message.tool_calls.
  2. Выполните функцию — синтаксический анализ аргументов и вызов локальной функции.
  3. Верните результат — добавьте сообщение с ролью tool и соответствием tool_call_id.
  4. Получение окончательного ответа — модель использует результат средства для создания естественного ответа.

Обработка полного цикла вызова инструмента

Ниже приведено полное приложение, которое объединяет определения инструментов, инициализацию пакета SDK и цикл вызова средства в один исполняемый файл.

Создайте файл с именем main.py и добавьте следующий полный код:

import json
from foundry_local_sdk import Configuration, FoundryLocalManager



# --- Tool definitions ---
tools = [
    {
        "type": "function",
        "function": {
            "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"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "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(c in allowed for c in expression):
        return {"error": "Invalid expression"}
    try:
        result = eval(expression)
        return {"expression": expression, "result": result}
    except Exception as e:
        return {"error": str(e)}


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


def process_tool_calls(messages, response, client):
    """Handle tool calls in a loop until the model produces a final answer."""
    choice = response.choices[0].message

    while choice.tool_calls:
        # Convert the assistant message to a dict for the SDK
        assistant_msg = {
            "role": "assistant",
            "content": choice.content,
            "tool_calls": [
                {
                    "id": tc.id,
                    "type": tc.type,
                    "function": {
                        "name": tc.function.name,
                        "arguments": tc.function.arguments,
                    },
                }
                for tc in choice.tool_calls
            ],
        }
        messages.append(assistant_msg)

        for tool_call in choice.tool_calls:
            function_name = tool_call.function.name
            arguments = json.loads(tool_call.function.arguments)
            print(f"  Tool call: {function_name}({arguments})")

            # Execute the function and add the result
            func = tool_functions[function_name]
            result = func(**arguments)
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(result),
                }
            )

        # Send the updated conversation back
        response = client.complete_chat(messages, tools=tools)
        choice = response.choices[0].message

    return choice.content




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

    # Download and register all execution providers.
    current_ep = ""

    def ep_progress(ep_name: str, percent: float):
        nonlocal current_ep
        if ep_name != current_ep:
            if current_ep:
                print()
            current_ep = ep_name
        print(f"\r  {ep_name:<30}  {percent:5.1f}%", end="", flush=True)

    manager.download_and_register_eps(progress_callback=ep_progress)
    if current_ep:
        print()

    # Select and load a model
    model = manager.catalog.get_model("qwen2.5-0.5b")
    model.download(
        lambda progress: print(
            f"\rDownloading model: {progress:.2f}%", end="", flush=True
        )
    )
    print()
    model.load()
    print("Model loaded and ready.")

    # Get a chat client
    client = model.get_chat_client()

    # Conversation with a system prompt
    messages = [
        {
            "role": "system",
            "content": "You are a helpful assistant with access to tools. "
            "Use them when needed to answer questions accurately.",
        }
    ]

    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

        messages.append({"role": "user", "content": user_input})

        response = client.complete_chat(messages, tools=tools)
        answer = process_tool_calls(messages, response, client)

        messages.append({"role": "assistant", "content": answer})
        print(f"Assistant: {answer}\n")

    # Clean up
    model.unload()
    print("Model unloaded. Goodbye!")




if __name__ == "__main__":
    main()

Запустите помощник по вызову инструментов:

python main.py

Выходные данные похожи на следующие:

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!

Модель решает, когда следует вызывать средство на основе сообщения пользователя. Для вопроса о погоде он вызывает get_weather, для математики он вызывает calculate, и для общих вопросов он отвечает напрямую без вызовов инструментов.

Репозиторий примеров

Полный исходный код примера для этой статьи доступен в репозитории GitHub foundry-samples. Чтобы клонировать репозиторий и перейти к образцу, используйте:

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

Установка пакетов

Если вы разрабатываете или поставляете на Windows, выберите вкладку Windows. Пакет Windows интегрируется со средой выполнения Windows ML — обеспечивает тот же интерфейс API с более широким спектром аппаратного ускорения.

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

Определение средств

Вызов функции позволяет модели запросить выполнение вашего кода и возврат результата. Доступные средства определяются как список схем JSON, описывающих имя, назначение и параметры каждой функции.

  1. serde_json Добавьте зависимость для обработки JSON:

    cargo add serde_json
    
  2. Откройте src/main.rs и добавьте следующие определения инструментов:

    // --- Tool definitions ---
    let tools: Vec<ChatCompletionTools> = serde_json::from_value(json!([
        {
            "type": "function",
            "function": {
                "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"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "calculate",
                "description": "Perform a math calculation",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description":
                                "The math expression to evaluate"
                        }
                    },
                    "required": ["expression"]
                }
            }
        }
    ]))?;
    

    Каждое определение инструмента включает в name себя, description что помогает модели решать, когда его использовать, и parameters схему, описывающую ожидаемые входные данные.

  3. Добавьте функции Rust, реализующие каждое средство:

    // --- Tool definitions ---
    let tools: Vec<ChatCompletionTools> = serde_json::from_value(json!([
        {
            "type": "function",
            "function": {
                "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"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "calculate",
                "description": "Perform a math calculation",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description":
                                "The math expression to evaluate"
                        }
                    },
                    "required": ["expression"]
                }
            }
        }
    ]))?;
    

    Модель не выполняет эти функции напрямую. Он возвращает запрос вызова средства с именем и аргументами функции, а код выполняет функцию.

Отправка сообщения, которое активирует использование средства

Инициализировать локальный пакет SDK Foundry, загрузить модель и отправить сообщение, на которое модель может ответить, вызвав средство.

// Initialize the Foundry Local SDK
let manager = FoundryLocalManager::create(
    FoundryLocalConfig::new("foundry_local_samples"),
)?;

// Download and register all execution providers.
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  {:<30}  {:5.1}%", ep_name, percent);
            io::stdout().flush().ok();
        }
    })
    .await?;
println!();

// Select and load a model
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?;
println!("Model loaded and ready.");

// Create a chat client
let client = model
    .create_chat_client()
    .temperature(0.7)
    .max_tokens(512)
    .tool_choice(ChatToolChoice::Auto);

// Conversation with a system prompt
let mut messages: Vec<ChatCompletionRequestMessage> = vec![
    ChatCompletionRequestSystemMessage::from(
        "You are a helpful assistant with access to tools. \
         Use them when needed to answer questions accurately.",
    )
    .into(),
];

Когда модель определяет, что требуется средство, ответ содержит tool_calls вместо обычного текстового сообщения. На следующем шаге показано, как обнаруживать и обрабатывать эти вызовы.

Запустите средство и отобразите результаты

После того как модель отвечает вызовом средства, вы извлекаете имя и аргументы функции, запускаете функцию и отправляете результат обратно.

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;
    }

    messages.push(
        ChatCompletionRequestUserMessage::from(input).into(),
    );

    let mut response = client
        .complete_chat(&messages, Some(&tools))
        .await?;

    // Process tool calls in a loop
    while response.choices[0].message.tool_calls.is_some() {
        let tool_calls = response.choices[0]
            .message
            .tool_calls
            .as_ref()
            .unwrap();

        // Append the assistant's tool_calls message via JSON
        let assistant_msg: ChatCompletionRequestMessage =
            serde_json::from_value(json!({
                "role": "assistant",
                "content": null,
                "tool_calls": tool_calls,
            }))?;
        messages.push(assistant_msg);

        for tc_enum in tool_calls {
            let tool_call = match tc_enum {
                ChatCompletionMessageToolCalls::Function(
                    tc,
                ) => tc,
                _ => continue,
            };
            let function_name =
                &tool_call.function.name;
            let arguments: Value =
                serde_json::from_str(
                    &tool_call.function.arguments,
                )?;
            println!(
                "  Tool call: {}({})",
                function_name, arguments
            );

            let result =
                execute_tool(function_name, &arguments);
            messages.push(
                ChatCompletionRequestToolMessage {
                    content: result.to_string().into(),
                    tool_call_id: tool_call.id.clone(),
                }
                .into(),
            );
        }

        response = client
            .complete_chat(&messages, Some(&tools))
            .await?;
    }

    let answer = response.choices[0]
        .message
        .content
        .as_deref()
        .unwrap_or("");
    let assistant_msg: ChatCompletionRequestMessage =
        serde_json::from_value(json!({
            "role": "assistant",
            "content": answer,
        }))?;
    messages.push(assistant_msg);
    println!("Assistant: {}\n", answer);
}

// Clean up
model.unload().await?;
println!("Model unloaded. Goodbye!");

Основные шаги в цикле вызова средства:

  1. Обнаружение вызовов инструментов — проверка response.choices[0].message.tool_calls.
  2. Выполните функцию — синтаксический анализ аргументов и вызов локальной функции.
  3. Верните результат — добавьте сообщение с ролью tool и идентификатором вызова соответствующего инструмента.
  4. Получение окончательного ответа — модель использует результат средства для создания естественного ответа.

Обработка полного цикла вызова инструмента

Ниже приведено полное приложение, которое объединяет определения инструментов, инициализацию пакета SDK и цикл вызова средства в один исполняемый файл.

Замените содержимое src/main.rs следующим полным кодом:

use foundry_local_sdk::{
    ChatCompletionRequestMessage,
    ChatCompletionRequestSystemMessage,
    ChatCompletionRequestToolMessage,
    ChatCompletionRequestUserMessage,
    ChatCompletionMessageToolCalls,
    ChatCompletionTools, ChatToolChoice,
    FoundryLocalConfig, FoundryLocalManager,
};
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};

// --- Tool implementations ---
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(|c| "0123456789+-*/(). ".contains(c));
            if !is_valid {
                return json!({"error": "Invalid expression"});
            }
            match eval_expression(expression) {
                Ok(result) => json!({
                    "expression": expression,
                    "result": result
                }),
                Err(e) => json!({"error": e}),
            }
        }
        _ => 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());
    }
    let num_str: String = chars[start..*pos].iter().collect();
    num_str.parse::<f64>().map_err(|e| e.to_string())
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // --- Tool definitions ---
    let tools: Vec<ChatCompletionTools> = serde_json::from_value(json!([
        {
            "type": "function",
            "function": {
                "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"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "calculate",
                "description": "Perform a math calculation",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description":
                                "The math expression to evaluate"
                        }
                    },
                    "required": ["expression"]
                }
            }
        }
    ]))?;

    // Initialize the Foundry Local SDK
    let manager = FoundryLocalManager::create(
        FoundryLocalConfig::new("foundry_local_samples"),
    )?;

    // Download and register all execution providers.
    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  {:<30}  {:5.1}%", ep_name, percent);
                io::stdout().flush().ok();
            }
        })
        .await?;
    println!();

    // Select and load a model
    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?;
    println!("Model loaded and ready.");

    // Create a chat client
    let client = model
        .create_chat_client()
        .temperature(0.7)
        .max_tokens(512)
        .tool_choice(ChatToolChoice::Auto);

    // Conversation with a system prompt
    let mut messages: Vec<ChatCompletionRequestMessage> = vec![
        ChatCompletionRequestSystemMessage::from(
            "You are a helpful assistant with access to tools. \
             Use them when needed to answer questions accurately.",
        )
        .into(),
    ];

    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;
        }

        messages.push(
            ChatCompletionRequestUserMessage::from(input).into(),
        );

        let mut response = client
            .complete_chat(&messages, Some(&tools))
            .await?;

        // Process tool calls in a loop
        while response.choices[0].message.tool_calls.is_some() {
            let tool_calls = response.choices[0]
                .message
                .tool_calls
                .as_ref()
                .unwrap();

            // Append the assistant's tool_calls message via JSON
            let assistant_msg: ChatCompletionRequestMessage =
                serde_json::from_value(json!({
                    "role": "assistant",
                    "content": null,
                    "tool_calls": tool_calls,
                }))?;
            messages.push(assistant_msg);

            for tc_enum in tool_calls {
                let tool_call = match tc_enum {
                    ChatCompletionMessageToolCalls::Function(
                        tc,
                    ) => tc,
                    _ => continue,
                };
                let function_name =
                    &tool_call.function.name;
                let arguments: Value =
                    serde_json::from_str(
                        &tool_call.function.arguments,
                    )?;
                println!(
                    "  Tool call: {}({})",
                    function_name, arguments
                );

                let result =
                    execute_tool(function_name, &arguments);
                messages.push(
                    ChatCompletionRequestToolMessage {
                        content: result.to_string().into(),
                        tool_call_id: tool_call.id.clone(),
                    }
                    .into(),
                );
            }

            response = client
                .complete_chat(&messages, Some(&tools))
                .await?;
        }

        let answer = response.choices[0]
            .message
            .content
            .as_deref()
            .unwrap_or("");
        let assistant_msg: ChatCompletionRequestMessage =
            serde_json::from_value(json!({
                "role": "assistant",
                "content": answer,
            }))?;
        messages.push(assistant_msg);
        println!("Assistant: {}\n", answer);
    }

    // Clean up
    model.unload().await?;
    println!("Model unloaded. Goodbye!");

    Ok(())
}

Запустите помощник по вызову инструментов:

cargo run

Выходные данные похожи на следующие:

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!

Модель решает, когда следует вызывать средство на основе сообщения пользователя. Для вопроса о погоде он вызывает get_weather, для математики он вызывает calculate, и для общих вопросов он отвечает напрямую без вызовов инструментов.

Очистите ресурсы

Вес модели остается в локальном кэше после выгрузки модели. Это означает, что при следующем запуске приложения шаг загрузки пропускается, а модель загружается быстрее. Дополнительная очистка не требуется, если вы не хотите освободить место на диске.