thinking和回答共享max_tokens配额,budget_tokens必须小于max_tokens;这意味着推理越充分,答案展示空间越少。
budget_tokens 是用来决定 Claude 在回答之前能够进行多长推理的拨盘。理解它的作用仍然值得花时间,因为它所揭示的约束——推理和回答共享同一个预算——并没有随着这个参数的移除而消失。
Extended thinking 通过一个携带 token 预算的 thinking 对象启用:
{
"model": "claude-sonnet-4-5",
"max_tokens": 16000,
"thinking": {
"type": "enabled",
"budget_tokens": 10000
},
"messages": [
{"role": "user", "content": "Find the race condition in this scheduler."}
]
}
两个文档化的约束管理着这个值。它有一个 1,024 token 的下限——低于该值请求会被拒绝——并且它必须严格小于 max_tokens。两者都在服务端验证,因此大于上限的预算会返回 400 而不是静默截断。
第二个约束才是有实际意义的。它存在的原因是 thinking token 也是输出 token:它们由模型生成、按输出费率计费、并像其他 token 一样计入 max_tokens。预算不是一个独立的配额。
因为两者都来自同一个上限,设置预算同时也是在为可见回答设置天花板。推导只有一行:
answer_ceiling = max_tokens - thinking_actually_used
with max_tokens = 16,000 and budget_tokens = 10,000:
worst case (model uses the whole budget) 16,000 - 10,000 = 6,000
typical case (model uses ~40% of budget) 16,000 - 4,000 = 12,000
best case (model barely thinks) 16,000 - 500 = 15,500
重要的不对称在于 budget_tokens 是一个目标,而不是预留。模型可能用不到预算的全部——在一个简单问题上通常会少用很多——没用掉的部分可以留给回答。它做不到的是超出预算。所以预算为回答长度设定了最坏情况,而你必须为最坏情况留出空间。
这产生的失败是独特的:响应中 stop_reason 是 max_tokens,包含一个长的 thinking 块和一个在句子中间中断的可见回答。模型并没有丢失思路。它把预算花在了推理上,然后没有空间继续写了。修复方法是提高 max_tokens,而不是降低预算——降低预算也会降低你开启 thinking 想要获取的质量。
一个有用的起始形态是 max_tokens 等于预算加上回答所需的空间,再加上缓冲空间。10,000 token 的预算配合 12,000 token 的上限在最坏情况下只留 2,000 给回答,这不够做任何实质性的事情。
上限够大后用流式。当预算大到值得设置时,通常会把 max_tokens 推到足够高,导致缓冲请求有传输超时的风险。
它出现在 usage.output_tokens 中,无论你是否展示它都按输出费率收费。
Thinking 作为自己的 content block 类型出现在 text block 之前:
{
"content": [
{
"type": "thinking",
"thinking": "The scheduler takes the lock after reading the queue length…",
"signature": "EqQBCgIYAhIM…"
},
{
"type": "text",
"text": "The race is between the length read and the lock acquisition…"
}
],
"stop_reason": "end_turn"
}
signature 字段是一个密码学标记,证明这个块是真实的且未被修改过。如果你要把 thinking 块作为对话历史发回去——在工具循环中必须这样做,以便模型能看到前一步自己的推理——这个块必须被完整回显,包括 signature。编辑文本后重新发送会被拒绝。
在流式响应中,相同的内容以 thinking_delta 事件的形式到达,随后是一个关闭该块的 signature_delta。参见流式事件类型。在配置为省略推理文本的模型上,thinking 块仍然会以空内容出现在流中——所以当块类型到达时显示"thinking"面板的渲染器会显示一个空面板,而不是什么都不显示。
这是 thinking 不再只是质量设置、而是你对话构建代码必须了解的东西的地方。
当启用 thinking 的模型调用工具时,assistant turn 包含一个 thinking 块和一个 tool_use 块。要继续循环,你需要把那个 turn 作为历史追加——thinking 块必须原样随其回去,signature 完整。这不是可选的,也不是装饰性的:模型需要看到导致这次调用的推理才能解释结果,而 signature 正是证明推理是它实际生成的。
三个具体的失败案例源于对此的错误处理:
丢弃该块。最常见的原因是历史构建代码按名称提取 text 和 tool_use 块并丢弃其他所有内容。请求被拒绝,错误会指出一个作者从未刻意处理过的块类型。
编辑文本。在存储前删除推理,或者通过格式化程序往返处理(规范化空白符),会破坏 signature。如果你必须不持久化推理文本,答案是不持久化那个 turn,而不是持久化一个被修改过的版本。
重排块顺序。thinking 块在数组中排在前面是有原因的。用 tool 调用在前重建 turn 是不同的消息。
预算也与循环交互,而且是乘数效应。每次迭代都是一次新的生成,带着自己的推理,所以一个十步的 agent 运行要付十次 thinking 成本,而且每一步都要重新读取一个现在包含所有先前推理的上下文。这就是启用 thinking 的 agent 循环比没有 thinking 的相同循环成本明显更高的具体原因,也是为什么通过观察单轮请求选择的预算在看它运行二十轮后会显得非常不同。
这是如果是一年前写的页面会出错的部分。budget_tokens 是它所发布的模型 generations 上的 extended-thinking 参数,而在最新的 generations 上它已经消失了:发送 {"type": "enabled", "budget_tokens": N} 给当前前沿的 Claude 模型会返回 400,而不是被采纳或忽略。
取代它的是 adaptive thinking——模型决定每次请求思考多少——配置为 {"type": "adaptive"},深度由一个单独的努力设置而不是 token 计数来引导。理由是固定 token 预算是一个糟糕的工具:在困难请求上太低,在简单请求上浪费,而且一旦面对混合工作负载就无法调优。
某个给定模型接受哪个参数是一个随每次发布变化的 per-model 事实,两种形式不可互换——错误的参数会返回 400,而不是回退。Anthropic 的 extended thinking 文档和 adaptive thinking 页面包含当前 per-model 支持情况。固定你的模型版本至少能让你错误得可预测。
如果你所在的 generation 使用 budget_tokens,把它当作对你回答的最坏情况预留来对待,并相应地调整 max_tokens。如果你所在的 generation 使用 adaptive thinking,问题从"多少 token"变成"多少努力",答案因工作负载而异而不是通用的:常规提取和分类用较低设置,thinking 存在的目的——漫长的多步问题——用较高设置。
无论如何,不变的是推理和回答共享一个预算;如果你不为两者都留出空间,模型就会没有空间说话。
Interleaved Thinking and Tool Use in Claude
Claude's Max Output Tokens per Model
Claude's Streaming Event Types, From message_start to message_stop