Tutorial: Erstellen eines KI-Assistenten mit Tool-Calling

Erstellen Sie einen KI-Assistenten, der über die Unterhaltung hinausgeht – er kann Funktionen aufrufen, um Aktionen auszuführen. Der Assistent entscheidet, wann eine Funktion benötigt wird, Sie führen sie aus und geben das Ergebnis zurück. Alles wird lokal mit dem Foundry Local SDK ausgeführt.

In diesem Tutorial erfahren Sie, wie:

  • Einrichten eines Projekts und Installieren des Foundry Local SDK
  • Definieren von Tools, die der Assistent aufrufen kann
  • Senden einer Nachricht, die die Verwendung des Tools auslöst
  • Ausführen des Tools und Zurückgeben von Ergebnissen an das Modell
  • Meistern der vollständigen Tool-Calling-Schleife
  • Bereinigen von Ressourcen

Voraussetzungen

  • Ein Windows, macOS oder Linux-Computer mit mindestens 8 GB RAM.

Beispielrepository

Der vollständige Beispielcode für diesen Artikel ist im Repository Foundry Local GitHub repository verfügbar. So klonen Sie das Repository und navigieren zur Beispielverwendung:

git clone https://github.com/microsoft/Foundry-Local.git
cd Foundry-Local/samples/cs/tutorial-tool-calling

Pakete installieren

Wenn Sie Windows entwickeln oder versenden, wählen Sie die Registerkarte Windows aus. Das Windows-Paket ist in die Windows ML Laufzeit integriert. Es bietet den gleichen API-Oberflächenbereich mit einer breiteren Hardwarebeschleunigung.

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

Die C#-Beispiele im GitHub Repository sind vorkonfigurierte Projekte. Wenn Sie von Grund auf neu erstellen, sollten Sie die Referenz zum Foundry Local SDK lesen, um weitere Details zum Einrichten Ihres C#-Projekts mit Foundry Local zu erhalten.

Definieren von Tools

Mit Toolaufrufen kann das Modell anfordern, dass Ihr Code eine Funktion ausführt und das Ergebnis zurückgibt. Sie definieren die verfügbaren Tools als Eine Liste von JSON-Schemas, die den Namen, den Zweck und die Parameter jeder Funktion beschreiben.

  1. Öffnen Sie Program.cs und fügen Sie die folgenden Tooldefinitionen hinzu:

    // --- 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}"
                });
        }
    }
    

    Jede Tooldefinition enthält eine name, eine description , die dem Modell hilft, zu entscheiden, wann es verwendet werden soll, und ein parameters Schema, das die erwartete Eingabe beschreibt.

  2. Fügen Sie die C#-Methoden hinzu, die jedes Tool implementieren:

    // --- 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}"
                });
        }
    }
    

    Das Modell führt diese Funktionen nicht direkt aus. Es wird eine Toolaufrufanforderung mit dem Funktionsnamen und den Argumenten zurückgegeben, und der Code führt die Funktion aus.

Senden einer Nachricht, die die Verwendung des Tools auslöst

Initialisieren Sie das Foundry Local SDK, laden Sie ein Modell, und senden Sie eine Nachricht, die das Modell beantworten kann, indem Sie ein Tool aufrufen.

// --- 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."
    }
};

Wenn das Modell feststellt, dass ein Tool benötigt wird, enthält ToolCalls die Antwort anstelle einer regulären Textnachricht. Im nächsten Schritt wird gezeigt, wie Sie diese Aufrufe erkennen und behandeln.

Ausführen des Tools und Zurückgeben von Ergebnissen

Nachdem das Modell mit einem Toolaufruf reagiert hat, extrahieren Sie den Funktionsnamen und die Argumente, führen die Funktion aus und senden das Ergebnis zurück.

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!");

Die wichtigsten Schritte in der Toolaufrufschleife sind:

  1. Erkennen von Toolaufrufen – Überprüfen response.Choices[0].Message.ToolCalls.
  2. Führen Sie die Funktion aus – analysieren Sie die Argumente, und rufen Sie die lokale Funktion auf.
  3. Geben Sie das Ergebnis zurück. Fügen Sie eine Nachricht mit der Rolle tool und dem entsprechenden ToolCallId hinzu.
  4. Erhalten Sie die endgültige Antwort – das Modell verwendet das Toolergebnis, um eine natürliche Antwort zu generieren.

