当前位置: 首页 > news >正文

LLM 输出校验网关:用 JSON Schema 和重试策略构建可靠的 AI 响应管道

LLM 输出校验网关:用 JSON Schema 和重试策略构建可靠的 AI 响应管道

一、AI 输出的不可靠性:当大模型返回了"差不多"但"不够精确"的数据

Prompt 工程师经常遇到这样的情况:你需要 LLM 输出一个结构化的 JSON,包含nameageemail三个字段。Prompt 里写得很清楚:"请以 JSON 格式输出,包含 name (string)、age (number)、email (string) 三个字段。"

LLM 返回的结果可能是:

{"name": "张三", "age": "28岁", "email": "zhangsan@email.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: JSONSchemaType<UserProfile> = { 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: JSONSchemaType<any>; maxRetries: number; defaultValue?: any; onFallback?: 'error' | 'default' | 'log'; } class LLMValidationGateway { private configs: Map<string, ValidationConfig> = new Map(); register(type: string, config: ValidationConfig) { this.configs.set(type, config); } async callWithValidation( prompt: string, outputType: string, ): Promise<any> { 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: 'unknown@unknown.com' }, onFallback: 'default', }); try { const result = await gateway.callWithValidation( '请提取以下文本中的用户信息:我是张三,今年 28 岁,邮箱 zhangsan@email.com', 'userProfile', ); console.log('提取结果:', result); // { name: '张三', age: 28, email: 'zhangsan@email.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 的地方做防御,在管道中统一做一次就够了。

http://www.jsqmd.com/news/1144433/

相关文章:

  • B站直播弹幕机器人:从零搭建智能直播间自动化系统的完整指南
  • uBlock Origin完整指南:5分钟打造无广告、高隐私的纯净浏览器体验
  • 智能本地化英雄联盟工具:5分钟提升你的游戏效率完整指南
  • 工业自动化中智能功率IC与MCU的精确控制方案
  • 企业AI落地:从“技术实验”到“组织进化”的螺旋上升之路
  • Qwen Code+Obsidian构建可执行的AI时代认知操作系统
  • Windows Cleaner:拯救C盘空间危机的全能系统优化工具
  • 百度网盘解析终极指南:3分钟实现高速下载的完整解决方案
  • 终极指南:在Linux系统上免费运行Photoshop CC 2022的完整解决方案 [特殊字符]
  • 终极GTNH汉化指南:3步让格雷科技新视野秒变中文界面
  • Three.js 冰面教程
  • 光流Warp操作解析:Forward与Backward 2种映射的原理、空洞与重影问题
  • 如何免费下载Steam创意工坊模组:WorkshopDL完整指南
  • vllm021中EPLB劣化问题原因排查:默认用torch Gloo而非NCCL搬运权重导致空泡
  • 线激光扫描三维成像:5种光条中心线提取算法对比与Python实现
  • 墙体可穿透,人员可无感:动态三维重构解锁营区全景透明管理
  • 3大核心能力重塑《明日方舟》游戏体验:MAA自动化助手的革命性突破
  • 超低功耗设计:NBM7100A与STM32F732IE延长电池寿命方案
  • 校园服饰细分赛道测算程序,学生平价国风,机能穿搭市场规模预估。
  • 高精度ADC MCP3551与MK24FN256VDC12 MCU的嵌入式系统设计
  • 3步搞定流媒体资源嗅探:猫抓扩展高效配置实战指南
  • STM32F215RE与A3910电机驱动方案详解
  • 高级浏览器资源嗅探扩展完全配置指南:从技术原理到实战应用
  • Visual C++运行库完整解决方案:告别软件兼容性问题
  • IIM-20670运动传感器与STM32L432KC的硬件适配与优化
  • macOS菜单栏管理新体验:用Ice打造清爽高效的工作空间
  • 高德地图路径规划 2.0 API 与 JS API 对比:3 种场景下的性能与适用性分析
  • YOLOv11 模型评估实战:使用 PyTorch 代码计算 mAP@0.5 与 mAP@0.5:0.95
  • 锂离子电池组平衡技术与BQ25887应用解析
  • MySQL 8.0 高可用架构实战:基于 MHA 与 Orchestrator 的 2 种故障切换方案