用 Schema 校验让工具输出可控
当模型输出要进入工具调用或工作流时,schema 不是文档装饰,而是系统契约。
没有 schema,模型和后端之间靠自然语言约定:请输出订单号、金额、原因、操作类型。这个约定在 demo 里看起来能跑,但一旦输入复杂、模型升级、prompt 修改或下游工具变多,字段缺失、类型错误和枚举漂移就会反复出现。
有 schema,也不代表万事大吉。schema 设计得太松,错误会流进工具;schema 设计得太重,模型输出更容易失败;schema 错误信息不可读,修复和重试就很难稳定。
这篇文章聚焦三件事:Zod、JSON Schema 和错误 schema 如何一起让工具输出可控。
场景
假设一个电商后台接入 AI 售后助手。客服输入用户诉求后,模型需要生成一个工具调用计划。
输入:
用户说收到的耳机左耳没有声音,订单号 O-92818,购买 5 天,希望直接退款。
期望输出:
这个 JSON 会进入售后工具。工具会检查订单归属、商品状态、退款规则和是否需要人工审批。
如果没有 schema,模型可能输出:
人能看懂,但工具不能安全执行。更危险的是,系统如果用宽松解析把它“猜着转成”工具参数,可能绕过审批。
问题类型
schema 校验主要处理三类问题。
第一类是结构问题:字段缺失、字段名不一致、类型错误、嵌套层级错误。
第二类是约束问题:枚举值不合法、字符串过长、金额为负、数组为空、日期格式错误。
第三类是错误表达问题:校验失败后,系统不知道该告诉模型什么、告诉用户什么、记录什么。
模型适合生成候选结构。schema 负责判断候选结构是否符合工具契约。业务系统负责判断即使结构正确,动作是否允许执行。
风险/收益
schema 校验的收益是把不稳定的语言输出变成可检查的边界。
主要收益包括:
- 后端只接受明确字段。
- 工具调用前能拦截错误参数。
- 前端可以基于类型稳定渲染。
- 失败可以按字段归因。
- 重试可以带着具体错误修复。
- schema 可以成为 prompt、代码和测试之间的共同契约。
成本包括:
- schema 需要设计和版本管理。
- schema 变更可能影响 prompt、前端、后端和历史数据。
- 过严的 schema 会增加失败率。
- 过松的 schema 会降低保护能力。
- 错误 schema 和降级流程需要额外设计。
什么时候简单 prompt 足够?
如果输出只用于人工阅读,或者字段不进入工具、不入库、不触发流程,prompt 加示例就可能足够。比如让模型把用户反馈整理成一段客服备注,客服会人工判断。
什么时候需要工程方案?
只要输出会调用工具、写业务系统、影响用户权益、生成可审计记录或进入无人值守流程,就必须有 schema 校验。高风险动作还要有业务规则、权限、人工确认和审计。
Zod、JSON Schema 和错误 schema
Zod 和 JSON Schema 解决的不是完全相同的问题。
Zod 更适合 TypeScript 项目里的运行时校验和类型推导。它让后端、前端或工作流代码可以直接用同一份 schema 校验模型输出,并得到类型安全。
JSON Schema 更适合作为跨语言、跨服务、工具接口和模型结构化输出的通用契约。它可以被很多语言和平台消费,也适合做 API 文档和契约测试。
错误 schema 则用于定义失败时的结构:校验错误如何表达,哪些错误可修复,哪些错误需要用户补充,哪些错误必须转人工。
方案阶梯
第一层:只定义字段说明
最轻量的做法是在 prompt 里写字段说明和示例。
适合低风险、字段少、人工复核的场景。它的成本低,但没有硬约束。后端仍然可能收到不可解析或不符合预期的输出。
第二层:Zod 运行时校验
在 TypeScript 项目里,Zod 是很自然的起点。
示例:
Zod 的好处是离业务代码近,可以直接得到解析后的类型,也可以把错误格式化后传给模型修复。
但 Zod 主要服务 TypeScript 运行时。如果多个服务、语言或外部工具都要共享契约,通常需要导出或同步到 JSON Schema。
第三层:JSON Schema 作为工具契约
JSON Schema 适合作为模型输出、工具参数和服务边界之间的通用协议。
示例:
additionalProperties: false 很重要。它能阻止模型随手增加 refundAmount、internalNote、approval: "skip" 这类未被系统设计过的字段。
第四层:业务校验
schema 通过后,还要做业务校验。
例如上面的 orderId 格式正确,不代表:
- 订单存在。
- 订单属于当前用户。
- 商品在可退款窗口内。
- 当前客服有权限申请退款。
- 这个原因码允许退款而不是换货。
schema 校验解决“形状正确”,业务校验解决“是否允许”。这两层不能混在一起。
第五层:错误 schema
很多团队只设计成功输出 schema,却没有设计失败输出 schema。
这会导致失败时系统只能记录一段报错字符串,无法自动决定下一步。
一个错误 schema 可以像这样:
错误 schema 的作用是让系统知道:
- 哪些错误可以让模型修复。
- 哪些错误应该重新生成。
- 哪些错误应该向用户补问。
- 哪些错误必须转人工。
- 哪些错误是权限或业务拒绝,不能通过重试解决。
第六层:版本和评估
schema 是接口,接口就会演进。
常见变更包括新增字段、修改枚举、拆分 action、收紧字符串长度、增加业务校验。每次变更都可能影响 prompt、模型输出、前端渲染、后端工具和历史数据。
因此上线后要记录:
- schema version。
- prompt version。
- model version。
- parse error。
- schema error。
- business error。
- retry count。
- 最终处理路径。
没有版本和 trace,团队很难判断一次模型升级或 schema 修改到底让稳定性变好了还是变差了。
方案选型表
反模式
第一种反模式:只校验字段存在,不校验枚举和类型。
字段存在但值不合法,和字段缺失一样危险。action: "refund_now" 如果没有被拦截,可能进入未设计的分支。
第二种反模式:允许额外字段自由通过。
额外字段看起来无害,但会制造隐性功能。下游某段代码如果开始读取这些字段,就绕开了正式契约。
第三种反模式:把业务规则塞进 prompt。
例如“只有购买 7 天内才能退款”可以写进 prompt 辅助模型判断,但真正执行前必须由后端规则校验。prompt 不是权限系统。
第四种反模式:schema 过度复杂。
一个工具调用 schema 里塞入多种 action、几十个互斥字段和多层嵌套,会让模型更难稳定输出。更好的做法通常是先分类 action,再使用对应 action 的小 schema。
第五种反模式:错误信息只给开发者看。
如果错误不能被模型修复、不能被用户理解、不能被运营处理,就只是日志。错误 schema 应该同时服务机器恢复和人工排查。
第六种反模式:schema 变更不做回归。
改一个枚举名、收紧一个正则、删除一个字段,都可能让历史样本大面积失败。schema 变更应该跑评估集,而不是只测当前手头样例。
工程清单
- 契约边界:schema 约束的是模型输出、工具参数,还是 API 响应。
- 工具用途:每个字段是否对应真实工具参数、UI 展示或审计需求。
- Zod:TypeScript 代码是否有运行时校验和类型推导。
- JSON Schema:跨服务或工具是否有统一 schema 和版本号。
- 额外字段:是否设置
additionalProperties: false或等价策略。 - 枚举:action、状态、原因码是否封闭且有业务含义。
- 空值:
null、unknown、空数组和字段缺失是否区分清楚。 - 业务校验:订单归属、金额范围、权限、状态流转是否在后端检查。
- 错误 schema:parse、schema、business、permission 错误是否结构化。
- 修复策略:哪些字段错误可修复,哪些必须补问或转人工。
- 版本管理:schema、prompt、model 是否一起记录。
- 回归评估:schema 变更是否跑历史样本和失败样本。
- 审计:工具执行前后的参数、结果和人工确认是否可追溯。
小结
schema 校验的价值不只是“防止 JSON 写错”,而是建立模型和工具之间的边界。
Zod 适合在 TypeScript 项目里快速获得运行时校验和类型安全。JSON Schema 适合跨服务、跨语言和工具契约。错误 schema 则把失败从一段日志变成系统可以处理的状态。
简单 prompt 适合人工可复核的低风险输出。只要模型输出要驱动工具、写入系统或影响用户权益,就应该用 schema 做硬边界,再叠加业务校验、权限控制、人工确认和可观测性。