用工具参数校验阻止 Agent 静默出错
文章针对 Agent 调用成功却产生错误业务结果的问题,提供工具 Schema 设计决策树、示例和检查工具。重点检查隐式类型转换、空值多义性,以及缺少枚举约束的自由文本字段。
AI 深度解读
Agent 跑完了,没报错,工单却挂到了错误客户名下。这类问题难查,因为工具接受了错误参数,模型还会在结果上补一段自信的说明。原文给出的解法是:先检查工具 Schema,再沿着解析、身份回传、状态取值、字段来源和运行波动逐层定位。
它值得读的地方,是把「模型偶尔不靠谱」拆成了可以验证的接口问题。但文中的故障是构造案例,阈值也没有对照实验支撑;附带的检查脚本和指标,还各有一处会误导排查的细节。
为什么要先查工具契约,再怀疑模型
工具 Schema 是模型调用工具时遵守的参数契约。问题在于,为人类调用者设计的宽松接口,交给模型后,可能把缺失、猜测和非法取值全部放行。
作者用六个概念描述这种风险:
- 类型强制转换面(Coercion surface),指错误类型被悄悄转换的入口。把
'3'转成3尚可解释,把'three'转成3已经包含猜测。 - 哨兵值歧义(Sentinel ambiguity),指
null、''、0、[]同时承担多种含义,例如既表示「没有找到」,又表示「尚未查询」。 - 封闭词表(Closed vocabulary),指用
enum或pattern限定取值,避免模型发明业务状态。 - 回传字段(Echo field),指模型必须原样复制的 ID、hash 或请求令牌,用来检查调用有没有对应到原始输入。
- 错误通道(Error channel),指工具把失败原因传回模型的机制,让它有依据修正参数。
- 必填字段密度(Required-field density),即每个工具的必填字段数除以总字段数,用来发现允许省略过多信息的接口。
工具返回成功,只能说明调用被接受,不能证明参数对应了正确的业务对象。
作者提供了只依赖标准库、支持 Python 3.9+ 的 schema_lint.py,运行方式是 python schema_lint.py ticket_tools.json。它用 R1 标记字段数至少为 3、必填密度低于 0.75 的情况;R2 标记没有 enum 或 pattern 的字符串;R3 提醒检查布尔开关;R4 提醒检查空值歧义。
这些规则是排查入口,不能当成质量评分。自由文本描述当然可以是字符串,布尔值也未必需要改成枚举。作者称四个工具的 Schema 首轮通常会出现 8 到 15 条提示,但没有提供样本来源。
脚本中最需要核对的是空值检查:
if node.get('default') is None or kind in ('null', ['string', 'null']):
findings.append(['R4', f'{path}: null default, sentinel ambiguity'])
注意第一行:没有声明 default 时,node.get('default') 也返回 None。因此,它会把「未设置默认值」也报成「默认值为空」。此外,脚本只递归处理 properties 和字典形式的 items,没有覆盖完整的 JSON Schema 组合结构。它适合做快速提醒,不能替代真正的校验器。
五条分支,分别拦住哪一种错误
第一条分支处理「输出解析不了」。例如模型在 JSON 前后夹了说明文字,json.loads 抛错,连续重试三次,也付了三次成本。
作者建议先确认端点是否支持结构化输出,支持就启用;不支持时,再提取第一个括号平衡的 {...} 并校验。这里的关键是校验:能提取出 JSON,不代表参数合法。原文提出解析失败低于每 50 次调用 1 次即可结束这一分支,但没有说明这个阈值的实验依据。
第二条分支处理「能解析,回传身份却错了」。输入是 request_id: r-8841,工具调用却带回 request_id: r-8814 或干脆缺失,后续结果就可能关联到错误记录。
做法是把回传字段加入 required,服务端再逐字比较,不一致就拒绝。只声明必填不够,存在一个字符串和复制了正确字符串,是两回事。
第三条分支处理「身份正确,业务状态漂移」。Schema 只写 status: string,模型就可能传入 "resolved-ish" 或 "Pending Review",写入后被看板归入未知状态。
应把字段限制为代码真正识别的 enum,拒绝时通过错误通道返回允许集合。作者建议保持词表简短,例如六个值,而非十六个。这是控制选择复杂度的经验建议,不能据此删掉真实业务状态。
第四条分支处理「结构合法,值却没有来源」。虽然分支标题说的是必填值为空,例子实际是模型编造了格式正确的 customer_id。作者建议拆成两阶段:先从系统提供的候选项里选择,再固定选择结果,填写其余字段。
它缩小了模型自由生成身份字段的空间,但候选项必须正确,第二阶段也必须真正锁定选择结果。R1 能提示接口允许省略过多字段,却无法证明某个 ID 是编造的。
第五条分支处理「同一输入反复运行,结果不稳定」。原文用五次中四次正确作例子,建议记录二十次运行的输入、Schema hash 和输出,再区分波动发生在字段选择还是字段值。
这些记录有助于定位问题,但仅凭字段选择波动,就断定一定是解码或采样问题,证据还不充分。应先固定可控条件,再排查剩余变量。
回传一致率能看什么,又会漏掉什么
作者用下面的函数计算回传一致率,建议修改前采集 20 个样本,修改后再采集 20 个,并随结果记录 Schema hash。
def echo_match_rate(samples, field='request_id'):
hits = sum(
1 for s in samples
if s.get('args', {}).get(field) == s.get('input', {}).get(field)
)
return hits / len(samples)
注意比较条件这一行:如果输入和参数都缺少 request_id,两侧都是 None,也会算命中。空样本还会触发除零。复用前应检查输入身份字段有效,并把输出缺失计为失败。
更关键的是,即使请求 ID 完全一致,客户选择、状态和正文仍可能出错。回传一致率是一项身份检查指标,不能代表整个任务的正确率。 修改枚举后,这个指标也可能完全不变,因此原文所说「指标没动就是修错分支」,判断过强。
作者提出的停止条件是:回传一致率接近或达到 1.0,所有决定代码分支的字段都有封闭词表,拒绝记录少到人工查看时不会觉得是噪声。这比追求零拒绝更合理,但二十个样本适合初步比较,不能据此证明低频错误已经消失。
我的判断
我认可它把约束放到服务端执行:身份不符就拒绝,非法状态就返回允许集合,缺少可靠来源就先选候选项。这些做法给错误留下了明确出口,也方便模型修复。
不过,强制类型、非空和必填,只能证明参数符合形式要求。客户是否选对,还需要来源或业务规则验证。与只改提示词相比,服务端约束更直接;与确定性代码相比,能从已有上下文直接取得的 ID,也没必要再让模型复制一次。
这套方法还有适用前提。你必须能修改工具 Schema;如果使用他人的 function-calling API,作者认为第三、第四分支不可用,只能依靠提示侧修正和回传检查。会发邮件、扣款的调用,也不能为了凑二十个样本反复执行,应保存调用快照,在 stub 中回放。少于三个工具、每次输出都有人审核的 Agent,作者建议跳过整套版本化测量流程。
文章还披露自己属于 MonkeyCode 产品推广。免费模型访问和免费服务器被推荐用于批量测量,运营方宣称额度约 1000 万 tokens;作者没有验证模型名、吞吐、硬件或额度是否永久。这部分不能充当方案有效性的证据,额度也应按实际使用当天的信息核对。
能拿来做什么
- 给现有工具做一次静态检查,优先查看身份字段、业务状态和空值含义,把 R1 到 R4 当成待核对提示,逐项判断是否真的存在风险。
- 在 Node 工具服务执行写入前校验请求身份和业务取值,并确认错误能传回模型;只写日志、模型收不到原因,就无法形成修复闭环。
- 对模型需要填写的客户 ID,优先从可信上下文直接传入;确实需要模型选择时,提供候选集合,并在后续步骤固定选择结果。
- 把 Schema hash 与变更放在同一次提交中,保存修改前后的样本;更换模型或端点后重新测量,有外部副作用的工具使用 stub 回放,同时检查身份一致率和业务结果。