Meistern der vollständigen Tool-Calling-Schleife

Dies ist die vollständige Anwendung, die Tooldefinitionen, SDK-Initialisierung und die Toolaufrufschleife in eine einzelne ausgeführte Datei kombiniert.

Ersetzen Sie den Inhalt von Program.cs durch den folgenden vollständigen Code:

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!");

Führen Sie den Werkzeugaufruf-Assistenten aus:

dotnet run

Es wird eine ähnliche Ausgabe angezeigt wie:

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!

Das Modell entscheidet, wann ein Tool basierend auf der Nachricht des Benutzers aufgerufen werden soll. Für eine Wetterfrage ruft es get_weather auf, für Mathematik ruft es calculate auf, und bei allgemeinen Fragen antwortet es direkt ohne Werkzeuganrufe.

Beispielrepository

Der vollständige Beispielcode für diesen Artikel ist im Repository Foundry Local GitHub repository verfügbar. So klonen Sie das Repository und navigieren zur Beispielverwendung:

git clone https://github.com/microsoft/Foundry-Local.git
cd Foundry-Local/samples/js/tutorial-tool-calling

Pakete installieren

Wenn Sie Windows entwickeln oder versenden, wählen Sie die Registerkarte Windows aus. Das Windows-Paket ist in die Windows ML Laufzeit integriert. Es bietet den gleichen API-Oberflächenbereich mit einer breiteren Hardwarebeschleunigung.

npm install foundry-local-sdk-winml openai

Definieren von Tools

Mit Toolaufrufen kann das Modell anfordern, dass Ihr Code eine Funktion ausführt und das Ergebnis zurückgibt. Sie definieren die verfügbaren Tools als Eine Liste von JSON-Schemas, die den Namen, den Zweck und die Parameter jeder Funktion beschreiben.

  1. Erstellen Sie eine Datei namens index.js.

  2. Fügen Sie die folgenden Tooldefinitionen hinzu:

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

    Jede Tooldefinition enthält eine name, eine description , die dem Modell hilft, zu entscheiden, wann es verwendet werden soll, und ein parameters Schema, das die erwartete Eingabe beschreibt.

  3. Fügen Sie die JavaScript-Funktionen hinzu, die jedes Tool implementieren:

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

    Das Modell führt diese Funktionen nicht direkt aus. Es wird eine Toolaufrufanforderung mit dem Funktionsnamen und den Argumenten zurückgegeben, und der Code führt die Funktion aus.

Senden einer Nachricht, die die Verwendung des Tools auslöst

Initialisieren Sie das Foundry Local SDK, laden Sie ein Modell, und senden Sie eine Nachricht, die das Modell beantworten kann, indem Sie ein Tool aufrufen.

// --- 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) => rl.question(prompt, resolve));

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();

Wenn das Modell feststellt, dass ein Tool benötigt wird, enthält tool_calls die Antwort anstelle einer regulären Textnachricht. Im nächsten Schritt wird gezeigt, wie Sie diese Aufrufe erkennen und behandeln.

Ausführen des Tools und Zurückgeben von Ergebnissen

Nachdem das Modell mit einem Toolaufruf reagiert hat, extrahieren Sie den Funktionsnamen und die Argumente, führen die Funktion aus und senden das Ergebnis zurück.

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

Die wichtigsten Schritte in der Toolaufrufschleife sind:

  1. Erkennen von Toolaufrufen – Überprüfen response.choices[0]?.message?.tool_calls.
  2. Führen Sie die Funktion aus – analysieren Sie die Argumente, und rufen Sie die lokale Funktion auf.
  3. Geben Sie das Ergebnis zurück. Fügen Sie eine Nachricht mit der Rolle tool und dem entsprechenden tool_call_id hinzu.
  4. Erhalten Sie die endgültige Antwort – das Modell verwendet das Toolergebnis, um eine natürliche Antwort zu generieren.

Meistern der vollständigen Tool-Calling-Schleife

Dies ist die vollständige Anwendung, die Tooldefinitionen, SDK-Initialisierung und die Toolaufrufschleife in eine einzelne ausgeführte Datei kombiniert.

Erstellen Sie eine Datei mit dem Namen index.js , und fügen Sie den folgenden vollständigen Code hinzu:

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) => rl.question(prompt, resolve));

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();

