进阶:工具调用

工具调用(Tool Calling / Function Calling)提供给大模型强大而且灵活的获取外部数据和执行操作的能力。

本章将详细介绍如何实现自定义工具。

工具调用流程:

工具调用需要应用与模型进行多轮对话。包含一下几个步骤:

  1. 向模型发送请求,包含可调用的工具
  2. 接收模型返回的工具调用
  3. 使用工具调用提供的输入,在应用程序执行代码
  4. 将执行结果作为第二次请求发送给模型
  5. 接收模型的最终响应(或更多工具调用)

如图所示:

ToolCallingFlow

实现

定义工具

打开 cmd/llm.go 文件,添加以下代码。

首先定义天气查询函数:

func GetWeather(location string) string {
    
    weatherData := map[string]interface{}{
        "location":    location,
        "temperature": 30,
        "unit":        "Celsius",
        "forecast":    "Sunny",
        "humidity":    35,
    }

    result, _ := json.Marshal(weatherData)
    return string(result)
}

描述工具的名称、功能和参数格式:

// 定义天气查询工具
var weatherDefinition = openai.FunctionDefinition{
	Name:        "get_weather",
	Description: "Get the current weather in a given location",
	Parameters: json.RawMessage(`{
		"type": "object",
		"properties": {
			"location": {
				"type": "string",
				"description": "The city to get the weather for, e.g. 杭州"
			}
		},
		"required": ["location"]
	}`),
}

字段说明:

  • Name: 工具的名称,用于识别该调用哪个工具。
  • Description: 工具的功能描述,LLM 会根据描述决定是否调用该工具。
  • Parameters: 使用 JSON schema 定义参数的名称,类型,以及描述。

定义参数结构体,用于解析 LLM 返回的参数:

// WeatherRequest 天气查询请求参数
type WeatherRequest struct {
    Location string `json:"location"`
}

将工具添加到请求中

在发送给 LLM 的请求中添加工具定义:

request := openai.ChatCompletionRequest{
    Model:       model,
    Messages:    h.messages,
    Temperature: 0.7,
    Stream:      true,
    Tools: []openai.Tool{
        ... // 其他工具
        {
            Type:     openai.ToolTypeFunction,
            Function: &weatherDefinition,
        },
    },
}

处理工具调用

在大模型返回的响应中,会包含工具调用名称和参数。由于我们使用流模式,参数会分多个片段发送。

为什么需要收集参数?

在流式模式下,工具调用可能分多次发送:

  1. 第一次:{"location":
  2. 第二次:"杭州"
  3. 第三次:}

需要将所有片段拼接成完整的 JSON:{"location":"杭州"}

初始化参数收集器:

在流处理循环开始前,初始化一个 Map 用于收集参数:

// 用 Map 收集工具调用参数(支持多个并发工具调用)
toolCallsMap := make(map[int]*openai.ToolCall)

// 处理流式响应
for {
    response, err := stream.Recv()
    // ... 错误处理
}

识别并收集天气工具调用:

保存工具调用,并收集参数:

// 添加天气工具的参数收集
if toolCall.Function.Name == "get_weather" {
    // 首次接收到该工具调用
    toolCallsMap[*toolCall.Index] = &toolCall
}

if toolCall.Function.Name == "" {
    // 参数片段,追加到对应的工具调用
    toolCallsMap[*toolCall.Index].Function.Arguments += toolCall.Function.Arguments
}

执行工具再次请求

处理工具调用:

当流结束时,执行收集到的所有工具调用:

if isFinished {
    // 检查是否有工具调用
    if len(toolCallsMap) > 0 {
        // 1. 构建 Assistant 消息
        var assistantToolCalls []openai.ToolCall
        for _, toolCall := range toolCallsMap {
            assistantToolCalls = append(assistantToolCalls, *toolCall)
        }

        assistantMsg := openai.ChatCompletionMessage{
            Role:      openai.ChatMessageRoleAssistant,
            Content:   fullResponse,
            ToolCalls: assistantToolCalls,
        }
        h.messages = append(h.messages, assistantMsg)

        // 2. 遍历,可能有多个工具调用
        for _, toolCall := range toolCallsMap {
            var req WeatherRequest
            
            // 解析参数
            if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &req); err == nil {
                // 调用实际的天气查询函数
                weather := GetWeather(req.Location)
                
                // 将工具结果添加到对话历史
                h.messages = append(h.messages, openai.ChatCompletionMessage{
                    Role:       openai.ChatMessageRoleTool,
                    Content:    weather,
                    ToolCallID: toolCall.ID,
                })
            }
        }

        // 3. 清空已处理的工具调用
        toolCallsMap = make(map[int]*openai.ToolCall)
        
        // 4. 更新请求消息(包含工具结果)
        request.Messages = h.messages

        // 5. 再次请求 LLM(让它基于工具结果生成回复)
        stream, err = h.client.CreateChatCompletionStream(h.ctx, request)
        if err != nil {
            h.logger.WithError(err).Error("Error creating chat completion stream")
        }
        continue  // 继续处理新的流
    }
    break  // 没有工具调用,结束循环
}

测试效果

运行程序

确保 RustPBX 已启动,然后运行更新后的客户端:

cd cmd
go run . \
  --endpoint ws://127.0.0.1:8080 \
  --tts aliyun --speaker longyumi_v2 \
  --asr aliyun \
  --openai-key your_dashscope_api_key \
  --model qwen-plus \
  --openai-endpoint https://dashscope.aliyuncs.com/compatible-mode/v1 \
  --greeting "你好,有什么可以帮你的吗"

测试对话

尝试以下问题:

问题预期行为
“杭州今天天气怎么样?”✅ 调用 get_weather("杭州")
“北京天气”✅ 调用 get_weather("北京")
“你好”❌ 不调用工具,直接回复
“1+1等于几?”❌ 不调用工具,LLM 直接计算

音频回复示例:

完整代码

完整的实现代码可以在这里查看: https://gist.github.com/yeoleobun/4b5707f2c23ac587b18d365019147a9a

总结

通过本章,你学会了:

  1. 什么是工具调用以及为什么需要它
  2. 如何定义工具函数和规格
  3. 如何在流式模式下收集工具参数
  4. 如何执行工具并将结果返回给 LLM

工具调用让你的 AI Agent 从“聊天机器人“升级为“能做事的助手“,是构建实用 AI 应用的关键技术!

下一步