Appearance
第8章 给能力装上把手——Tool
小a给 Agent 加了两个“能力”:一个叫 run,参数是一个字符串;另一个叫 read_file。结果模型几乎每次都选 run,参数传得乱七八糟,他盯着日志一头雾水。
“我明明给了它能力,它怎么不会用?”他问老z。
“问题不在‘有没有能力’,而在‘能力好不好用’。”老z说,“Function Calling 定义了结构化调用的协议,但‘能调用函数’不等于‘有可用能力’。模型是否正确选择工具,常常取决于接口文字、参数边界和失败结果。”
本章讨论 Tool 设计,不覆盖循环调度。
先把名字对齐
| 中文术语 | 英文全称或缩写 | 本书工作定义 | 不等于什么 |
|---|---|---|---|
| 工具 | Tool | 可被宿主声明、校验与执行的具体能力 | 模型输出的一段自然语言 |
| 工具规格 | ToolSpec | 名称、描述、输入 schema、结果契约的声明 | 执行器本身 |
| 运行时校验 | runtime validation | 对模型给出的实际值做 schema 与业务检查 | 类型声明自动生效 |
| 执行边界 | execution boundary | 权限、路径、网络、子进程等真正发生副作用的位置 | prompt 中的禁止语句 |
| 输出预算 | output budget | 限制工具结果进入模型上下文的大小与形状 | 丢弃审计原始记录 |
8.1 Tool 是一份可执行契约
“那 Tool 到底是什么?”小a问。
“一个 Tool 至少包含名称、面向模型的描述、输入 schema、执行实现和结果语义。”老z说,“Function Calling 是模型表达请求的机制;Tool 是被声明和执行的具体能力单元。你可以把它想成工具箱里的一把工具——把手上贴着标签,写着‘这是什么、怎么用、会有什么结果’,但真正挥动它的是你的程序。”
ts
// 示意
const readFile = {
name: "read_file",
description: "读取工作区内的 UTF-8 文本文件;传入相对路径。",
input: { path: "string" },
execute: async ({ path }) => ({ /* 标准化结果 */ })
}小a问:“描述和执行,为什么要分开?写一块儿不行吗?”
“因为它们给两个不同的对象看。”老z说,“描述给模型看,决定它选不选这把工具、参数怎么填;执行给宿主看,决定动作怎么真落地。混在一起,模型容易读到执行细节而分心,宿主也容易被模型照抄的描述误导。分开后,模型只看到‘操作手册’,执行细节留在宿主这一侧受控。”
“那模型能看到执行结果吗?”小a追问。
“看到的是结果,不是过程。”老z说,“工具执行了什么命令、读了哪些字节、走了哪条网络路径——这些模型都感知不到,它只拿到执行器愿意交回的那份结构化结果。这层隔离,让真实副作用只发生在执行边界上,模型无法越界。”
小a想了想:“所以工具其实是两层契约——一层面向模型,一层面向宿主。”
“正是。”老z说,“面向模型的那层决定它会不会被选、参数对不对;面向宿主的那层决定动作安不安全、要不要执行。两层各自独立演化,互不污染。”
“这两层各自会怎么漂移?”小a追问。
“漂的方向不同。”老z说,“面向模型那层最常见的漂移,是描述改了但没同步给模型可见的标签——比如执行器内部加了重试,但描述里还写着‘失败立即返回’,模型据此判断要不要自己重试就会出错。面向宿主那层最常见的漂移,是权限收窄了但工具描述没改——模型还在按旧描述请求越界路径,宿主一次次拒绝,模型却不知道为什么。两层漂移的表现不同,但都让模型对工具的假设和工具的实际行为脱节。”
| 漂移来源 | 面向模型那层的表现 | 面向宿主那层的表现 |
|---|---|---|
| 描述与实现不一致 | 模型按旧描述填参数、选工具 | 宿主收到与描述不符的请求 |
| 权限收窄未同步 | 模型仍请求越界动作 | 拒绝率上升,模型不知为何 |
| 结果语义变更 | 模型按旧字段解读结果 | 下游解析对不上新格式 |
“所以两层要一起版本化。”老z补充,“改了执行器就回头检查描述有没有过时,改了描述就回头检查权限策略还对不对得上。两层的版本号最好一起记进调用日志,出问题时才能判断是哪层先动的。”
“那两层一起版本化,具体落点在哪?”小a追问。
“落点是把‘描述版本’和‘实现版本’都记进调用日志。”老z说,“模型在某一版描述下选了工具,执行器用的是另一版实现——出问题时,日志里两个版本号一对,就知道是模型按旧标签行事,还是实现先于描述改了。版本号的作用不是追溯责任,是压缩排查范围:先看哪层变了,再看那层变的对不对。”
8.2 描述和 schema 是模型的操作手册
“那模型凭什么决定用哪个工具?”小a问。
“凭标签。描述应回答:什么时候用、不能用来做什么、输入怎样解释、结果是什么。”老z说,“schema 应尽量表达必填字段、枚举、范围和互斥关系。不要把多个独立意图塞进一个含糊的 run(action, data) 工具;模型和审计者都会难以判断实际影响。你那个 run 就是典型——标签太糊,模型只能瞎猜。”
“那工具是不是越细越好?”小a问。
“不是。好的粒度不是越细越好。”老z摇头,“过多近似工具会提高选择错误率和提示词成本;过大的万能工具又扩大权限和测试面。以一个能独立授权、独立失败和独立审计的动作作为默认粒度,通常较易治理。”
小a追问:“万能 run 到底坏在哪?参数合法不就行了?”
“参数合法,不代表动作合法。”老z说,“run(action, data) 的 schema 只能约束 action 是个字符串、data 是个对象,约束不了它到底是 ls 还是 rm -rf。模型选了哪个 action,你事后才从日志里拼出来——授权、审计、回滚全都无从谈起。万能工具把‘安全靠 schema’退化成了‘安全靠事后看日志’。”
“那拆到多细算够?”小a问。
“看这个动作能不能单独被授权。”老z说,“读文件和写文件,权限完全不同,就该是两把工具,各自有自己的审批策略;但‘读 a.ts’和‘读 b.ts’没必要拆成两把,它们共享同一套授权。拆的边界,跟着授权边界走,不跟着函数数量走。”
“那有没有拆太细的反例?”小a问。
“有。”老z说,“把‘读文件’拆成 read_text_file、read_json_file、read_yaml_file 三把——它们的授权策略完全一样,失败语义也一样,拆开只增加了模型的选择负担。模型要在三者里挑一个,挑错了就得多绕一轮。该合并的强求拆细,和该拆的硬塞进一把,是一样坏的。”
| 粒度选择 | 例子 | 问题 |
|---|---|---|
| 太粗 | run(action, data) 万能工具 | schema 抓不住动作语义,授权和审计无从谈起 |
| 太细 | read_text_file / read_json_file / read_yaml_file | 同一授权拆成三把,增加选择错误率 |
| 合适 | read_file(path) 配合 write_file(path, content) | 读写各自独立授权,同类文件共享一把 |
“判断粒度,有一个粗略的对照。”老z画了一张表:
| 信号 | 倾向更粗 | 倾向更细 |
|---|---|---|
| 授权策略 | 几把工具共享同一权限 | 各自动作需要不同审批 |
| 失败语义 | 失败原因和处理方式相同 | 失败后果差异大 |
| 审计需求 | 合并记录够用 | 必须单独追溯每个动作 |
| 选择频率 | 模型几乎总选同一个 | 模型需要明确区分 |
“这四个信号一致指向粗,就合并;一致指向细,就拆;互相打架,优先看授权——授权边界是最难事后补的,其他三个可以靠描述和日志弥补。”
要点
描述是模型的操作手册。粒度以“能独立授权、独立失败、独立审计”为准——不是越细越好,也不是越大越全。
8.3 结果设计决定下一步质量
“工具返回结果有什么讲究?”小a问。
“工具结果应该区分成功、空结果、输入错误、权限拒绝、可恢复故障和不可恢复故障,并携带可供后续决策的上下文。”老z说,“例如‘文件不存在:src/a.ts;已检查工作区根目录’比原样暴露系统异常更容易让模型修正路径,也更少泄露环境信息。”
“那长输出呢?”小a问。
“长输出需要截断、分页或摘要,并保留用户或执行器可追溯的原始记录位置。”老z说,“把整份日志无上限送回模型,既浪费上下文,也可能把敏感内容扩大到不必要的范围。”
小a追问:“截断和分页,该用哪个?”
“看模型还要不要剩下的。”老z说,“剩下的内容对任务没用,截断——给个摘要加‘已省略 N 行’就行;剩下的是后续步骤要的证据,分页——给游标,让模型按需翻。截断是‘丢’,分页是‘留个入口’——选错的话,要么上下文被撑爆,要么模型因为拿不到证据而卡住。”
“那原始记录呢?”小a问。
“不能丢,但不必进上下文。”老z说,“执行器把完整的原始日志留在文件或存储里,回给模型的只是摘要加引用位置。审计需要的是原始记录,模型需要的是结论——别让两者挤在同一份返回里。”
“所以结果设计其实是在管两本账。”小a说。
“对,一本给模型用、一本给人查。”老z说,“分清这两本账,工具才算有了真正的结果语义。”
“那结果的状态分类,能不能画一张完整的表?”小a问。
“可以。”老z列了六种状态:
| 结果状态 | 含义 | 模型合理的下一步 | 典型误用 |
|---|---|---|---|
| 成功 | 动作完成,返回有效数据 | 用结果推进任务 | 当成永远成功,跳过字段检查 |
| 空结果 | 合法执行但无匹配 | 换查询条件或承认无数据 | 误当成失败重试 |
| 输入错误 | 参数合法但语义错(路径不存在) | 修正参数 | 笼统报错让模型猜 |
| 权限拒绝 | 路径或动作越界 | 放弃或请求授权 | 把拒绝当超时反复重试 |
| 可恢复故障 | 临时锁、限流 | 等待后重试 | 当成不可恢复直接放弃 |
| 不可恢复故障 | 远端崩溃、数据损坏 | 转人工或换路径 | 盲目重试放大故障 |
“这六种状态对模型是六条不同的岔路。”老z说,“如果你只返回‘成功’或‘失败’两种,模型在‘失败’这一个大筐里,完全分不清该重试、该换参数还是该放弃——它只能猜,猜错了就空转或越界。”
“那有没有‘状态对、内容却误导’的情况?”小a问。
“有,状态和内容必须一致。”老z说,“最常见的反例是:状态写 success,内容里却夹着一句异常栈;或者状态写 truncated,却把全文都塞了回来。模型是按状态决定下一步的——状态撒谎,模型就按错的路走。状态是导航,摘要是理由,引用是证据,三者要对得上;对不上时,宁可返回保守的状态,也不要报喜不报忧。”
8.4 能力最小化与失败语义
“工具要多大权限?”小a问。
“工具只应拥有完成当前任务所需的最小权限。”老z说,“读取、写入、网络、子进程和凭据访问应分别声明;危险动作需要明确审批点。模型不能通过工具描述获得额外权限,执行器也不能因为模型语气坚定而放宽策略。”
小a追问:“schema 里写明 path 是字符串,权限不就限死了吗?”
“限死的是类型,不是范围。”老z说,“path: string 接受任何字符串,包括 ../../etc/passwd。schema 合法,不代表路径在工作区内、不代表这个工具该读它。权限的最后一道闸,必须在执行边界上——检查路径是否越界、是否在允许的目录里,而不是默认模型给的都对。”
“失败是不是就是‘报个错’?”小a问。
“不是。失败不是异常分支的尾巴。”老z的语气认真起来,“对于 Agent,失败结果正是下一次决策的输入;因此错误要稳定、可分类、可观察,而不是只返回一句‘出错了’。”
“那失败结果和 8.3 说的结果分类,是同一回事?”小a问。
“是的,是同一条线的两端。”老z说,“8.3 讲成功路径的结果怎么设计,这里讲失败路径的结果怎么设计——它们用的是同一套信封:状态码、面向模型的摘要、可追查的引用。**成功的返回要让模型继续,失败的返回要让模型知道还能不能继续、往哪继续。**两类返回都不该是一句裸异常。”
“那失败结果怎么写才算可读?”小a问。
“三个要素:状态分类、面向模型的话、可追查的引用。”老z画了个对照:
text
差的失败返回:{ "error": "ENOENT" }
好的失败返回:
{ "status": "input_error",
"summary": "文件不存在:src/a.ts;已检查工作区根目录",
"ref": "log/2024-01-15/run#42" }“差的那个返回,模型看到 ENOENT 只能猜——是路径错?是权限?还是磁盘问题?好的那个直接告诉模型‘你给的路径在工作区里查不到’,模型下一步就知道该换路径还是放弃。失败返回的每一笔投入,都是在替模型省一轮盲目重试。”
8.5 输出是下一轮的输入接口
“结果格式有什么建议?”小a问。
“工具输出既服务人,也服务模型。因此应有稳定的信封,而不是把异常栈和无限日志原样拼进上下文。”老z说,“一个实用的结果包含状态、面向模型的摘要、受限的结构化数据、截断标记和供人追查的引用位置。”
json
// 示意
{ "status": "truncated", "summary": "找到 200 个匹配项,已返回前 20 个",
"items": ["…"], "continuation": { "cursor": "next" } }“那工具多少算合适?”小a问。
“粒度的收益与代价也在这里体现:独立的读取、搜索和写入工具便于单独授权和测试;过度拆分又会增加选择次数、轮数与上下文负担。”老z说,“应从可独立授权、失败和审计的动作出发,而不是从函数数量出发。”
小a问:“如果结果里本来就有敏感内容呢?比如配置文件里有密钥。”
“执行器要在进上下文前脱敏,不能寄望模型自己保密。”老z说,“模型不会因为你提示它‘别外泄’就真的不说——它下一步可能把这段内容原样写进日志工具,或者吐给用户。脱敏发生在结果进入上下文之前,是一道工程动作,不是一句 prompt。”
“那结果信封的字段,是不是也得带版本号?”小a问。
“最好带。”老z说,“模型会从旧工具的结果里学到一个格式,你的信封换了字段名,它还在按旧格式解析——这不是模型笨,是版本漂移。信封上留一个 schema_version,改字段时 bump 一下,模型看到新版本就知道别再按旧的读。信封版本号和工具描述一起进调用日志,出问题时能判断是结果格式变了,还是下游解析跟不上。”
“那旧结果还留在上下文里呢?”小a追问。
“旧信封和旧描述是同一枚硬币的两面。”老z说,“模型记忆里的旧格式不会因为你 bump 版本号就消失——它是从过去的调用历史里长出来的。版本号能帮助新的调用别再按旧格式读,但已经进入上下文的旧结果,该重新拉取还是得重新拉取。版本号管‘未来的调用’,缓存清理管‘过去的痕迹’,两件事别混成一件。”
示意:
search_text命中数过多时,返回页大小和游标,让模型缩小查询或请求下一页;不要把整个依赖目录塞回上下文。若工具返回包含密钥的配置文件,执行器应在进入模型上下文前做脱敏,而不能寄望模型自行保密。
8.6 Tool 与相邻机制的边界
“Tool 和前面那些概念,边界在哪?”小a问。
“Function calling 解决模型如何表达 tool call;ToolSpec 解决可调用能力如何被说明;执行边界决定请求是否真能影响文件、网络或进程。”老z说,“Hook 是在生命周期插入检查/观察的机制,MCP 是连接外部工具和资源的协议边界;它们都不自动替代 Tool 的输入校验与最小权限。”
text
tool call → runtime validation → policy check → execute
→ success/empty/denied/invalid_input/timeout/truncated
→ bounded structured result → 下一轮 context“反例是什么?”小a问。
“两个。反例一是工具成功产生十万行日志:执行器应分页、截断或保留引用,不能把它们全部注入 context。反例二是 schema 合法但路径越界或命令危险:必须在 execution boundary 拒绝。”老z说,“收益是每类能力可独立授权、测试和审计;代价是描述、结果语义与输出预算需要持续维护。”
8.7 工具要能独立测试
“那这么多边界,怎么保证工具本身没问题?”小a问。
“给工具做独立测试——不经过模型,直接喂参数、断言结果。”老z说,“工具测试和 Agent 测试是两层:Agent 测试测的是‘模型选没选对工具、参数传得对不对’,工具测试测的是‘给定参数,执行器做没做对’。模型测试覆盖不了执行器的边界——一个 schema 合法但路径越界的请求,模型可能永远不产生,但工具测试必须直接构造它并断言被拒绝。”
“工具测试至少要覆盖哪些?”小a追问。
“按结果语义逐类覆盖。”老z说,
| 测试维度 | 覆盖的输入 | 断言的输出 |
|---|---|---|
| 正常路径 | 合法参数 | 期望结果、正确状态 |
| 空结果 | 无匹配查询 | empty 状态而非报错 |
| 输入错误 | 缺字段、类型错 | invalid_input 状态 |
| 权限拒绝 | 越界路径、危险命令 | denied 状态 |
| 可恢复失败 | 文件锁定、限流 | 带上下文的失败信息 |
| 不可恢复 | 参数永远非法 | 明确失败,不重试 |
| 输出截断 | 超长结果 | 截断标记、引用位置 |
“还有一类最容易漏。”老z说,“执行器自身的健壮性测试:并发调用会不会串数据、重复调用有没有幂等保护、超时后会不会泄漏资源。这些不涉及模型,但出问题时最隐蔽——表现为偶发的数据错乱,日志里看不到模型选错工具的痕迹。”
“那工具测试和模型评测的关系?”
“互补。”老z说,“工具测试证明‘工具是对的’,模型评测证明‘模型会用这个工具’。工具先测——它错了,模型再怎么聪明也用不对;工具对了,再去测模型会不会选它、会不会传对参数。测试顺序也是依赖顺序:先验证工具本身,再验证工具被使用。”
“那工具依赖外部服务怎么办?测试时要真发请求吗?”小a追问。
“不能发,也不该发。”老z说,“工具测试要让执行器跑在可控边界里——文件系统用临时目录代替,网络调用返回固定响应,时钟也可以注入。目标只有一个:同一输入,同一结果。一旦测试里真发外部请求,网络波动就会把‘工具坏了’和‘今天网络差’混成同一个红点,你分不清该改代码还是该怪环境。真实的端到端调用单独放集成测试,并留出明确的失败信号。”
“那工具测试过了,上线就不会出事?”
“不能这么保证。”老z说,“测试覆盖的是你构造过的输入,覆盖不了真实数据的形状——模型可能产生测试里没写过的参数组合,外部系统可能返回假实现里不存在的字段。所以工具测试保证的是‘给这些输入行为正确’,不是‘任何输入都正确’。上线后还要有观测:每个结果信封的状态和引用都留痕,出现没见过的状态组合时能看见,而不是等事故来发现。”
小a追问:“MCP 接进来的工具,校验谁负责?”
“调用方负责。”老z说,“MCP 把外部能力连进来,但它不替你判断这把工具在你这套权限策略下能不能用。协议解决互操作,授权永远在调用方这一侧——所以 MCP 工具进上下文之前,要走的校验和自家工具一模一样。”
“Hook 和执行边界,又是什么关系?”小a问。
“Hook 是挂上去的检查点,执行边界是检查要守的那条线。”老z说,“Hook 让你在‘执行前’插入一道审批或观察,但真正拒绝越界动作的,是执行边界上的策略校验。Hook 提供时机,策略提供判断;没有策略,Hook 只是看一眼就放行。”
“那这些机制混在一起,各自守哪一段?”小a问。
老z画了一条完整的调用链:
text
模型请求
│
├─ schema 校验 ← 形状约束(类型、必填、枚举)
│
├─ Hook: 执行前 ← 插入审批、观察、日志
│
├─ 策略校验 ← 权限、路径、动作合法性
│ └─ 执行边界 ← 副作用真正发生在这里
│
├─ Hook: 执行后 ← 记录、脱敏、通知
│
└─ 结果信封 ← 状态 + 摘要 + 引用 → 回填上下文“每一站挡的是不同性质的错。”老z说,“schema 挡格式错,策略挡权限错,Hook 挡流程错,结果信封挡信息错。少了哪一站,那类错就会直接落到下一站或落到事故。”
| 层 | 挡什么 | 挡不住什么 |
|---|---|---|
| schema 校验 | 字段类型错、必填漏、枚举值外 | 路径越界、语义错 |
| Hook(执行前) | 流程缺审批、缺观察 | 动作本身的合法性 |
| 策略校验(执行边界) | 路径越界、权限不足、危险动作 | 业务意图是否正确 |
| 结果信封 | 信息过载、敏感泄露、状态含糊 | 上游已经发生的副作用 |
“那这些机制里,谁对‘后果’负责?”小a问。
“执行边界对后果负责,其他层对信号负责。”老z说,“schema 校验说‘参数形状不对’,Hook 说‘流程缺审批’,策略说‘动作越界’——它们都是把话说在前面;真正让副作用发生或没发生的是执行边界。出事后回查,先看执行边界有没有按收到的信号行动:信号都对、执行器没执行,是执行器的问题;信号本身漏了,才是上游的问题。”
8.8 工具的生命周期与撤销
“那工具除了被调用,还有没有别的重要时机?”小a问。
“工具不是被调用的那一刻才存在。”老z说,“它有完整的生命周期:注册、发现、调用、返回、失效。前面几章一直在讲调用和返回,但注册和失效经常被忽略——而它们恰恰是排障时最难定位的环节。”
“注册会出什么问题?”
“重复注册、覆盖注册、幽灵注册。”老z说,“重复注册是两个模块注册了同名工具,后者覆盖前者,模型以为用的是 A 语义,实际执行的是 B;幽灵注册是工具注册了但从未从工具列表移除,模型一直能看到它、一直可能误选。注册表要有明确的注册和注销成对操作,并且注销要在日志里留痕。”
“那注册表里登记了,执行器里却没有对应实现呢?”小a问。
“这是生命周期里最隐蔽的一类故障。”老z说,“工具列表和可执行实现如果来自两处登记,有一天必然对不上——注册表说‘有这把工具’,执行器却说‘没实现’。模型按注册表选中它,调用直接落到一个你从没想过的异常;它不报权限错、不报路径错,就是安静地失败。工具列表和实现应该出自同一份登记,注册、实现、注销成对管理,而不是两边各记各的。”
“失效呢?”
“工具可能被下架、改名或权限收窄。”老z说,“失效的工具应该从模型可见的工具列表里移除,而不是留在里面等模型选了再拒绝。一个模型看不到的工具,永远不会被误选;一个能看到却永远被拒的工具,只会让模型一次次绕圈。”
“那‘撤销’呢?工具执行错了能不能撤销?”
“看副作用类型。”老z说,“只读操作没有撤销问题——读错了重读就行;写操作要看有没有撤销接口——文件改错了可以靠版本控制恢复,但网络请求发出去了就撤不回来。撤销不是工具的默认能力,它需要被显式设计:要么提供反向操作,要么在副作用发生前用审批拦下。没有撤销能力时,唯一的护栏是执行前的检查。”
| 副作用类型 | 撤销能力 | 没有撤销时的护栏 |
|---|---|---|
| 只读 | 无需撤销 | 无 |
| 写文件 | 版本控制可恢复 | 写前审批、写后验证 |
| 网络外发 | 通常不可撤销 | 外发前确认、记录审计 |
| 删除 | 取决于目标系统 | 强制审批、二次确认 |
“那工具自己执行超时了,算什么?”小a追问。
“要分清是‘还在跑’还是‘跑完了结果没回来’。”老z说,“执行器还在干活时超时,是中断——你可以等、可以放弃,但要知道进程可能还在写东西;动作已经完成、只是响应没送达时超时,是‘结果丢失’——这时候盲目重试,副作用可能执行两次。判断依据是你能不能确定动作是否已生效:日志里已经写明就不要再重试;不能确定的,用幂等键约束重试。”
“所以工具设计里,‘少提供危险工具’本身就是一种安全手段。”小a说。
“对。工具少,模型可选的危险面就小;工具多,每个都要自己的护栏。”老z说,“新增一个工具的代价,不只是写实现,还有注册管理、生命周期维护和撤销设计——这些都该在动手前想清楚。”
“那这章的结论和下一章怎么接?”小a问。
“工具是给模型装上的把手,但把手再多,一次也只能挥动一下。”老z说,“真实任务需要把‘读、改、验’一轮轮串起来——那是下一章 Agent Loop 的事:模型提一步,工具落一步,再回到模型决定下一步。工具解决‘能不能做’,循环解决‘要不要一直做下去’。”
小结
工具是给模型装上的把手,让它能真正动手干活。一个完整的工具由五部分组成:能力、描述、schema、执行器和结果语义,缺一不可。其中规格和执行器分属两侧——规格是给模型看的"说明书",告诉它这个工具叫什么、接受什么参数、做什么事;执行器是真正动手的"手",只有宿主程序能调用。这种分离让模型能根据描述选对工具、用对参数,而真正的副作用只发生在执行边界上,模型本身感知不到执行的细节。运行时校验在这条链路上承担过滤职责,因为模型会给出 schema 范围之外的值,不校验就会把脏数据送进执行器。
决定一个工具能不能安全进入自动化流程的,是三道硬约束:最小权限决定了它能碰什么,输出边界决定了它的结果占多少上下文,可读失败决定了模型能否从错误中恢复。这三者都靠工程实现,不靠 prompt 里的"请小心"。可读失败之所以关键——返回"文件不存在:src/a.ts"比返回"Error: ENOENT"更能让模型改对路径。权限控制落在执行层而不是描述层,因为描述只是文字,模型可以无视,但执行器拒绝时模型绕不过去。
动手核验
为 search_text 设计 schema 和结果格式,分别覆盖无匹配、路径越界、结果过多和正常命中。请同事只根据描述选择工具并构造参数;若对方经常误用,先改描述或粒度,再考虑增加工具。