大模型输出为什么要做三层校验:从 JSON 到业务事实
大模型很擅长理解人话,也很擅长把结果组织成看起来完整的句子。但在真正的业务系统里,“看起来合理”远远不够。
比如用户让 AI 查询订单,大模型可能返回这样一段内容:
{
"order_id": "12345",
"include_items": true
}
这段内容看上去没有问题,可它只说明模型提出了两个参数。它没有证明订单 12345 真的存在,也没有证明当前用户有权查看这个订单。
因此,大模型应用通常需要连续做三层校验:先确认内容能不能读,再确认格式是否符合约定,最后确认它是否符合真实业务规则。
先理解“校验”是在防什么
可以把大模型想象成一个理解能力很强、但不能直接替你盖章的助手。
它可以从“帮我查一下 12345 订单,顺便看商品明细”这句话里提取出订单号和查询选项,也可以判断用户大概想做什么。但它不能代替程序读取数据库,不能代替权限系统判断身份,也不能因为自己生成了一段 JSON,就让这段数据自动变成事实。
程序接收到模型结果后,至少要回答三个问题:
- 这段内容是不是合法的 JSON?
- 即使是合法 JSON,字段和类型是否符合接口约定?
- 即使格式都正确,里面的内容是否符合当前业务?
这三个问题分别对应语法校验、Schema 校验和业务校验。
第一层:语法校验,先确认“能不能读”
语法校验是最基础的一层,关注的是数据本身能不能被程序解析。
合法 JSON 需要使用双引号:
{
"order_id": "12345"
}
下面这些内容对人来说可能一眼就能看懂,但对 JSON 解析器来说并不合法:
{'order_id': '12345'} // 使用了单引号
{"order_id": "12345",} // 最后多了逗号
{order_id: "12345"} // 字段名没有双引号
模型有时会在 JSON 外面加一段解释文字,有时会漏掉引号、括号或逗号。如果程序直接把结果当作对象使用,可能在最早的一步就报错。
语法校验通常只做一件事:尝试解析。
let data;
try {
data = JSON.parse(modelText);
} catch {
throw new Error('模型返回的内容不是合法 JSON');
}
这一层解决的是“程序能不能读懂”,并不关心 order_id 是否存在,也不关心字段名是否写对。
第二层:Schema 校验,再确认“长什么样”
Schema 可以理解为一份数据说明书。它提前写清楚:允许哪些字段、哪些字段必填、每个字段是什么类型,以及值需要满足哪些基本限制。
例如订单查询接口可以规定:
| 字段 | 类型 | 是否必填 | 基本要求 |
|---|---|---|---|
order_id | 字符串 | 是 | 只能包含数字,长度为 5 到 20 位 |
include_items | 布尔值 | 否 | 默认值为 false |
| 其他字段 | 不允许 | - | 防止模型随意增加参数 |
这样一来,下面的数据虽然是合法 JSON,但仍然不能通过 Schema 校验:
{
"order_id": 12345,
"include_items": "是"
}
原因是 order_id 应该是字符串,include_items 应该是布尔值。下面的数据也不合格:
{
"include_items": true
}
因为必填的 order_id 缺失了。
Schema 校验可以使用 JSON Schema、Zod、Pydantic 等工具实现。以 Zod 的思路表示,大致是:
const orderQuerySchema = z.object({
order_id: z.string().regex(/^\d{5,20}$/),
include_items: z.boolean().optional().default(false),
});
大模型平台提供的结构化输出或 Function Calling,也是在帮助模型更接近这个结构。但平台约束不能取代服务端校验。模型可能调用了错误的接口版本,客户端可能被绕过,第三方模型也可能没有完全遵守约定,所以后端仍然要把收到的数据重新校验一遍。
这一层解决的是“数据结构是否符合接口约定”,还没有证明数据真实存在。
第三层:业务校验,最后确认“能不能做”
业务校验检查的是现实世界中的规则。它通常需要访问数据库、权限系统或其他业务服务。
例如 order_id 已经通过了语法和 Schema 校验,但程序还要继续检查:
- 订单
12345是否真的存在; - 订单是否属于当前用户或当前租户;
- 当前用户是否有查看订单的权限;
- 订单是否处于允许查询的状态;
- 用户请求的操作是查询,还是隐藏在自然语言中的修改、退款等高风险动作。
因此,完整流程应该是:
用户输入
-> 模型提取候选参数
-> 语法校验:能否解析
-> Schema 校验:字段和类型是否正确
-> 业务校验:数据存在、权限和状态是否允许
-> 后端执行查询
-> 返回真实结果
关键在于,“模型提取出的订单号”只是候选参数,不是数据库事实。只有后端查询到了订单,并且确认当前用户可以访问,它才可以出现在最终答案里。
三层校验分别挡住什么问题
| 校验层 | 主要问题 | 典型错误 | 能否证明业务事实 |
|---|---|---|---|
| 语法校验 | 内容能不能解析 | 括号没闭合、单引号、尾逗号 | 不能 |
| Schema 校验 | 数据结构是否符合约定 | 类型错误、字段缺失、值格式错误 | 不能 |
| 业务校验 | 当前业务是否允许 | 订单不存在、无权限、状态不允许 | 可以作为执行前确认 |
三层校验不是重复劳动,而是处理不同类型的错误。只做第一层,程序可能读到结构完全错误的数据;只做前两层,系统可能把不存在的订单当成真实订单;只做业务校验,又可能在查询数据库前就被格式错误拖垮。
一个常见误区:格式正确不代表答案正确
很多人第一次接触结构化输出时,会把 Schema 当成“防幻觉开关”。其实它更像一个表格模板:它可以要求模型把内容填进指定的格子,却不能保证模型填入的事实是真的。
例如下面的数据完全符合 Schema:
{
"order_id": "99999",
"include_items": false
}
但如果数据库里没有订单 99999,它依然是错误请求。如果订单存在,但属于另一个用户,它依然不能被当前用户查看。
所以更稳妥的分工是:
- 模型负责理解意图、提取信息、选择候选工具;
- Schema 负责限制输入形状,减少接口歧义;
- 代码负责权限、状态、事务、幂等和真实数据;
- 数据库和业务服务负责给出最终事实。
这也是为什么高风险操作不能只依赖 Prompt。删除、付款、退款、发布内容等操作,应该额外增加确认、审批、权限和审计。
实际开发时可以记住的检查清单
处理大模型返回值时,可以按照下面的顺序设计:
- 不信任模型返回的原始文本,先做解析和长度限制。
- 解析成功后,使用 Schema 检查字段、类型、必填项、枚举和格式。
- Schema 通过后,再根据当前登录用户和租户执行权限校验。
- 查询数据库或业务服务,确认资源真实存在且状态允许。
- 对写入、删除、付款和发布等副作用操作增加幂等、确认、审批和审计。
- 把每一层失败记录成不同错误类型,方便统计究竟是格式问题、模型问题还是业务问题。
大模型应用的可靠性,不是让模型永远不犯错,而是让模型犯错时,错误不会直接变成错误的业务结果。三层校验的价值,就在于把“模型说了什么”和“系统最终允许做什么”明确分开。
