工具即提示:Agent-Computer Interface(ACI)设计方法论
系列导航:00 系列导航 · 01 单 Agent 总论 · 02 ReAct 原理 · 03 手写内核 ·04 ACI 工具设计· 05 上下文工程 · 06 两个增强变体 · 07 上线前清单
小提一句: 由于平台每日发布笔记数量限制,目前系列笔记还没全部上传。后续内容会持续更新,大家可以先点个关注、收藏,以免错过~
引子:这是整个系列投入产出比最高的一篇
第 1 篇给过一张表:单步成功率p决定长程任务的一切。
| 单步成功率 p | 10 步成功率 | 20 步成功率 |
|---|---|---|
| 0.90 | 35% | 12% |
| 0.95 | 60% | 36% |
| 0.98 | 82% | 67% |
| 0.99 | 90% | 82% |
现在的问题是:怎么把 p 从 0.90 提到 0.98?
换更强的模型?能提一点,但贵,且很快遇到瓶颈。真正有效的手段是ACI(Agent-Computer Interface)设计,也就是把工具接口设计对。这是 Anthropic 在构建 SWE-bench Agent 时得出的头号经验,他们的原话是:
投入在工具定义上的提示工程精力,应当与投入在 system prompt 上的同等。
换句话说:工具描述不是文档,它是提示词的一部分,而且是每一步都会被重读的那部分。
一、ACI 与 HCI:模型是一种什么样的"用户"
Anthropic 用的类比是:像做人机交互(HCI)设计一样做 ACI 设计。这个类比很好,但要知道模型这个"用户"和人有几个本质区别:
| 维度 | 人类用户 | 模型"用户" |
|---|---|---|
| 交互通道 | 视觉 + 直觉 + 常识 | 只有文本,且只能看到你放进上下文的文本 |
| 错误处理 | 看到报错会试错、会查文档 | 只能从错误信息里学,且这次学到的下次不一定记得(除非你还留在上下文里) |
| 记忆 | 长期记忆、跨会话 | 本次上下文内的记忆,出了窗口就是陌生人 |
| 耐心 / 成本 | 免费 | 每读一个 token 都要钱和时间 |
| 常识 | 有,能脑补缺失信息 | 形式上"有",但脑补出来的就是幻觉 |
| 反馈循环 | 可以问同事 | 只能靠你返回的错误信息 |
由此得到 ACI 设计的第一原则:
不要假设模型能推断任何东西。任何它没有在上下文里看到的信息,对它都不存在;任何它看起来"推断出来"的东西,都可能是编的。
这条原则长出了下面全部 12 条军规。
二、工具定义的七要素模板
一个合格的工具定义应该长这样(以search_logs为例):
- search_logs 用途 :按服务名与时间范围检索错误日志,返回日志条目摘要。 何时用 :已经确定要查某个具体服务的日志时。 何时不用:尚未确定错误发生在哪一层(网关 / 服务 / DB)时——先用 query_metrics 定位层级。 参数 :service string 必填 服务名;若 5xx 产生于网关层,此处填 "gateway" since string 必填 绝对时间,格式 "YYYY-MM-DD HH:mm:ss"; 不知道当前时间请先调用 get_current_time level string 可选 枚举 error|warn|info,默认 error limit integer 可选 1-100,默认 20 返回 :最多 limit 条日志摘要,每行 "<时间> <服务> <消息>"; 若被截断会注明匹配总数。 常见错误:传入相对时间(如 "30分钟前")会导致解析失败,必须使用绝对时间。七要素:
| 要素 | 作用 | 缺了会怎样 |
|---|---|---|
| 用途 | 一句话说明它是干什么的 | 模型靠猜 |
| 何时用 | 正向触发条件 | 该用的时候不用 |
| 何时不用 | 负向边界 | 不该用的时候乱用(最常被省略) |
| 参数(含必填/枚举/默认值/单位/格式) | 消除参数歧义 | 参数乱填、缺参数、格式错 |
| 返回(含形状与截断行为) | 让模型知道能拿到什么 | 重复调用、误判"没有结果" |
| 常见错误 | 把踩过的坑写回描述 | 同一个错反复犯 |
| 与其他工具的分界 | 见"何时不用" | 工具混淆,选择准确率暴跌 |
其中“何时不用”是我最想强调的一条。我们在自己系统里做过统计,加上负向边界后,工具误选率下降最明显的就是这一项。因为模型犯的错往往不是"不知道该用什么",而是"两个都像对的"。
三、12 条军规
A 组:命名与边界
- 工具名是自解释的动宾短语,且两两互斥。
反面教材:get_data/query/fetch_info三个工具同时存在,模型(和人类)都无法区分。判断标准:把工具名和描述盖住用途字段,只看名字能否猜出该不该用。 - 每个工具显式写"何时不用"。
见上。尤其是能力有重叠的工具对(如search_logsvsquery_metrics),必须互相点名:“若你要找的是数值随时间的变化趋势,用 query_metrics 而不是本工具。” - 能合并就合并,不能合并就切干净。
高频共现的两个操作(如"查指标"和"查该指标的历史基线")应合成一个带参数的工具,而不是两个。切分的原则是按"决策语义"切,不按"后端接口"切。别把你们微服务的边界直接暴露给模型。
B 组:参数设计
- 格式贴近模型在自然文本中见过的形式。
模型的先验来自互联网文本。它见过无数次"2026-09-30 14:20:00",几乎没见过"2026-09-30T14:20:00.000+08:00|slot:3"。用前者。 - 不要制造"格式开销"(format overhead)。
Anthropic 的原话:避免让模型做它不擅长且需要精确计数的体力活。三类典型陷阱:
| 陷阱 | 例子 | 后果 |
|---|---|---|
| 要求精确计数 | “传入要修改的行号范围” | 模型数错行数,改错地方 |
| 要求复杂转义 | “把这段代码的字符串转义后传入” | 转义漏一个反斜杠,语法错 |
| 要求重排/排序 | “按依赖顺序传入模块列表” | 顺序错,执行失败 |
修法一律是:让工具接受更宽松的输入,由代码做规范化。比如让工具接受"文件路径 + 函数名"而不是"行号"。
- 参数要自带防错能力(Poka-yoke)。
见第 4 节,这是本组最重要的一条。
C 组:返回设计
- 返回"可据以决策的信息",而不是原始数据 dump。
模型需要的是"这个指标在 t-11m 突增",不是 1800 个采样点。让工具在服务端做降采样、聚合、异常标注,把计算放在工具里,而不是放在模型的上下文里。 - 截断必须显式标注,并告诉模型怎么拿更多。
第 3 篇的truncate()已经处理了这点。补充:截断信息里最好带一句"如需查看第 N 到 M 条,调用read_range(handle, start, end)"。 - 结构化优先,但保持自然语言可读。
纯 JSON 对模型友好但费 token;纯文本省 token 但易误读。折中:用简短的自然语言行 + 明确的字段名,如<时间> <服务> <消息>的固定行格式。模型对固定行格式的解析准确率极高,且比 JSON 省 30~50% token。
D 组:错误与反馈
- 错误信息必须可操作:说清"错在哪 + 怎么改"。
| ❌ | ✅ |
|---|---|
Error 400 | [ToolError] search_logs 的 since 参数需要绝对时间("YYYY-MM-DD HH:mm:ss"),收到 "30分钟前"。请先调用 get_current_time 获得当前时间再换算。 |
File not found | [ToolError] read_repo_file 找不到 "/src/db/pool.py"。仓库中的候选路径:["order-service/db/pool.py", "common/db/pool.py"]。路径必须是从仓库根开始的绝对路径。 |
第二条里那个"候选路径"是神来之笔:把纠错所需的信息直接放进错误里,模型一次就能改对,而不是把"找不到文件"当成一次探索去反复试。
- 空结果必须区分"真的没有"与"查询条件错了"。
[]是最糟糕的返回:模型无法判断是"没有错误日志(好事)“还是"服务名写错了(坏事)”。返回:
匹配 0 条。可能原因:(a) 该服务在此时间窗内确无 error 级日志; (b) service 名称不正确(已知服务:order, gateway, payment)。 建议先调用 list_services 确认服务名。- 把线上踩过的坑写回工具描述。
工具描述应该是一个活的文档。每当你从 trace 里发现一类新的工具误用,就往描述的"常见错误"里加一条。这是 ACI 唯一正确的迭代方式——不是拍脑袋重写,而是由失败案例驱动。第 4 节的混淆矩阵和第 7 节的评测,就是在告诉你该往哪儿加。
四、Poka-yoke:让错误在接口层面不可能发生
“防错”(poka-yoke)源自丰田生产体系,指的是通过设计让错误无法发生,而不是靠操作者小心。
Anthropic 的经典案例:他们的 SWE-bench Agent 在移出根目录后,用相对路径频繁出错。修法不是"在 prompt 里提醒模型用绝对路径",而是把工具参数改成强制绝对路径。此后该错误归零。
这个案例的精髓在于:约束放在代码里,而不是放在提示里。提示是建议,代码是物理定律。
一批可以直接抄的 poka-yoke 清单:
| 风险 | 弱做法(提示) | 强做法(接口) |
|---|---|---|
| 路径写错 | “请使用绝对路径” | 参数 schema 要求以/开头,收到相对路径时自动基于仓库根解析并在返回中告知解析结果 |
| 时间写错 | “请使用绝对时间” | 提供get_current_time工具;参数接受"30m"这类枚举化相对偏移并由服务端换算 |
| 枚举写错 | 描述里列出枚举 | 用 JSON Schemaenum约束,非法值直接拒绝并回候选 |
| 删改误操作 | “请谨慎操作” | dry_run参数默认 True;写操作标记side_effect=True走授权钩子 |
| 返回过大 | “结果可能很长” | 强制分页 + 上限(如limit ≤ 100),超限直接拒绝并提示收窄条件 |
| 参数单位歧义 | “window 单位是分钟” | 参数名带单位:window="30m",或只接受带单位的字符串 |
| 危险命令 | “不要执行 rm” | 工具层做命令白名单,非白名单直接拒绝 |
所以:每当你想在 prompt 里写"请注意……"的时候,先问一句:这个约束能不能放进代码?能放进代码的,绝不留给提示。
五、工具数量:为什么"全集"是个坏主意
5.1 甜点区
⚠️ 经验值,缺严格定量研究,但在多个团队复现过:< 10 个精选工具,通常优于 50 个全集。
原因有三:
- 选择准确率随候选数下降。这是最直接的原因:候选翻倍,误选率不是线性上升,而是在语义相近的工具上集中爆发。
- 工具描述本身是固定 token 开销。算一笔账:
20 个工具 × 平均 150 token = 3000 token 工具定义 每一步都要重发(除非命中前缀缓存) 12 步 × 3000 = 36,000 token —— 纯工具描述,不含任何任务内容这还没算它对注意力的稀释。
3. 工具越多,边界越难写干净。50 个工具里不可能两两互斥,必然有重叠,重叠就是误选的温床。
5.2 超过 10 个怎么办:三层路由
⚠️ 第三层有个真实风险:模型不知道自己不知道什么。它不会去search_tools一个它压根没听说过的工具。所以长尾层只适合"模型大概率知道该能力存在、只是不知道确切名字"的场景(如公司内部几十个数据接口)。如果能力本身模型没概念,检索工具救不了你。
5.3 与 MCP 的关系
MCP(Model Context Protocol)解决的是工具接入的标准化(怎么把外部系统接进来),不解决工具选择的质量问题。恰恰相反,MCP 让接入变得极其容易,反而使"工具全集"问题更严重:能接不等于该接。接入之后,本文的裁剪与路由逻辑依然要自己做。
六、返回值的形状:一个被严重低估的杠杆
同样的信息,返回形状不同,模型的下一步决策质量可以差很多。四条具体建议:
6.1 给"结论"而不是"原料"。query_metrics返回 1800 个采样点 vs 返回"降采样 6 点 + 一句『在 t-11m 发生突增(斜率 > 20×)』"。后者可能只有 50 token,但包含了模型真正需要的决策依据。让工具承担计算,让模型承担判断。
6.2 空结果要给归因建议。(见军规 11)
6.3 大内容给句柄,不给全文。
❌ Observation: <20000 token 的完整日志> ✅ Observation: 匹配 187 条,已存入 handle=log://a3f1。前 5 条预览: t-11m order ConnectionPoolExhausted: timeout waiting for connection ... 调 read_range("log://a3f1", start, end) 可读取指定区间。这同时解决了三个问题:上下文膨胀、注意力稀释、“模型以为自己看完了全部”。
6.4 让返回自带"下一步提示"。
在返回末尾加一行[下一步建议] 若需确认该变更的 diff,调用 read_repo_file(path, ref="v2.14.0")。这属于"引导"而非"控制",模型可以忽略它,但在它没主意时非常有效。⚠️ 注意别滥用:加太多会让返回变长,且可能诱导模型放弃自主判断。
七、怎么评测你的工具集:一个 60 行的评测脚本
ACI 改得好不好,不能靠感觉。下面这个脚本能测出工具选择准确率和参数质量,跑一次几分钟:
importjson,asyncio,collections# 评测用例:只测"给定上下文,模型该选哪个工具"CASES=[{"ctx":"订单服务 5xx 突增,需要确认是从哪一分钟开始涨的。","expect_action":"query_metrics","expect_args_keys":["service","metric"]},{"ctx":"已确认错误产生于网关层,现在要看网关在这一时段的错误日志。","expect_action":"search_logs","expect_args_keys":["service","since"]},{"ctx":"我怀疑是 v2.14.0 改了连接池配置,想看这个文件的这个版本。","expect_action":"read_repo_file","expect_args_keys":["path"]},# ... 建议 >= 50 条,覆盖每个工具的正例与易混淆反例]PROBE="""你有以下工具: {specs} 当前情况:{ctx} 请只输出一个 JSON 动作块,表示你的下一步: ```json {{"action": "...", "args": {{...}}}} ```"""asyncdefeval_aci(tools,llm,cases=CASES):specs=tools.render()hit=0arg_ok=0unparsable=0confuse=collections.Counter()forcincases:raw=awaitllm(PROBE.format(specs=specs,ctx=c["ctx"]))thought,call=parse_output(raw)# 复用第 3 篇的解析器ifcall.name=="__unparsable__":unparsable+=1continueifcall.name==c["expect_action"]:hit+=1else:confuse[(c["expect_action"],call.name)]+=1# 混淆对ifall(kincall.argsforkinc["expect_args_keys"]):arg_ok+=1n=len(cases)print(f"工具名准确率 :{hit/n:.1%}")print(f"必填参数完整率:{arg_ok/n:.1%}")print(f"格式非法率 :{unparsable/n:.1%}")print("\n最易混淆的工具对(期望 → 实际):")for(exp,got),kinconfuse.most_common(10):print(f"{exp}→{got}{k}次")return{"action_acc":hit/n,"arg_acc":arg_ok/n,"unparsable":unparsable/n}怎么用这个结果:
| 指标 | 健康阈值 | 不达标时怎么办 |
|---|---|---|
| 工具名准确率 | ≥ 95%(核心工具 ≥ 98%) | 看混淆对,给这两个工具加"何时不用";仍不行就合并它们 |
| 必填参数完整率 | ≥ 95% | 参数描述里把必填参数的作用写清楚,或提供默认值 |
| 格式非法率 | ≤ 2% | 换更强的模型,或改进解析层(第 3 篇) |
那个混淆矩阵是本脚本最有价值的输出。它会精确告诉你哪两个工具在模型眼里长得一样。这是你改写描述的唯一依据。我见过最典型的混淆是search(语义检索)与lookup(精确查找),修法是合并成一个带mode参数的工具。
⚠️ 提醒:这个测的是单步工具选择,不等于任务成功率。它可以作为 p 的一个快速代理指标,但不能替代端到端评测(第 7 篇)。
八、案例:OpsAgent 的工具改造前后
改造前(典型的"API 文档式"定义):
- query_metrics(service, metric, window): 查询指标 - search_logs(service, since, limit): 搜索日志 - read_file(path, ref): 读文件 - run_cmd(cmd): 执行命令 - finish(answer): 结束问题:query_metrics和search_logs边界不清;since没说时间格式;read_file没说路径基准;run_cmd完全开放(危险);没有任何"何时不用"。
改造后:
- query_metrics 用途:查询服务的监控时序(已降采样),用于判断指标的形态(突增/渐变/周期)与起始时间点。 何时不用:要查的是具体日志文本 → 用 search_logs;要看代码 → 用 read_repo_file。 参数:service string 必填;metric string 必填(枚举:http_5xx_rate|p99_latency|qps|error_count) window string 可选 默认 "30m",格式 "<数字><m|h|d>" 返回:降采样后的 "时间-数值" 序列(≤12 点)+ 一句形态判断(如"在 t-11m 突增")。 - search_logs (见第 2 节七要素模板) - read_repo_file 用途:读取代码仓库中某文件在某版本的内容。 参数:path string 必填,**必须是仓库根的绝对路径,以 / 开头**; ref string 可选 版本/tag/commit,默认当前主干 返回:文件内容(>800 行时截断并注明);不存在时返回 3 个最相近的候选路径。 - run_readonly_cmd 用途:在受限 shell 中执行**只读**命令(ls/git show/git log/git diff/cat)。 何时不用:任何会修改状态的操作(部署、回滚、写文件)——本工具会直接拒绝,请改为给出建议等人工执行。 参数:cmd string 必填 返回:stdout(截断至 2000 字符);非白名单命令返回拒绝原因与白名单。 - finish 用途:当你已有足够证据支撑结论时调用。 参数:answer string 必填 根因 + 证据链(每步引用 Observation)+ 建议动作四处关键改动:
metric改成枚举,消除"指标名乱编"。read_repo_file的路径约束 + 候选路径纠错,消灭"文件找不到"的反复试错。run_cmd→run_readonly_cmd+ 白名单,权限最小化从工具定义层面就完成了。- 每个工具都有"何时不用",消除混淆。
这套改动,配合第 7 篇的评测,通常能把单步成功率往上推 5~10 个百分点。按第 1 篇的表,那是 20 步任务从 36% 到 67% 的差距。这是整个系列里最便宜的一次改进。
九、小结
这一篇的核心就一句:工具定义不是文档,是每一步都会被模型重读一遍的提示词,所以值得你投入跟 system prompt 同等的心力。
落到执行层面,最便宜也最确定的收益来自两件事。一是把七要素模板连同"何时不用"当铁律,先把工具边界写死;二是用那份评测脚本跑出混淆矩阵,盯住模型究竟在哪两个工具之间犯晕,再照着改描述。注意别凭猜测迭代——混淆矩阵是唯一可靠的依据。
下一篇聊上下文工程。长程任务里它是另一个容易被低估、但同样直接决定 p 的杠杆。
上一篇:03 手写生产级ReAct内核
下一篇:05 长程 Agent 的上下文工程:截断、摘要压缩与检索回放 - 需要明天上传笔记,今天的发布限额到了
参考
- Anthropic,Building Effective Agents, 2024-12(ACI、poka-yoke、绝对路径案例、工具定义与提示工程同等投入)。
- Yao et al.,ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023, arXiv:2210.03629。
- Liu et al.,Lost in the Middle: How Language Models Use Long Contexts, TACL 2024(上下文长度与注意力衰减)。