Tool 的 description 比模型选择更决定你的 agent 上限。
Day 3 我们说 harness 是 agent 的 OS。OS 之上跑的是工具,而工具的样子由 tool schema 决定。一条几乎所有人都低估的事实:你写在 description 里的那几行英文,对最终成功率的影响,比换模型还大。Anthropic 在 SWE-bench 的内部消融里反复证实——同一个 Sonnet,仅重写 6 个工具的 description,pass@1 能差 10+ 分。tool schema 就是 prompt 的一部分,它和 system message 一样进 KV cache、参与 attention、影响下游每一个 token 的分布。这一期讲四件事:怎么把 schema 当 prompt 写、为什么 20 个工具往往不如 6 个、原子 vs 组合工具的真实 trade-off、以及 parallel tool calls 这个被严重浪费的能力。最后给出 Anthropic 工程团队的「7 条 tool design 经验」清单。
name,是 description 和 input_schema 里每个字段的措辞。tool definition 最终是怎么进入模型的?Anthropic 在 Tool Use Overview 文档里讲过:所有注册的 tool 会被序列化成一段结构化文本,拼到 system prompt 末尾。也就是说,从模型视角看,tools=[...] 不是「函数注册表」,而是一段它必须读懂的文档。它要靠这段文档决定:什么时候调?调哪个?参数怎么填?什么时候不该调?
这就解释了几个被反复观察到的现象:
description 从 1 行扩到 5 行,准确率明显提升——因为你给的不只是名字,是使用场景。description(注意不是参数的名字)写清楚单位、格式、边界,比改名字 start_date → start_date_iso8601 有用得多。模型读 description,不太「读」名字。["fast","accurate","creative"] 命中率高 2-3 倍。底层机制:tools block 进 KV cache 后,每个 token 生成都会 attend 这段文本。description 越具体、越场景化,模型在「要不要调」「调哪个」这两个决策点上得到的 conditioning 越强。这不是玄学,是 attention 的物理事实。
同一个查天气工具,两种写法的命中率差距:
# —— BAD:把 schema 当函数签名 ——
{
"name": "get_weather",
"description": "Get weather",
"input_schema": {
"type": "object",
"properties": {
"location": {"type":"string"},
"unit": {"type":"string", "enum":["c","f"]}
}
}
}
# —— GOOD:把 schema 当 prompt ——
{
"name": "get_weather",
"description": "Look up the CURRENT weather (now ± 1h) for a single city.
Use ONLY for present-time questions like 'is it raining in Tokyo now'.
DO NOT use for: forecasts >24h ahead (use get_forecast), historical
weather (use get_weather_history), or air quality (use get_aqi).",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name in English, optionally followed by
country, e.g. 'Tokyo', 'Paris, FR'. Do not pass GPS coords."
},
"unit": {
"type": "string", "enum": ["celsius","fahrenheit"],
"description": "Temperature unit. Default to celsius unless the user
explicitly mentions °F or asks in a US locale context."
}
},
"required": ["location"]
}
}
三处关键升级:(1)description 写明使用场景 + 反例(DO NOT use for…),这是降低跨工具误选的最便宜手段;(2)每个参数自己的 description 写清格式、约束、默认推断规则;(3)enum 值用全称(celsius 而非 c),可读性 = 模型理解度。把这三条当 checklist,每个工具都过一遍。
"description": "do X"——模型不知道什么时候不该调,于是会在边界场景过度调用。(2)依赖工具名表达语义(get_user_v2_by_email_only)——模型看 description,不看 snake_case 名字的 token 拆分。(3)参数 description 全省略——模型只能从 type 推格式,遇到日期 / 路径 / ID 这种就靠猜。
这是一个常被忽视的工程事实:tool 数量和准确率不是单调关系,而是一条倒 U 曲线。给 0 个工具,没事干;给 3-7 个正交工具,agent 表现最好;给 20+ 个工具,模型开始混淆、误选、漏选、用错参数。Berkeley Function Calling Leaderboard(BFCL)的多轮场景里能看到这条曲线,Anthropic Building Effective Agents 里也明说:「Tool definitions deserve as much prompt engineering attention as your main prompt.」
为什么会退化?三个真实原因:
search_docs / find_in_docs / lookup_documentation),模型在它们之间摇摆。这也是为什么 Claude Code 的核心 tool registry 只有 ~10 个原子工具(Read / Edit / Write / Bash / Grep / Glob / Task / WebFetch / WebSearch / TodoWrite)——其余能力通过 MCP 按需挂载,而不是常驻。
当你的 MCP / agent 已经堆了 30+ 工具,应用 3 步「工具减肥法」:
# Step 1:按调用频率排序,看长尾
sqlite3 agent.db "SELECT tool_name, COUNT(*) c FROM tool_calls
GROUP BY tool_name ORDER BY c DESC;"
# 长尾里的 tool(< 1% 调用)几乎都可以删掉或合并
# Step 2:找「语义双胞胎」并合并
# search_files / find_files / list_matching → 合并为 search_files(pattern, mode)
# Step 3:按场景而非按 API 切分
# BAD:get_user_by_id / get_user_by_email / get_user_by_username
# GOOD:get_user(query: {id?|email?|username?}) ← 一个工具,input 自描述
另一个反直觉技巧:「dynamic tool surface」。Cursor / Claude Code 在不同 mode 下暴露不同工具集——plan mode 物理隐藏 Write/Edit,让模型不需要在「读」「写」两类工具间分散选择压力。你也可以在自己的 harness 里按 task type 切换 tool registry:研究任务给 web / fetch,写代码任务给 read / edit / bash。
给 agent 一个 edit_file(path, old, new) 和给它一个 refactor_function(path, fn_name, new_impl),是两种世界观。前者是原子工具(atomic)——模型自己组合;后者是组合工具(composite / macro)——一个调用完成多步业务逻辑。这是 tool design 里最重要、最被忽略的 trade-off:
Claude Code 选了偏原子的路线:Read / Edit / Write / Bash 这种 Unix 哲学的小工具,组合性靠模型。这让它能处理任意 coding 任务,但要求模型有非常强的规划能力——也是为什么它在弱模型上效果一般。相反,传统 RPA 工具走偏组合路线:每个流程一个专用 tool(process_invoice / onboard_employee),可靠但脆,遇到新流程就要写代码。
真实工程中,两种工具应该分层共存:底层原子工具暴露给「探索 / 调试 / 一次性任务」,上层组合工具暴露给「高频 / 可靠性敏感 / 已模式化的任务」。
一个真实场景:让 agent 给 git repo 做 release。原子路线 vs 组合路线:
# —— 原子路线:4 个底层工具,模型自己组合 ——
tools = [run_bash, read_file, write_file, git_command]
# agent 必须自己规划:bump version → update changelog → commit → tag → push
# 优点:万一规划要变(先跑测试再 bump)也能自己改;新 repo 直接用
# 缺点:跑 10 次有 1-2 次顺序错 / 漏 tag / 提交到错分支
# —— 组合路线:1 个 macro tool ——
tools = [{
"name": "release",
"description": "Run the full release flow: bump version, regenerate
CHANGELOG, commit with 'chore: release vX.Y.Z', tag, push branch+tag.
Aborts on any failing step. Use when user asks to 'cut a release' or
'publish a new version'. Does NOT publish to npm—call npm_publish after.",
"input_schema": {"type":"object",
"properties":{"bump":{"type":"string","enum":["patch","minor","major"]}},
"required":["bump"]}
}]
# 优点:跑 100 次都按同一个 flow,eval 简单
# 缺点:换个 repo 流程不一样就废了;agent 不能针对异常情况微调
实战决策树:
open_file / read_lines / close_file 这种 1980 年代 C API,每次操作要 3 个 tool call。
从 Sonnet 3.5 开始,Claude 在一次 assistant turn 里可以返回多个 tool_use block;OpenAI 的 GPT-4o / o3 也支持类似能力。harness 只需要识别多个 tool_use 后并发执行、把多个 tool_result 拼回下一轮就行。这件事的红利是巨大的:
但能让它「真的」并行起来,有四个工程前提,缺一个就退化回串行:
asyncio.gather / 线程池。把 §3 的原子 harness 升级成并发执行:
import asyncio, anthropic
client = anthropic.AsyncAnthropic()
async def dispatch(block): # 单个 tool 异步执行
handler = TOOLS[block.name]["handler_async"]
try:
out = await handler(block.input)
except Exception as e:
out = f"ERROR: {type(e).__name__}: {e}"
return {"type":"tool_result", "tool_use_id":block.id, "content":str(out)}
async def agent(task, max_iters=20):
msgs = [{"role":"user","content":task}]
sys = ("You are a careful agent. "
"IMPORTANT: when multiple tool calls are independent, " # ← 关键
"emit them in the SAME response so they run in parallel.")
for _ in range(max_iters):
r = await client.messages.create(model=MODEL, system=sys,
tools=SCHEMAS, messages=msgs, max_tokens=4096)
msgs.append({"role":"assistant", "content":r.content})
if r.stop_reason == "end_turn": return r
uses = [b for b in r.content if b.type == "tool_use"]
results = await asyncio.gather(*[dispatch(b) for b in uses]) # ← 并发
msgs.append({"role":"user", "content": results})
实测:让 agent「读 README.md / package.json / .github/workflows/ci.yml 然后告诉我这是个什么项目」——串行版 ~9s,并行版 ~3.5s,准确率几乎无差。读 5+ 文件时差距更大。
edit_file(same_path) 互相覆盖。给写类工具加 file-level 锁。(3)误以为 multi-tool 就是 parallel——模型在同一个 response 里返回多个 tool_use 才是 parallel;分多 turn 调多 tool 是 sequential。
把前面四点浓缩成一份可贴墙的 checklist。下次设计或 review 一组 tool 时逐条过:
"fast" 不如 "fast: prioritize latency, accept ±10% accuracy"。handler 抛出的 error 要包含「为什么错 + 下一步怎么办」,因为 agent 会读它来 self-correct。"ENOENT" 没用,"File '/x.json' not found. Use list_dir to see available files." 才有用。create_or_update 而非 create。agent 重试是常态。这 7 条来自 Anthropic Building Effective Agents 与 Tool Use Best Practices 的工程指南。把它做成一个 PR checklist 模板,每次新增工具时填一遍——agent 项目能少 80% 的 silent failure。