1. 调用的四步
任务要求"找出支付回调在哪处理"。模型没有直接回答,而是发起了一次工具调用。把它拆开看,四步。
第一步,声明进请求。 宿主在请求里放了一份工具清单,其中一条写着:名字是 search_code,说明是"在代码库里按正则搜索内容,返回文件路径与行号",参数是一个叫 pattern 的字符串。这份清单是模型的全部视野——它看不到工具的实现代码。
第二步,模型做选择。 模型读完清单,决定用 search_code,并生成一组键值:{"pattern": "handlePaymentCallback"}。它同时给出一个调用编号,用来在后续多轮里对应这次调用。
第三步,运行时校验后执行。 宿主拿到这组参数,先按结构校验(pattern 是否为字符串、是否必填),再按策略校验(这个路径在不在允许范围内、这次调用是否重复),然后才真正执行。执行权和校验权都在程序这一侧,不在模型这一侧。
第四步,结果回填。 执行结果被写回上下文,模型据此决定下一步:找到三处,读其中一处,或者继续换关键词搜。
四步里只有第三步与第四步是工具自己的责任。前两步出问题,改执行代码没有用——第一步出问题要改声明,第二步出问题多半是声明写得不够清楚。
这四步在协议层怎么表达,见《常见协议》的「Function Calling 的完整闭环」与「tool_use 与 tool_result」两节:OpenAI 用 tools[].function 加 tool_calls,Anthropic 用 input_schema 加 tool_use 与 tool_result 内容块。字段名不同,四步的结构相同。
1.1 为什么执行权必须在程序侧
一个自然的疑问:既然模型能写代码,为什么不把参数直接交给 shell 或数据库执行?
因为参数错误会被放大成真实副作用。模型把路径写成 ../../etc/passwd,或者把 rm -rf /tmp/data 里的路径拼错成 /tmp,一次误生成就变成一次不可逆操作。中间的校验层防的是模型犯错,与恶意无关。
这条分工在《基础概念》6.3 节有一张表(LLM、Runtime、Tool Adapter、Environment 四者的职责)。这里要补的是为什么不能合并:合并之后,模型输出的每一个字节都有了执行效力,而模型对自己的输出没有可靠的校验能力——它不知道文件是否真的存在,也不知道这次删除是否已经在上一轮做过。
2. 工具是模型与真实世界之间的一条接口
把上面四步抽象一层,会看到三个角色和一条接口。
| 角色 | 它知道什么 | 它不知道什么 |
|---|---|---|
| 模型 | 有哪些工具、每个工具的参数结构 | 工具怎么实现、环境现在什么状态 |
| 运行时 | 全部工具的实现、权限策略、当前状态 | 模型为什么选这个工具 |
| 环境 | 真实状态与真实副作用 | 有模型存在 |
工具是前两者之间的接口,也是模型认知环境状态的唯一途径。没有通过接口进来的信息,对模型来说等于不存在。
这句话有一个直接的工程含义:模型的世界由工具声明的质量决定。一个能读取日志的工具,让模型"知道"日志里有什么;一个只能统计行数的工具,让模型只能"知道"有多少行。同样一个模型,换一套工具,能做到的事就变了。
2.1 接口三段的职责
一段完整的工具接口包含三部分,它们的职责不同。
| 段 | 内容 | 由谁产生 | 由谁消费 |
|---|---|---|---|
| 声明 | 名字、说明、参数结构 | 工具开发者 | 模型 |
| 参数 | 一次调用的具体取值 | 模型 | 运行时 |
| 返回 | 执行结果 | 运行时与环境 | 模型 |
三段之间有一条容易被忽略的约束:声明里承诺的返回结构,必须与真实的返回一致。声明说"返回标题、摘要、URL",实际返回的是纯文本,模型会在解析上出错,而错误发生在第四步,看起来像"模型理解能力不行"。
这条约束没法靠类型系统保证(返回值通常不受 schema 约束),只能靠测试与审阅。
3. 五类工具与两条分类轴
工具不是一类东西。按"谁发起调用"与"作用在什么对象上"两条轴,可以摆成五类。
| 类型 | 调用方向 | 作用对象 | 例子 |
|---|---|---|---|
| 感知 | Agent 主动调用 | 读外部状态 | 网页搜索、知识库检索、读文件、搜文件内容 |
| 执行 | Agent 主动调用 | 改外部状态 | 执行命令、写文件、调外部接口、发邮件 |
| 协作 | Agent 主动调用 | 另一个 Agent 或人 | 创建子 Agent、给子级发消息、取消、列举 |
| 用户沟通 | Agent 主动调用 | 人 | 回复消息、发结构化卡片、发通知 |
| 事件触发 | Agent 注册、外部触发 | Agent 自己 | 定时器、监控后台命令、接外部事件源 |
两轴不是交叉分类框架——每一类在"作用对象"上的取值都是专属的,两轴的作用是让人快速看清定位。
五类的设计重点各不相同,这是分类的实际用途。
感知工具控制输出量。 一次搜索可能返回几万字,一份文档可能几百页。这类工具的设计核心是"一次给多少、怎么翻页、超阈值怎么截",拿到与否是次要问题。
执行工具控制权限。 误删的文件无法恢复,错误的命令可能导致服务中断,不当的接口调用可能产生真实财务损失。错误代价高,安全约束是核心。
协作工具控制归属。 谁持有控制流、谁承担失败、谁能终止。这三件事没定清,协作就会停在某个环节不结束。
用户沟通工具控制触达。 人可能不在线,消息可能跨多个渠道。这类工具要处理的是"异步送达"与"重复打扰"的平衡。
事件触发工具控制注册与唤醒。 它有两个时刻:注册时 Agent 声明自己关心什么,触发时外部事件唤醒 Agent。这两个时刻的语义完全不同,设计要分开考虑。
第五类与前三类的差别最大:前三类是 Agent 说什么就做什么,第五类是 Agent 说了之后等着,什么时候发生不由它决定。
3.1 前三类之外的两类为什么单独说
事件触发与用户沟通这两类,设计离不开事件驱动的异步运行时——它们要么在等外部事件,要么在向可能不在线的人投递。这部分内容与实时交互一起放在别处讨论,本篇只到前三类为止。
4. 一条能力做成什么形态
五类说的是"这条能力属于哪一类"。还有一个更前置的问题:同一条能力,该做成什么形态。
以"部署应用"为例。三种做法都成立。
形态一,专用工具:
deploy_app(service, version, env)
—— 一个调用完成全部步骤,参数受结构约束
形态二,通用执行器:
只给命令执行工具,模型自己拼:
npm run build && docker build -t app:v1 . && kubectl apply -f deploy.yaml
形态三,Skill:
写一份说明,列出六步流程与每步的判定条件,
模型读完之后用通用执行器逐步执行三者的成本结构不同。
| 形态 | 常驻成本 | 参数传递 | 谁能改 | 失败方式 |
|---|---|---|---|---|
| 专用工具 | 每个数百字节到数 KB | 结构化,受 schema 约束 | 改代码并部署 | 参数错、权限拒绝 |
| 通用执行器 | 一份定义,覆盖大量场景 | 自由文本,需自行转义 | 不改 | 命令拼错、转义出错 |
| Skill | 目录一行,正文按需加载 | 自然语言描述 | 改一份文档 | 步骤理解偏、漏步 |
4.1 默认往通用一侧放
一个四则运算计算器不如一个装了 sympy 的代码解释器。理由是模型本身有代码生成能力,给它一个执行环境等于给了一个元能力:一个解释器可以替代几十个专用工具,还能处理设计时没想到的情况。
即使确实需要专用工具,粒度也应偏向整合。extract_pdf_text、extract_docx_content、extract_pptx_content 三个工具的共性是从文档里取文本,更好的设计是一个 read_document 加一个 file_type 参数。整合降低了选择负担(只需理解"读文档就用它"),也让扩展新格式变成加一个枚举值。
4.2 四种情况退回专用
通用性有它管不到的地方,四种情况值得保留独立工具。
安全与权限。 生产数据库写操作、支付、删除数据这类需要精细授权与审计留痕的操作,一个开放的代码执行器做不到。专用工具能在参数级别限制范围。
屏蔽平台差异。 文件搜索用命令也能做,但不同系统的参数与输出格式不同。专门的工具能给出统一的、带行号的清晰反馈,也省掉了模型自己处理差异的成本。
使用频率极高。 高频操作值得一个专属入口,哪怕功能上已被通用工具覆盖。省下的是每次自己拼命令的出错概率。
参数结构复杂。 涉及嵌套对象、多字段联合校验、复杂类型约束的操作,结构化 schema 能有效引导模型正确传参;而命令行传参要处理引号与特殊字符转义,规则比 JSON 复杂得多,且各系统不同。
第四条值得展开:Skill 对参数的保真性要求更高。一个 Skill 里写"把参数以 JSON 写入临时文件,再用命令行读入这个文件",就是在绕开转义问题。这不是技巧问题,是形态选择带来的必然代价。
反过来说,Skill 对人类编写者更友好——不需要会编程,改一段文字比改代码再测试部署轻得多。这也是"变更频率"这一维的来源:流程还在频繁调整时用 Skill,稳定下来的底层操作做成工具。
5. 声明写在哪里、占多少
无论做成哪种形态,只要是专用工具,它就必须有一条声明进请求。声明的成本是固定且每轮支付的。
设第 \(i\) 个工具的名字与参数结构占 \(s_i\) 字节、说明占 \(d_i\) 字节,\(n\) 个工具的总占用是:
\(s_i\) 通常 80 到 150 字节(名字、参数名、类型、必填标记),\(d_i\) 通常 200 到 650 字节。说明占七成以上。
实际体量比直觉大。Claude Code 全部工具提示词文件合计约 135 KB,其中几个单项:
| 工具 | 提示词体量 | 里面写了什么 |
|---|---|---|
| 命令执行 | 20.8 KB | 后台运行、超时、沙箱开关、提交规范、多条陷阱 |
| 子代理 | 11.9 KB | 角色列表、分叉语义、并行要求、写法约束 |
| PowerShell | 9.8 KB | 与命令执行对齐的 Windows 版本 |
| 待办列表 | 9.5 KB | 使用时机、格式、何时更新 |
| 技能调用 | 8.3 KB | 加载规则、调用形式、与工具的关系 |
| 延迟工具搜索 | 4.3 KB | 两段式协议与失败策略 |
单轮请求的工具部分就要几十 KB。代入 \(n = 200\)、平均 450 字节,是 90 KB,在 200K Token 窗口里约占 11%——而这 11% 与当前任务是否相关无关。
成本随 \(n\) 线性增长,判断负担不是线性的。工具越多,模型越可能在相似工具之间选错或漏选。这解释了为什么单纯缩短说明解决不了问题:\(d_i\) 减半,\(c\) 减半,\(n\) 不变,选择困难照旧。
正确的优化顺序是先减少常驻数量(延迟加载),再考虑缩短长度。延迟加载单独成篇。
延迟加载的收益还有一个前提:每条工具本身的设计成立。设计有问题的工具,延迟之后只会更难被发现——排查时先要解决「它到底在不在当前工具集里」。
6. 说明怎么写
说明的质量直接决定选得对不对。三份递进的定义,同一件事。
{
"name": "search",
"description": "搜索内容",
"parameters": {"type": "object"}
}第一份几乎无法使用:参数结构为空,模型不知道传什么;说明没有说清搜什么、返回什么。
{
"name": "search_web",
"description": "搜索公开网页",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}}
}
}第二份能跑了,但仍有缺口:多长的查询合适?返回几条?能不能搜内部文档?
{
"name": "search_web",
"description": "搜索公开网页。适合查找当前行业资料和竞品信息;不用于读取团队内部文档。返回标题、摘要、发布日期和 URL。query 必须是具体问题,不要传入整段任务描述。",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "要搜索的具体主题"},
"max_results": {"type": "integer", "minimum": 1, "maximum": 10}
},
"required": ["query"]
}
}第三份写清四件事:使用场景、排除场景、返回内容、输入粒度。
四件事里最容易被省略的是排除场景。写"不用于什么"看起来浪费字节,但它解决最常见的一类误选——模型知道这个工具能搜,就什么都搜。反例说明往往比能力说明更有用:多数调用失败的根因在于模型不知道工具不能做什么。
6.1 参数用具体例子
抽象规范不如一个能直接套用的例子。写"timestamp:RFC3339 格式",模型在专注一件事时能理解;但它在同时处理多个工具、从历史里提取信息、权衡多个决策时,只能分出一小部分注意力确认格式。
改写成"timestamp:RFC3339 格式,例如 2024-03-15T14:30:00Z",模型可以直接照抄形状。手机号同理:"E.164 格式(国家代码加号码,无空格),例如 +8613888888888"。
6.2 返回格式要说清
"返回 JSON 数组,每个元素含 title、url、snippet 三个字段"这类说明能减少解析错误。
对耗时长的工具,注明代价有助于规划调用顺序:"此工具要下载完整页面,大型网站可能五到十秒;只需要元信息时改用 get_page_metadata"。
6.3 附带一到五个调用示例
schema 能描述类型,描述不了调用方式与典型参数组合——时间戳是秒还是毫秒、过滤条件怎么嵌套,这些隐式约定靠例子最容易传达。
加入示例后,调用准确率往往明显提升,在一些基准上从约 72% 提升到 90%(具体数值因任务而异)。
6.4 已知陷阱写在说明里
Claude Code 的搜索工具说明里写了几条具体陷阱:正则里的字面大括号要转义、跨行匹配要显式开启、结果被截断时考虑更精确的模式。这几条把常见错误前置到了错误可能发生的地方,比事后调试便宜得多。
6.5 命名也是区分度
工具数量超过几十个时,名字的信息量开始决定选择质量。
svc1_search、svc2_search 这类名字没有领域词,模型只能靠说明区分,而说明在长列表里容易被跳过。search_issues、search_docs、search_web 把范围写进名字,选择就变成模式匹配。
命名与分组解决的是两件事:分组解决同名冲突,命名解决同名共存时的可读性。
6.6 一条调试原则
模型频繁选错工具时,先检查说明,不要先怀疑模型。多数选择错误的根因在于说明不清——适用范围模糊、缺反例、参数含义含糊。改说明的投入产出比通常远高于换一个更强的模型。
7. 参数怎么传才不失真
参数错误分两类,schema 只管第一类。
| 类型 | 例子 | 谁能发现 |
|---|---|---|
| 形状不合法 | 必填缺失、类型错、枚举越界 | schema |
| 语义不正确 | 路径存在但指错文件、查询词太宽、时间范围反了 | 工具实现或模型自己 |
第二类无法在 schema 层面拦截,只能靠三件事降低概率。
枚举优于自由文本。 只有几种合法取值时用枚举声明。模型从枚举里选比从零生成稳得多,schema 层面就能拒绝非法值。
必填最小化。 只把真正必须的参数设为必填。必填项越多,模型越可能在信息不足时编造。其余参数给合理默认值。
一次调用完成一件事。 如果完成一个语义动作需要三次调用(创建、配置、启动),考虑合并。每次调用都是一次往返,往返多了中途出错概率上升。
第三个原则有个例外:合并会让单个工具的语义变复杂、说明变长。判断标准是能否用一句话说清"它做什么"。能,就合并;不能,就保持分开。
7.1 静默转换:一种比功能缺失更隐蔽的错
工具在执行前悄悄"修正"模型的输入,是一种比功能缺失更隐蔽的反模式。
一个真实案例:某编辑工具接收 old_string 与 new_string,在文件里精确匹配替换。参数传递层会把中文弯引号静默转成英文直引号。于是出现一种模型无法理解的失败——它用读取工具看到文件里有弯引号(读取工具原样返回,没做转换),于是原样传入;但参数层已经转成直引号,与文件内容不匹配,工具返回"未找到"。模型反复试、反复失败,因为它无法理解为什么工具找不到自己明明看到的内容。
写入方向的问题更麻烦:模型本意写入弯引号(中文排版的正确选择),参数层却换成直引号。模型以为写对了,实际文件已被改。它若再读一次验证,看到的又是转换后的直引号,于是陷入困惑。
7.2 静默注入:模型不知情地多传了参数
另一类保真性违规是工具在模型不知情时追加参数。某工具的 git commit 会自动附加一个标记参数,用于标识提交由 AI 生成。如果用户的 Git 版本较旧、不支持该参数,这个被静默注入的参数就会让提交报错。模型可能反复调整提交信息措辞、尝试不同参数组合,但无论怎么改都会失败——因为失败的原因不在它的输入里。
两类问题指向同一条原则:模型感知到的世界与工具操作的世界之间,不能存在系统性偏差。
确实需要规范化输入时(例如统一编码),必须在说明里写明,并在返回里告知模型。否则工具的"智能修正"制造的是一次模型无法自行诊断的故障。
8. 结果怎么返回
工具返回一段长字符串是最常见的设计缺陷。模型要自己从中提取状态、结论、后续动作,而它可能读错,尤其在内容被截断时。
四段式把这件事故意拆开:
{
"status": "success",
"summary": "找到 5 个结果,其中 3 个与会议纪要相关。",
"items": [
{"title": "企业协作趋势报告", "url": "https://example.com/report", "date": "2027-02-11"}
],
"artifact": "artifacts/search/call_01.json",
"truncated": false
}| 字段 | 作用 | 谁用 |
|---|---|---|
status | 决定下一步动作 | 模型判断继续、重试、改参、报告 |
summary | 一两句话给结论 | 模型默认读这段 |
items | 结构化数据 | 需要精确判断时 |
artifact | 原始产物路径 | 出错后追溯 |
truncated 这个布尔值看起来多余,但它解决一个具体问题:模型无法从内容判断"是不是全部"。显式声明截断,模型才知道该不该继续取。
8.1 搜索类只返回候选
搜索的结果应当是结构化的候选列表(标题、位置、摘要片段),由模型先浏览再决定深入读哪一条,不必把拼接后的全文一次性给出。
结果多时提供分页或游标:默认返回前若干条,并注明总数与获取下一页的方式。翻不翻页由模型决定,不是由工具替它决定。
8.2 读取类支持偏移与上限
读文件、读日志这类工具应支持偏移与条数上限,按需读取指定片段。
必须截断时,明确标注省略了多少、如何继续读。静默截断是危险的——模型会以为自己看到了全部,基于不完整的信息做判断。
8.3 失败分类决定恢复动作
失败不能只回一个错误字符串,因为不同的失败需要不同的恢复动作。
| 分类 | 含义 | 恢复动作 | 典型错误 |
|---|---|---|---|
| 可重试 | 临时故障 | 稍后重试,通常带退避 | 限流、锁冲突、网络抖动 |
| 需改参 | 参数非法或模式错误 | 改参数再调 | 正则语法错、枚举越界 |
| 环境问题 | 依赖缺失、路径不可达、权限拒绝 | 换路径或报告环境 | 文件不存在、权限不足 |
| 中止 | 超时、调用方取消 | 可重试,也可放弃 | 超时、取消信号 |
四类混在一起时,模型的默认行为是重试,而"需改参"这一类重试多少次都不会成功。
一个正面例子:某个实现的搜索工具让四类错误码各自对应一种恢复动作,并且明确区分"成功且有结果"(退出码 0)与"成功的空搜索"(退出码 1)。把空结果与失败分开,模型才不会把"查无此项"当成"工具坏了"。
9. 权限与副作用的三层
schema 只能检查形状。真实防护要三层。
模型生成参数
↓ 第一层:输入验证 —— 形状、必填、范围、路径是否越界、命令是否含拼接
↓ 第二层:权限与策略 —— 工作目录、允许的动作、重复动作、风险等级
↓ 第三层:独立审查 —— 对不可逆动作做第二视角判断
执行
↓ 结果:状态、摘要、结构化数据、产物路径9.1 第一层:快速失败
检查所有参数的合法性:路径里有没有 ../ 造成的越界、命令参数里有没有分号或管道符拼接的额外命令、类型与格式对不对。
关键是快速失败——发现异常立即拒绝,不尝试"智能"修正。修正会引入第 7 节讲的那类系统性偏差。
9.2 第二层:权限与策略
文件操作限制在特定工作目录;命令执行维护禁止清单;外部接口检查配额与速率。
禁止清单只是最基础的防护,不能作为唯一手段——攻击者可以用变形命令绕过字符串匹配。更稳的做法是结合语义解析,理解命令的实际意图。
这一层还要处理两件常被漏的事。
重复动作。 同一个删除请求被执行两次,结果通常比执行一次更糟,而且模型不知道为什么失败。工具要么做幂等,要么显式拒绝重复。
不可逆动作。 删除、覆盖、对外发送。这类操作建议独立授权:把"不可逆"作为工具的一个属性,由外层策略决定是否需要额外确认,不必每次调用都问人。
9.3 第三层:独立审查
对不可逆且影响重大的操作,加一个独立视角。
事前审批:执行前,一个模型提议行动,另一个独立模型审查并批准。两个模型应来自不同家族、能力相近——不同来源带来认知多样性,能力相近才能互相理解。提示词上,两者的底层规则一致,关注点不同:提议方强调完成任务,审批方强调风险与规则。
审批失败后不应简单重试,而应把拒绝理由作为一次工具结果加入轨迹。从模型视角看,这就像一次调用失败返回了错误与修正建议——它已经具备处理这类输入的能力。
事后验证:操作完成后,用不同模态检验结果。生成了文档就渲染成图看排版;改了配置就在沙盒里跑一遍看是否生效。换模态的价值在于避开同一盲区。
并行旁路校验:主模型决定执行某个调用时,一个轻量的独立调用同时判断这次调用是否安全。它只读结构化字段(工具名与参数),不读主模型的自由文本——一旦读,藏在文本里的诱导话术就能影响判断。
审查对象不同,对模型能力的要求也不同:审查开放式思考需要能力相近的模型,判断"这条命令是否危险"这类分类问题,轻量模型足够。
并行旁路校验还要配一个拒绝熔断:连续多次拒绝时不应无限重试,应转人工判断。
9.4 让安全检查在体验上隐形
一种做法是把"展示"与"放行"拆开并行:界面先显示"正在读取文件",后台同时做安全检查。用户感受到的是进度,不是等待。
10. 完整样例:代码搜索工具
三处都做到位的工具不多,代码搜索是其中一个。把它的设计拆开,能看清前几节的判断如何变成具体选择。
10.1 声明
九个可调项,其中三项直接服务于"控制返回体积"。
| 参数 | 取值 | 默认 | 作用 |
|---|---|---|---|
| 输出模式 | 内容、文件路径、计数 | 文件路径 | 决定返回什么形状 |
| 结果上限 | 整数,0 表示不限 | 250 | 决定返回多少条 |
| 偏移 | 整数 | 0 | 从第 N 条开始 |
默认输出模式选"文件路径",不选"内容",这个选择很关键:多数搜索的真实目的是"找到是哪些文件",内容可以在确定文件之后按行读取。默认给路径,把"读内容"变成一次显式选择。
说明里还有两条效率提示:按文件类型过滤比按通配符更高效(前者用语言类型索引,后者要遍历路径);多行模式默认关闭,需要跨行匹配时显式开启。
另有一条隐含过滤:版本控制目录(.git、.svn、.hg、.bzr、.jj、.sl)自动排除。这类目录里的内容几乎总是噪声,排除它们相当于给结果做了一次免费去重。
10.2 参数
三个参数之间的相互作用写得很细:上下文行数(前、后、前后)只在内容模式下有效,其他模式忽略;行号默认开启;偏移与结果上限的语义是"先跳过 N 条,再取 M 条",等价于命令行的 tail -n +N | head -N。
这种"参数只在某模式下有效"的设计有代价:模型可能填了无效组合。但它比重载三个不同工具更省声明空间——一个工具加九个参数,比三个工具各带三个参数占得少。
10.3 返回
截断的表达方式值得单独看,两条规则。
第一,只有在真的发生截断时才报告上限值。没有截断时不报告,避免模型误以为"有更多结果"而去翻页。
第二,上限为 0 表示不限,但说明里明确写"谨慎使用——大结果集浪费上下文"。
第一条是第 8 节 truncated 字段的更精细版本:不只告诉"是否截断",还告诉"当前限的是多少",让模型能判断该不该用偏移继续取。
结果体积还有一个上限:内容超过约 20 KB 时落盘,模型拿到的是预览加路径。没有这个上限的会话里,密集使用搜索会让工具结果占到几万 Token。
10.4 三处在这个工具上的配合
声明用默认值压低体积(默认只给路径、默认 250 条上限)。 参数用枚举把选择收窄(三种输出模式,不是自由文本)。 返回在真的截断时才报告上限,避免误导。
三处配合的结果是:同一个工具在"只想看看有哪些文件"与"要读具体匹配行"两种场景下都能用,而不需要两个工具。
代价是参数多、交互复杂。一个只有三个参数的工具更容易被正确使用,但它无法同时服务两种场景。这个取舍与第 7 节讲的"合并工具"是同一条判断:合并后能否用一句话说清它做什么。这个工具能——"按模式搜索代码并返回匹配"。
11. 四家对照
| 维度 | Claude Code | Codex | OpenCode | DeepSeek Harness |
|---|---|---|---|---|
| 声明体量 | 最大(单工具 20 KB 级) | 中等 | 较小 | 中等 |
| 返回声明 | 最大结果字符数 | 按内容类型选策略 | schema | 强制输出结构声明 |
| 失败表达 | 错误文本 | 错误码 | 错误类型 | 四类错误码 |
| 权限粒度 | 工具名级 | 策略与命名空间 | 权限动作级 | 执行守卫 |
| 结果截断 | 20,000 字符触发落盘 | 截断加外置 | 2,000 行 / 50 KiB | 超内联上限写外置 |
一处值得单独说:DeepSeek Harness 的输出声明是强制的,且出现在"不得泄漏到模型请求中"的字段清单里。这个设计把"工具返回什么"定为强制约定:调用方不必猜返回结构,宿主可以按声明的结构校验。
Claude Code 走的是另一条路:把大量约束写进说明(例如搜索工具的那几条陷阱),正确使用靠说明保证。代价是说明很长,收益是模型在失败前就知道该避开什么。
12. 失败模式
四类,按四步里的位置归类。
选错。 症状:模型反复试不同工具,或选一个明显不合适的。成因:名字或说明区分度不足。修在声明。
填错。 症状:参数被 schema 拒绝,或参数合法但结果无用。成因:自由文本参数、必填项过多、说明没给例子。修在参数段。
被静默改掉。 症状:模型反复失败且无法理解原因。成因:参数层做了转换或注入。修在传递层。
读错。 症状:模型把部分成功读成成功,或把截断当完整。成因:返回是长文本、截断没标注、失败没分类。修在返回段。
四类里第三类最难发现,因为它不报错,只是效果持续变差。一个检查方式:拿说明里承诺的返回字段,与最近一次真实调用的返回逐项对照,看是否都真的存在。这个检查很便宜,但很少有团队做。
12.1 诊断顺序
拿到"工具用不对"的反馈之后,按这个顺序查。
- 看是选错还是填错。选错查声明,填错查参数。
- 选错的话,用"明显该选它"与"明显不该选它"各测一次。两次都对,说明是长列表里的干扰问题,不是说明问题。
- 填错的话,看是 schema 拒绝还是执行失败。前者改 schema,后者看失败分类。
- 都不是的话,查返回。模型是不是把部分成功读成成功,或把截断当完整。
四步走完基本能定位。跳过第一步直接改说明是最常见的误修——填错参数时改说明不会有帮助。