Claude Opus 4.7 思考过程不见了——一次从现象到全平台修复的踩坑记录
升到 Opus 4.7 之后,思考过程没了。不是变短,不是折叠,是肉眼一片空白。VSCode 插件没有,终端 claude 没有,我自己写的小工具也没有。三端齐崩,搞了大半天才把链路完整摸清楚——这不是单一 bug,是 API 默认行为变化 + 客户端实现遗漏两层叠加的结果。
记录一下定位过程和最终能跨端见效的修复。
现象
VSCode 插件里聊天,以前 Opus 4.6 时代每条助手回复上面会有一个可展开的”思考”小条,点开能看到模型的推理摘要。换到 Opus 4.7 之后这个小条整个消失——没折叠,没”已思考 N 秒”,就是不存在。
终端跑 claude --print "..." 也一样,stream-json 输出里看不到 thinking 块的内容。
最初我以为是某个客户端版本问题,升级到当时最新的 VSCode 插件,没用。
诊断路径
第一反应:看 jsonl,排除”客户端不渲染”
Claude Code 把每条会话写到 ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl,每行一条记录。我直接 grep "type":"thinking" 看最近的会话:
grep '"type":"thinking"' ~/.claude/projects/<latest-session>.jsonl | tail -1输出:
{ "type": "thinking", "thinking": "", "signature": "<很长的 base64 字符串>"}thinking 字段是空字符串,只剩 signature。这说明 thinking 块在协议层仍然存在,但明文内容根本没下发——客户端再怎么渲染都是空。问题不在客户端。
第二步:扫所有历史会话找 cutover 时间
既然内容是服务端就清空的,那一定有个切换时间点。我写了个 Python 一把梭:
import json, glob, osfrom collections import defaultdict
plain = defaultdict(list)redacted = defaultdict(list)
for f in glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')): with open(f) as fh: for line in fh: if '"type":"thinking"' not in line: continue rec = json.loads(line) if rec.get('type') != 'assistant': continue model = rec.get('message', {}).get('model') or 'unknown' ts = rec.get('timestamp', '') for b in rec.get('message', {}).get('content', []): if b.get('type') != 'thinking': continue if len(b.get('thinking', '')) > 0: plain[model].append(ts) else: redacted[model].append(ts)跑出来按 model 分组:
Opus 4.6 明文 1778 条(02-28 ~ 04-18) + redacted 46 条(04-02 ~ 04-11)Opus 4.7 明文 0 条 + redacted 3154 条(04-17 起)Sonnet 4.5 明文 374 条 + redacted 0清晰得不能再清晰:
- 2026-04-02
4.6 首次出现 redacted,但跟明文混存了 16 天(灰度期) - 2026-04-17 起
4.7 100% redacted,从 GA 第二天就这样 - Sonnet 4.5 完全没受影响
第三步:反汇编 claude binary 找控制开关
到这一步问题已经清楚了
claude --help 里没有任何 thinking 相关参数。但 binary 里一定有判定逻辑——我直接 strings 抓,grep thinking:
strings $(which claude) | grep -E 'thinking|display' | grep -E 'summarized|omitted'抓到几个关键片段(从 minified JS 里挖出来的):
// 模型字段定义{ type: N.literal("adaptive"), display: N.enum(["summarized", "omitted"]).optional() }
// 控制流if (w.thinkingDisplay === "summarized" || w.thinkingDisplay === "omitted") H5.display = w.thinkingDisplay // CLI flag 优先级最高else if (!S8() && u8().showThinkingSummaries === !0) H5.display = "summarized" // settings 次之
// 文档片段"Set `thinking.display` to `\"summarized\"` to restore visible progress"逻辑很清晰:有两条 opt-in 路径,默认 omitted(Opus 4.7),要拿到摘要要么传 CLI flag 要么开 settings。
但事情没那么简单——u8().showThinkingSummaries === !0 那条前面还有个 !S8() 判断,具体是什么我没继续读源码。我的猜测是这条判断在过滤”—print 模式 / 子进程模式 / 非 TTY”,这种模式下 settings 被跳过——但这只是看代码结构推的,没验证过。如果你严格知道 S8() 是什么,欢迎纠正。
根因(双重叠加)
一 的 API 默认变化
这是设计决策,不是 bug:
- 2026-02-12 起
开始灰度 redact-thinking-2026-02-12,部分 thinking 块被替换为redacted_thinking(安全审查触发) - 2026-04-16 Opus 4.7 GA:
thinking.display默认值改为"omitted"——这是 breaking change,但没有错误提示,响应延迟反而稍快,所以很多人压根没意识到
理由 Anthropic 没明说,但行业一般推断是防 prompt 提取 / 防 chain-of-thought 蒸馏到竞品 / 防越狱(thinking 里能看到模型的”自我审查”逻辑)。
二 插件的实现 bug
claude-code issue #49902 给出了 VSCode 插件层面的根因——这一段我直接引用 issue 评论里反汇编的发现:
// resources/native-binary/claude 里 model-picker 的 switchelse if (_ === "opus" && Rz()) return UVH([...q, Sl7(!1)]);else if (_ === "opus[1m]" && Rz()) return UVH([...q, sVq(!1)]);else if (_ === "claude-opus-4-6" && Rz()) return UVH([...q, El7(H, !1)]);else if (_ === "claude-opus-4-6[1m]" && Rz()) return UVH([...q, hl7(H, !1)]);else { /* 落到 Custom model 分支,剥离 thinking 能力 metadata */ }没有 claude-opus-4-7 也没有 claude-opus-4-7[1m] 分支。新模型落到 Custom model 兜底路径,thinking 能力 metadata 在这一层就被剥掉了——所以即使你设置了 showThinkingSummaries: true,VSCode 插件这一层也根本不会去渲染。
binary 内 claude-opus-4-6[1m]: 6 处出现binary 内 claude-opus-4-7[1m]: 0 处出现issue 评论里有人确认,即使升级到 2.1.126(目前最新),问题依旧存在,issue 还是 OPEN。
三层 workaround,从浅到深
A. Shell wrapper(只覆盖终端)
最简单,往 ~/.zshrc 加一个 function:
claude() { command claude --thinking-display summarized "$@"; }--thinking-display 是 binary 里有但 --help 不显示的隐藏 flag(代码里 .hideHelp() 标的)。这条 function 让你以后跑 claude xxx 都默认带 flag。
局限
B. Settings.json 加 showThinkingSummaries
{ "showThinkingSummaries": true}写到 ~/.claude/settings.json,理论上 binary 里那个判定(u8().showThinkingSummaries === true)会读到。
局限:前面提的 !S8() 那个未知判断很可能在 --print 模式下跳过 settings——我观察到改了之后终端 claude --print 仍然没明文,说明这条路径可能不是全场景生效。但 VSCode 插件的实现 bug 让这条 settings 也无效——它压根不到判定层就被 model-picker 剥光了。
C. CLAUDE_CODE_EXTRA_BODY env 注入(推荐,跨端)
issue 评论里挖到的真招——往 ~/.claude/settings.json 的 env 字段塞:
{ "env": { "CLAUDE_CODE_EXTRA_BODY": "{\"thinking\":{\"type\":\"adaptive\",\"display\":\"summarized\"}}" }}这个 env 变量被 CLI 在构造 API request 时直接合并到 body——绕过客户端的 model-picker、绕过 settings 判定、绕过所有客户端层的 bug,直接告诉 Anthropic API “我要 summarized”。
实测三端都生效
唯一前提:改完后所有正在运行的 Claude Code 客户端都要重启(env 在进程启动时读),否则旧进程仍是改之前的状态。
验证生效
改完之后发一条触发推理的消息,然后扫 jsonl:
JSONL=$(ls -t ~/.claude/projects/*/*.jsonl | head -1)grep '"type":"thinking"' "$JSONL" | tail -1 | \ python3 -c "import sys, jsonb = json.loads(sys.stdin.read()).get('message', {}).get('content', [])[0]print('thinking 字段长度:', len(b.get('thinking', '')))"输出 0 → 没生效;输出非零数字 → 拿到了摘要,客户端这下能渲染了。
不要只看 UI,UI 没渲染≠数据没下发。先看 jsonl 才能区分”是 API 没给”还是”客户端没渲染”。
通用经验
第一,API 默认变化是最难定位的一类 bug。客户端没动你的代码、文档没大字标红、错误也不报——只是某个字段悄悄从有内容变成了空字符串。这种”默默 breaking”的回归,只能靠跨版本对比历史数据(jsonl/日志)反推时间线。
第二,客户端 bug + API 默认双重叠加是最毒的组合。任何一边的修复单独都不够。我一开始只盯着 settings 和 CLI flag,以为是 opt-in 不够,但 VSCode 插件那一层 model-picker 直接剥掉 metadata,settings 设了也白设。
第三,反汇编 minified binary 是 power user 的最后手段。变量名是 S8、u8、H5 这种乱码,但字符串常量、enum 值、错误消息文本是清晰的——thinking_display、summarized、omitted、adaptive 这些字面量全在 strings 里,顺着这些常量的上下文反推控制流,基本能把开关挖出来。
第四,厂商隐藏接口往往是真正的开关。--thinking-display 是 hideHelp() 的;CLAUDE_CODE_EXTRA_BODY 没在任何官方 docs 出现,只在 binary strings 和社区 issue 里能挖到。--help 看不到的不代表不存在,反过来,--help 显示的 flag 反而经常是已废弃的兼容路径。
第五,jsonl/日志是最朴素也最强的诊断工具。比官方 changelog 准、比错误消息准、比客户端 UI 准——它就是模型实际给你回了什么的字面记录。任何一次”行为变了但说不清楚为啥变了”的诊断,第一步都应该是去 grep 历史日志找时间分布。