17 · 扩展思考的处理:签名、模型绑定,和那些能调一整天的坑

扩展思考能显著提升 Agent 解难题的能力,却带来一组独特的坑:思考块必须整段保留、带着不可篡改的签名、而且签名和具体模型绑定。讲清思考块的三条铁律、它和流式/历史/模型回退的相互作用,以及为什么它是最容易让你调一整天的特性。

作者 David更新于

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 块丢掉(在一个包含工具调用的思考轨迹里,丢了思考块会破坏轨迹完整性)。思考块对你是只读的、不透明的——你只负责原样搬运,不许加工。

② 思考块的签名和“生成它的那个模型”绑定。 这是最阴险的一条。签名是特定模型签发的。如果你把一段带签名的思考块,replay 给另一个模型(比如上一篇的模型回退触发了、切到了备用模型),那个模型不认这个签名,请求当场 400。所以——一旦发生模型回退,必须先把历史里的思考签名块清理/剥离掉,再喂给新模型。 这两个特性(模型回退和思考)单独看都对,组合起来却会打架,而且症状(一个莫名其妙的 400)完全指不到根因上。

③ 一段“思考轨迹”必须完整保留,直到它彻底结束。 一次带工具调用的思考(思考 → 发起工具调用 → 拿到结果 → 继续),这整条轨迹里的思考块都得留着,不能中途删。你不能刚思考完、工具还没跑完,就把思考块清掉——那会破坏这条尚未闭合的轨迹。

这三条规则合起来,可以浓缩成一句话:思考块是模型“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 具备了在真实世界里稳定运行的韧性:

篇章韧性来自
15 错误恢复把出错当常态:退避、回退、恢复、防螺旋
16 成本追踪把花销看清:四类计价、覆盖所有出口、预算硬闸
17 思考处理把特殊内容保管好:签名、模型绑定、生命周期

我们的 Agent 现在自主、快速、可控、可扩展、上下文管理精细、还扛得住真实世界的抖动。单个 Agent 能做的,它基本都会了。

最后一个阶段,我们把视野从“一个 Agent”扩展到“一个系统”:当一个 Agent 打不过时,就上一群(子 Agent);当内置工具不够时,就接入外部工具生态(MCP);以及,怎么把这一切持久化下来,支持关掉再回来接着干(会话历史)。进入第七阶段。


本篇对应代码:配套仓库为 runAgent 加入可选的扩展思考配置,并演示思考块的原样保管(src/agent.ts)。git checkout article-17。

本页目录