我是小兵,一个动手派AI架构师。「AI工程化实战」系列第 1 篇。
凌晨 3:07,我被 P0 报警震醒。
监控面板上,订单解析服务过去一小时抛出了第 217 次 JSONDecodeError——不是 217 次请求,是 217 次异常。LLM 吐出的"JSON"里混进了一个没闭合的大括号,还带一句"很抱歉,以下是提取结果"。下游订单系统排着队等这份数据,断了整整 11 分钟。
我们守着一行 temperature=0、写了 30 行 prompt 强调"必须输出合法 JSON",大模型照样随机抽风。
这不是运气问题,是概率问题。单节点成功率 95%,5 步链路塌到 77%,20 步只剩 36%——误差必然叠加。真正要回答的是:出错之后谁来兜住。
下面用 4 层确定性金字塔把这套思路落地:把概率模型封装成上游业务方眼中的确定性服务——契约稳定、格式可靠、失败可预期。
一、问题定义:输出不稳定,是三件事不是一件事
大模型输出不稳定不是一件事,是三件事,破坏面完全不同:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
大多数团队的应对方式,恰好三连错:
|
|
|
|---|---|
temperature=0 当救命稻草 |
|
|
|
|
|
|
|
三个做法错在同一处:都在错误的层解决问题。字节级确定是伪命题——模型是概率生成器,你连它的 GPU 浮点都控制不了。真正能交付的是契约级确定。
所以换个角度:把 LLM 当成一家不可靠的第三方供应商,像支付通道、短信通道那样。业务方从不过问通道内部稳不稳定,只签一份 SLA:契约成立、格式可靠、失败可预期、降级有预案。把期望从"每次都正确"换成"每次契约都成立",工程化就有抓手了。
二、4 层确定性金字塔
从下往上:L1-L2 增强输出确定性(格式/结构越来越稳),L3-L4 增强失败可预期性(失败从事故变成流程)。原则只有一条:只在需要的层花钱,每层都知道自己不保证什么。
┌─────────────────────────────────┐│ 业务方视角: ││ 契约稳定 · 格式可靠 · 失败可预期 │└───────────────┬─────────────────┘┌───────────────┴─────────────────┐│ 第 4 层 编排级 ││ 确定性骨架 + 概率叶子 ││ LangGraph 状态机,控制流给代码 │└───────────────┬─────────────────┘┌───────────────┴─────────────────┐│ 第 3 层 容错级 ││ 校验 → 分类重试(≤3 轮)→ 降级兜底 ││ 失败可预期、成本封顶 │└───────────────┬─────────────────┘┌───────────────┴─────────────────┐│ 第 2 层 契约级 ││ JSON Schema 强约束 (strict:true) ││ 采样期 token mask,物理上无法违规 │└───────────────┬─────────────────┘┌───────────────┴─────────────────┐│ 第 1 层 采样级 ││ temperature=0 + seed + top_p≈0 ││ 尽力而为,不承诺字节级确定 │└─────────────────────────────────┘
先把两个关键数字交代清楚:5%是裸调用基线——深度嵌套(>20 字段)schema 下 GPT-4o 的 Schema 校验失败率(官方口径 95% 通过);0.1%是扁平(<5 字段)schema 下的最优档位通过率 99.9%。这两个数字来自不同 schema 复杂度,不是"优化前→优化后"的对比——标题用 5%→0.1% 示意金字塔的工作带宽,你实际能到多少,取决于你的 schema 复杂度。
第 1 层 采样级:temperature=0 的真相
直接点破:temperature=0 ≠ 确定性,它只是一个尽力而为的参数。
OpenAI 官方文档明确写过:Chat Completions 默认非确定性,temperature=0 减少随机性但不保证确定性。原因有三个:GPU 浮点运算非确定性、MoE 架构的专家路由、服务端持续微调与模型权重更新。seed 参数提供"大多数情况下"的确定性,同样不保证 100%。
目前最接近确定性的组合长这样:
SAMPLING = {”temperature”: 0,# 把采样分布压到最尖”top_p”: 0.000001,# 近贪婪采样;不设 0,部分服务把 top_p=0 当非法值”seed”: 20260807,# 固定种子,”大多数情况”下复现同一输出}
这套组合有个"伪确定性"问题要提前说:本地单测 10 次全一致,不代表线上一致。批量处理、多机并行、不同 GPU 实例下,字节级差异随时出现。采样级只能当金字塔的第一层——它省不掉,但永远不够。
第 2 层 契约级:JSON Schema 强约束
这是金字塔的核心承重层。先给结论:生产环境用 Structured Outputs,把 JSON Mode 当遗留方案。
|
|
json_object)
|
strict:true)
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
两者关键区别在于:Structured Outputs 的 strict:true 是采样层约束——OpenAI 把你的 JSON Schema 编译成有限状态机语法,模型的 token 采样器被 mask,物理上无法输出违反 schema 的 token。它不是事后校验,是生成时就限定。这就是"契约级"三个字的分量。
可靠性随 schema 复杂度递减,这是 2026 年 GPT-4o 的官方口径:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
契约级有两个补丁要提前打:
-
供应商也不可靠。 社区曾报告供应商静默变更 schema 验证器(无变更日志),导致已在生产的 schema 突然校验失败。所以字段要版本化:契约字段只加不改,废弃字段标 deprecated而不是删除,客户端持久化 Schema 版本号。 -
客户端侧二次校验是刚需,不是可选项。 DeepSeek、通义、智谱都支持 JSON-Schema,但官方文档自己建议客户端本地二次校验;更关键的是,标着"OpenAI 兼容"的代理不一定严格强制 schema。别把契约押在供应商的实现上,本地 model_validate再兜一层,成本几乎为零。
另付一笔质量税:约束解码在复杂 prompt 上可能造成 0-3% 的语义质量下降——模型倾向选"安全的"枚举默认值,而不是输出更精确的自由文本。多数业务场景里,3% 的语义损耗远比 5% 的解析失败便宜。
契约级只保证格式,不保证内容——内容对错是评测的事,下一篇讲。
第 3 层 容错级:校验-重试-兜底三段式
契约级把失败率压到 0.1%,但 0.1% 依然会真实发生。容错级解决的是:失败来的时候,系统怎么体面地应对。
第一段:校验。Pydantic 模型即契约,拿到输出先 model_validate,字段缺失、类型错位、枚举越界、金额对不上,全部在这里现形。
第二段:重试。最关键的一步是错误分类——重试只对"等一等可能变好"的错误开放:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
429 必须拆开看:rate_limit 限流可等,insufficient_quota 配额耗尽等一万年也没用。重试参数按行业推荐值走:maxRetries=3~5、maxDelay=30~60s,退避用全抖动(full jitter)——每次在 [0, min(2^attempt, max_delay)] 区间随机取睡眠时间,不同客户端退避窗口天然错开。抖动必须加:没有它会出现惊群效应,多个客户端同一秒重试,把一个 429 放大成一千个 429。还要尊重响应头里的 Retry-After,它优先于你自己算出来的退避时间。
第三段:兜底。重试耗尽仍失败时,返回语义化错误码(extract_failed、quota_exhausted)而不是裸异常,上游按错误码分流到人工/默认流程。业务方只签契约,永远接不到事故。
第 4 层 编排级:确定性骨架 + 概率叶子
单次调用稳了,多步 Agent 链路怎么办?开头推演过:5 步 77%、20 步 36%,误差叠加不会因为单步变好而消失。
编排级的解法是 LangGraph 这类状态机,2025-2026 年的主流做法是:确定性 StateGraph 是生产安全的骨架,LLM 推理只限定在特定节点内。三条铁律:
-
下一步确定时用静态边,只在模型真正需要决策时用条件边。控制流是代码的事,LLM 只做原子填充——它负责"填内容",不负责"指方向"。 -
每个节点是纯函数: input State → output State。同一个 state 进来,必然产出同一个 state,可单测、可回放。 -
对不可逆副作用(写库、支付、外部 API)用 interrupt_before停在落库前,等人审批。状态转换前校验必填字段,字段不齐不进下一步。
记住这句话:状态机是控制流,LLM 是数据流。我们用程序保证流程走得通,用模型保证内容填得对。模型出错是局部的、可重试的;流程出错是全局的、灾难性的。所以流程永不交给模型。
选层决策树:四层都上不划算
四层都上成本最高,多数团队也不需要。按这个决策树选,够用即止:
第一步:下游怎么消费输出?├─ 人读(聊天/报告)→ 只上第1层(采样级)就够└─ 代码读(JSON/结构化)→ 至少上第2层(契约级)第二步:第2层够不够?看模型可信度:├─ 前沿 API 模型(GPT-4o/Claude/豆包/通义)→ 2+3 层├─ 开源中杯(7B~14B,解析率 64%~98%)→ 3 层必须,4 层按需└─ 小模型(≤3B,解析率 26%)→ 别指望第2层,重心放第3/4层第三步:失败代价多大?├─ 只读展示 → 4 层可省,为确定性花大钱不值└─ 写库/支付/发货 → 4 层必上,interrupt_before 强制人工确认
三、代码实战:两个最关键片段
完整可运行代码(instructor + Pydantic 全量实现、错误分类函数 is_retryable、全抖动退避、降级兜底)在 CSDN 原文里。这里只保留两个最能说明思想的片段。
片段一:契约定义。Pydantic 模型即 JSON Schema——一份契约双端使用,instructor 编译成 schema 约束模型采样,本地再拿它校验输出。
class OrderStatus(str, Enum):PENDING = ”pending”; PAID = ”paid”SHIPPED = ”shipped”; CANCELLED = ”cancelled”class OrderItem(BaseModel):product_id: str = Field(pattern=r”^SKU-\d{6}$”)quantity: int = Field(ge=1, le=999)unit_price_cents: int = Field(ge=0)class Order(BaseModel):order_id: str = Field(min_length=6)customer_name: str = Field(min_length=1)status: OrderStatusitems: list[OrderItem] = Field(min_length=1)total_cents: int = Field(ge=0)
注意两个细节:枚举就是白名单,expired 这类编造值直接校验失败;金额一致性是业务规则,模型自己算不准——原文里的 total_must_match 校验器会按字段声明顺序,在 items 校验完后用明细求和兜住 total_cents,算错当场 ValueError。
片段二:编排级骨架。确定性路由 + 概率叶子,状态迁移全是代码枚举的分支。
def extract_node(state):# 概率叶子:全金字塔唯一允许 LLM 出场的节点order, code = extract_order(state[”raw_text”])return {**state, ”order”: order, ”outcome”: code, ”attempts”: state[”attempts”] + 1}def decide_next(state):# 确定性路由:可穷举、可单测if state[”outcome”] == ”ok”: return ”done”if state[”attempts”] >= state[”max_attempts”]: return ”fallback”return ”retry”builder = StateGraph(ExtractState)builder.add_node(”extract”, extract_node)builder.add_conditional_edges(”extract”, decide_next,{”done”: ”done”, ”retry”: ”extract”, ”fallback”: ”fallback”})app = builder.compile(interrupt_before=[”done”])# 落库/支付前强制人工确认
一个必须做的减法:三层重试的预算算下来是 LangGraph max_attempts(3) × tenacity(3) × instructor max_retries(3) =最坏情况约 27 次模型调用。生产上建议只保留一层重试,让成本预算可预期。原文代码保留三层嵌套演示完整骨架,但上线前务必做减法。
四、踩坑记录:三个真坑,每个都付过费
坑 1:temperature=0 在批量/并行/不同硬件下不保证字节级确定
症状:本地单测 10 次全一致,上批量脚本一跑,同一输入出现 3 种输出,下游对账脚本当场报警。排查:对比日志发现不同 GPU 实例返回的 system_fingerprint 不一致。修复:接受采样级"尽力而为"的定位,把"输出一致"的验收标准从字节级改成schema 级——一致性靠契约级+容错级保证,不靠温度参数。
坑 2:小模型 + JSON Mode 照样吐非法 JSON
症状:用 7B 开源模型 + json_object,返回的是 Markdown 代码块包着的 JSON,解析直接炸。这不是运气差——OrderBench 2026.5 的数据:Llama 3.1 8B 合法 JSON 只有 64.2%,Phi-3.5-mini 83.2% 但重复率比同类高 5-50 倍,SmolLM2 1.7B 只有 26.1%。根因:JSON Mode 是"引导"不是"保证",它不 mask token;标着"OpenAI 兼容"的厂商/代理实现参差不齐,有的只做字符串包裹不做 schema 校验。修复:客户端校验是最后一道防线,校验失败走纠错重试;小模型当生产者时,把容错级当主战场而不是指望契约级。
坑 3:无阈值重试导致成本翻倍与死循环
症状:某次 429 高峰时段,重试逻辑疯转,当日账单比基线翻了 2.4 倍。排查:重试代码没做错误分类,把 400 这类"重试无意义"的错误也放进去了;退避没加抖动,惊群效应又触发更多 429。根因:把"重试"当成了万能药,忽略了重试的本质是给临时故障一个恢复窗口。修复:is_retryable 错误分类 + maxRetries=3 硬阈值 + 指数退避带随机抖动 + 优先尊重 Retry-After 响应头。现在每次重试前都会想清楚:这个错误,等一等会变好吗?
五、选型对比:结构化输出的四条技术路线
|
|
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
json_object、各家兼容接口
|
|
|
|
|
|
|
json_schema、各家工具调用
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
语法约束解码这行必须单独讲,因为坑最多。JSONSchemaBench(9,558 个真实 schema)2026 年评测:Guidance最快,比非约束生成快约 50%(6-9ms/token vs 15-16ms),GitHub-Hard schema 覆盖率 41%;XGrammar也很快(约 1,726 tokens/s),但覆盖率只有 28%,而且有 38 次"欠约束失败"——输出了非法 JSON 却报告成功,比慢更可怕;Outlines最慢,比非约束慢一个数量级(30-46ms/token),覆盖率仅 3%。vLLM 2026.7 用户实测,Guidance 比 XGrammar 快约 2 倍。选库顺序:Guidance → XGrammar → Outlines。
最后一条底线数据,来自 OrderBench 2026.5,值得贴在工位上:100% schema 有效 ≠ 内容正确。GPT-OSS 120B 做到了 100% schema 有效,但语义成功率只有 83%;Llama-3.3-70B 的 JSON F1 高达 0.9956,语义依旧有缺口。格式问题可以被工程化锁死,内容问题必须靠评测兜住——这就是下一篇的事。
六、总结
把大模型输出不稳定从"不可控的概率"变成"可控的工程",靠的就是这 4 层:采样级接受现实(temperature=0 只是尽力而为)、契约级锁死格式(把失败率从 5% 压到 0.1%)、容错级体面失败(分类重试 + 降级兜底)、编排级守住流程(确定性骨架 + 概率叶子)。核心一句话打穿:
不求模型次次对,只求契约次次立。
但有个边界得说清楚:确定性金字塔修好了管道,可管道里流的是干净水还是脏水,管道自己不知道。schema 有效、格式完美、契约成立的输出,内容可能是幻觉、可能是错的订单号。下一篇我们用 Golden Set + CI 自动化评测,给管道装上水质检测——把"格式对"和"内容对"分开度量。
评论区聊聊:你线上遇到最离谱的一次 LLM 输出错误是什么?是格式炸了还是内容错了?

