为模型 JSON 输出建立上线验收门禁
文章建议先验证模型输出的结构契约,再衡量延迟,统计直接合规率、模式违规率及一次修复后的恢复率。检查同时覆盖字段间约束,并明确少量试验不能证明负载能力或长期可用性。
AI 深度解读
模型接口返回 200 OK,业务就能放心往下走吗?作者提醒:如果返回内容无法解析,或字段之间互相矛盾,一次看似成功的请求可能把错误数据送进数据库。相比之下,503 至少会明确进入错误处理流程。
这篇文章给出的办法,是在上线前增加一道「输出契约门禁」:先定义什么结果能用,再测原始合规率、结构违规率,以及恰好一次修复后的恢复率。它适合检查依赖结构化输出的 AI 原型,但不能代替性能测试,也不能证明接口长期可靠。
先定义什么能用,再讨论模型快不快
作者所说的契约(contract),就是程序对模型输出的明确要求。JSON 能解析只是第一关,还需要检查四层约束:
- 结构:必填字段是否存在,类型是否正确。
- 枚举:字符串只能取约定值,不能接受模型自行生成的近义词。
- 跨字段规则:例如
severity == "high"时,needs_human必须为true。 - 输出形式:只能返回 JSON,不能夹带解释文字或 Markdown 代码围栏。
最后一层很容易被忽略。如果程序先剥掉代码围栏再解析,系统已经替模型修复了一次。修复可以保留,但必须单独计数,否则提示词稍作调整,原来被兼容逻辑遮住的问题就可能暴露。
能解析的 JSON,不一定是业务能接受的数据。
原文以工单分级为例,用 Node 20+、零依赖脚本调用兼容 OpenAI 的 /chat/completions。下面这段定义了请求条件和校验规则,解决的是「到底怎样才算合格」,尚未包含完整测试循环:
// contract-gate.mjs — Node 20+, no dependencies
import { createHash } from "node:crypto";
import { writeFileSync } from "node:fs";
const BASE = process.env.BASE_URL; // e.g. https://host/v1
const KEY = process.env.API_KEY;
const MODEL = process.env.MODEL;
const TRIALS = Number(process.env.TRIALS ?? 50);
const THRESH = Number(process.env.MIN_CONFORMANCE ?? 0.98);
const SYSTEM = [
"Return exactly one JSON object. No prose. No markdown fences.",
'Shape: {"ticket_id": string, "severity": "low"|"high", "needs_human": boolean}',
].join("\n");
const USER = 'Triage: "checkout button does nothing on Safari 17".';
const promptHash = createHash("sha256")
.update(SYSTEM + "\n" + USER).digest("hex").slice(0, 12);
const violation = (o) => {
if (o === null || typeof o !== "object" || Array.isArray(o)) return "not-an-object";
if (typeof o.ticket_id !== "string" || o.ticket_id.trim() === "") return "ticket_id";
if (o.severity !== "low" && o.severity !== "high") return "severity";
if (typeof o.needs_human !== "boolean") return "needs_human";
if (o.severity === "high" && o.needs_human !== true) return "invariant:high";
return null;
};
async function callOnce(extra = []) {
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${KEY}` },
body: JSON.stringify({
model: MODEL,
temperature: 0,
max_tokens: 200,
messages: [
{ role: "system", content: SYSTEM },
{ role: "user", content: USER },
...extra,
],
}),
signal: AbortSignal.timeout(30_000),
});
return { status: res.status, body: await res.text() };
}
重点看 invariant:high 那一行:字段类型全部正确,仍可能违反业务规则。另外,temperature: 0 必须与实际部署一致。作者举例,如果测试用 0.7、上线用 0,测试证据就没有描述真正运行的配置。
失败要分层,修复不能混进首次通过率
作者建议每次测试只记一个主要失败阶段,按检查顺序归类:
| 分类 | 具体含义 |
|---|---|
http |
HTTP 状态码不是 200 |
envelope |
无法解析接口响应外壳,或取不到 choices[0].message.content |
parse |
模型正文无法解析成 JSON |
schema |
字段、枚举或跨字段规则校验失败 |
pass-after-fence-strip |
去掉代码围栏后才通过 |
clean |
无需修复,直接通过 |
envelope 特别适合排查部署环境问题:接口虽然返回 200,正文却可能是 HTML 错误页、网络认证页或代理生成的页面。只统计「模型生成失败」,会把网络路径上的问题算到模型头上。
对 parse 或 schema 失败,作者允许追加一次修复请求:带上原始 assistant 输出和准确的违规信息,让模型重新回答,再单独记录是否恢复。剥离代码围栏则另记,不应把它和追加请求修复混为一谈。
为什么只修一次?作者认为,继续重试既消耗额度,也会掩盖首次遵守契约的能力。这个上限是工程取舍,文章没有给出一次与多次修复的对照实验。
三个数字因此需要分开看:首次直接通过多少,结构校验失败多少,一次修复又救回多少。恢复率还应写清分母,避免把「失败样本中的恢复比例」和「全部测试中的恢复比例」放在一起比较。
门禁必须拦住合并,证据也要有有效期
脚本默认运行 50 次,MIN_CONFORMANCE 默认是 0.98。原文给出的启动命令如下,作用是把端点、密钥、模型和测试次数传入脚本:
BASE_URL="$BASE" API_KEY="$KEY" MODEL="$MODEL" TRIALS=50 node contract-gate.mjs
要注意 TRIALS=50:一次失败就占 2%。这只是有记录的冒烟测试,不是统计保证。作者提出约十分钟可以得到初步证据,但没有提供实际运行耗时;请求本身还设置了 AbortSignal.timeout(30_000)。
原始合规率低于门槛时,脚本应以退出码 1 结束,让 CI 阻止合并。业务不变量违规则必须单独拦截,即使通过率达到 0.99,也不能放过一次「高严重程度却不需要人工处理」。
每次运行还要保存并提交一份 JSON,至少包含:
timestamp、endpoint_host、model_string、prompt_hash。trials、clean、pass_after_fence_strip、http、envelope、parse、schema。recovered_after_repair,以及最多五条截断的原始失败片段。
示例使用 sha256 生成提示词摘要,截取前 12 位;分类代码中的原始失败片段截取前 160 个字符。提示词摘要变化、模型字符串变化,或证据超过十四天,都要重跑。
作者还建议在原型实际部署的环境执行,因为出口代理、DNS 和平台错误页会影响结果。文章提到 MonkeyCode 当时提供 10M tokens 免费额度及免费服务器,同时明确披露这是产品推广合作。额度需要以控制台为准,不能作为长期承诺。
我的判断
我认可把业务规则放进代码、把修复从首次通过中拆出来。这比只看 HTTP 成功率更接近实际风险,也比持续重试直到成功更容易定位问题。
但这篇提供的是检查方案,没有公开某个模型跑完 50 次后的结果,也没有模型间对照数据。0.98、一次修复、十四天有效期,都是作者选择的工程参数,不能直接当成普适标准。
示例也有几个边界。它反复测试同一条 Safari 17 工单,覆盖不了真实输入的多样性;ticket_id 只检查非空字符串,并未证明编号对应正确工单;校验器没有拒绝额外字段。结构合格与内容正确,仍是两回事。
交付完整性同样要打个问号:原文没有展示循环聚合、修复执行、证据写盘和退出码实现,分类代码还夹杂模板残片,提到的决策表也未出现。读者拿到的是实现骨架,不能直接视为可运行的完整门禁。
与结构化输出能力或 JSON Schema 校验相比,这套方案更强调端到端行为和业务不变量;具体端点是否支持那些替代能力,原文没有说明。若输出只供人阅读,作者也承认严格 schema 可能增加无谓成本。延迟、吞吐、并发、费用上限和可用性,则需要另测。
能拿来做什么
- 给 Node 服务接入模型生成的工单分级、字段提取等结果时,在进入业务流程或写库前校验字段和跨字段规则,明确拒绝无效结果。
- 为已有 JSON 清洗逻辑增加计数,区分原始通过、剥离围栏后通过和一次修复后通过,再决定是否继续依赖这些兼容处理。
- 把端点验收放进 CI,并在实际部署环境运行;首次通过率不足或出现关键业务规则违规时,用非零退出码阻止合并。
- 将提示词摘要、模型字符串、测试时间和失败样本随变更保存;沿用 50 次冒烟测试时,在评审说明中明确样本范围和局限。