Führen Sie den Werkzeugaufruf-Assistenten aus:

node index.js

Es wird eine ähnliche Ausgabe angezeigt wie:

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!

Das Modell entscheidet, wann ein Tool basierend auf der Nachricht des Benutzers aufgerufen werden soll. Für eine Wetterfrage ruft es get_weather auf, für Mathematik ruft es calculate auf, und bei allgemeinen Fragen antwortet es direkt ohne Werkzeuganrufe.

Beispielrepository

Der vollständige Beispielcode für diesen Artikel ist im Repository Foundry Local GitHub repository verfügbar. So klonen Sie das Repository und navigieren zur Beispielverwendung:

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

Pakete installieren

Wenn Sie Windows entwickeln oder versenden, wählen Sie die Registerkarte Windows aus. Das Windows-Paket ist in die Windows ML Laufzeit integriert. Es bietet den gleichen API-Oberflächenbereich mit einer breiteren Hardwarebeschleunigung.

pip install foundry-local-sdk-winml openai

Definieren von Tools

Mit Toolaufrufen kann das Modell anfordern, dass Ihr Code eine Funktion ausführt und das Ergebnis zurückgibt. Sie definieren die verfügbaren Tools als Eine Liste von JSON-Schemas, die den Namen, den Zweck und die Parameter jeder Funktion beschreiben.

Erstellen Sie eine Datei namens, main.py und fügen Sie die folgenden Tooldefinitionen hinzu:

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

Jede Tooldefinition enthält eine name, eine description , die dem Modell hilft, zu entscheiden, wann es verwendet werden soll, und ein parameters Schema, das die erwartete Eingabe beschreibt.

Fügen Sie als Nächstes die Python Funktionen hinzu, die jedes Tool implementieren:

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

Das Modell führt diese Funktionen nicht direkt aus. Es wird eine Toolaufrufanforderung mit dem Funktionsnamen und den Argumenten zurückgegeben, und der Code führt die Funktion aus.

Senden einer Nachricht, die die Verwendung des Tools auslöst

Initialisieren Sie das Foundry Local SDK, laden Sie ein Modell, und senden Sie eine Nachricht, die das Modell beantworten kann, indem Sie ein Tool aufrufen.

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!")

Wenn das Modell feststellt, dass ein Tool benötigt wird, enthält tool_calls die Antwort anstelle einer regulären Textnachricht. Im nächsten Schritt wird gezeigt, wie Sie diese Aufrufe erkennen und behandeln.

Ausführen des Tools und Zurückgeben von Ergebnissen

Nachdem das Modell mit einem Toolaufruf reagiert hat, extrahieren Sie den Funktionsnamen und die Argumente, führen die Funktion aus und senden das Ergebnis zurück.

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

Die wichtigsten Schritte in der Toolaufrufschleife sind:

  1. Erkennen von Toolaufrufen – Überprüfen response.choices[0].message.tool_calls.
  2. Führen Sie die Funktion aus – analysieren Sie die Argumente, und rufen Sie die lokale Funktion auf.
  3. Geben Sie das Ergebnis zurück. Fügen Sie eine Nachricht mit der Rolle tool und dem entsprechenden tool_call_id hinzu.
  4. Erhalten Sie die endgültige Antwort – das Modell verwendet das Toolergebnis, um eine natürliche Antwort zu generieren.

Meistern der vollständigen Tool-Calling-Schleife

Dies ist die vollständige Anwendung, die Tooldefinitionen, SDK-Initialisierung und die Toolaufrufschleife in eine einzelne ausgeführte Datei kombiniert.

Erstellen Sie eine Datei mit dem Namen main.py , und fügen Sie den folgenden vollständigen Code hinzu:

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()

Führen Sie den Werkzeugaufruf-Assistenten aus:

python main.py

Es wird eine ähnliche Ausgabe angezeigt wie:

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!

Das Modell entscheidet, wann ein Tool basierend auf der Nachricht des Benutzers aufgerufen werden soll. Für eine Wetterfrage ruft es get_weather auf, für Mathematik ruft es calculate auf, und bei allgemeinen Fragen antwortet es direkt ohne Werkzeuganrufe.

Beispielrepository

