供应商上调 max_tokens 默认值,导致你的尾部响应长度翻倍
你的事故时间线显示没有部署。你的代码没有变。你的流量组合没有变。你的提示词也没有变。然而,你的 p99 输出长度在一周内翻了一番,下游渲染层开始截断响应,而且在流量没有请求更长答案的情况下,你的输出 Token 账单增长了 38%。这种变化是真实的,回归是可以衡量的,但你的版本控制系统中没有记录——因为发生变动的值是你的代码从未发送过的。
供应商提高了一个隐式默认值。发布说明将其归类为“改进的长内容表现”。有问题的参数是 max_tokens,你的应用程序从第一天起就忽略了它,因为文档中记录的默认值很慷慨,而且你的输出很少接近这个值。默认值从 4096 移动到 8192,以适应供应商新模型中更长的推理过程。无论你是否想要,你的应用程序都获得了新的默认值,因为缺少参数本身就是一种配置选择——而供应商拥有更改其背后值的权利。
这种故障模式下,供应商侧的“无操作”(no-op)发布会作为行为变化、成本变化和用户体验(UX)变化同时在你的系统中传播,而你团队唯一的诊断信号是月底寄来的账单。
缺少参数也是一种配置
当你的客户端发送一个不带 max_tokens 的请求时,你并不是在停用该参数。你是在接受供应商选择的当前默认值,且当该值变动时不会收到任何通知。你的应用程序行为是由你未发送的每一个选项参数化的,而你与供应商在这些选项上的合同对他们来说是单方面的。
这不是理论。OpenAI 的聊天完成(Chat Completions)API 在某些路径下省略 max_tokens 时,历史上会将响应截断在 ~512 个 Token 左右,而在其他路径下则是 4096 个,具体取决于模型和端点系列。Azure OpenAI 对于许多 GPT-4 级模型默认设置为 4096,除非显式配置,而通过不同 SKU 访问的同一模型会返回不同的默认值。Claude 的 Messages API 要求必须设置 max_tokens,但输出上限(你的账户允许设置的最大值)会随着新的 Beta Header 发布而变化:Sonnet 4.6 通过 output-300k-2026-03-24 这一 Beta 版本从 64k 增长到了 300k。较新的推理模型在发出 500 个可见 Token 之前,会合理地消耗 20,000 到 40,000 个思考(thinking)Token,这使得相同可见输出的建议 max_tokens 设置提高了一个数量级。
每一个这些数字都是存在于你代码之外的配置。当供应商移动其中任何一个数字时,你应用程序的最坏情况行为也会随之移动,而你发现的唯一方法是观察下游的影响。
省略参数背后的潜在假设是默认值是稳定的。供应商从未做出过这样的承诺。文档描述的是当前的默认值,而不是契约值(contract value)。 仔细阅读,你会发现诸如“目前默认为”或“可能会调整以优化质量”之类的措辞——这些语言明确保留了更改的权利。那些构建在参数缺失基础上的团队,实际上是构建在一个注定会发生变动的值之上。
故障是如何组合的
38% 的成本飙升是最容易检测的部分,因为你的财务看板会捕捉到它。而昂贵的故障是那些悄无声息地发生的故障。
你的 UI 是针对旧的最坏情况设计的。一个响应卡片的 max-height 设置为适合 1500 个 Token 渲染后的 Markdown,并设置了溢出隐藏。现在尾部 2500-3000 个 Token 的响应在屏幕上被截断了,你的用户看到的是看起来很权威但在句子中间结束的输出。你的支持渠道收到的反馈通常被定性为幻觉或格式错误,因为用户端没有人知道“模型写的内容超出了 UI 渲染能力”这个概念。追踪记录显示,模型在其生成的 2800 个 Token 中表现正确。UI 才是撒谎的那一层。
你的延迟预算(latency budget)假设了一个输出分布。流式传输保持了首字延迟(TTFT)较低,但总渲染时间随输出长度增加。针对“完整答案到达”的 p99 延迟从 4.2 秒变为 7.8 秒,你原本针对旧分布设置的 SLO 在生产环境中开始失效,而没有任何代码更改可以解释这一点。你的告警阈值是根据旧的曲线形状调整的。
你的下游消费者假设了一个长度合同。负责接收模型输出并将其输入工具的智能体(Agent),在测试时是基于能容纳在单个工具调用参数预算内的摘要进行的。一些新的更长的输出超出了该预算。以前成功的工具调用开始因截 断而失败,智能体进行重试,重试遇到同样的截断,而堆栈顶部的症状是一个混乱的智能体循环,且没有明显的上游原因。
每一个这些都是你应用程序对自己持有的合同,以输出 Token 为单位,而供应商的默认值更改默默地破坏了这些合同。在发生变化的层级,这些都不可见。在用户生活的层级,这些都清晰可见。
锁定代码涉及的每一个参数
这种防御性做法是机械化的:显式发送每一个调用参数,并将该值作为常量锁定在你的代码库中,即使该值与文档中的默认值一致。其成本仅是为每个参数增加一行代码。其收益在于,记录在案的默认值是你代码的默认值,而不是供应商的,任何行为的变化现在都是你代码库的变化,会通过正常的评审流程进行发布。
这适用于 max_tokens、temperature、top_p、top_k、frequency_penalty、presence_penalty、stop、seed、系统提示词格式、响应格式以及任何与工具相关的预算——即 API 暴露的整个选项表面。如果参数有供应商可以更改的默认值,你就设定它。如果参数对你的路由没有明显的价值,你就选一个并记录原因,而不是让“缺失”意味着“由他们今天决定”。
有人会本能地争辩说,显式默认值是噪音:反正数值与文档中的默认值一致,那行代码没有任何作用。这种直觉是错误的。数值只是 今天 与文档中的默认值一致。那行代码是你的应用程序与明天静默发生的重参数化(reparameterization)之间唯一的屏障。输入 max_tokens: 2048 的成本只需支付一次。而不输入的成本则在每次默认值变动时支付,体现在那些毫无预警的事故响应和账单核对中。
- https://help.openai.com/en/articles/5072518-controlling-the-length-of-openai-model-responses
- https://platform.claude.com/docs/en/about-claude/models/overview
- https://platform.claude.com/docs/en/build-with-claude/extended-thinking
- https://www.vellum.ai/llm-parameters/max-tokens
- https://www.digitalocean.com/community/tutorials/model-silent-versioning-problem
- https://tokenmix.ai/blog/thinking-tokens-billing-trap-2026
- https://www.traceloop.com/blog/catching-silent-llm-degradation-how-an-llm-reliability-platform-addresses-model-and-data-drift
- https://arxiv.org/abs/2511.07585
- https://muxup.com/2025q2/recommended-llm-parameter-quick-reference
- https://glaforge.dev/posts/2024/09/23/some-good-practices-when-integrating-an-llm-in-your-application/
