工具是 Agent 的手脚,设计不好手脚就打架
绝大多数"Agent 不听话"的问题,根源不在模型,在工具设计。
我在 Anthropic 和 OpenAI 的工程博客里反复看到同一个结论:换更强的模型,不如把工具设计好。一个能用的 Agent 通常只需要 3~10 个设计精良的工具,而不是 30 个凑数的。这篇把工具设计的几个关键点讲透,包括粒度、命名、参数、错误反馈,以及工具数量膨胀时怎么办。
粒度:太粗不会用,太细走岔路
工具粒度是第一个要拍板的事。做一个"打开文件并读取前 100 行"的工具,还是拆成"打开文件"+"读取"两个?
经验法则是:一个工具应该对应一个用户在自然语言里会单独提到的操作。
判断标准很简单——想象用户说这句话:"帮我打开 config.json 然后读一下"。用户把这两件事明确分开了,所以应该是两个工具。再换一句:"帮我查一下北京天气"。用户没说"先连网络再发 HTTP 再解析 JSON",所以 get_weather 应该是一个工具,把里面的细节藏起来。
太粗的工具,模型会抱怨"工具能力不够用,我想做的做不了"。太细的工具,模型会在工具之间反复横跳,上下文膨胀、token 爆炸。大部分人犯的错是太细——因为写代码的人的本能就是拆函数。写 Agent 工具时要刻意反过来,能合就合。
好的例子: search_code(query: str, language: str | None = None) -> list[Match]
差的例子: list_files() + read_file() + grep_in_file() 三个工具让模型自己组合
命名:用动词开头,让语义 self-evident
模型看到工具名时,它没看到你的注释和文档——至少第一眼没看到。好的命名能让模型在看到名字的瞬间就知道用来干什么。
几条可以照抄的规则:
用动词开头。search_docs 比 docs 好,send_email 比 email_handler 好。动词让模型在决策时的语感和自然语言指令对齐。
名字里带对象。search_docs 比 search 好。当工具库有 10 个工具时,模型靠名字就能快速定位。
避免缩写和内部术语。evict_cache 看起来酷,但对模型是混淆信号。clear_cache 或 invalidate_cache 更好。公司内部系统名字比如 call_spanner 对模型没有任何意义。
同类工具用一致前缀。file_read / file_write / file_list 一看就是一组,模型在需要操作文件时会自然地从这组里选。
参数:Schema 是第二份说明书
参数的 JSON Schema 不只是"告诉运行时怎么校验",它本质上是模型决策时的另一份说明书。每个参数的 description 写得好不好,直接决定模型调用时填得对不对。
{
"type": "function",
"function": {
"name": "search_code",
"description": "在代码库里搜索匹配查询的代码片段。支持自然语言语义搜索和精确关键词搜索。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索内容。可以是自然语言描述(例如 '处理用户登录的函数')或精确关键词(例如 'handleLogin')",
},
"language": {
"type": "string",
"enum": ["python", "typescript", "go", "rust"],
"description": "限定编程语言。不填则搜所有语言",
},
"max_results": {
"type": "integer",
"default": 10,
"description": "最多返回多少个匹配。默认 10,性能允许可以到 50",
},
},
"required": ["query"],
},
},
}
几个细节:
description 要写"为什么"不只是"是什么"。上面 query 那段告诉模型两种用法,模型看到后会根据任务选择合适的查询风格。
用 enum 限制可枚举参数。不仅能防止模型乱填,还能让模型知道"哦原来只有这几种选择"。
default 写清楚。模型看到 default 会默认不填,避免每次都要编一个值。
required 精简。非必填的就别列进 required,给模型留灵活度。
错误反馈:Agent 的"老师"
Agent 最大的学习机会来自调错了之后收到的错误信息。这一点被很多人低估。
返回 {"error": "failed"} 是最糟的——模型不知道怎么错的,只能重试。好的错误反馈长这样:
def search_code(query, language=None, max_results=10):
if max_results > 100:
return {
"error": "参数错误",
"message": f"max_results 最大 100,你传了 {max_results}",
"hint": "如果需要更多结果,请分批查询",
}
if not query.strip():
return {
"error": "参数错误",
"message": "query 不能为空",
"hint": "如果你想列出所有代码,用 list_files 工具",
}
...
error + message + hint 三段式。error 给模型一个可匹配的错误类型,message 给模型具体情况,hint 告诉模型下一步怎么做。
特别注意那个 hint——它相当于工具开发者在告诉模型"你找错工具了,去试 X"。这种反馈是 Agent 能"学"的最短路径。
类似的模式在系统级错误(网络、权限、超时)里同样重要:
return {
"error": "permission_denied",
"message": f"无法读取 {path},需要管理员权限",
"hint": "此路径不应该被 Agent 访问。请尝试 /tmp 或 ./output 下的路径",
}
对比只返回 "Permission denied" 这种 Linux 风格的错误——后者模型完全不知道该换路径还是该申请权限,只能瞎试。
工具数量膨胀:20+ 工具之后的灾难
当你的 Agent 有 30 个工具时,它会开始出现一些奇怪的行为。模型调用错误工具的概率随工具数量非线性增长。原因是每一个工具的 schema 都要塞进 prompt,到 30 个时 system prompt 已经好几千 token,模型注意力稀释。
有几种常见的解法:
工具分组 + 两级路由。把工具按领域分组:file_* / db_* / web_* / notification_*。第一轮让模型只看"分组列表"和对应的简短描述,选一个组;第二轮再把这个组的全部工具展开给它。这个模式叫 hierarchical tool calling,在 Cursor、Claude Code 这些生产 Agent 里都用。
工具过滤。根据当前任务类型动态筛选工具。例如用户说"帮我部署",就只暴露 deploy_* 和 git_* 相关工具,不暴露 email_*。过滤逻辑可以用 embedding 相似度:把用户输入向量化,和每个工具的 description 向量化后做相似度排序,选 top-K。
合并工具。重新审视是不是真需要这么多工具。很多时候 get_user_by_id / get_user_by_email / get_user_by_phone 可以合成一个 find_user(query, by="id|email|phone")。
Anthropic 公开的工程经验是一个 Agent 超过 20 个工具就要警惕,超过 40 几乎一定会出问题。
并行工具调用
现代 API(OpenAI / Anthropic / 大部分开源模型)都支持一次返回多个 tool_calls。用户问"北京和上海天气分别怎样"时,模型可以一次发两个 get_weather 请求,你应该并行执行:
import asyncio
async def run_tools_parallel(tool_calls):
async def run_one(tc):
fn = TOOLS[tc.function.name]
args = json.loads(tc.function.arguments)
return tc.id, await asyncio.to_thread(fn, **args)
results = await asyncio.gather(*[run_one(tc) for tc in tool_calls])
return results
不做并行,一个需要查 10 个地点天气的任务会老老实实跑 10 次 API。做了并行,一次就返回。对长任务的延迟优化是决定性的。
需要注意的是并行调用需要工具之间没有依赖。如果模型错误地并行发起了"先建文件夹再在文件夹里写文件"这两个调用,就会出错。解法是让模型在 prompt 里意识到"有依赖的工具要分两轮调用,无依赖的才能并行"——推理模型基本都能自己处理好。
工具即 API,API 即产品
最后一句值得记的原则:工具就是给模型用的 API,API 的所有设计原则都适用。
一个好的人类 API——命名清晰、参数合理、错误信息友好、文档完善——同样是好的模型 API。反过来也成立。所以如果你已经在写人用的 SDK,写 Agent 工具就是把 SDK 再精心设计一遍,因为模型是个"读说明书非常字面、稍不留神就理解偏"的用户。
下一篇讲 Plan-and-Execute——当 ReAct 遇到需要 20 步的任务,为什么光靠"思考一步做一步"会漂移,以及怎么用"先规划再执行"来锁定全局目标。
相关阅读
- Writing tools for agents (Anthropic) — Anthropic 的工具设计指南
- OpenAI Function Calling Best Practices — 官方建议
- The Unix Philosophy of Agents — 小工具组合胜过大工具
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:03. 工具设计的艺术:粒度、命名、错误反馈与工具爆炸
本文链接:https://www.sshipanoo.com/blog/ai/ai-agent/03-工具设计/
本文最后一次更新为 天前,文章中的某些内容可能已过时!