3. 工具调用基础
好,咱们进入第三章。这一章讲的是工具调用的核心流程。说白了,就是 Agent 怎么决定用哪个工具、怎么把参数填对、怎么处理返回结果。
我在项目里见过不少同学,模型选得挺好,Prompt 写得也漂亮,结果卡在工具调用这一步——要么参数传错了,要么工具选得驴唇不对马嘴。嗯,今天咱们就把这块彻底掰开揉碎。
3.1 请求-响应模式:一次完整的工具调用长什么样
先看最基础的模式。一次工具调用,本质上就是一次请求-响应循环。我习惯把它拆成四个阶段:
- 意图识别:LLM 分析用户输入,判断要不要调工具、调哪个。
- 参数构建:LLM 生成工具需要的参数(JSON 格式)。
- 工具执行:系统拿到参数,调用真实 API 或函数。
- 结果回填:把工具返回的结果塞回 LLM 的上下文,让它继续生成回答。
核心要点:这四个阶段不是串行的,而是嵌套在 LLM 的推理循环里。每次工具调用,LLM 都会“暂停”一下,等结果回来再继续。
举个例子。用户说:“帮我查一下北京的天气。” 流程是这样的:
- LLM 识别出“查天气”这个意图 → 匹配到
get_weather工具。 - LLM 生成参数:
{"city": "北京"}。 - 系统调用天气 API,返回
{"temp": 22, "condition": "晴"}。 - LLM 拿到结果,组织成自然语言:“北京今天 22 度,晴天。”
你想想看,这个循环里最容易出问题的是哪一步?我个人经验是参数构建。模型经常把参数名写错,或者漏掉必填字段。
3.2 工具选择策略:基于 LLM 的意图识别
工具选择,说白了就是让 LLM 从一堆工具里挑一个最合适的。这不是简单的关键词匹配,而是语义级别的意图识别。
我常用的策略有三种,咱们用表格对比一下:
| 策略 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 函数描述匹配 | 给每个工具写一段自然语言描述,LLM 根据描述选 | 工具数量少(<10 个),描述清晰 | 工具多了容易混淆 |
| 嵌入向量检索 | 把工具描述转成向量,用语义相似度召回 | 工具数量多(几十上百个) | 需要维护向量库,有延迟 |
| 两阶段混合 | 先向量检索粗筛,再让 LLM 精排 | 大规模工具库,要求高准确率 | 实现复杂,成本高 |
我的建议:刚开始做项目,先用函数描述匹配。等工具超过 15 个、出现选择错误时,再上向量检索。别一上来就搞复杂的。
这里有个关键点——工具描述怎么写。我踩过坑。以前我写工具描述特别简略,比如“查询订单状态”。结果 LLM 经常把“查询物流”也匹配到这个工具上。
后来我改了写法:
// 不好的写法
"description": "查询订单状态"
// 好的写法
"description": "查询用户订单的当前处理状态,包括待支付、已支付、已发货、已完成。不包含物流轨迹查询。"
你看,加上边界说明(“不包含物流轨迹查询”),模型就很少选错了。这叫负例描述,很管用。
3.3 参数填充与校验:别让模型乱填参数
参数填充是工具调用里最细碎的活,也是最容易出 bug 的地方。我把它分成三步:
3.3.1 参数定义要严格
每个参数都要说清楚:
- 类型:string、number、boolean、array、object。
- 是否必填:required 字段里列清楚。
- 枚举值:如果参数只能取固定值,用 enum 限制。
- 描述:写清楚这个参数是干什么的,格式要求。
举个例子,一个完整的参数定义:
{
"name": "create_order",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "用户 ID,格式为 'U' 开头加 8 位数字"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"product_id": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 }
},
"required": ["product_id", "quantity"]
}
},
"payment_method": {
"type": "string",
"enum": ["alipay", "wechat", "card"]
}
},
"required": ["user_id", "items"]
}
}
注意:参数描述里不要写模糊的词,比如“合适的数量”。模型不知道什么叫“合适”。要写“最小 1,最大 99”。
3.3.2 参数填充的常见错误
我曾经遇到过这么个情况:模型把用户说的“帮我查一下张三的订单”,填成了 {"user_name": "张三"},但工具接口要求的是 user_id。结果查不到数据。
这类问题我总结了一下:
| 错误类型 | 表现 | 解决办法 |
|---|---|---|
| 字段名映射错误 | 用自然语言里的词代替参数名 | 在描述里写明“参数名必须严格使用 xxx” |
| 类型错误 | 数字填成字符串,数组填成逗号分隔 | 在描述里给示例值,比如 "example": 100 |
| 遗漏必填参数 | required 字段没填全 | 在描述里强调“此参数必填,不可省略” |
| 枚举值超出范围 | 填了 enum 里没有的值 | 用 enum 限制,并在描述里列出所有可选值 |
3.3.3 校验机制:别信模型,要校验
模型生成的参数,一定要做服务端校验。我见过太多人直接把模型输出的 JSON 扔给 API,结果出各种问题。
校验分两层:
- 格式校验:JSON 是否合法?字段类型对不对?必填字段有没有?
- 业务校验:用户有没有权限?数据存不存在?值在不在合理范围?
格式校验可以用 JSON Schema 自动做。业务校验得自己写逻辑。
// 伪代码示例
function validateAndCall(toolName, params) {
// 1. 格式校验
const schema = getToolSchema(toolName);
const valid = validateJsonSchema(params, schema);
if (!valid) {
return { error: "参数格式错误,请重新生成" };
}
// 2. 业务校验
if (toolName === "create_order") {
if (!userExists(params.user_id)) {
return { error: "用户不存在" };
}
if (params.items.length === 0) {
return { error: "订单不能为空" };
}
}
// 3. 调用工具
return callTool(toolName, params);
}
小技巧:校验失败时,把具体的错误信息返回给 LLM,让它重新生成参数。比如“参数 user_id 格式错误,应为 U 开头加 8 位数字”。模型看到这个,通常能自己修正。
3.4 完整流程:一张图看懂
下面这张图把整个工具调用流程串起来了。我建议你仔细看看,尤其是校验失败后的重试循环——这是很多系统忽略的。
3.5 避坑指南:我踩过的几个坑
最后分享几个实战中遇到的坑,你遇到了能少走弯路。
坑一:模型生成参数时,会把用户输入里的同义词直接填进去
比如用户说“用微信支付”,模型填了 "payment_method": "微信支付",但枚举值是 "wechat"。解决办法:在参数描述里写明“必须使用英文枚举值,不要用中文”。
坑二:工具返回结果太大,把 LLM 的上下文撑爆了
我曾经查一次订单,返回了 200 条记录,直接把 干满了。解决办法:在工具描述里写明“默认只返回前 10 条”,或者让工具支持分页参数。
坑三:模型在工具调用失败后,会自己编一个结果
嗯,你没看错。如果校验失败后没有把错误信息返回给 LLM,模型可能会说“好的,已为您创建订单”,实际上啥也没干。所以一定要把错误信息明确返回,并且让模型知道“工具调用失败,请重试”。
好了,这一章的内容就这些。工具调用流程看着简单,但每个环节都有细节。你只要把意图识别、参数构建、校验、结果回填这四个环节做扎实了,大部分问题都能避免。