结构化输出重试循环:你被忽视的算力浪费
打开你的结构化输出仪表盘。它自豪地显示着类似 “98.4% 的 Schema 合规率” 这样的数字。这就是成功率——即第一次尝试就生成有效 JSON 对象的请求比例。团队为剩下的 1.6% 构建了一个重试封装器(retry wrapper),发布上线,然后就没再管了。两个季度后,推理费用增长了 15%,而请求量仅增长了 4%。首席财务官(CFO)想要个解释。工程师们给不出解释,因为跟踪结构化输出成功率的仪表盘并不跟踪结构化输出的成本。
仪表盘隐藏的部分在于:失败路径并非只有一次重试。第一次重新提示(re-prompt)修复了缺失的 enum 字段,但引入了一个格式错误的嵌套数组。第二次重新提示修复了数组,但丢掉了一个必填键。第三次尝试终于通过了验证,但到那时,该请求已经消耗了四次完整的推理调用加上最初的生成过程,而你的单次请求 Token 计数器显示的是 总和,而不是循环过程。从计数器的角度来看,这是一个昂贵的请求。从成本线的角度来看,这是一个你从未定价的随机循环。
这篇文章将探讨该循环究竟对你的算力预算产生了什么 影响,为什么你现有的观测能力(observability)无法察觉到它,以及哪些规范可以使其变得可见且可控。
让成本增加 15% 的那 2%
一旦你把账算清楚,结果是残酷的。假设你的结构化输出失败率为 2%,并且你的重试封装器在放弃或进入降级路径前平均尝试三次。那 98% 的正常请求每次花费 1 倍的 Token。而那 2% 失败的请求每次花费 4 倍的 Token(一次初始尝试加三次重试)。计算一下:0.98 × 1 + 0.02 × 4 = 1.06 倍基准成本——这意味着为了 “98% 的成功率”,整个系统支付了 6% 的溢价。
但这只是下限,而非上限。重试路径使用的 Prompt 比成功路径 更长,因为封装器会附加之前损坏的输出和一条 “修复此错误” 的指令。因此,每次重试的输入 Token 是原始输入的 1.5 到 2 倍。重试路径往往也针对更难的输入——即那些最初导致失败的输入——这意味着模型正在生成更谨慎、更长的输出。当你考虑到重试 Prompt 的开销和更长的输出时,这 2% 的失败请求通常会消耗 12% 到 18% 的算力预算。
那些将结构化输出视为单次推理调用的团队,正在为一个他们从未定价的随机循环买单。仪表盘显示 “结构化输出工作正常”。账单却显示它只是有时工作,且代价高昂,而且是以一种无人标记的方式在运行。
失败路径并非干净利落
如果重试总能收敛,那么这只是个定价问题,而不是架构问题。但事实并非如此。
现代约束解码 API(OpenAI 的 Structured Outputs、Anthropic 的 tool-use schemas、vLLM 的 XGrammar、Outlines 基于 FSM 的解码)对于分布内(in-distribution)输入,已将 Schema 合规失败率降至 1% 以下。剩下的失败集中在长尾内容上:深度嵌套的 Schema、大型枚举联合体(enum unions)、模型倾向于使用代码块格式化的自由文本字段、多语言数值,以及分词器(tokenizer)的子词边界与 Schema 字符级约束冲突的边缘情况。对于这些输入,模型并不是随机失败的。它会在同一种结构特征上一次又一次地失败。
相应地,重试循环是在对抗一个正在对抗自身解码分布的模型。你在生产环境链路追踪(traces)中实际看到的景象是:
- 尝试 1:在包含 14 个键的嵌套 Schema 中遗漏了一个必填字段。
- 尝试 2:在得到错误输出和 “修复缺失字段” 的指令后,修复了该字段,但将另一个字段移到了其父对象之外——这是一种封装器难以描述的结构性漂移。
- 尝试 3:修复了结构性漂移,但生成了一个有效
enum值的拼写错误(比如用 "INVTL" 代替 "INVAL")。 - 尝试 4:要么最终正确,要么进入降级代码路径。
每一次尝试都是在略有不同的 Prompt 条件下的重新生成。这里不存在单调收敛。四次尝试中可能有两次能通过 Schema 验证;但这没用,因为封装器会接受第一个通过验证的结果,而那个结果可能在语义上仍然是错误的(一个字段修复了,另一个却被悄无声息地截断了)。“结构化输出是一种约束性生成契约,模型可能会以重试放大的方式失败” 这种表述,比 “结构化输出 98% 的时间都有效” 更为准确。
迭代次数限制是错误的度量单位
大多数重试封装器都会限制迭代次数:“最多重试 3 次”。这是一个错误的控制选项。迭代是离散的计数,而成本是连续的变量。一个失败并在小型、确定性的 Schema 上重试的请求,每次尝试可能只花费额外的 200 个 Token。但在一个包含 6,000 个 Token 且具有深度嵌套 Schema 的文档上失败的请求,每次尝试的成本会超过 8,000 个 Token,因为封装器正在将之前损坏的输出重新反馈进去。
限制三次迭代意味着在处理长文档时,你最坏情况下的失败路径会消耗 24,000 多个额外 Token——大约是同一文档成功路径成本的 7 倍。这种限制对于约束成本毫无作用。它限制的是 尝试次数,这只是一个与账单核心指标并不挂钩的替代变量。
解决方法是以 Token 而不是迭代次数来限制循环:“在进入降级路径前,该请求最多允许消耗成功请求预算的 2 倍”。实现起来很简单——跟踪各次尝试的输入 + 输出 Token,当累计支出超过阈值时中止,并路由到降级路径。阈值本身变成了一个可以按功能设置的可调参数:低风险的摘要生成可以容忍 1.2 倍预算;长文档的高风险提取可以给予 3 倍预算;而调试模式则可以不受限制,并配合报警通知。
这一项规范——以 Token 计量的重试预算——决定了你是 “拥有一个重试策略”,还是 “已经为你的重试策略定好了价”。
你所缺失的模式失败仪表盘
一个显示 “98.4% 结构化输出成功率” 的仪表盘是在对你团队发布的所有模式(schema)进行平均化。这对于优化循环来说是一个错误的粒度,因为失败并不是均匀分布的。一两个模式往往占据了大部分重试流量,而在这些模式中,又是一两个特定字段导致了大部分失败。
你需要的——而且几乎没有团队会默认构建的——是一个按哪个字段验证失败进行切分的仪表盘。对于生产环境中的每个模式,记录:哪个顶级字段导致了失败,是缺失必填项、枚举不匹配、类型强制转换错误,还是结构性错误,以及输入的特征(长度、语言、是否存在代码块)。经过两周的记录,你会发现整个提示词库中可能只有三个字段消耗了 70% 的重试预算。它们通常是:
- 带有默认值的可选字段,但模型不断输出
null,违反了enum约束。 - 模型倾向于用 markdown 围栏包裹的自由文本字段。
- 一个嵌套对象,其必填键与模型训练分布中类似名称的字段冲突(模型输出了错误的那个)。
一旦这些字段按名称可见,你就可以修复它们——通常是通过模式重新设计而不是修改提示词。让 enum 同时接受 null 和占位符。在后处理中剥离代码围栏,而不是强迫模型抑制它们。重命名冲突的键。这些都不是什么高深的技术;唯一的障碍是仪表盘没有把这些“罪魁祸首”暴露出来。团队在不知道哪个模式、哪个字段、哪种输入形状的情况下,盲目地调试 “结构化输出失败 ”。
回退机制优于无限循环
第三项纪律是团队最抵触的,因为这感觉像是接受了失败。但这也是保持账单真实性的关键:当循环发散时,一个更倾向于退而求其次的结构化输出而非无限重试的回退路径(fall-through path)。
具体来说:如果一个请求达到了重试 token 预算仍未生成有效对象,包装器(wrapper)不应继续尝试。根据功能的不同,它应该选择以下三个选项之一:
- 返回最佳的部分输出 —— 模式违规最少的尝试,并附带一个标志,供下游服务决定是否采用。
- 返回带原因代码的类型化 null —— 空对象加上
{ "_status": "schema_failed", "_field": "summary" },这样调用方可以选择优雅降级(跳过该功能、显示备用 UI、上报人工)。 - 返回无约束的生成内容 —— 模型的自由回答,并附带明确标记说明它未满足契约。这通常比三次无法收敛的额外重试更有用。
团队抵触这一点是因为 “我们不返回损坏的数据”。但实际上你已经在这么做了;损坏的数据只是隐藏在三次额外的推理调用和误导性的成功率背后。使回退机制显式化,将成本从不可见(计算预算)转变为可见(一小部分响应中的标志)。这是一个明显更好的交易——你可以决定如何处理显式失败;但你无法决定如何处理你看不懂的账单。
回退机制还为模式改进创造了一个驱动力。如果 0.5% 的响应返回了 _status: schema_failed,这是一个产品经理可以做出反应的数字。“我们在重试上花费了 15% 的计算资源” 则不是一个能让人产生反应的数字,因为计算资源是一个账单条目,而不是一个面向用户的产物。
单次调用归因:首次尝试 Token vs. 重试 Token
最后一项纪律的实现成本最低,但不执行却最令人尴尬:在单次调用级别将 token 归因拆分为首次尝试和重试,并在成本报告中同时呈现两者。
大多数 LLM 网关在每个请求中只记录一个 tokens_in 和 tokens_out。如果你的包装器在内部进行重试,这些数字就是总和——它们掩盖了循环。解决方法是每次尝试记录一个条目,标记 attempt_index 和稳定的 request_id,并将每次尝试的成本汇总到每请求成本报告中,显式展示重试开销。
这样做之后,两件事变得可能:
- 诚实的单位经济学。 你可以回答 “功能 X 的结构化输出调用实际成本是多少?” 并得到一个包含重试长尾的准确数字。目前,这个数字介于 “营销宣传” 和 “实际账单” 之间,且无法调和。
- 按租户或按功能的成本归因。 如果一个客户的输入分布比另一个触发了更多重试,你会发现这一点。如果一个功能的模式相比同类功能支付了 4 倍的重试税,你也能看到。这正是决定在何处投入模式重新设计精力所需的数据。
实现成 本仅是一个结构化日志字段和一个汇总查询。而不这样做的代价是,关于 “结构化输出是否昂贵?” 的每一次讨论都只能基于传闻。
把它看作随机循环,而非单次调用
核心架构层面的认知正是 TODO 中暗示的那一点:结构化输出是一种 受约束的生成契约,模型可能会以一种放大重试次数的方式失败。“单次推理调用”的心智模型不过是一个假象,之所以能存在,是因为网关将循环聚合为了一个数字。
一旦你接受了你正在运行一个随机循环,工程上的逻辑就会顺理成章。你不再通过计算迭代次数来限制随机循环,而是通过支出来限制。你不再从整体上调试随机循环,而是根据分歧发生的位置进行切片分析。你不再通过加大循环力度来从随机循环中恢复,而是构建一个回退机制,将无声的计算浪费转化为显式的、可观察的失败。你不再按成功路径为随机循环定价,而是按长尾路径定价。
在 2026 年,那些正确处理结构化输出的团队已经不再将重试包装层视为质量修复手段,而是将其视为成本控制界面。真正重要的仪表盘不是 Schema 合规百分比,而是重试 Token 与首次尝试 Token 的比例,并按 Schema 和失败字段进行切片。当该比例上升时,就意味着需要修改提示词或 Schema。当该比例下降时,你可以将省下的额度用在真正能被察觉到的地方。
那 2% 的失败率本身不是问题。问题在于这 2% 的失败率乘上了一个无限制、无归因的循环。限制它,归因它,并在其 发生偏离时让它显性地失败。你的账单会开始告诉你真相,这通常是解决任何问题的第一步。
- https://tokenmix.ai/blog/structured-output-json-guide
- https://collinwilkins.com/articles/structured-output
- https://openai.com/index/introducing-structured-outputs-in-the-api/
- https://sophiabits.com/blog/openai-structured-outputs-deep-dive
- https://www.aidancooper.co.uk/constrained-decoding/
- https://medium.com/@docherty/controlling-your-llm-deep-dive-into-constrained-generation-1e561c736a20
- https://fireworks.ai/blog/why-do-all-LLMs-need-structured-output-modes
- https://portkey.ai/blog/the-complete-guide-to-llm-observability/
- https://www.truefoundry.com/blog/observability-in-ai-gateway
- https://www.promptfoo.dev/docs/guides/evaluate-json/
