在前文ReAct、Planning、Memory Agent中,我们仅用了「计算器」这一个内置工具,而真实场景下的Agent需要调用外部工具(天气API、课程查询、支付接口等)突破能力边界——这就是ToolUse模型的核心价值:让LLM通过Function Calling调用外部工具,从“文本生成器”变成“能落地做事的智能体”。
本篇彻底讲清楚ToolUse的4个核心问题:
- 1. 模型“调用”工具到底是什么意思?
- 2. 如何把Go函数(工具)正确暴露给模型?
- 3. 为什么工具描述直接决定Agent稳定性?
- 4. 工具太多模型乱选怎么办?
一、核心误区:模型不会“真的”执行工具
很多人对Function Calling的第一认知是“模型自己调API”——这是典型误解!
1. 本质:模型是“决策者”,程序是“执行者”
LLM本质是文本生成器:它不会直接执行四则运算、查天气,只会输出一段结构化的JSON(比如{"name":"get_weather","arguments":{"city":"北京"}});而真正去运行calculator、getWeather函数的,是Go程序。
2. 通俗比喻
把模型比作“被关在隔音房的顾问”,把Go程序比作“跑腿的助理”:
- • 你(用户):“北京今天天气怎么样?”(对应Demo中
agent1的UserPrompt); - • 你告诉顾问:“你可以用get_weather工具查天气,用calculator工具算数学题”;
- • 顾问(模型):判断需要查天气,输出工具调用指令(JSON);
- • 助理(Go程序):通过
executeTool函数解析指令,调用getWeather函数拿到模拟天气数据; - • 助理把结果递回给顾问,顾问生成最终回答。
整个过程的核心分工:
模型:决定 “调不调工具、调哪个、传什么参数”;
程序:执行工具调用、返回结果 —— 各做各擅长的事。
// selectTool:模型仅做“决策”——选工具、生成参数
func selectTool(client *openai.Client, userMsg string) (openai.ChatCompletionMessage, error)
// executeTool:程序做“执行”——解析参数、运行工具函数
func executeTool(toolCall openai.ToolCall) string二、工具暴露给模型的核心:四要素
把Go函数(calculator/getWeather)变成模型能“看懂并调用”的形式,本质是给模型写一份“工具说明书”,这份说明书必须包含四要素(缺一不可):
| 要素 | 给谁看 | 核心作用(结合Demo) |
|---|---|---|
| Name(工具名) | 模型+程序 | 模型用它指定“调哪个工具”,程序用它做“执行分发”(Demo中calculator/get_weather是唯一标识) |
| Description(描述) | 模型 | 模型靠它判断“这个工具什么时候该用”(Demo中“用户问数学计算时用calculator”) |
| Parameters Schema(参数Schema) | 模型+程序 | 约束模型传参格式(Demo中city限定为北京/上海等枚举值),程序用它做参数校验 |
| Result(结果) | 模型 | 工具执行结果返回给模型(Demo中getWeather返回的“北京28℃,晴天”) |
Demo中的工具四要素实现(toolList)
var toolList = []openai.Tool{
{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: "calculator", // Name要素:唯一标识
Description: "四则运算计算器,用户问数学计算时使用", // Description要素:明确使用场景
Parameters: map[string]interface{}{ // Parameters Schema要素
"type": "object",
"properties": map[string]interface{}{
"expression": map[string]interface{}{
"type": "string", // 限定参数类型
"description": "数学表达式,格式:数字 运算符 数字(如 150 + 250)", // 解释语义+例子
"required": true, // 声明必填参数
},
},
"required": []string{"expression"},
},
},
},
{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: "get_weather", // Name要素
Description: "天气查询,用户问某城市天气时使用", // Description要素
Parameters: map[string]interface{}{ // Parameters Schema要素
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string", // 限定类型
"enum": []string{"北京", "上海", "广州", "深圳"}, // 限定可选值
"description": "城市名称", // 解释语义
"required": true, // 必填
},
},
"required": []string{"city"},
},
},
},
}三、关键:工具描述写得好不好,直接决定Agent稳定性
假如你工具描述是“四则运算计算器,用户问数学计算时使用”——这是基础版写法,我们通过对比能更清晰看到描述质量对Agent的影响:
| 描述写法 | 问题/效果(结合Demo) |
|---|---|
| “计算器” | 太模糊!模型可能在用户问天气时调用calculator,导致Agent行为混乱 |
| “四则运算计算器,用户问数学计算时使用” | 基础版:能区分核心场景,适配2个工具的简单场景,但未明确“不适用场景” |
| “四则运算计算器,支持加减乘除;适用场景:用户问“X+Y等于多少”等数学计算问题;不适用场景:用户问天气、课程等非计算问题” | 工业版:模型能100%精准判断,即使工具数量增加到5个也不会乱选 |
写好Description的3个技巧
- 1. 做什么:明确工具功能(如“四则运算计算器,支持加减乘除”);
- 2. 什么时候用:列举典型场景(如“用户问150+250等于多少时使用”);
- 3. 什么时候不用:列举排除场景(如“用户问天气时不使用”)。
四、参数Schema:让模型“传对参数”的关键
Demo中getWeather函数里,模型很容易传“帝都”“Beijing”等非标准值——参数Schema就是给模型定“传参规矩”,核心要包含5个属性(Demo已实现核心属性):
| Schema属性 | 作用 | 示例(weather工具city参数) |
|---|---|---|
| required | 标记必填项(少了模型会补) | city设为true:必须传城市,否则模型追问用户 |
| enum | 限定值范围(避免乱传) | 限定["北京","上海","广州","深圳"] |
| type | 限定参数类型(避免类型错误) | 设为string:避免模型传数字110代表北京 |
| description | 解释语义+例子(降低传错概率) | “城市名称,示例:北京” |
| default | 未传时的默认值(提升容错) | 可补充default: "北京",Demo暂未实现 |
Demo中的参数校验(程序端兜底)
即使Schema做了约束,仍需在Go程序中二次校验:
func getWeather(city string) string {
weatherData := map[string]string{
"北京": "28℃,晴天,适合出门",
"上海": "30℃,多云,注意防晒",
"广州": "32℃,雷阵雨,带伞",
"深圳": "31℃,阴天,舒适",
}
w, ok := weatherData[city]
if !ok {
return fmt.Sprintf("不支持的城市:%s(仅支持北京/上海/广州/深圳)", city)
}
return fmt.Sprintf("%s今天:%s", city, w)
}五、多工具混乱?解决办法:控制数量+分层选择
Demo只有2个工具,模型不会乱选,但工具数量增加后会出现决策混乱:
| 工具数量 | 模型状态 |
|---|---|
| 3~5个 | 初版Agent的“舒适区”,稳定性最高 |
| 10个以内 | 描述写得好还能稳定 |
| 10个以上 | 准确率明显下降,必须做分层处理 |
工具超过10个的3种应对策略
1. 工具分组(最简单)
把工具按“领域”分组(如“计算类”“查询类”),让模型先选“组”,再选“具体工具”:
// 工具分组
var toolGroups = map[string][]openai.Tool{
"计算类": {toolList[0]}, // calculator
"查询类": {toolList[1]}, // get_weather
}
// 第一步:让模型先选组(
func selectToolGroup(client *openai.Client, userMsg string) (string, error) {
systemPrompt := `仅返回工具组名:计算类/查询类。
规则:1. 数学计算→计算类;2. 天气查询→查询类;3. 只返回组名。`
// 调用LLM逻辑(略)
}2. 动态工具检索(最常用)
把工具描述向量化,用户提问时只传Top-3相关工具给模型,减少选择压力。
3. 多Agent架构(工业级)
每个子Agent只负责一组工具,由“总管Agent”分发任务。
六、完整ToolUse模型实战代码
以下是可直接运行的Demo代码:
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/sashabaranov/go-openai"
)
// ==================== 1. 工具执行逻辑 ====================
// calculator 四则运算:解析「数字 运算符 数字」格式的表达式。
func calculator(expression string) string {
parts := strings.Fields(expression) // 按空白分词,不受空格数量影响
if len(parts) != 3 {
return fmt.Sprintf("表达式格式错误(应为「数字 运算符 数字」):%s", expression)
}
num1, err1 := strconv.ParseFloat(parts[0], 64)
num2, err2 := strconv.ParseFloat(parts[2], 64)
if err1 != nil || err2 != nil {
return fmt.Sprintf("数字解析失败:%s", expression)
}
var res float64
switch parts[1] {
case "+":
res = num1 + num2
case "-":
res = num1 - num2
case "*":
res = num1 * num2
case "/":
if num2 == 0 {
return "错误:除数不能为 0"
}
res = num1 / num2
default:
return fmt.Sprintf("不支持运算符:%s", parts[1])
}
return fmt.Sprintf("%.2f %s %.2f = %.2f", num1, parts[1], num2, res)
}
// getWeather 天气查询(模拟数据)。
func getWeather(city string) string {
weatherData := map[string]string{
"北京": "28℃,晴天,适合出门",
"上海": "30℃,多云,注意防晒",
"广州": "32℃,雷阵雨,带伞",
"深圳": "31℃,阴天,舒适",
}
w, ok := weatherData[city]
if !ok {
return fmt.Sprintf("不支持的城市:%s(仅支持北京/上海/广州/深圳)", city)
}
return fmt.Sprintf("%s今天:%s", city, w)
}
// ==================== 2. 工具定义(LLM 可选工具清单)====================
var toolList = []openai.Tool{
{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: "calculator",
Description: "四则运算计算器,用户问数学计算时使用",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"expression": map[string]interface{}{
"type": "string",
"description": "数学表达式,格式:数字 运算符 数字(如 150 + 250)",
},
},
"required": []string{"expression"},
},
},
},
{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: "get_weather",
Description: "天气查询,用户问某城市天气时使用",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"enum": []string{"北京", "上海", "广州", "深圳"},
"description": "城市名称",
},
},
"required": []string{"city"},
},
},
},
}
// ==================== 3. 工具执行分发 ====================
// executeTool 根据工具名解析参数并执行对应工具。
func executeTool(toolCall openai.ToolCall) string {
switch toolCall.Function.Name {
case "calculator":
var p struct {
Expression string `json:"expression"`
}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &p); err != nil {
return fmt.Sprintf("参数解析失败:%v", err)
}
return calculator(p.Expression)
case "get_weather":
var p struct {
City string `json:"city"`
}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &p); err != nil {
return fmt.Sprintf("参数解析失败:%v", err)
}
return getWeather(p.City)
default:
return fmt.Sprintf("未知工具:%s", toolCall.Function.Name)
}
}
// ==================== 4. ToolUse Agent ====================
// ToolUseAgent 工具使用 Agent:选工具 → 执行 → 返回结果。
type ToolUseAgent struct {
UserPrompt string // 用户指令
SelectedTool string // 选中的工具名
ToolResult string // 工具执行结果
TotalLatency int64 // 总耗时(毫秒)
}
// newClient 创建 DeepSeek 客户端。
func newClient() (*openai.Client, error) {
apikey := os.Getenv("DEEPSEEK_API_KEY")
if apikey == "" {
return nil, fmt.Errorf("DEEPSEEK_API_KEY 环境变量未设置")
}
cfg := openai.DefaultConfig(apikey)
cfg.BaseURL = "https://api.deepseek.com"
return openai.NewClientWithConfig(cfg), nil
}
// selectTool 调 LLM 从工具清单中选择合适工具并生成参数。
func selectTool(client *openai.Client, userMsg string) (openai.ChatCompletionMessage, error) {
resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
Model: "deepseek-v4-flash", // DeepSeek 模型,若不存在可改 deepseek-chat
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: "你是 ToolUse Agent,根据用户问题选择合适的工具并生成参数。"},
{Role: openai.ChatMessageRoleUser, Content: userMsg},
},
Tools: toolList,
Temperature: 0.1,
})
if err != nil {
return openai.ChatCompletionMessage{}, fmt.Errorf("LLM 调用失败:%w", err)
}
if len(resp.Choices) == 0 {
return openai.ChatCompletionMessage{}, fmt.Errorf("LLM 未返回结果")
}
return resp.Choices[0].Message, nil
}
// Run 执行完整流程:选工具 → 执行工具 → 返回结果。
func (a *ToolUseAgent) Run() {
fmt.Printf("\n===== ToolUse Agent =====\n用户指令:%s\n", a.UserPrompt)
client, err := newClient()
if err != nil {
a.ToolResult = err.Error()
return
}
start := time.Now()
// 步骤1:LLM 选工具并生成参数
msg, err := selectTool(client, a.UserPrompt)
if err != nil {
a.ToolResult = err.Error()
return
}
if len(msg.ToolCalls) == 0 {
a.ToolResult = "模型未选择任何工具"
return
}
a.SelectedTool = msg.ToolCalls[0].Function.Name
fmt.Printf("🔧 选中工具:%s\n", a.SelectedTool)
// 步骤2:执行工具
a.ToolResult = executeTool(msg.ToolCalls[0])
fmt.Printf("📊 执行结果:%s\n", a.ToolResult)
a.TotalLatency = time.Since(start).Milliseconds()
}
// ==================== 5. 主函数:测试 ToolUse Agent ====================
func main() {
// 测试1:天气查询(应选 get_weather)
agent1 := &ToolUseAgent{UserPrompt: "北京今天天气怎么样?"}
agent1.Run()
fmt.Printf("✅ 最终答案:%s(耗时 %dms)\n", agent1.ToolResult, agent1.TotalLatency)
// 测试2:数学计算(应选 calculator)
agent2 := &ToolUseAgent{UserPrompt: "150 + 250等于多少?"}
agent2.Run()
fmt.Printf("✅ 最终答案:%s(耗时 %dms)\n", agent2.ToolResult, agent2.TotalLatency)
// 测试3:天气查询(没工具选择)
agent3 := &ToolUseAgent{UserPrompt: "杭州今天天气怎么样??"}
agent3.Run()
fmt.Printf("✅ 最终答案:%s(耗时 %dms)\n", agent3.ToolResult, agent3.TotalLatency) // 测试3:天气(没工具选择)
}代码运行结果(真实执行输出)
===== ToolUse Agent =====
用户指令:北京今天天气怎么样?
🔧 选中工具:get_weather
📊 执行结果:北京今天:28℃,晴天,适合出门
✅ 最终答案:北京今天:28℃,晴天,适合出门(耗时 1191ms)
===== ToolUse Agent =====
用户指令:150 + 250等于多少?
🔧 选中工具:calculator
📊 执行结果:150.00 + 250.00 = 400.00
✅ 最终答案:150.00 + 250.00 = 400.00(耗时 899ms)
===== ToolUse Agent =====
用户指令:杭州今天天气怎么样??
✅ 最终答案:模型未选择任何工具(耗时 0ms)
七、工具的安全边界:避免“致命调用”
但工具越多风险越高——若工具能执行rm -rf /、转账等操作,模型一旦选错参数,后果不可逆。新手落地时要做好3层防护:
- 1. 权限控制:工具执行逻辑仅赋予最小权限(如
getWeather仅能读取模拟数据,无文件操作权限); - 2. 参数白名单:像Demo中
city参数用enum限定可选值,避免传危险值; - 3. 执行前校验:所有工具调用前,程序先校验参数(如Demo中
getWeather检查城市是否在白名单)。
八、核心总结
1. ToolUse模型的核心逻辑
- • 模型是“决策者”(通过
selectTool选工具、传参数),程序是“执行者”(通过executeTool调工具、返结果); - • 工具暴露给模型的核心是“四要素”(Name/Description/Parameters Schema/Result);
- • 好的Description(明确适用/不适用场景)是Agent稳定的关键。
2. 多工具管理的核心技巧
- • 工具数控制在3~5个(初版),超过10个做分组/动态检索/多Agent;
- • 参数Schema要加
required/enum/type,且程序端必须二次校验。
3. 落地关键
- • 可先扩展“工具分组”,再尝试动态工具检索;
- • 始终做好参数校验和权限控制,避免安全风险。
ToolUse模型是Agent从“能思考”到“能做事”的核心桥梁——我们能快速落地基础工具调用能力,后续可扩展至链式工具调用、多轮工具交互等高级场景。
MiaoAll