Der vollständige Beispielcode für diesen Artikel ist im Repository Foundry Local GitHub repository verfügbar. So klonen Sie das Repository und navigieren zur Beispielverwendung:

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

Pakete installieren

Wenn Sie Windows entwickeln oder versenden, wählen Sie die Registerkarte Windows aus. Das Windows-Paket ist in die Windows ML Laufzeit integriert. Es bietet den gleichen API-Oberflächenbereich mit einer breiteren Hardwarebeschleunigung.

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

Definieren von Tools

Mit Toolaufrufen kann das Modell anfordern, dass Ihr Code eine Funktion ausführt und das Ergebnis zurückgibt. Sie definieren die verfügbaren Tools als Eine Liste von JSON-Schemas, die den Namen, den Zweck und die Parameter jeder Funktion beschreiben.

  1. Fügen Sie die Abhängigkeit für die serde_json JSON-Verarbeitung hinzu:

    cargo add serde_json
    
  2. Öffnen Sie src/main.rs und fügen Sie die folgenden Tooldefinitionen hinzu:

    // --- 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"]
                }
            }
        }
    ]))?;
    

    Jede Tooldefinition enthält eine name, eine description , die dem Modell hilft, zu entscheiden, wann es verwendet werden soll, und ein parameters Schema, das die erwartete Eingabe beschreibt.

  3. Fügen Sie die Rust-Funktionen hinzu, die jedes Tool implementieren:

    // --- 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"]
                }
            }
        }
    ]))?;
    

    Das Modell führt diese Funktionen nicht direkt aus. Es wird eine Toolaufrufanforderung mit dem Funktionsnamen und den Argumenten zurückgegeben, und der Code führt die Funktion aus.

Senden einer Nachricht, die die Verwendung des Tools auslöst

Initialisieren Sie das Foundry Local SDK, laden Sie ein Modell, und senden Sie eine Nachricht, die das Modell beantworten kann, indem Sie ein Tool aufrufen.

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

// 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(),
];

Wenn das Modell feststellt, dass ein Tool benötigt wird, enthält tool_calls die Antwort anstelle einer regulären Textnachricht. Im nächsten Schritt wird gezeigt, wie Sie diese Aufrufe erkennen und behandeln.

Ausführen des Tools und Zurückgeben von Ergebnissen

Nachdem das Modell mit einem Toolaufruf reagiert hat, extrahieren Sie den Funktionsnamen und die Argumente, führen die Funktion aus und senden das Ergebnis zurück.

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!");

Die wichtigsten Schritte in der Toolaufrufschleife sind:

  1. Erkennen von Toolaufrufen – Überprüfen response.choices[0].message.tool_calls.
  2. Führen Sie die Funktion aus – analysieren Sie die Argumente, und rufen Sie die lokale Funktion auf.
  3. Geben Sie das Ergebnis zurück – fügen Sie eine Nachricht mit der Rolle tool und der entsprechenden Werkzeugaufruf-ID hinzu.
  4. Erhalten Sie die endgültige Antwort – das Modell verwendet das Toolergebnis, um eine natürliche Antwort zu generieren.

Meistern der vollständigen Tool-Calling-Schleife

Dies ist die vollständige Anwendung, die Tooldefinitionen, SDK-Initialisierung und die Toolaufrufschleife in eine einzelne ausgeführte Datei kombiniert.

Ersetzen Sie den Inhalt von src/main.rs durch den folgenden vollständigen Code:

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("tool-calling-app"),
    )?;

    // 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(())
}

Führen Sie den Werkzeugaufruf-Assistenten aus:

cargo run

Es wird eine ähnliche Ausgabe angezeigt wie:

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!

Das Modell entscheidet, wann ein Tool basierend auf der Nachricht des Benutzers aufgerufen werden soll. Für eine Wetterfrage ruft es get_weather auf, für Mathematik ruft es calculate auf, und bei allgemeinen Fragen antwortet es direkt ohne Werkzeuganrufe.

Bereinigen von Ressourcen

Die Gewichtungen des Modells verbleiben im lokalen Cache, nachdem Sie ein Modell entladen haben. Dies bedeutet, dass beim nächsten Ausführen der Anwendung der Downloadschritt übersprungen wird und das Modell schneller geladen wird. Es ist keine zusätzliche Bereinigung erforderlich, es sei denn, Sie möchten Speicherplatz freigeben.