日期:2026-07-30
这个项目源于一次线上事故复盘。客户是一家用大模型做订单智能核单的电商企业,模型读取订单备注和客服聊天记录,输出一个 JSON 结构:是否加急、配送时段偏好、发票要求等字段,下游履约系统直接解析入库。事故那天,模型在输出 JSON 前多说了一句"好的,以下是提取结果:",下游解析器直接抛异常,核单队列积压了四个小时。复盘时我们把一周的调用日志拉出来统计,结果触目惊心:JSON 一次解析成功率只有约 91%——缺字段、枚举值拼错、数字输出成中文"三十"、嵌套层级错乱、markdown 代码围栏混入,五花八门。业务侧的应对是失败就重试,最多重试三次,白白烧掉近一成的 token 成本,还是有漏网的坏输出。
客户的诉求很直接:模型输出的 JSON 要像接口返回一样可靠。我们基于所采用的 AI 私有化部署底座的模型服务化层做了结构化输出改造,项目周期约一个半月。
改造覆盖了客户三类结构化输出场景:订单核单(中等复杂度 schema,十几个字段带枚举)、合同要素抽取(深嵌套 schema,条款数组套子对象)、客服意图分类(简单 schema,两三个字段)。
技术路线是三层防线。第一层是约束解码:推理服务开启语法约束模式,把 JSON Schema 编译成解码时的 token 级约束,模型在生成阶段就只能产出符合语法的 JSON——括号必然闭合、字段名必然合法、枚举值只能从候选里选。这一层把"语法坏"的问题从源头消灭。第二层是语义校验:语法合法不代表语义正确,比如日期字段填了一个格式合法但明显荒谬的值。校验器按 schema 之外的业务规则做二次检查(日期范围、金额上限、字段间一致性),不通过的进入修复流程。第三层是修复与降级:优先做确定性修复(枚举值模糊匹配纠正、数字中文转阿拉伯),修不了的用一次"修复提示"让模型对照错误信息重生成,仍失败则降级——核单场景降级为转人工,分类场景降级为兜底类别。
三类场景共用同一套 schema 注册中心:业务方在注册中心登记 schema 与版本,网关按 schema 版本路由到对应的约束配置,schema 变更走灰度而不是直接全量。
第一个挑战是约束解码与生成质量的博弈。开启硬约束后,语法层面无懈可击,但我们在评测里发现一个隐蔽问题:某些字段的抽取准确率反而降了。分析后发现,原先模型会先输出一段自然语言推理再给 JSON,链式思考帮它想清楚了;硬约束禁止了 JSON 之外的任何输出,等于剥夺了思考空间。解法是给 schema 加一个可选的先导字段 reasoning,放在其他字段之前,允许模型在结构内先写两句分析,下游解析时忽略这个字段。就这么一个小改动,复杂抽取场景的字段准确率回升了约 4 个百分点,代价是每请求多消耗少量 token,业务侧认为完全值得。
第二个挑战是深嵌套结构的稳定性。合同抽取的 schema 有四层嵌套、数组不定长,约束解码在超长输出时偶发"数组尾部质量塌方"——前十个条款抽得很好,后面开始敷衍。我们的对策是拆解:把整份合同一次性抽取改为先分段、每段独立抽取、再由代码合并,单次生成的输出长度控制在两千 token 以内。结构化输出的可靠性和输出长度强相关,这是我们这个项目里最重要的经验之一。
第三个挑战是修复成本控制。修复重试不能无限做。我们给每个请求设了修复预算:确定性修复不限(纯代码,无成本),模型修复最多一次,且修复提示只携带错误字段和错误原因,不重发全部上下文,token 开销压到原请求的三分之一以下。同时在监控上按 schema 版本统计失败模式分布,高频失败模式反哺给提示词和 schema 设计——比如发现"发票抬头"字段经常被填成公司简称,就在 schema 描述里补了一句抽取规则,失败率立刻下来。
案例片段(已脱敏):约束解码与修复策略的核心配置——
yaml structured_output: mode: grammar_constrained # schema 编译为解码约束 schema_registry: order_check@v3 leading_field: reasoning # 结构内思考字段,下游忽略 max_output_tokens: 2048 validation: rules: - field: delivery_date, check: "within(now, now+90d)" - field: invoice_title, check: "not_empty if need_invoice" repair: deterministic: [enum_fuzzy_match, cn_num_to_arabic, strip_fence] llm_repair: { max_attempts: 1, context: errors_only } fallback: manual_review一条真实的修复日志(已脱敏):field=urgency, raw="加急的", enum_fuzzy_match -> "URGENT", repaired=true, cost_tokens=0。确定性修复覆盖了全部失败的约六成,这部分修复零成本、零延迟。
上线两个月后的数据(脱敏示意值):JSON 一次解析成功率从约 91% 提升到 99.7%,加上修复层后端到端成功率约 99.95%;重试率从约 9% 降到 0.8%,单请求平均 token 成本下降约 12%(重试减少抵消了 reasoning 字段的开销还有富余);下游解析故障引发的工单归零,核单队列再未出现积压;合同抽取场景分段改造后,条款级抽取准确率提升约 6 个百分点,超长合同的处理时长反而缩短了约四分之一(分段并行)。schema 注册中心上线后,三次 schema 变更全部通过灰度平滑完成,没有再出现"上游改字段、下游炸解析"的联动事故。
第一,结构化输出要靠约束解码而不是提示词哀求,"请务必只输出 JSON"这句话的可靠性天花板就是 90% 出头。第二,硬约束会挤压模型的思考空间,用结构内的先导思考字段把空间还给它,这是约束与质量兼得的关键技巧。第三,输出越长越不稳定,深嵌套大结构要拆段生成、代码合并。第四,修复分层:确定性修复无限用、模型修复限一次、修不了就降级,预算意识要建在架构里。第五,schema 要有注册中心和版本灰度,结构化输出的契约管理和 API 契约管理是同一件事。