17 · 扩展思考的处理:签名、模型绑定,和那些能调一整天的坑
扩展思考能显著提升 Agent 解难题的能力,却带来一组独特的坑:思考块必须整段保留、带着不可篡改的签名、而且签名和具体模型绑定。讲清思考块的三条铁律、它和流式/历史/模型回退的相互作用,以及为什么它是最容易让你调一整天的特性。
17 · 扩展思考的处理:签名、模型绑定,和那些坑
第六阶段收尾。现代模型有一种特殊能力——扩展思考(extended thinking):在给出答案前,先进行一段可见的、结构化的推理。它能显著提升 Agent 解决难题的能力,但也带来一组独特到反直觉的工程坑。
一、背景:多了一种你必须原样保管的内容块
开启思考后,模型的响应里会多出一类内容块:思考块(thinking)。它长这样:
{
role: 'assistant',
content: [
{
type: 'thinking',
thinking: '让我先分析一下这个 bug……报错在 auth.ts:42,可能是……',
signature: 'EqoBCkgI...(一长串加密签名)', // ← 注意这个
},
{ type: 'text', text: '我找到问题了,是……' },
],
}注意那个 signature——它是模型对这段思考的加密签名,用来证明“这段思考确实是我在这个上下文里生成的、没被篡改过”。正是这个签名,是所有思考相关的坑的根源。
二、思考块的三条铁律
处理思考块,有三条不容违反的规则。违反任何一条,轻则报错,重则让你对着一个诡异的 400 调一整天。
① 思考块必须整段、原样保留在历史里——包括那串签名,一个字节都不能改。 你不能“顺手把思考内容精简一下”再放回历史(签名会对不上,API 直接拒);也不能只留 text、把 thinking 块丢掉(在一个包含工具调用的思考轨迹里,丢了思考块会破坏轨迹完整性)。思考块对你是只读的、不透明的——你只负责原样搬运,不许加工。
③ 一段“思考轨迹”必须完整保留,直到它彻底结束。 一次带工具调用的思考(思考 → 发起工具调用 → 拿到结果 → 继续),这整条轨迹里的思考块都得留着,不能中途删。你不能刚思考完、工具还没跑完,就把思考块清掉——那会破坏这条尚未闭合的轨迹。
这三条规则合起来,可以浓缩成一句话:思考块是模型“working memory”的一次带签名的快照;你的唯一职责是完整、原样、在正确的生命周期内保管它,别碰它的任何一个字节。 想清楚这一点,大部分思考相关的坑就都能绕开了。(还有一种 redacted_thinking 块——出于安全被加密隐藏的思考,同样只需原样保管即可。)
三、和前面几个系统的相互作用
思考不是一个孤立特性,它和我们前面搭的好几个系统都有微妙的相互作用,值得逐一点一下:
- 和流式:思考内容也是流式到达的(有自己的
thinking_delta)。你可以实时把它展示给用户(让用户看到模型在“想什么”),但要注意签名是在思考块结束时才给的——和工具参数那个“逐字到达、末尾才完整”的模式如出一辙。 - 和历史 / 上下文压缩:压缩历史时,思考块要么完整保留、要么整段移除(连同它所在的轨迹),绝不能“部分保留”。半段带签名的思考块是历史里的一颗雷。
- 和模型回退:如前所述,回退前必须剥离思考签名。这是两个正确特性组合出的一个必须专门处理的交叉点。
- 和
max_tokens:思考也消耗输出 token,而且往往消耗不少。开了思考,你的max_tokens预算要相应放大,否则容易被截断。
四、实现细节:enable、保管、别加工
用起来,开启思考只是请求里加一个配置:
const stream = client.messages.stream({
model, max_tokens: 16000,
thinking: { type: 'enabled', budget_tokens: 8000 }, // ← 给思考留 8000 token 预算
messages, tools,
})难的从来不是“开启”,而是之后对思考块的保管纪律:
// ✅ 对的:把完整的 content(含 thinking 块及其签名)原样放回历史
messages.push({ role: 'assistant', content: res.content })
// ❌ 错的:过滤掉 thinking 块,只留 text —— 破坏轨迹,后续可能 400
messages.push({ role: 'assistant', content: res.content.filter(b => b.type !== 'thinking') })
// ❌ 错的:精简 thinking 内容 —— 签名对不上,直接被拒你会发现,思考的处理再次呼应了缓存那篇“别原地改要复用的消息”的纪律——凡是要回流给 API 的东西,保持字节原样。思考块把这条纪律推到了极致:它不仅要字节原样,还带着一个会“验真”的签名,和一个“认签名的”特定模型。把“原样搬运、按生命周期保管、别碰字节”当成处理这类内容的默认心法,你就能躲开绝大多数坑。
五、第六阶段总结
健壮性阶段到此完整。 这一章的三篇,让 Agent 具备了在真实世界里稳定运行的韧性:
我们的 Agent 现在自主、快速、可控、可扩展、上下文管理精细、还扛得住真实世界的抖动。单个 Agent 能做的,它基本都会了。
最后一个阶段,我们把视野从“一个 Agent”扩展到“一个系统”:当一个 Agent 打不过时,就上一群(子 Agent);当内置工具不够时,就接入外部工具生态(MCP);以及,怎么把这一切持久化下来,支持关掉再回来接着干(会话历史)。进入第七阶段。
本篇对应代码:配套仓库为 runAgent 加入可选的扩展思考配置,并演示思考块的原样保管(src/agent.ts)。git checkout article-17。