对 Thought 的复盘:从推理引擎到信息提示
我最早把 Thought 看得很重。
原因也简单。它最像“模型真的懂了”。你看一段 Thought,里面有判断、有取舍、有下一步动作,读起来比 tool call 还像脑子本身。tool call 反而像个外壳:名字、参数、schema,都很机械。
后来系统做深了,这个感觉慢慢反过来了。
真正能碰世界的,不是那段写得很像推理的文字,而是后面那条结构化执行链。有没有 schema 校验,有没有审批,有没有沙箱,有没有 call id,对没对上 output,exit code 是多少,失败是谁判的——这些东西才真的决定“事情有没有发生”。
Thought 当然还在,而且很有用。问题不在它有没有用,问题在另一头:它该不该拥有执行权。
我最近把 Claude Code 和 Codex 的相关源码连着看了一遍,最后越看越觉得,这两套系统虽然长得很不一样,但都在做同一件事:把 Thought 从“推理引擎”慢慢降成“信息提示”。
不是删掉它。是把它的权力拿掉。
先别急着夸 Thought,最该先问的是:系统到底听谁的
我后来越来越喜欢拿一个很土的问题当起点:
如果 Thought 里写的是“先不要执行”,但后面已经出来了结构化 tool call,系统到底听谁的?
这个问题很值钱,因为它不讨论理念,直接问控制权。
Claude Code 这边答案很硬。真正触发执行的是 tool_use,入口在 claude-code-main/src/query.ts:829。后面接的是参数校验和权限判断,在 claude-code-main/src/services/tools/toolExecution.ts:599 和 claude-code-main/src/utils/permissions/permissions.ts:1158。连自动权限分类那条链都明确把 assistant text 排除了,在 claude-code-main/src/utils/permissions/yoloClassifier.ts:297。
Codex 也差不多。真正的工具路由走结构化调用,在 codex-src/codex-rs/core/src/tools/router.rs:117。shell/exec 的审批判断看的是结构化请求和策略,在 codex-src/codex-rs/core/src/tools/orchestrator.rs:117 和 codex-src/codex-rs/core/src/exec_policy.rs:226。默认路径里,没有“先读一遍 reasoning,再决定要不要执行”这一步。
这件事一旦看清,后面很多东西就顺了。
系统真正信的不是“模型解释得像不像那么回事”,而是结构化动作有没有过独立控制层。
Thought 可以说很多话,但它不是仲裁者。最多算旁边那张说明纸。
Claude Code 挺能说明问题:它保留 Thought,但并不把它当圣旨
如果只看表面,Claude Code 对 Thought 还挺客气的。
流式返回时,thinking 和 text 都会先进 assistant message,在 claude-code-main/src/services/api/claude.ts:1995。下一轮发 API 前,又会统一走 normalize,在 claude-code-main/src/utils/messages.ts:1989。
看上去像是“认真保存推理过程”。
但你往后看几个边角分支,就会发现味道完全不是那回事。
孤儿的 thinking-only message 会被过滤,在 claude-code-main/src/utils/messages.ts:4991。
最后一条 assistant 多出来的 trailing thinking 会被裁掉,在 claude-code-main/src/utils/messages.ts:4781。
模型或签名不兼容时,带 signature 的 thinking block 也会被移除,在 claude-code-main/src/utils/messages.ts:5061。
这几刀其实很说明态度。
如果 Thought 真是执行语义的一部分,这些地方就该非常保守,因为你一剪可能就把关键因果剪坏了。可现在系统敢这么剪,说明它默认 Thought 不是“必须原样保真的控制材料”,而是一类要被治理的消息内容。
更明显的是 compact。
Claude Code 做 compact 时,不是想办法保留 Thought 原文,而是直接把整段历史改写成 summary。生成 compact 时会要求模型给 <analysis> 和 <summary>,但最后真正塞回上下文的只有 <summary>,<analysis> 会被剥掉,在 claude-code-main/src/services/compact/prompt.ts:306。而且 compact 请求本身还会把 thinking 关掉,在 claude-code-main/src/services/compact/compact.ts:1292。
这一段我看完以后,基本就不太会再把 Thought 想成什么“高于执行链的真推理记录”了。
更像什么?更像上下文材料。
材料可以保留,可以摘要,可以裁剪,可以为了 API 约束换一种长相。
只要别让它直接去开权限就行。
所以 Claude Code 的做法,在我看来不是“不要 Thought”,而是把 Thought 重新塞回 message hygiene 这套体系里。
它有用,但它首先是 message,不是 authority。
Codex 走得更硬一点:reasoning 和执行证据不要混
Codex 这边的味道不太一样。
它也保留 reasoning。流式阶段,reasoning summary/content 会发给前端,在 codex-src/codex-rs/core/src/codex.rs:7256。OutputItemDone 时,ResponseItem::Reasoning 会被记进 turn item 和 history,在 codex-src/codex-rs/core/src/stream_events_utils.rs:203 和 codex-src/codex-rs/core/src/event_mapping.rs:112。下一轮构造请求时,history 也会继续把这些东西带进去,在 codex-src/codex-rs/core/src/context_manager/history.rs:112。
所以它也没把 Thought 扔掉。
但它特别明显的一点是:执行证据和 reasoning 不是一个待遇。
最典型的是 FunctionCallOutput。
FunctionCallOutput 有严格配对要求。缺失会补,孤儿会删,在 codex-src/codex-rs/core/src/context_manager/normalize.rs:14 和 codex-src/codex-rs/core/src/context_manager/normalize.rs:122。进 history 时,还会走专门的 payload 截断,在 codex-src/codex-rs/core/src/context_manager/history.rs:353。
reasoning 就没这套规格。它会被记录,会被展示,会影响下一轮 prompt,但它不是那条被系统重点校正、重点配对、重点维护的执行证据链。
成功失败的判定权也不在它手里。
exec 类工具最后是不是成功,系统看的是执行器侧状态,比如 exit_code == 0,在 codex-src/codex-rs/core/src/tools/events.rs:311。sandbox denied、timeout、审批拒绝,也都是系统统一映射成失败输出,在 codex-src/codex-rs/core/src/tools/events.rs:323。
这套设计说白了就一句:你可以写 reasoning,但系统状态不跟着你的口头说法走。
这也是我觉得 Codex 挺清醒的地方。
它没有去证明 reasoning 一定 faithful。它只是先把更重要的事做了:别让 reasoning 和 execution evidence 混在一起。
真正的降权,不是删掉 Thought,是系统已经敢改写它了
我现在判断一个系统有没有真的把 Thought 降权,看的不是“它还在不在”,而是另一件事:
系统敢不敢改写它。
敢,基本就说明它已经不是最高控制语义了。
Claude Code 就很典型。历史 compact 以后,原始 Thought 不再是必须保留的东西,系统保的是 summary。
Codex 也一样,compaction 会重写历史,remote compact 甚至会明确过滤掉 ResponseItem::Reasoning,在 codex-src/codex-rs/core/src/compact_remote.rs:205。
如果你真的把 Thought 当成“系统最终信任的推理证据”,这一步会让人很不安。因为证据被改写了。
但如果你换个看法,这事就一下顺了:
可见 Thought 从一开始就不是系统最后信的那层东西,它只是对模型、对开发者、对调试流程都还有用的一种信息提示。
提示可以改写。
执行证据不行。
这是两回事。
我现在反而不太关心“这段 Thought 到底是不是模型真实内部推理的忠实映射”这种问题了。这个问题当然重要,但那更像模型解释性问题,不该再是执行边界问题。
真正该守住的是另一句更工程的话:
就算这段 Thought 没那么忠实,系统也不能因为它直接出事。
问题是,事情做到这一步还不算完。
因为系统虽然不再拿 Thought 直接开门,却还是会在别的地方继续消费它。
问题没这么干净,Thought 的直接权力是没了,间接影响还在
如果文章写到这里就收住,味道会有点假。
前面一直在讲降权、边界、执行权回收,读起来很容易形成一种印象:行,Thought 已经被请出控制层了,事情基本处理完了。
真实系统没这么利索。
Thought 的直接执行权确实被拿掉了,但它没有真正退出。它只是从正门下来了,开始从别的地方回渗。
我现在越来越不把零信任边界想成一堵密封墙。更像两圈。
里面那圈比较硬,Thought 不能直接触发工具,不能直接拿权限,不能替代结构化执行证据。
外面那圈软很多,摘要、监控、hook、上下文塑形这些地方,Thought 还在往后续行为里渗。
麻烦就在这儿。
很多风险不是“系统重新信了 Thought”,而是系统为了效率、可观测性和扩展性,又把它一点点接了回来。
先看 Claude Code 这边最典型的一块:compact summary。
compact summary 会进入下一轮上下文,而且是作为 user message 进去的,在 claude-code-main/src/services/compact/compact.ts:614。这句话表面上只是“摘要继续参与后续生成”,但工程上它做的事更重:它在重写模型记住这段历史的方式。
原始历史里其实有什么?有犹豫,有分叉,有试错,有没想清楚时临时走的一步,也有“先这么干,等会再看”的脏痕迹。compact 一做,这些东西会被压成一段更短、更顺、更像已经想明白了的 summary。
真正容易把系统带偏的,往往不是 summary 明显写错。
更常见的是它把原来那种没定下来的状态抹平了。
比如一轮里模型其实是在 A、B 两个方向之间来回试,最后只是因为上下文预算、工具返回质量或者当时的局部判断,先临时走了 A。compact 以后,summary 很可能会长成另一种味道:前面已经确认 A 更合理,后面继续沿 A 往下走。
这里最麻烦的地方,不是事实错了,而是叙事稳定下来了。
下一轮模型接到的,不再是“这里其实犹豫过、试过、还没彻底定”,而是“前面已经形成了一个比较稳定的理解”。这样做的好处当然很实际:上下文更省,系统更连续,重复推理更少。
代价也很实际:系统会更一致,但也会更难回头。
所以我现在会把 compact 看成一个很重的动作。
它不是单纯压 token,它是在做一次历史重解释。压掉的很多时候不是细节,而是分叉感、犹豫感、未决状态。
Claude Code 自己也知道这里只靠 summary 不够。一个很小但很说明问题的细节是,summary 不保留 tool_reference,所以系统还得额外补 preCompactDiscoveredTools 这类东西,在 claude-code-main/src/services/compact/compact.ts:603。
我很喜欢这种代码细节,因为它不靠概念说服你。它直接告诉你:
summary 会塑形后续行为,但 summary 又不够可靠,所以某些执行相关的信息不能只靠它带着走,得另外补。
说白了,compact summary 最危险的地方,不是它写错了,而是它太像已经写对了。
再看 Codex 这边。
默认执行路径不读 reasoning 文本,这条边界其实挺硬。
但 ARC monitor 会拿最后一条非空 encrypted_reasoning 作为输入之一,在 codex-src/codex-rs/core/src/arc_monitor.rs:284。后面的 MCP auto-approve 又会消费 ARC 结果,可能把动作变成 AskUser 或 SteerModel,在 codex-src/codex-rs/core/src/mcp_tool_call.rs:485。
这里一下就出现张力了。
前面系统刚刚把 Thought 从直接执行链里降出去,后面风控又把它接回来当输入。不是因为没人知道 reasoning 不可靠,而是完全不用它,很多高层语义信号又确实看不见。
这就是灰区最难受的地方。
你不看 reasoning,风控会少一层“它现在大概在往哪走”的信号。
你一旦看 reasoning,又等于把那个本来不 fully trusted 的东西重新接回判断链。
这里我会更愿意用“利用”而不是“信任”这个词。
系统不是在信 reasoning,系统是在利用它补一层语义判断。可一旦开始利用,它就自动变成潜在攻击面。
攻击面不一定表现成特别戏剧化的提示注入。更常见的反而是很土的系统偏移。
比如模型写出一段对风控更“无害”的 reasoning,但真正的动作倾向没怎么变。
再比如,开发者慢慢开始把 ARC 的输出理解成“模型意图已经被系统二次理解过了”,于是对这条旁路产生过度信任。
这两种偏移都很烦。第一种是在跟风控玩表述,第二种是在组织里偷偷长出新的信任幻觉。
明面上大家都知道主执行链不信 Thought,结果旁路又在给 Thought 补地位。
hook 这块更像真实工程现场。
Claude Code 的 post-sampling hooks 能读完整消息历史,在 claude-code-main/src/utils/hooks/postSamplingHooks.ts:45。Codex 的 stop hook 会收到 last_assistant_message,在 codex-src/codex-rs/core/src/codex.rs:5752。
这类地方最容易出的问题,不只是“能读到 Thought,所以有泄露风险”。那当然也是风险,但还不是最典型的坑。
更典型的坑是,开发者会很自然地把 Thought 当成比结构化动作更早、更真实的意图信号。
这种错特别自然,因为它太像工程优化了。
有人会想:模型在 Thought 里都说自己不确定了,那我先别让它继续。
有人会想:它在 Thought 里提到了某个目标,我先替它预取一点资源。
也有人会想:它既然都把原因解释这么完整了,这次报错大概不是乱来,自动重试一下。
每一条听起来都不像胡来,甚至有点聪明。
问题是,这些判断都在做同一件事:把 Thought 从信息层偷偷提回决策层。
所以 hook 真正危险的地方,不只是它能看见什么,而是开发者会不会借着它,重新发明一条非正式的信任链。
主系统已经很克制了,不让 Thought 直接开门。
扩展层一句 if reasoning contains ...,门边上又被人凿了个洞。
这也是为什么我越来越不想把灰区写成“还有一些边角要继续盯”。
不够。
更接近真实的说法应该是:Thought 的直接权力已经被拿掉了,但它会以摘要、监控、hook、上下文塑形这些形式重新渗回系统。
零信任做到这里,真正难的已经不是“要不要给 Thought 降权”,而是你把它从正门请走以后,怎么防它从侧门再回来。
两套系统都在降权,但落点不一样
灰区摊开以后,再回头看 Claude Code 和 Codex,差别会更清楚。
它们都在降权,也都没把边界做成完全密封。
但它们最先收紧的地方不一样,这会直接影响灰区长在什么位置上。
Claude Code 更像是在防 Thought 把消息边界弄脏。
它保留 Thought,但会 normalize。
会过滤孤儿 thinking,裁 trailing thinking,移除不兼容的 signature-bearing blocks。
会 compact,而且明确只留 <summary>,不留 <analysis>。
用户做权限确认时,看到的也是结构化工具动作,不是模型那段脑内旁白。
它最先解决的是一个很消息层的问题:
这段东西怎么存,怎么裁,怎么压,怎么展示,怎么不把下一轮上下文拖坏。
Codex 更像是在防 Thought 混进执行证据链。
它让 reasoning 留在 history 和 UI 里,但工具路由只认结构化 call。
成功失败由执行器、审批、sandbox 定。
FunctionCallOutput 有严格配对和规范化,reasoning 没这个待遇。
reasoning 可以去 ARC、去 compaction、去 hook,但不要和 execution evidence 混在一块。
它最先解决的是另一个问题:
系统到底该信哪条链。
一个偏 message hygiene,一个偏 execution boundary。
方法很不一样,但主线很一致:Thought 可以继续存在,但别再让它直接开门。
我现在更愿意把 Thought 看成备注,不是钥匙
写到这一步,我对 Thought 的看法其实没变得更冷,反而是更实用了。
我不会说它不重要。
调试时它很重要,查偏航时很重要,做 summary、做监控、做用户态解释时也很重要。少了这层,很多问题会更难排。
但我现在确实不太愿意再把它叫成“推理引擎”。
至少从系统设计上看,这个叫法太容易让人产生误解。
一旦你把“推理”听成“授权”,边界就松了。
更稳一点的理解是:Thought 是信息提示层。
它告诉系统和开发者,模型大概在看什么,偏向哪条路,刚才为什么犹豫,接下来可能想干什么。
这些信息很值钱。
可最后真能把动作送进真实世界的,不能是这些话。
应该是那条独立的、结构化的、可校验的执行链。
所以我现在脑子里对 Thought 的画面很简单。
它不是钥匙。
它更像钥匙旁边那张备注纸。
备注纸挺有用,上面可能写着:“这门后面是机房,进去前先断电。”“上次有人从这边进去,出了事故。”你当然应该看,很多时候不看还真会走弯路。
但最后能开门的,不该是那张纸。