LLM 输出校验网关用 JSON Schema 和重试策略构建可靠的 AI 响应管道一、AI 输出的不可靠性当大模型返回了差不多但不够精确的数据Prompt 工程师经常遇到这样的情况你需要 LLM 输出一个结构化的 JSON包含name、age、email三个字段。Prompt 里写得很清楚请以 JSON 格式输出包含 name (string)、age (number)、email (string) 三个字段。LLM 返回的结果可能是{name: 张三, age: 28岁, email: zhangsanemail.com}age是字符串28岁而不是数字。这个差别对于后面的代码是致命的——age 1的期望结果是29实际结果是28岁1。单独调整 Prompt 可以解决这个 case但明天换一个模型版本或者用户换了一种问法age 可能又变成二十八。靠 Prompt 调优来保证输出格式的一致性就像在流沙上建房子——你永远不能确定下一脚踩在实地上。解决方案是在 LLM 输出后插入一层校验网关。这一层不关心 LLM 怎么生成的内容只关心输出的格式是否符合预期。graph LR A[用户输入] -- B[LLM 调用] B -- C[原始输出] C -- D{JSON Schema 校验} D --|通过| E[返回给下游] D --|失败, 重试次数 N| F[重试: 反馈错误信息] F -- B D --|失败, 重试次数 N| G[降级: 返回默认值或错误] G -- H[记录异常日志] style D fill:#ffd43b,color:#000 style G fill:#ff6b6b,color:#fff style E fill:#51cf66,color:#fff本文将实现一个完整的 LLM 输出校验网关覆盖 JSON Schema 校验、自动重试和降级策略。二、校验网关的架构设计三层防护校验网关的核心职责是保证 LLM 输出符合预期格式。它应该作为 LLM 调用链中的独立环节存在而不是混在业务代码中。Layer 1 — 格式解析尝试将 LLM 输出解析为目标格式JSON。LLM 的输出可能被包裹在 markdown 代码块json ... 中这一层先提取纯净的 JSON 字符串。Layer 2 — Schema 校验使用 JSON Schema 验证 JSON 的结构、类型和约束。不符合的直接拒绝。Layer 3 — 重试与降级校验失败时将错误信息反馈给 LLM要求重新生成。重试超过阈值后触发降级策略。降级策略有三种选择返回默认值对于非关键字段、抛出错误对于关键字段、人工介入对于金融/医疗等高风险场景。三、完整的校验网关实现import Ajv, { JSONSchemaType } from ajv; import OpenAI from openai; const ajv new Ajv({ allErrors: true }); const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 定义输出 Schema interface UserProfile { name: string; age: number; email: string; } const userProfileSchema: JSONSchemaTypeUserProfile { type: object, properties: { name: { type: string, minLength: 1 }, age: { type: number, minimum: 0, maximum: 150 }, email: { type: string, format: email }, }, required: [name, age, email], additionalProperties: false, }; // 校验网关配置 interface ValidationConfig { schema: JSONSchemaTypeany; maxRetries: number; defaultValue?: any; onFallback?: error | default | log; } class LLMValidationGateway { private configs: Mapstring, ValidationConfig new Map(); register(type: string, config: ValidationConfig) { this.configs.set(type, config); } async callWithValidation( prompt: string, outputType: string, ): Promiseany { const config this.configs.get(outputType); if (!config) { throw new Error(Unknown output type: ${outputType}); } let lastError ; let lastResponse ; for (let attempt 0; attempt config.maxRetries; attempt) { const context attempt 0 ? prompt : this.buildRetryPrompt(prompt, lastResponse, lastError); const response await openai.chat.completions.create({ model: gpt-4o-mini, messages: [{ role: user, content: context }], temperature: attempt 0 ? 0.3 : 0, // 重试时降低温度 response_format: { type: json_object }, }); const raw response.choices[0].message.content || ; lastResponse raw; // Layer 1: 提取 JSON const json this.extractJSON(raw); if (!json) { lastError 输出不是有效的 JSON 格式; continue; } // Layer 2: Schema 校验 const validate ajv.compile(config.schema); const valid validate(json); if (valid) { return json; // 成功 } const errors validate.errors || []; lastError errors .map(e ${e.instancePath}: ${e.message}) .join(; ); } // Layer 3: 降级 return this.handleFallback(config, lastError, lastResponse); } // 从 LLM 输出中提取纯净的 JSON private extractJSON(raw: string): any { // 尝试直接解析 try { return JSON.parse(raw); } catch {} // 尝试提取 markdown 代码块中的 JSON const match raw.match(/(?:json)?\s*([\s\S]*?)/); if (match) { try { return JSON.parse(match[1].trim()); } catch {} } // 尝试提取第一个完整的 JSON 对象 const objectMatch raw.match(/\{[\s\S]*\}/); if (objectMatch) { try { return JSON.parse(objectMatch[0]); } catch {} } return null; } // 构建重试 Prompt将错误信息反馈给 LLM private buildRetryPrompt( originalPrompt: string, failedResponse: string, errorMessage: string, ): string { return [ originalPrompt, , ---, 你上一次的输出不符合格式要求。错误如下${errorMessage}, 你的上一次输出${failedResponse.substring(0, 200)}, 请修正后重新输出确保格式完全符合要求。, ].join(\n); } // 降级处理 private handleFallback( config: ValidationConfig, error: string, lastResponse: string, ): any { console.error(LLM validation fallback: ${error}); if (config.onFallback default config.defaultValue ! undefined) { return config.defaultValue; } throw new Error(校验失败${config.maxRetries 1} 次尝试后${error}。最后一次输出${lastResponse.substring(0, 200)}); } } // 使用示例 async function main() { const gateway new LLMValidationGateway(); gateway.register(userProfile, { schema: userProfileSchema, maxRetries: 2, defaultValue: { name: 未知, age: 0, email: unknownunknown.com }, onFallback: default, }); try { const result await gateway.callWithValidation( 请提取以下文本中的用户信息我是张三今年 28 岁邮箱 zhangsanemail.com, userProfile, ); console.log(提取结果:, result); // { name: 张三, age: 28, email: zhangsanemail.com } } catch (err) { console.error(提取失败:, err); } }四、性能与成本的平衡额外延迟校验本身是毫秒级的JSON 解析 Schema 校验。但如果触发重试会额外增加一次 LLM 调用通常耗时 1-5 秒。这是校验网关的主要性能成本。Token 消耗重试时Prompt 包含原始 Prompt 错误反馈 上一次的失败输出。这会额外消耗 200-500 Token。建议重试上限设为 2 次总成本增加不超过 50%。监控指标建议对校验网关监控以下指标首次校验通过率期望 85%重试后通过率期望 95%降级触发次数期望 1%如果首次校验通过率持续 70%说明 Prompt 设计有问题应该优化 Prompt 而非依赖重试。不适用场景实时对话场景延迟要求 200ms——重试的延迟不可接受允许模糊输出的场景如聊天机器人——过度约束会损害回答的自然度输出格式极其复杂嵌套层级 5——JSON Schema 的表达能力有限五、总结LLM 输出校验网关用 JSON Schema 自动重试保证了 AI 输出的格式可靠性。三层防护解析 → 校验 → 降级让下游系统不再需要为 LLM 的随机行为做防御性编程。落地路径先把项目中所有依赖 LLM 输出的地方列出来为每个定义 JSON Schema再实现一个通用的校验网关如本文代码统一替换原有的信任 LLM 输出的逻辑最后通过监控指标首次通过率、重试率持续调优 Prompt。少即是多——不要在每个调用 LLM 的地方做防御在管道中统一做一次就够了。
LLM 输出校验网关:用 JSON Schema 和重试策略构建可靠的 AI 响应管道
发布时间:2026/7/8 0:19:41
LLM 输出校验网关用 JSON Schema 和重试策略构建可靠的 AI 响应管道一、AI 输出的不可靠性当大模型返回了差不多但不够精确的数据Prompt 工程师经常遇到这样的情况你需要 LLM 输出一个结构化的 JSON包含name、age、email三个字段。Prompt 里写得很清楚请以 JSON 格式输出包含 name (string)、age (number)、email (string) 三个字段。LLM 返回的结果可能是{name: 张三, age: 28岁, email: zhangsanemail.com}age是字符串28岁而不是数字。这个差别对于后面的代码是致命的——age 1的期望结果是29实际结果是28岁1。单独调整 Prompt 可以解决这个 case但明天换一个模型版本或者用户换了一种问法age 可能又变成二十八。靠 Prompt 调优来保证输出格式的一致性就像在流沙上建房子——你永远不能确定下一脚踩在实地上。解决方案是在 LLM 输出后插入一层校验网关。这一层不关心 LLM 怎么生成的内容只关心输出的格式是否符合预期。graph LR A[用户输入] -- B[LLM 调用] B -- C[原始输出] C -- D{JSON Schema 校验} D --|通过| E[返回给下游] D --|失败, 重试次数 N| F[重试: 反馈错误信息] F -- B D --|失败, 重试次数 N| G[降级: 返回默认值或错误] G -- H[记录异常日志] style D fill:#ffd43b,color:#000 style G fill:#ff6b6b,color:#fff style E fill:#51cf66,color:#fff本文将实现一个完整的 LLM 输出校验网关覆盖 JSON Schema 校验、自动重试和降级策略。二、校验网关的架构设计三层防护校验网关的核心职责是保证 LLM 输出符合预期格式。它应该作为 LLM 调用链中的独立环节存在而不是混在业务代码中。Layer 1 — 格式解析尝试将 LLM 输出解析为目标格式JSON。LLM 的输出可能被包裹在 markdown 代码块json ... 中这一层先提取纯净的 JSON 字符串。Layer 2 — Schema 校验使用 JSON Schema 验证 JSON 的结构、类型和约束。不符合的直接拒绝。Layer 3 — 重试与降级校验失败时将错误信息反馈给 LLM要求重新生成。重试超过阈值后触发降级策略。降级策略有三种选择返回默认值对于非关键字段、抛出错误对于关键字段、人工介入对于金融/医疗等高风险场景。三、完整的校验网关实现import Ajv, { JSONSchemaType } from ajv; import OpenAI from openai; const ajv new Ajv({ allErrors: true }); const openai new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 定义输出 Schema interface UserProfile { name: string; age: number; email: string; } const userProfileSchema: JSONSchemaTypeUserProfile { type: object, properties: { name: { type: string, minLength: 1 }, age: { type: number, minimum: 0, maximum: 150 }, email: { type: string, format: email }, }, required: [name, age, email], additionalProperties: false, }; // 校验网关配置 interface ValidationConfig { schema: JSONSchemaTypeany; maxRetries: number; defaultValue?: any; onFallback?: error | default | log; } class LLMValidationGateway { private configs: Mapstring, ValidationConfig new Map(); register(type: string, config: ValidationConfig) { this.configs.set(type, config); } async callWithValidation( prompt: string, outputType: string, ): Promiseany { const config this.configs.get(outputType); if (!config) { throw new Error(Unknown output type: ${outputType}); } let lastError ; let lastResponse ; for (let attempt 0; attempt config.maxRetries; attempt) { const context attempt 0 ? prompt : this.buildRetryPrompt(prompt, lastResponse, lastError); const response await openai.chat.completions.create({ model: gpt-4o-mini, messages: [{ role: user, content: context }], temperature: attempt 0 ? 0.3 : 0, // 重试时降低温度 response_format: { type: json_object }, }); const raw response.choices[0].message.content || ; lastResponse raw; // Layer 1: 提取 JSON const json this.extractJSON(raw); if (!json) { lastError 输出不是有效的 JSON 格式; continue; } // Layer 2: Schema 校验 const validate ajv.compile(config.schema); const valid validate(json); if (valid) { return json; // 成功 } const errors validate.errors || []; lastError errors .map(e ${e.instancePath}: ${e.message}) .join(; ); } // Layer 3: 降级 return this.handleFallback(config, lastError, lastResponse); } // 从 LLM 输出中提取纯净的 JSON private extractJSON(raw: string): any { // 尝试直接解析 try { return JSON.parse(raw); } catch {} // 尝试提取 markdown 代码块中的 JSON const match raw.match(/(?:json)?\s*([\s\S]*?)/); if (match) { try { return JSON.parse(match[1].trim()); } catch {} } // 尝试提取第一个完整的 JSON 对象 const objectMatch raw.match(/\{[\s\S]*\}/); if (objectMatch) { try { return JSON.parse(objectMatch[0]); } catch {} } return null; } // 构建重试 Prompt将错误信息反馈给 LLM private buildRetryPrompt( originalPrompt: string, failedResponse: string, errorMessage: string, ): string { return [ originalPrompt, , ---, 你上一次的输出不符合格式要求。错误如下${errorMessage}, 你的上一次输出${failedResponse.substring(0, 200)}, 请修正后重新输出确保格式完全符合要求。, ].join(\n); } // 降级处理 private handleFallback( config: ValidationConfig, error: string, lastResponse: string, ): any { console.error(LLM validation fallback: ${error}); if (config.onFallback default config.defaultValue ! undefined) { return config.defaultValue; } throw new Error(校验失败${config.maxRetries 1} 次尝试后${error}。最后一次输出${lastResponse.substring(0, 200)}); } } // 使用示例 async function main() { const gateway new LLMValidationGateway(); gateway.register(userProfile, { schema: userProfileSchema, maxRetries: 2, defaultValue: { name: 未知, age: 0, email: unknownunknown.com }, onFallback: default, }); try { const result await gateway.callWithValidation( 请提取以下文本中的用户信息我是张三今年 28 岁邮箱 zhangsanemail.com, userProfile, ); console.log(提取结果:, result); // { name: 张三, age: 28, email: zhangsanemail.com } } catch (err) { console.error(提取失败:, err); } }四、性能与成本的平衡额外延迟校验本身是毫秒级的JSON 解析 Schema 校验。但如果触发重试会额外增加一次 LLM 调用通常耗时 1-5 秒。这是校验网关的主要性能成本。Token 消耗重试时Prompt 包含原始 Prompt 错误反馈 上一次的失败输出。这会额外消耗 200-500 Token。建议重试上限设为 2 次总成本增加不超过 50%。监控指标建议对校验网关监控以下指标首次校验通过率期望 85%重试后通过率期望 95%降级触发次数期望 1%如果首次校验通过率持续 70%说明 Prompt 设计有问题应该优化 Prompt 而非依赖重试。不适用场景实时对话场景延迟要求 200ms——重试的延迟不可接受允许模糊输出的场景如聊天机器人——过度约束会损害回答的自然度输出格式极其复杂嵌套层级 5——JSON Schema 的表达能力有限五、总结LLM 输出校验网关用 JSON Schema 自动重试保证了 AI 输出的格式可靠性。三层防护解析 → 校验 → 降级让下游系统不再需要为 LLM 的随机行为做防御性编程。落地路径先把项目中所有依赖 LLM 输出的地方列出来为每个定义 JSON Schema再实现一个通用的校验网关如本文代码统一替换原有的信任 LLM 输出的逻辑最后通过监控指标首次通过率、重试率持续调优 Prompt。少即是多——不要在每个调用 LLM 的地方做防御在管道中统一做一次就够了。