跳到主要内容

结构化输出重试循环:你被忽视的算力浪费

· 阅读需 13 分钟
Tian Pan
Software Engineer

打开你的结构化输出仪表盘。它自豪地显示着类似 “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 计量的重试预算——决定了你是 “拥有一个重试策略”,还是 “已经为你的重试策略定好了价”。

你所缺失的模式失败仪表盘

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates