# 合法 JSON 只是第一层

传输成功、JSON 能解析、字段符合 Schema、值说的是真的 —— 这是四件不同的事。分清它们，才会知道 Repair 和传输重试为什么不能混用，以及校验失败时最坏的选择是什么。

让模型按格式返回数据，很容易产生一种「已经结构化了」的错觉。请求返回 200，`JSON.parse` 没抛异常，字段名也都对 —— 看起来可以放心往业务里用了。

但只要把这类输出真的接进下游，很快会撞上另一类问题：**格式完全正确，内容不能用。**

原因是「结构化」这个词把好几件事混成了一件。拆开看，从模型输出到可用的数据，中间至少隔着四层。

## 四层，不是一个东西

**第一层，传输成功。** 请求没超时、没被限流、返回了 200。这一层失败的原因和模型无关，是网络和供应商的事。

**第二层，JSON 能解析。** 语法正确，括号配平，不是一个被截断的字符串。`JSON.parse` 这一关过了，说明它是 JSON，仅此而已。

**第三层，字段符合领域 Schema。** 类型对、必填项在、枚举值在允许集合里、该是数组的地方不是对象。这一层管的是**契约** —— 你声明了要什么形状，它给你的是不是那个形状。

**第四层，值说的是真的。** 字段叫 `company`，里面填的公司确实存在；`startDate` 是个合法日期，也确实是入职日期。这一层管的是**内容**，而代码到这一层就管不了了。

为什么要把这四层分开？因为它们各自需要完全不同的处理手段。传输层的问题要重发请求，契约层的问题要校验和修复，内容层的问题要靠上一篇文章讲的那种证据绑定 —— 用错工具，就是在错误的地方使劲。

而且这四层失败的表现完全不同：第一层会报错，第二层会抛异常，**第三层最危险 —— 它常常不报错，只是静静地少一个字段，或者给一个你没预料到的枚举值。** 代码以为拿到了完整数据，一路跑到渲染才崩，或者更糟：没崩，产出了一份缺东西的简历。

## 三种具体的失败形态

契约层的失败通常长这样：

**必填字段缺失。** 模型觉得这个信息不重要就省略了。JSON 合法，Schema 不合法。

**枚举取了非法值。** 你要求 `level` 只能是三个值之一，它给了第四个看起来更贴切的词。

**多包了一层。** 你要的是一个对象，它返回 `{ "data": { ... } }`。语法没错，形状错了，而且错得很「合理」—— 很多 API 确实长这样。

这三种在测试里是三个不同的用例，失败位置也不一样。分开覆盖它们，比笼统地测「格式错的时候怎么办」有用得多。

## Repair 不是重试

这是整篇里最值得单独记的一点：

**传输重试是重发同一个请求。** 同一个输入，再来一次。它解决的是网络抖动、供应商抽风、超时这类问题 —— 模型没问题，是链路有问题。

**Repair 是把校验失败的信息放进一个新请求。** 输入变了：多了「你刚才给的输出违反了这些约束」的反馈。模型看着错误改一次。

两者的前提完全不同。传输失败时请求内容是对的，所以原样重发；校验失败时请求内容不够，所以必须带上新信息。**对校验失败做传输重试，等于让模型在没有任何新信息的情况下重新猜一次** —— 它可能碰对，但那不是修复，是抽奖。

实际实现的顺序是这样的：先直接按 Schema 校验；不过，再尝试几个已知的包装层和保守归一化（比如外面多包一层这种可识别的形状）；再不过，才进入 Repair；Repair 之后仍不过，就走稳定失败。

## 为什么要给 Repair 设上限

默认最多一次。

无上限的 Repair 表面上很稳健 —— 一直试到对为止嘛。但代价是延迟不可控（每一轮都是一次完整的模型调用）、成本不可控，而且更根本的问题：**如果一个输出需要修五次才能通过，说明这个任务的约束和模型的默认行为之间有系统性偏差**，多修几次只是把这个问题掩盖过去。

一次上限的含义是：允许修一次拼写级别的偏差，但不接受需要反复纠正的输出。前者是噪声，后者是设计问题。

## 两次都错，最坏的选择是放行

那 Repair 之后还是错，怎么办？

最坏的做法是把错数据放行 —— 要么把不合法的字段默默丢掉，要么给个默认值填上，要么放宽校验让它过。这么做的诱惑在于「至少流程没断」，但它把失败从**一个已知的、带错误码的位置**，推迟到了**一个未知的、下游的位置**。等它在下游爆出来，你已经失去了「错误发生在哪一层」这个信息。

正确的做法是返回一个**稳定、可诊断的失败**：错误码固定、能指出违反了哪条约束、能被上层捕获并决定降级。宁可这次任务失败，也不要让一份形状不对的数据流进产物。

这里的关键是「稳定」两个字。同一个失败原因，任何时候都以同样的方式失败 —— 这样它才能被断言、被监控、被处理，而不是变成一个偶发的、需要人工翻日志的怪现象。

## 一个反模式：为了让测试变绿而放宽 Schema

最后说一个很容易发生的滑坡。

校验失败的时候，改 Schema 让它通过，比查清楚模型为什么不合规要快得多。必填改选填、枚举加一个值、类型从严格改成宽松 —— 测试立刻绿了。

但校验层报错不是障碍，是**信号**：它说明你声明的契约和实际拿到的数据之间有差距。差距的来源可能是 prompt 写得不够明确、可能是归一化没覆盖某种包装、也可能是重试策略不够。**这几种可能都不指向「契约写错了」。**

把闸门开大，等于把一个会在入口拦住的错误，变成了一个会在下游出现的问题。测试绿了，故障只是换了地方。

判断标准很简单：**放宽约束之后，那个失败的输入现在能通过校验了 —— 它带来的数据是你可以接受的吗？** 如果答案是不能，那你要修的不是 Schema。

## 边界在哪

整套东西的目标可以一句话概括：**把不可靠的模型输出，收敛成可靠的类型。**

「收敛」这个词是准确的 —— 它不承诺模型不出错，它承诺的是模型的错误会在一个确定的位置被拦下来，并且以确定的方式报告。第四层（值是否真实）不在这套机制的保证范围里，那要靠证据绑定去解决；这两层防线管的是不同的失败，不能互相替代。

Last updated on September 29, 2026