进阶:工具调用
工具调用(Tool Calling / Function Calling)提供给大模型强大而且灵活的获取外部数据和执行操作的能力。
本章将详细介绍如何实现自定义工具。
工具调用流程:
工具调用需要应用与模型进行多轮对话。包含一下几个步骤:
- 向模型发送请求,包含可调用的工具
- 接收模型返回的工具调用
- 使用工具调用提供的输入,在应用程序执行代码
- 将执行结果作为第二次请求发送给模型
- 接收模型的最终响应(或更多工具调用)
如图所示:
实现
定义工具
打开 cmd/llm.go 文件,添加以下代码。
首先定义天气查询函数:
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 返回的参数:
llm.go
// WeatherRequest 天气查询请求参数
type WeatherRequest struct {
Location string `json:"location"`
}
将工具添加到请求中
在发送给 LLM 的请求中添加工具定义:
llm.go
request := openai.ChatCompletionRequest{
Model: model,
Messages: h.messages,
Temperature: 0.7,
Stream: true,
Tools: []openai.Tool{
... // 其他工具
{
Type: openai.ToolTypeFunction,
Function: &weatherDefinition,
},
},
}
处理工具调用
在大模型返回的响应中,会包含工具调用名称和参数。由于我们使用流模式,参数会分多个片段发送。
为什么需要收集参数?
在流式模式下,工具调用可能分多次发送:
- 第一次:
{"location": - 第二次:
"杭州" - 第三次:
}
需要将所有片段拼接成完整的 JSON:{"location":"杭州"}
初始化参数收集器:
在流处理循环开始前,初始化一个 Map 用于收集参数:
llm.go
// 用 Map 收集工具调用参数(支持多个并发工具调用)
toolCallsMap := make(map[int]*openai.ToolCall)
// 处理流式响应
for {
response, err := stream.Recv()
// ... 错误处理
}
识别并收集天气工具调用:
保存工具调用,并收集参数:
llm.go
// 添加天气工具的参数收集
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
总结
通过本章,你学会了:
- 什么是工具调用以及为什么需要它
- 如何定义工具函数和规格
- 如何在流式模式下收集工具参数
- 如何执行工具并将结果返回给 LLM
工具调用让你的 AI Agent 从"聊天机器人"升级为"能做事的助手",是构建实用 AI 应用的关键技术!
下一步
🔗 OpenAI Function Calling
详细了解 Function Calling 机制
📄️ RustPBX 功能概述
详细了解 RustPBX 功能