云计算百科
云计算领域专业知识百科平台

别信模型说的“这是合法 JSON”:结构化输出的前端校验与自动修复

别信模型说的“这是合法 JSON”:结构化输出的前端校验与自动修复

一、模型承诺的 JSON 经常是坏的:前端不能裸接结构化输出

某 AI 报表产品用大模型生成图表配置,prompt 明确要求输出 JSON。上线后线上约 12% 的响应解析失败:有的缺必填字段、有的把数字写成字符串、有的末尾多一个逗号、有的甚至裹了一层 markdown 代码块。前端没有校验直接 JSON.parse,崩在渲染管线里,整张图空白。这事我见过太多团队栽进去——把模型当可信数据源,不校验就塞进渲染。

大模型的输出本质是概率采样。即使 prompt 约束、即使开启 JSON mode、即使用 function calling,仍存在字段缺失、类型偏差、多余字符、嵌套错位等情况。模型承诺的“合法 JSON”在生产环境中并不可靠。

前端必须把模型输出当“不可信外部输入”,像校验用户表单一样校验。校验失败时,尝试自动修复(补默认值、转类型、删多余字段、修语法),修复仍不合法则回退让模型重生成,或降级到默认模板。这是一套完整的校验-修复-回退链路。

二、JSON Schema 校验与修复策略:结构化输出的兜底机制

JSON Schema 是描述 JSON 结构的标准。它定义字段名、类型、必填、枚举、范围、嵌套结构。前端用 ajv 等库做校验,能拿到精确的错误位置与类型,而不是笼统的“解析失败”。

模型输出的常见错误模式有五类。第一,缺必填字段:模型漏了某个字段。第二,类型偏差:数字写成字符串、布尔写成 0 或 1、数组写成对象。第三,多余字段:模型自作主张加了 schema 之外的字段。第四,JSON 语法错:尾逗号、单引号、注释、代码块包裹。第五,嵌套结构错:数组包对象变成对象包数组,层级错位。

针对这些错误,修复策略分四层。第一层语法修复:用 jsonrepair 等库修复尾逗号、单引号、代码块包裹等语法问题,让字符串能被 JSON.parse。第二层类型转换:字符串数字转 number、字符串布尔转 boolean、字符串 null 转 null。第三层补默认值:缺字段按 schema 中的 default 补,无 default 则按类型补零值。第四层删多余字段:schema 中 additionalProperties 为 false 时,剔除未定义字段。

回退边界要清晰。修复后仍不合法的,回退让模型重生成,并把校验错误信息作为 prompt 反馈,引导模型修正。重生成仍失败的,降级到默认模板或空状态,绝不让坏数据进入渲染。

综上,结构化输出的可靠性来自分层兜底:校验先拦非法、修复再补缺失、回退保住可用结果。每一层失败都有下一步接住,模型输出从「碰运气」变成「有兜底」,不会因单点异常卡死业务。

三、生产级结构化输出校验修复器实现

下面给出一个可复用的校验修复器封装。它集成语法修复、schema 校验、自动修复与回退重生成。

import Ajv from 'ajv';
import { jsonrepair } from 'jsonrepair';

const ajv = new Ajv({ allErrors: true, strict: false });

interface RepairOptions {
schema: object;
// 重生成最大次数,超过即降级,避免无谓消耗 token
maxRetries?: number;
// 回调模型重生成,把错误反馈传回去引导修正
regenerate?: (feedback: string) => Promise<string>;
}

