跳到主要内容

通过了 Schema 验证的虚假工具参数

· 阅读需 9 分钟
Tian Pan
Software Engineer

Agent 调用 fetch_order,参数为 order_id: "ORD-739241"。Schema 接受了它 —— 三个字母、一个连字符、六位数字,完全符合模式。工具返回了 404。Agent 开始含糊其辞,生成了 "ORD-739242" 并再次调用,又得到一个 404,接着又生成了 "ORD-739243"。你的仪表盘记录了三次成功的工具调用和三次干净的 Schema 校验。客户在等待。在追踪记录的某个地方,安全栈的每一层都报告为绿色,而模型正在全速虚构标识符。

团队认为 Schema 拦截了错误。Schema 确实拦截了它能拦截的东西:形状(shape)。它检查了参数是否为字符串,是否匹配正则表达式,以及必填字段是否存在。Schema 无法检查 ORD-739241 是否对应数据库中的真实订单,因为 Schema 根本不知道数据库的存在。这种差距 —— 句法上的合理性与语义上的正确性之间 —— 正是大多数生产环境工具调用 bug 的所在地,而且这种失败非常隐蔽,唯一的信号就是客户的困惑。

这不是模型的问题。这是一个早于模型出现的架构错误。将 JSON Schema 视为安全层,与将电子邮件字段的正则表达式视为可送达性检查属于同类错误:格式是必要的,但格式并不是你关心的属性。模型的工作是提议一个句法有效的调用。你的工作是在工具运行之前,决定该调用是否指向真实存在的东西。

形状不代表真实

严格的结构化输出和受限解码使得强制模型工具调用符合正确形状几乎变得零成本。提供商基于语法的解码器在每一步都会屏蔽无效标记,因此从结构上讲,输出是一个格式良好的 JSON 对象,必填字段齐全,枚举值属于列表之一,整数在声明范围内。这是一个极具意义的保证 —— 就在一年前,生产栈的一半还是用于修复无效 JSON 的字符串修复代码 —— 这种解放使得团队经常高估它的作用。

这种保证仅限于形状。字面字符串 "ORD-999999999" 与真实 ID 一样,都能完美满足模式 ^ORD-\d{6}$。1812 年的 ISO 日期可以通过日期格式检查。属于不同租户的用户 UUID 也能通过 UUID 格式校验。Schema 说明模型返回了正确类型的对象,但它并没有说明该对象指向了任何实际存在的东西。

反方向亦然。失败的 Schema 检查并不能说明底层意图是否合理;模型可能幻觉出了一个不存在的 priority 字段,Schema 理所当然地拒绝了它,但 Schema 无法告诉你该请求是否试图表达一些你的工具界面尚不支持的正当需求。结构化输出让形状错误变得明显,却让引用错误变得悄无声息。

重试循环是你的成本中心

失败模式不是第一个 404,而是第二个、第三个和第四个。

一个在“得力助手”案例上训练过的模型,在收到错误时会尝试重试。如果 Agent 框架原样暴露错误 —— “未找到订单 ORD-739241” —— 模型最可能的下一步行动就是认为自己弄错了数字,并提议一个邻近的 ID。每次调用都通过了 Schema 校验。每次调用都消耗 Token。每次调用都延迟了面向用户的回答。在没有迭代上限的环境中,循环只有在模型放弃或框架介入时才会终止;在有上限的环境中,循环结束时已经把你的预算花在了虚假 ID 上。

成本不仅是金钱。成本还在于你的审计日志现在包含了一系列在任何审查者看来都像是 Agent 在正常工作的工具调用。三次查询相似的订单 ID 正是困惑的人类操作员会做的事;事后阅读追踪记录无法告诉你这些 ID 从未从任何来源提取过。重试将幻觉“洗白”成了符合调查模式的行为。等到有人注意到时,解释已经不存在于任何人的记忆中了。

在工具运行前验证引用对象

缺失的一层是存在性检查(existence check),它运行在模型提议的调用和工具执行之间。它不需要很复杂。它只需要回答一个问题:参数是否指向工具将要查询的权威系统中的现有事物?

