Skip to Content
设计Adaptive Output Token Escalation输出 Token 限制与升级设计

输出 Token 限制与升级设计

默认使用模型声明的输出限制,除非用户或环境配置了 max_tokens。仅当响应仍然达到 MAX_TOKENS 限制时,才会使用升级和多轮恢复机制。

问题

每个 API 请求都会根据 max_tokens 预留固定比例的 GPU 槽位。较低的默认值可以减少槽位预留,但也更容易导致正常的大型响应被截断。在文件写入工作流中,这会产生不完整的工具调用参数,并迫使调度器拒绝部分写入。

解决方案

默认使用模型声明的输出限制。当响应被截断(模型达到 max_tokens 限制)时:

  1. 升级到模型的完整输出限制(当当前限制较低时,以 64K 为下限)
  2. 如果仍然被截断,通过将部分响应保留在历史记录中并注入继续消息来进行恢复,最多尝试 3 次
  3. 如果恢复次数耗尽,则回退到工具调度器的截断指导

这优先保证了大型生成和文件编辑任务的正确性。需要较低预留的运维人员仍然可以设置 QWEN_CODE_MAX_OUTPUT_TOKENS,系统会尊重该显式值。

架构

请求 (max_tokens = 用户/环境值或模型输出限制) ┌─────────────────────────┐ │ 响应被截断? │──── 否 ──▶ 完成 ✓ │ (MAX_TOKENS) │ └───────────┬──────────────┘ │ 是 ┌──────────────────────────────────────────────────┐ │ 第 1 层:升级到模型输出限制 │ │ ┌────────────────────────────────────────────┐ │ │ │ 从历史记录中弹出部分响应 │ │ │ │ 重试 (isContinuation: false → 重置 UI) │ │ │ │ 以 max(64K, 模型输出限制) 重新发送 │ │ │ └────────────────────────────────────────────┘ │ └───────────┬──────────────────────────────────────┘ ┌─────────────────────────┐ │ 仍然被截断? │──── 否 ──▶ 完成 ✓ │ (MAX_TOKENS) │ └───────────┬──────────────┘ │ 是 ┌──────────────────────────────────────────────────┐ │ 第 2 层:多轮恢复(最多 3 次) │ │ ┌────────────────────────────────────────────┐ │ │ │ 将部分响应保留在历史记录中 │ │ │ │ 推送用户消息:"直接继续..." │ │ │ │ 重试 (isContinuation: true → 保留 UI 缓冲区)│ │ │ │ 使用更新后的历史记录重新发送 │ │ │ │ 模型从上次中断处继续 │ │ │ └──────────────┬─────────────────────────────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ │ 成功? │── 是 ──▶ 完成 ✓ │ │ └──────┬──────┘ │ │ │ 否(仍然被截断) │ │ ▼ │ │ 尝试次数 < 3? ── 是 ──▶ 循环回到 ↑ │ └───────────┬──────────────────────────────────────┘ │ 否(已耗尽) ┌──────────────────────────────────────────────────┐ │ 第 3 层:工具调度器回退 │ │ ┌────────────────────────────────────────────┐ │ │ │ 拒绝被截断的 Edit/Write 工具调用 │ │ │ │ 返回指导:"你必须拆分为更小的部分——先写骨架, │ │ │ │ 然后增量编辑。" │ │ │ └────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘

Token 限制确定

有效的 max_tokens 按以下优先级顺序解析:

优先级来源值(已知模型)值(未知模型)升级行为
1(最高)用户配置 (samplingParams.max_tokens)min(userValue, modelLimit)userValue不升级
2环境变量 (QWEN_CODE_MAX_OUTPUT_TOKENS)min(envValue, modelLimit)envValue不升级
3(最低)模型/默认输出限制modelLimitDEFAULT_OUTPUT_TOKEN_LIMIT = 32K升级到模型限制(64K 下限)+ 恢复

“已知模型”是指在 OUTPUT_PATTERNS 中有明确条目的模型(通过 hasExplicitOutputLimit() 检查)。对于已知模型,有效值始终限制在模型声明的输出限制内,以避免 API 错误。未知模型(自定义部署、自托管端点)会直接透传用户的值,因为后端可能支持更大的限制。

此逻辑在三个内容生成器中实现:

  • DefaultOpenAICompatibleProvider.applyOutputTokenLimit() — OpenAI 兼容提供商
  • DashScopeProvider — 从默认提供商继承 applyOutputTokenLimit()
  • AnthropicContentGenerator.buildSamplingParameters() — Anthropic 提供商

升级机制

升级逻辑位于 geminiChat.ts 中,放置在主重试循环之外。这是有意为之:

  1. 重试循环处理瞬态错误(速率限制、无效流、内容验证)
  2. 截断不是错误——它是被提前截断的成功响应
  3. 升级流中的错误应直接传播给调用者,而不应被重试逻辑捕获

升级步骤 (geminiChat.ts)

