本文由 cooliang 根据 Agent 的实际开发经历编写,AI 协助润色。
上一篇文章记录了 Tool 数量增长之后,为什么开始采用 Tool Set 和小范围 Tool Search。它主要解决的是“这一轮应该把哪些 Tool 提供给模型”。但在实际开发中,即使候选范围已经缩小,另一个问题仍然会出现:明明已经提供了专用 Tool,模型却没有调用它。
例如,系统已经有 read_file,模型仍可能通过 generic_bash 执行 cat;已经有 list_directory,它却选择运行 ls。从任务结果看,两条路径或许都能读到文件,但从 Agent 系统的角度看,它们并不等价。
专用 Tool 通常包含路径限制、权限检查、长度截断、结构化返回和审计信息。Bash 则绕过了这些边界,还把命令解释、转义和平台差异带进执行过程。问题因此不只是“模型选得不够漂亮”,而是原本设计好的能力边界没有真正生效。
这篇文章想记录的,是我在学习与应用过程中整理出的 Tool 命中率处理思路:哪些方法是在提高模型选对的概率,哪些方法是在模型选错时保护系统,以及为什么两者不能互相替代。
Tool 命中率不只是有没有调用 Tool
最初谈“命中率”时,很容易只统计模型有没有发起 Tool Call。但这项指标过于宽松。
用户要求读取文件时,模型调用了 Bash,也属于 Tool Call;模型选中了 read_file,却把目录填进 path 参数,同样无法完成任务;模型第一次选错,经执行器拒绝后改用正确工具,和一次命中的体验也不相同。
因此,我更愿意把一次工具使用拆成几层:
用户意图
→ 是否需要工具
→ 选择哪个工具
→ 参数是否正确
→ 执行结果是否有效
→ 失败后能否纠正
“Tool 命中率”主要对应中间的工具选择,但最终还应同时观察任务完成率、参数正确率、兜底工具使用率和拒绝后的纠正成功率。否则,为了让某个工具的调用数字变好,反而可能牺牲真实任务的完成效果。
第一步不是补一句“优先使用专用 Tool”
发现模型绕过专用 Tool 后,最直接的处理通常是在 Prompt 中增加一句:
优先使用专用工具,不要使用通用 Bash。
这句话有帮助,但还不够。对模型来说,“优先”只是一个偏好,并没有说明 read_file 和 Bash 的边界,也没有说明什么情况下可以例外。
相比笼统地要求优先,更有效的是把规则写进 Tool 本身的语义中:
读取指定文件的内容。
查看文件时必须使用此工具;支持行范围、编码和最大读取长度。
不得使用 generic_bash 执行 cat、head、tail 或同类命令代替。
工具名称也应尽量直接对应用户意图,例如:
read_file
list_directory
search_file_contents
而不是:
handle_file
execute
cat_tool
名称决定模型最先看到的语义线索,描述则负责补充使用条件、禁止条件和相似工具之间的区别。两者共同构成路由信息,而不只是面向开发者的文档。
正反示例比抽象规则更接近真实选择
Tool 选择是一个对比过程。模型不是孤立地判断某个工具“能不能用”,而是在当前候选中判断“哪个更适合”。因此,只描述正确工具往往不够,还需要指出最容易混淆的错误路径。
例如:
用户:查看 /etc/hosts
正确:read_file(path="/etc/hosts")
错误:generic_bash(command="cat /etc/hosts")
这样的示例同时表达了三件事:当前意图属于文件读取,read_file 是主要路径,Bash 即使能够完成也不应被选择。
如果系统中存在多个容易混淆的 Tool,我会优先为“最常发生的误选”编写对比例子,而不是堆积大量正常案例。Few-shot 的价值不是展示工具能工作,而是帮助模型看清决策边界。
能力重叠会制造路由歧义
如果两个 Tool 都可以完成同一件事,模型选错并不完全是模型的问题,也可能是工具设计没有提供清晰答案。
例如,下面三个工具都接受路径和命令式参数:
manage_file
run_command
generic_bash
模型需要自行推断它们之间微妙的优先级。上下文一长、任务一复杂,就容易选择能力最宽的那个,因为通用工具看起来更有把握。
更稳定的设计目标是:
一个明确的用户意图,对应一条主要工具路径。
读取、写入和删除也不应混在一个万能的 manage_file 中。它们的权限、风险和副作用不同,拆成 read_file、write_file 和 delete_file 后,模型更容易理解,执行器也更容易实施不同的安全策略。
当然,能力重叠不可能完全消失。专用 Tool 与 Bash 天然重叠,高阶业务 Tool 与底层原子 Tool 也可能重叠。无法消除时,就需要明确优先级:
read_file > generic_bash
list_directory > generic_bash
search_file_contents > generic_bash
这里的 > 不是性能比较,而是路由规则:即使 Bash 命令更短,只要专用 Tool 能完成,就应该使用专用 Tool。
专用 Tool 不好用,模型就会寻找出口
有时模型选择 Bash,并不是没有读懂描述,而是专用 Tool 的能力不足。
如果 read_file 只能一次返回整个文件,不支持行范围、大文件截断和编码选择,那么面对一个很大的日志文件时,Bash 加 tail 的确更符合任务需要。仅靠 Prompt 禁止它,并没有解决功能缺口。
因此,提高命中率也包括完善专用 Tool。以文件读取为例,常用能力至少可以包括:
- 指定行范围;
- 指定或自动识别编码;
- 限制最大返回长度;
- 对大文件进行截断并返回截断信息;
- 支持游标或分页继续读取。
这和上一篇文章中讨论的工具颗粒度有关。专用 Tool 既要有清晰边界,也要足以完成对应意图中的常见任务。否则,系统一边要求模型走专用路径,一边又迫使它在真实需求面前绕路。
候选 Tool 越多,描述写得再好也会互相干扰
单个 Tool 的名称和说明都可以很清楚,但当几十个 Tool 同时进入上下文,模型仍然需要在大量相近描述中选择。
这也是 Tool Set 和 Tool Search 对命中率的价值。它们不只是节省 Tool Schema 占用的 Token,也在减少当前决策的候选数量。
例如,可以先按照领域划分:
filesystem
process
network
database
spreadsheet
文件读取任务只提供文件系统相关 Tool;进程排查任务只提供进程查询和检查相关 Tool。如果一个领域内仍然有很多工具,再通过 Tool Search 找到少量具体候选。
在更明确的场景中,甚至不需要完全交给 LLM 自由选择。程序可以根据页面、资源类型或已经确定的意图,直接缩小候选集:
if intent == "read_file":
available_tools = [read_file]
如果工具很多,也可以采用两阶段路由:第一阶段只选择能力类别,第二阶段再从该类别中选择具体 Tool。这样做增加了一步决策,却避免了几十个不同领域的工具在同一轮里竞争。
对通用 Bash,需要单独设计开放策略
通用 Bash 是命中率问题里最特殊的工具。它几乎什么都能做,所以只要和专用 Tool 同时出现,就天然具有很强的竞争力。
如果 Bash 实际使用很少,最直接的方案是延迟开放。第一轮只向模型提供专用 Tool,以及一个类似 request_bash_access 的能力。只有当专用 Tool 确实无法完成任务时,第二轮才提供 Bash。
这个方法很强,因为它从候选集中消除了错误路径;代价也很明确:真正需要 Bash 的任务会增加一次模型调用和等待时间。
如果 Coding Agent 经常需要 Bash,完全隐藏又不现实,那么更适合采用执行前拦截。Bash 可以始终存在,但在命令真正执行前,由 PreToolUse 规则检查它是否正在替代已有专用 Tool:
cat / head / tail → 使用 read_file
ls / dir → 使用 list_directory
grep / rg → 使用 search_file_contents
拦截结果不应只返回一句“禁止执行”,而应提供足够的纠正信息:
{
"code": "USE_SPECIALIZED_TOOL",
"recommendedTool": "read_file",
"recommendedArguments": {
"path": "/etc/hosts"
},
"reason": "文件读取必须通过受控的专用工具完成"
}
这样,拒绝本身也成为下一轮模型决策的输入。系统不只阻止错误路径,还告诉模型如何回到正确路径。
Schema 也是 Tool 语义的一部分
模型选对了工具,不代表调用一定正确。模糊的参数 Schema 会把“工具命中”变成“参数失败”。
我更倾向于在 Schema 中使用明确的必填字段、类型、枚举和范围限制,并关闭未声明参数:
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要读取的文件绝对路径,不得传入目录"
},
"encoding": {
"type": "string",
"enum": ["auto", "utf-8", "gb18030"]
},
"maxBytes": {
"type": "integer",
"minimum": 1,
"maximum": 1048576
}
},
"required": ["path"],
"additionalProperties": false
}
严格 Schema 不只是为了执行前校验,它也在帮助模型理解能力边界。参数越像一个模糊的字符串容器,模型越容易把 Tool 当成另一种命令行;参数越能表达领域概念,调用行为就越稳定。
Prompt 负责引导,执行器负责保证
经过这些处理后,我对 Tool 命中率的认识和业务约束类似:不能只依赖 Prompt,也不能只依赖执行器。
Prompt、名称、描述和示例负责提高第一次选对的概率。候选集收缩和 Tool Search 负责降低选择难度。PreToolUse 和执行器则负责在模型仍然选错时阻止越界。
执行器至少应该拒绝:
- 使用 Bash 绕过已有专用 Tool;
- 超出当前授权范围的调用;
- 非法或越界路径;
- 危险命令和高风险参数;
- 不符合 Schema 的输入。
这几层的职责并不相同:
工具设计 让正确路径容易被理解
候选集管理 让无关路径尽量不出现
Prompt 与示例 让模型倾向选择正确路径
PreToolUse 在执行前纠正错误路由
Executor 保证权限、参数和业务边界
如果只做前几层,命中率再高也会留下概率性的缺口;如果只做后两层,系统虽然安全,却会频繁拒绝和重试,用户感受到的延迟与不稳定仍然存在。
没有评测集,就只能凭感觉调整
Tool 描述改完以后,拿两三个例子手工测试,很容易得到“已经有效”的结论。但模型版本、上下文长度、工具组合和用户表达变化后,原来的问题可能再次出现。
因此,需要准备一组持续运行的路由评测:
- 正向样本:明确应该调用目标 Tool;
- 负向样本:语义相近,但不应该调用;
- 模糊样本:多个 Tool 最容易混淆;
- 组合样本:需要连续调用多个 Tool;
- 对抗样本:用户直接要求通过 Bash 绕过专用能力。
除了工具选择准确率,我还会关注:
误调用率
漏调用率
Bash 兜底率
首次命中率
拒绝后纠正成功率
参数正确率
最终任务完成率
其中,“首次命中率”和“拒绝后纠正成功率”应该分开。两次调用最后虽然都完成了任务,但它们对延迟、Token 和用户体验的影响不同。
评测集的另一个价值,是让工具描述可以被当成代码一样迭代。每次修改名称、描述、Schema 或候选集策略,都可以观察它改善了哪些样本,又让哪些原本正确的路径发生退化。
我现在更认可的组合方案
把这些方法放在一起,我更认可的并不是某一个“提高命中率的技巧”,而是一组从软引导到硬约束的组合:
语义化的工具名称与边界
+ 正反示例和明确优先级
+ 足够实用的专用工具
+ Tool Set / Tool Search 缩小候选范围
+ PreToolUse 拦截错误路由
+ Executor 强制权限和安全规则
+ 评测集持续回归
如果 Bash 很少使用,就默认隐藏并按需解锁;如果 Bash 是 Coding Agent 的核心能力,就保留它,但通过执行前策略阻止它替代已有专用 Tool。
这里最重要的区分是:模型层优化提高概率,系统层约束提供保证。
只优化名称、描述和 Prompt,无法证明模型下一次一定选对;只在执行器里硬拦截,又会让大量请求经历失败和重试。真正稳定的 Tool 路由,需要让模型更容易做对,也需要让错误选择无法绕过系统边界。
结语
开发 Agent 时,Tool 命中率看起来像一个模型能力问题,深入之后却会发现,它同时涉及接口设计、信息架构、权限控制、运行时协议和测试工程。
模型为什么选择 Bash,不能只归因于它“不听指令”。可能是工具名字模糊,可能是能力重叠,可能是专用 Tool 不够实用,也可能是系统一次提供了太多候选。找到具体原因,比继续向 Prompt 里增加强调语句更重要。
最终,我希望建立的也不是一个“永远选对 Tool”的模型,而是一个分层的系统:正确路径足够清楚,错误路径能够被识别,危险路径无法被执行,策略变化还能通过评测及时发现。
如果用一句话概括:
提高 Tool 命中率,不是反复要求模型选对,而是让正确选择更容易,让错误选择有机会纠正,并让越界选择无法生效。