对于 ID 形状的参数,这通常是对工具本会访问的同一个存储库进行查找,但成本很低 —— 一个索引化的 EXISTS 查询、一个 Redis 集合成员测试,或者针对密集场景的布隆过滤器(Bloom filter)。成本是每次工具调用增加一次额外的往返;收益是工具仅针对可解析的参数运行,重试循环永远不会开始,因为是由框架而非工具告诉模型未找到 ID。

结构化的返回路径与检查本身同样重要。当查找失败时,框架不应抛出异常或原样返回 404。它应该返回一个模型被指示去处理的结果:一个带有 status: "not_found" 字段的 JSON 对象,一个可选的 did_you_mean 近似匹配列表(来自实际数据,而非模型的想象),以及一个指令,规定下一步行动应该是请求用户澄清,而不是尝试另一个 ID。当框架提供一个优雅退出的结构让模型去填充时,模型在“优雅放弃”方面表现得要好得多。

对于不是 ID 但仍具有引用性质的参数 —— 如客户姓名、SKU 或自由格式字段中的日期 —— 同样的模式也适用于模糊匹配。在工具运行前将参数解析为真实实体,将歧义作为澄清问题反馈给用户,并拒绝调用指向未解析引用的工具。Agent 感知上的“聪明程度”会下降,因为它会问更多问题;但 Agent 实际的正确性会上升,因为它停止了对虚构实体的操作。

置信度:廉价的第二信号

对于从用户消息中提取的实体——比如“我上周二下的订单”——有第二个值得利用的信号:提取过程的置信度。如果你的实体提取步骤本身是一个模型调用,那么提取出的 token 的对数概率(logprobs)就是一个可用的代理指标。如果它是一个独立的分类器,你已经有了一个分数。无论哪种方式,阈值问题都是一样的:置信度高于多少时智能体执行操作,低于多少时进行询问?

从业者反馈,实体 token 的联合置信度在 0.85 以上通常无需确认即可使用,0.70 到 0.85 之间则需要提问澄清,而低于此数值的则根本不应触发工具调用。确切的阈值取决于具体的业务领域——客服智能体和支付智能体对错误的容忍度截然不同——但架构的核心点是不变的:置信度是你已经拥有的数字,拒绝查看它只是一种选择。在不公开 logprobs 的情况下,自洽性采样(Self-consistency sampling)是一种成本更高但更健壮的替代方案,即多次运行相同的提取过程,并将不一致视为低置信度。

要避免的一点是让模型在结构化输出中自我报告其置信度。“我有 95% 的把握订单 ID 是 ORD-739241”只是一个披着数字外衣的幻觉;模型无法通过自然语言准确校准自身的确定性。请使用模型外部计算的信号——如采样、logprobs 或独立的验证器——或者干脆不使用任何信号。

架构层面的认知

Schema(模式)和 Referent(引用对象)存在于技术栈的不同层级,将它们视为单一的安全边界会导致 bug 变得隐形。

JSON Schema 是工具调用协议的一部分。它属于模型与函数调用 API 之间的契约,用于验证模型是否针对声明的接口发出了语法有效的请求。引用对象检查(Referent check)属于你的应用层。它验证即使请求格式正确,是否指向了一个你可以操作的实际对象。Schema 由平台团队负责;而引用对象检查则由负责工具所读取数据的人员负责。当没有人负责第二层时,团队看到的只是第一层的绿色勾选标记,从而得出系统是安全的结论。

在架构评审中指出这一点是最廉价的干预手段。每个带有引用参数的工具都需要一个文档化的预执行检查,缺失该检查应被视为阻碍发布的 blocker(阻断性问题),而非技术债务项。读取参数纯函数(如计算器、单位转换器、正则表达式测试器)的工具不需要它;但进行查询或修改操作的工具则需要。分界线在于参数是否引用了“状态”(state),而几乎所有有意义的工具都会涉及状态。

混淆 Schema 与安全性的团队正在为每一次无声歧义带来的幻觉重试买单,而账单最终会以客户投诉而非报警通知的形式寄达。能够识别这一差距并构建引用对象检查的团队,只需多花一次往返交互(round-trip)的时间,就能终结这一类 bug。Schema 正在履行其设计的职责。幻觉发生在它之上的层级,而那一层需要你来构建。

References:Let's stay in touch and Follow me for more thoughts and updates