1. 流成功完成 (lastError === null) 2. 最后一个 chunk 的 finishReason === MAX_TOKENS 3. 守卫检查通过: - maxTokensEscalated === false(防止无限升级) - hasUserMaxTokensOverride === false(尊重用户意图) 4. 计算升级后的限制:max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output')) 5. 从聊天历史中弹出部分模型响应 6. 产生 RETRY 事件 (isContinuation: false) → UI 丢弃部分输出并重置缓冲区 7. 使用 maxOutputTokens: escalatedLimit 重新发送相同请求

恢复步骤 (geminiChat.ts)

如果升级后的响应也被截断(finishReason === MAX_TOKENS),恢复循环最多运行 MAX_OUTPUT_RECOVERY_ATTEMPTS (3) 次:

1. 部分模型响应已在历史记录中(由 processStreamResponse 推送) 2. 推送恢复用户消息:OUTPUT_RECOVERY_MESSAGE 3. 产生 RETRY 事件 (isContinuation: true) → UI 保留文本缓冲区以继续 4. 使用更新后的历史记录重新发送(模型看到其部分输出 + 恢复指令) 5. 如果仍然被截断且还有剩余尝试次数,则循环回到步骤 1 6. 如果恢复尝试抛出异常(空响应、网络错误): - 从历史记录中弹出悬空的恢复消息 - 跳出恢复循环

RETRY 时的状态清理 (turn.ts)

Turn 类接收到 RETRY 事件时,它会清除累积的状态以防止不一致:

  • pendingToolCalls — 清除以避免重复的工具调用,以防第一个被截断的响应包含已完成的工具调用,并在升级后的响应中重复
  • pendingCitations — 清除以避免重复的引用
  • finishReason — 重置为 undefined,以便使用新响应的 finish reason

isContinuation 标志会传递给 UI,以便 UI 决定是重置文本缓冲区(升级)还是保留它们(恢复)。

常量

定义在 geminiChat.tstokenLimits.ts 中:

常量用途
ESCALATED_MAX_TOKENS64,000当模型限制较低时,作为升级的下限
MAX_OUTPUT_RECOVERY_ATTEMPTS3升级后的最大多轮恢复尝试次数

有效的升级后限制为 max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output'))

模型升级后限制
Claude Opus 4.6131,072 (128K)
GPT-5 / o-series131,072 (128K)
Qwen3.x65,536 (64K)
未知模型64,000(下限)

设计决策

为什么不使用 8K 默认值?

  • 8K 默认值是一种槽位预留/容量优化,而非正确性要求。它以正确性(大型响应被截断)为代价换取后端吞吐量(请求会根据 max_tokens 预留成比例的 GPU 槽位,因此较低的值会减少过度预留)。
  • 大型文件生成和编辑工具调用合理地可能超过 8K,因此 8K 默认值会将正常请求变成 截断 → 升级 的往返过程(在最坏的情况下,还会变成重试循环)。
  • Claude Code 保留了相同的 8K 上限,但将其置于功能标志(tengu_otk_slot_v1)之后,该标志默认对第三方提供商关闭(“未在 Bedrock/Vertex 上验证”)——即其对非第一方服务的默认行为正是“使用模型声明的限制”。Qwen Code 的提供商均为第三方 / OpenAI 兼容 / 自托管,因此匹配该默认关闭行为是安全的选择;假设低默认值对所有后端都安全则不然。
  • 容量权衡并未丢失,只是变成了可选:在容量受限的自托管后端上,运维人员可以设置 QWEN_CODE_MAX_OUTPUT_TOKENS(例如 8000)来恢复较低的每请求预留。故意不重新引入 GrowthBook 风格的功能标志——Qwen Code 没有此类基础设施,且环境变量已满足需求。

为什么升级到模型限制而不是固定的 64K?

  • 具有更高输出限制的模型(Claude Opus 128K、GPT-5 128K)被不必要地限制在 64K
  • 使用模型的实际限制可以捕获绝大多数长输出,而无需进行第二次重试
  • ESCALATED_MAX_TOKENS (64K) 作为未知模型的下限,此时 tokenLimit() 返回默认的 32K

为什么使用多轮恢复而不是渐进式升级?

  • 渐进式升级(例如 16K -> 32K -> 64K)需要每次重新生成完整响应
  • 多轮恢复保留部分响应并让模型继续,从而节省 token 和延迟
  • 与重新生成大型响应相比,恢复消息的成本很低(每个约 40 个 token)
  • 3 次尝试的限制可防止无限循环,同时覆盖大多数实际情况

为什么升级在重试循环之外?

  • 截断是成功的情况,而不是错误
  • 升级流中的错误(速率限制、网络故障)应直接传播,而不是使用错误的参数被静默重试
  • 保持重试循环专注于其原始目的(瞬态错误恢复)
  • 恢复错误被单独捕获,以避免中止整个对话
Last updated on