export class StructuredOutputGuard {
private schema: object;
private maxRetries: number;
private regenerate?: (feedback: string) => Promise<string>;
// ajv 编译后的校验函数,复用避免重复编译开销
private validate: ReturnType<Ajv['compile']>;

constructor(opts: RepairOptions) {
this.schema = opts.schema;
this.maxRetries = opts.maxRetries ?? 2;
this.regenerate = opts.regenerate;
this.validate = ajv.compile(opts.schema);
}

// 主入口:原始字符串 -> 合法数据
async parse(
raw: string
): Promise<{ ok: true; data: unknown } | { ok: false; reason: string }> {
let current = raw;
let retries = 0;

while (retries <= this.maxRetries) {
const parsed = this.tryParse(current);
if (!parsed.ok) {
// 语法层都修不好,直接走重生成
const next = await this.askRegenerate(parsed.reason);
if (!next) return { ok: false, reason: '语法修复失败且无法重生成' };
current = next;
retries++;
continue;
}

const valid = this.validate(parsed.value);
if (valid) return { ok: true, data: parsed.value };

// schema 校验失败,尝试自动修复
const repaired = this.autoRepair(parsed.value, this.validate.errors ?? []);
const reValid = this.validate(repaired);
if (reValid) return { ok: true, data: repaired };

// 修复后仍不合法,带错误反馈重生成
const feedback = this.buildFeedback(this.validate.errors ?? []);
const next = await this.askRegenerate(feedback);
if (!next) return { ok: false, reason: 'schema 校验失败且无法重生成' };
current = next;
retries++;
}
return { ok: false, reason: '超过最大重试次数' };
}

// 第一层:JSON.parse,失败则用 jsonrepair 兜底
private tryParse(
raw: string
): { ok: true; value: unknown } | { ok: false; reason: string } {
try {
return { ok: true, value: JSON.parse(raw) };
} catch {
try {
// 修复尾逗号、单引号、代码块包裹等常见语法问题
return { ok: true, value: JSON.parse(jsonrepair(raw)) };
} catch (e) {
return { ok: false, reason: `语法不可修复: ${(e as Error).message}` };
}
}
}

// 自动修复:按 ajv 错误类型分发,深拷贝避免污染原数据
private autoRepair(data: any, errors: any[]): any {
if (typeof data !== 'object' || data === null) return data;
const repaired = JSON.parse(JSON.stringify(data));
for (const err of errors) {
const path = err.instancePath.split('/').filter(Boolean);
switch (err.keyword) {
case 'type':
this.fixType(repaired, path, err.params.type);
break;
case 'required':
this.fillDefault(repaired, err.params.missingProperty);
break;
case 'additionalProperties':
this.removeExtra(repaired, path, err.params.additionalProperty);
break;
}
}
return repaired;
}

private fixType(obj: any, path: string[], types: string) {
const target = path.reduce((o, k) => o?.[k], obj);
if (target == null) return;
// 字符串数字转 number,字符串布尔转 boolean
if (types.includes('number') && typeof target === 'string') {
const n = Number(target);
if (!isNaN(n)) this.setPath(obj, path, n);
} else if (types.includes('boolean') && typeof target === 'string') {
this.setPath(obj, path, target === 'true');
}
}

private fillDefault(obj: any, key: string) {
// 缺字段补 null 零值,业务层再判空处理
if (obj[key] === undefined) obj[key] = null;
}

private removeExtra(obj: any, path: string[], key: string) {
const target = path.reduce((o, k) => o?.[k], obj);
if (target && typeof target === 'object') delete target[key];
}

private setPath(obj: any, path: string[], value: any) {
let cur = obj;
for (let i = 0; i < path.length – 1; i++) cur = cur[path[i]];
cur[path[path.length – 1]] = value;
}

// 把校验错误拼成模型可读的反馈,引导重生成
private buildFeedback(errors: any[]): string {
const lines = errors.map((e) => `路径 ${e.instancePath || '根'}: ${e.message}`);
return `上一次输出存在以下问题,请修正:\\n${lines.join('\\n')}`;
}

private async askRegenerate(feedback: string): Promise<string | null> {
if (!this.regenerate) return null;
try {
return await this.regenerate(feedback);
} catch {
// 重生成本身失败也兜底,不让链路中断
return null;
}
}
}

关键点在于三处。其一,分层处理:语法层用 jsonrepair,schema 层用 ajv,修复层按错误类型分发。其二,自动修复深拷贝后再改,不污染原始数据,便于回退。其三,重生成带错误反馈,形成闭环。某 AI 报表产品接入后,解析失败率从 12% 降到 0.3%,剩余 0.3% 走默认模板兜底。

四、自动修复的代价:静默错误、语义漂移与适用边界

自动修复并非无损。

静默错误是最隐蔽的代价。自动补默认值可能掩盖模型理解错误。模型本应输出“销售额”却漏了字段,修复器补了 0,用户看到的就是“销售额为 0”,而真实情况是模型没理解对。这种错误比解析失败更危险,因为用户感知不到。必须记录每次修复日志,便于事后排查与 prompt 迭代。

语义漂移是第二类风险。类型转换可能改变语义。“true” 转成 true 没问题,但“是”转成 boolean 就丢义。中文环境下,模型可能输出“是”或“否”代替 true 或 false,强行转换会丢信息。修复策略需结合业务语义,不能一刀切。

性能开销不可忽视。复杂 schema 校验在大对象上耗时明显。某次 schema 含 200 个字段的嵌套配置,ajv 校验单次耗时 80 毫秒。需在 schema 编译期做缓存(ajv 本身支持 compile 复用),避免每次重新编译。

重生成成本是最后一项。回退重生成增加 token 消耗与延迟。若模型质量差,重生成可能仍失败。需设最大重试次数,超限即降级,避免无谓消耗。

适用边界:报表配置、表单预填、结构化数据提取等容错性高的场景收益最高。金融、医疗、法务等高精度场景不适合自动修复,那里应强校验失败即拒绝,由人工介入。

五、总结

结构化输出的前端校验,核心是把模型当不可信数据源,建立校验-修复-回退的完整链路。落地建议:第一,用 JSON Schema 定义结构,ajv 做精确校验,拿到错误位置与类型。第二,语法层用 jsonrepair 修复尾逗号、单引号、代码块包裹等常见问题。第三,schema 层按错误类型自动修复:类型转换、补默认值、删多余字段。第四,修复仍不合法则带错误反馈重生成,设最大重试次数。第五,重生成超限降级到默认模板,绝不让坏数据进入渲染。第六,记录修复日志,便于排查静默错误与迭代 prompt。这条路在容错性高的 AI 应用场景下能跑通,回报是值得的。。第六,记录修复日志,便于排查静默错误与迭代 prompt。这条路在容错性高的 AI 应用场景下能跑通,回报是值得的。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 别信模型说的“这是合法 JSON”:结构化输出的前端校验与自动修复
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!