工具是 Agent 的手脚。模型再聪明,如果工具描述含糊、参数反直觉、错误信息无用,它也只能在错误的路径上反复尝试。
工具是给模型用的 API
给人用的 API 文档可以假设读者会读完整页、会查错误码、会理解业务背景。给模型用的工具没有这些假设:
- 它可能只看名称和一行描述就决定调用;
- 它不会「打开文档」;
- 它对参数的含义只能从字段名和描述推断;
- 它会把错误信息当成下一步的依据。
所以工具设计的目标不是「功能齐全」,而是在有限信息下做出正确选择并正确调用。
一个坏工具长什么样
{
"name": "do_stuff",
"description": "处理订单相关操作",
"parameters": {
"type": "object",
"properties": {
"data": { "type": "string" }
}
}
}问题一目了然:
- 不知道「处理」是查询、修改还是退款;
data是 JSON、ID 还是自然语言?- 模型什么时候该用它?什么时候不该用?
- 失败了会怎样?
模型只能猜。猜错之后,它往往会重试、换参数,浪费步数。
好工具的五个要素
1. 名称:动词 + 名词
✓ search_orders ✓ create_refund ✓ get_invoice_pdf
✗ orderTool ✗ handle ✗ process2名称是模型最先看到的信息。search_ 暗示只读,create_ 暗示写入,delete_ 暗示危险。不要用模糊的 manage_、handle_。
同一领域用统一前缀,便于模型区分:
orders_search / orders_get / orders_cancel
docs_search / docs_read / docs_list2. 描述:写清楚「不用」比「用」更重要
一个可用的模板:
用途:查询订单状态与物流信息。
何时使用:用户提供了订单号,且问题涉及配送、签收、退款进度。
何时不要使用:用户没有订单号时,先询问;需要修改地址时用 update_shipping。
参数:order_id 为 8 位订单号(如 A1024B7C),不是用户 ID。
返回:status、carrier、tracking_no、last_event、eta。
副作用:只读,可安全重试。尤其要写「何时不要使用」。很多误用不是因为模型不知道这个工具能做什么,而是因为它不知道还有更合适的工具。
3. 参数:越少越好,越具体越好
| 原则 | 说明 |
|---|---|
| 参数数量 ≤ 5 | 超过 5 个,模型漏填/填错的概率明显上升 |
| 扁平结构 | 避免 {filter: {date: {from, to}}},改成 date_from / date_to |
| 枚举优先 | status 用 enum,不要让模型自由发挥 |
| ID 不让模型编 | 先搜索拿到 ID,再传给写操作 |
| 单位写清楚 | amount_cents 比 amount 更不容易错 |
| 默认值少用 | 默认值会掩盖模型没想清楚的情况 |
一个常见错误是让模型传 user_id。模型不知道当前用户是谁,也不该知道。用户身份应该由服务端从会话中注入,而不是让模型填写。
4. 返回值:为模型裁剪,而不是原样透传
工具返回的对象通常有两类消费者:模型和程序。给模型的版本应该:
- 短:只保留决策需要的字段;
- 稳定:字段名和结构不要随 API 版本漂移;
- 带摘要:一句话说明结果,便于模型快速理解;
- 带分页:结果多时给
next_cursor,而不是一次返回 500 条; - 带来源:文档类返回
source、page、updated_at。
{
"summary": "找到 3 个订单,最近一笔已签收。",
"items": [
{ "id": "A1024B7C", "status": "delivered", "total": 299.0, "currency": "CNY" }
],
"next_cursor": null,
"truncated": false
}如果底层 API 返回 50KB,先在工具层裁剪。让模型读原始 JSON,既贵又容易迷失。
5. 错误:让模型知道下一步该做什么
错误不是异常,是观察。设计一个稳定的错误结构:
{
"ok": false,
"error": {
"code": "ORDER_NOT_FOUND",
"message": "未找到订单 A1024B7C。请确认订单号,或让用户提供下单手机号后四位。",
"retryable": false,
"details": { "order_id": "A1024B7C" }
}
}错误分类建议:
| 类型 | 例子 | 模型应该做什么 |
|---|---|---|
| 参数错误 | 日期格式不对、金额为负 | 修正参数后重试 |
| 未找到 | 订单不存在 | 询问用户,不要反复猜 |
| 权限不足 | 无权访问该账户 | 停止,转人工 |
| 暂时性错误 | 超时、503 | 退避重试一次 |
| 限流 | 429 | 等待或降级 |
| 业务拒绝 | 超出退款政策 | 解释原因,不要换工具绕过 |
关键字段是 retryable。没有它,模型只能凭感觉重试,或者直接放弃。
它们既占 token,又可能泄露内部结构。返回人可以理解的错误码和修正建议,详细堆栈写进服务端日志。
副作用:读与写必须分开
工具按风险分级:
| 级别 | 例子 | 策略 |
|---|---|---|
| 只读 | 搜索、查询、读文件 | 可自由调用,可并行,可缓存 |
| 可逆写 | 创建草稿、加标签 | 记录审计,失败可回滚 |
| 不可逆写 | 退款、发邮件、删数据 | 幂等 + 审批 + 限额 |
| 高危 | 改权限、转账、执行任意代码 | 沙箱 + 人工 + 多重校验 |
具体做法:
- 读写工具分开命名,不要一个
manage_order包办; - 写操作要求幂等键(
idempotency_key),重试不会重复执行; - 提供
dry_run参数,先返回「将会发生什么」; - 金额、数量、目标账户由服务端二次校验;
- 不可逆动作返回
requires_approval,由工作流挂起等待人工。
def create_refund(order_id: str, amount: float, idempotency_key: str, dry_run: bool = False):
order = db.get_order(order_id)
if order is None:
return err("ORDER_NOT_FOUND", "订单不存在,请先确认订单号", retryable=False)
if amount > order.paid_amount:
return err("AMOUNT_EXCEEDS_PAID", f"退款金额不能超过实付 {order.paid_amount}", retryable=False)
if amount > 500:
return {"ok": False, "requires_approval": True, "reason": "金额超过自动退款上限"}
if dry_run:
return {"ok": True, "preview": {"order_id": order_id, "amount": amount}}
result = payments.refund(order_id, amount, key=idempotency_key)
return {"ok": True, "refund_id": result.id, "eta_days": result.eta_days}工具数量:少而清晰
工具越多,选择空间越大,误用概率越高。经验值:
- 单一 Agent 5–15 个工具比较舒适;
- 超过 20 个,考虑分组、动态加载或拆分子 Agent;
- 功能重叠的工具(
search_docs和find_documents)必须合并。
动态工具加载的思路:先让模型选择「领域」,再只暴露该领域的工具。这样每次上下文里的工具定义更少,选择更准。
并行调用:把独立性写进描述
模型不会自动知道两个调用可以并行。可以在工具描述或系统提示中说明:
查询多个独立订单、多个城市天气、多个文档时,可以在一次回复中并行调用;
有依赖的调用(例如先搜索再读取)必须串行。宿主侧要正确处理:
- 同时发起多个调用;
- 分别捕获成功/失败;
- 按
tool_call_id合并结果; - 部分失败时返回每个调用的状态,而不是整体失败。
工具也需要测试
工具的质量不能靠「上线看看」。至少做三类测试:
- 契约测试:给定参数,返回结构是否符合 Schema;错误码是否稳定。
- 选择测试:给模型 20 个典型请求,看它是否选对工具、填对参数。统计混淆矩阵。
- 故障测试:超时、空结果、权限拒绝、部分失败时,Agent 是否能恢复或优雅停止。
一个很有效的做法是给每个工具准备 5 条「应该调用」和 5 条「不应该调用」的样本。工具描述每次修改都跑一遍,能提前发现歧义。
反模式
一个 execute_sql 或 run_code 能完成所有事,但也意味着模型可以读任意表、执行任意代码。除非在强沙箱里,否则不要给这种能力。
user_id、tenant_id、role 这类字段必须由服务端注入。让模型填,等于把越权入口交给提示注入。
浏览器工具返回原始 HTML 会瞬间塞满上下文。先抽取正文、标题、关键链接,再返回结构化结果。
「操作失败」无法帮助模型修正。至少给出错误类型、原因和可尝试的下一步。
检查清单
- 工具名是动词 + 名词,读写可区分
- 描述包含用途、适用/不适用场景、返回、副作用
- 参数 ≤ 5 个、扁平、枚举优先、单位明确
- 身份与租户信息由服务端注入
- 返回经过裁剪,带摘要、来源、分页
- 错误结构化,带 code / message / retryable
- 写操作幂等、限额、可审批、可 dry-run
- 工具数量受控,重叠工具已合并
- 每个工具有选择测试与故障测试
- 工具调用有超时、日志与权限校验
小结
- 工具是给模型用的 API,可发现性和可恢复性比功能数量重要。
- 名称、描述、参数、返回、错误,五个部分都要为「模型能否正确决策」服务。
- 读写分离、幂等、审批和沙箱,是副作用管理的底线。
- 工具需要像代码一样测试,尤其是「不该调用」的场景。
下一篇进入架构:把这些零件组装成一个能循环的系统。