feat(cot): 思考气泡纳入中间叙述,并为无思考的 turn 补占位节点 - #1311
Conversation
Claude Code 默认关闭 extended thinking,一个 turn 常常一条 thinking 都没有: 转写里只有 text 块与工具调用。而 extractCotEntries 只认 thinking/tool_use/ tool_result,text 块被整体丢弃,气泡于是渲染成一排孤立的工具节点——既没有 可读的开场,工具节点也因为 lastReasoningId 未设置而挂不到任何 reasoning 父节点上。 两处修改: - CotEntry 新增 `text` kind,承载模型在工具之间写给用户的旁白。它与 `thinking` 在气泡里同为 reasoning 段落,但协议上分开:一是让「模型在想」 与「模型在说」可区分,二是占位判据要靠它。转写是流式消费的,「这是不是 最后一段」在提取时无从判断,因此收尾的最终答案也会进气泡尾部——这比静默 丢掉每一句中间叙述便宜得多。 - turn 首个 entry 就是工具调用时,先补一个「思考中…」占位 reasoning 节点。 一次修好两件事:气泡有了开场,后续工具节点也有了父节点。判据就是 lastReasoningId 未设置,因此真实 thinking 或旁白领头的 turn 永远看不到它, 同一 turn 内也只会插入一次。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa
|
你好!评审群已自动创建:pr 1311 思考气泡纳入中间叙述补占位节点 但你暂未拉入群中——自动拉群名单里你的 GitHub 账号尚未关联飞书信息。请自行把 GitHub 账号和飞书信息补进名单文档:https://bytedance.larkoffice.com/wiki/WJ1nwWbtxi89erkNGNbcgkt9nUe ,补好后后续复审会自动拉你进群。 这是自动流程,如有疑问请联系维护者。谢谢! |
|
感谢这个 PR,问题定位和动机都写得很清楚——「extended thinking 默认关闭 ⟹ 一整轮没有 验证结果(先说结论:功能本身是对的)
一个建议合入前处理的问题:沉默轮会冒出裸 sentinelbotmux 有个约定:消息不是发给自己时,最终回复只输出 但气泡这条通道没有 sentinel 过滤( 扫了 200 个真实 transcript / 1493 个 turn 统计频率:
合计 219/1493 ≈ 14.7%,不算边角情况。 建议在 一个想请维护者拍板的取舍(不是缺陷)气泡是独立的一条飞书消息、和最终回复卡不同条,所以纯聊天轮(无工具无思考)的气泡内容就是答案原文,用户会把同一句话看两遍。实测线上占比 121/1493 ≈ 8.1%。 你在描述里已经明确写了这是有意取舍(「收尾的最终答案也会进入气泡尾部……这比静默丢掉每一句中间叙述便宜得多」),所以我没把它当缺陷;只是 8.1% 这个量级值不值,属于产品判断,留给维护者定。如果结论是要治,一个不破坏流式前提的方向是:仅当该轮存在至少一个 tool_call 时才把 顺带一提
以上是自动评审的初步意见,最终以维护者审阅为准。除 sentinel 那条外其余都不阻塞,改好后我很乐意重跑一遍验证。 |
更正:我上一条评论里有两处是错的复审阶段实测下来,我前面给的修法和 F2 影响面数字都有问题,在这里更正。抱歉给了错误的方向。 ① 修法更正:不能在
|
| 输入 | 结果 |
|---|---|
BOTMUX_NOTHING_TO_SEND |
"" → 跳过该 entry |
BOTMUX_NO_REPLY(legacy token) |
"" → 跳过 |
改好了。\n\nBOTMUX_NOTHING_TO_SEND |
改好了。(保留 prose) |
| 普通旁白 | 原样保留 |
我打算输出 BOTMUX_NOTHING_TO_SEND 说明用法(inline 提及) |
原样保留,不误杀 |
所以建议的做法是:observeCotEntries 里对 kind === 'text' 的 entry 调 stripTrailingBridgeSentinelLine,结果为空就跳过该 entry,否则用 stripped 后的文本。
② 补一个边界:这个过滤建议挂 !adoptMode
bridge-fallback-gate.ts:30-33 有一条既有规则:
Adopt mode never suppresses: 在
/adopt里被接管的 CLI 不知道 botmux 存在,transcript drain 是它到飞书的唯一通道。
也就是 adopt 会话里模型吐出字面 BOTMUX_NOTHING_TO_SEND 是合法正文,gate 自己在 adopt 模式下就故意不剥。气泡侧的过滤如果不跟着这条走,会把 adopt 会话的真实答案从气泡里抹掉 —— 变成反方向的同类 bug。
③ F2 影响面数字更正:8.1% → 约 2.0%
我先前说「纯聊天轮占 121/1493 ≈ 8.1%」,这个数偏高。我用的口径是 tools == 0,但把口径拆开重数(同 200 个 transcript、1494 轮)后:
| 口径 | 条数 |
|---|---|
tools==0(我先前用的) |
121 (8.1%) |
tools==0 && thinking==0 |
101 (6.8%) |
| 上一行 且 至少有一个非空 text 块 | 30 (2.0%) |
原因:那 101 轮里有 71 轮压根没有任何非空 text 块(典型是「调用了 botmux send 后最终回复为空」那种),它们 entries 为 0、根本不会建气泡,不该算进 F2 影响面。
真实影响面是 约 2%(另一位 reviewer 独立数出 2.3%,同量级)。F2 的结论不变(仍是有意取舍、非阻断),但 2% 和 8% 在「值不值得处理」上是不同量级的判断输入,所以更正一下。
未变的部分
F1(沉默轮冒出裸 sentinel 气泡)的阻断结论和量级不变,且经两人独立统计确认:约 14%(219/1493 与 207/1453 两次独立测算)。前面列的验证结果(tsc 绿、57/57、邻域 243/243、反变异 5 枪全红、占位 id 不撞、60KB 净增 9 轮、codex 侧结构性不受影响)也都不变。
以上仍是自动评审意见,最终以维护者审阅为准。
补充:
|
| 结论 | 依据 | |
|---|---|---|
| F1 沉默轮泄漏裸 sentinel | 建议合入前处理 | 两人独立测算约 14% 的轮次受影响 |
| F2 纯聊天轮答案重复 | 非阻断(产品取舍) | 影响面约 2%,作者已在描述中说明是有意取舍 |
| 修法 | observeCotEntries 对 kind==='text' 调 stripTrailingBridgeSentinelLine,空则跳过;且挂 !adoptMode |
落点与 helper 边界均已实测 |
功能实现本身、测试质量(反变异 5 枪全红)、CI(9/9 绿)、跨 CLI 影响面都没有问题。仍是自动评审意见,最终以维护者审阅为准。
问题
Claude Code 默认关闭 extended thinking,于是一个 turn 常常一条
thinking都没有——转写里只有text块与工具调用。而extractCotEntries只认thinking/tool_use/tool_result,text块被整体丢弃。结果是气泡渲染成一排孤立的工具节点:
lastReasoningId从未被设置,拿不到parentMessageId,全部平铺在顶层。模型在工具之间写给用户的旁白,此前在三个通道里都消失了:CoT 提取跳过它,最终回复卡的
trailingAssistantText只取最后一个tool_use之后的收尾文本(那是有意的,避免 narration collage),流式卡则是终端画面而非结构化消息。改动
1.
CotEntry新增textkind,承载中间叙述。它与thinking在气泡里同为 reasoning 段落,但协议上保持分开:一是让「模型在想」与「模型在说」可区分,二是占位判据要靠它。转写是流式消费的,「这是不是最后一段」在提取时无从判断,因此收尾的最终答案也会进入气泡尾部。这比静默丢掉每一句中间叙述便宜得多,单条不设上限,由 worker 既有的累计上限兜底。空白
text块跳过——空节点在气泡里就是一段空白。2. turn 首个 entry 就是工具调用时,先补一个「思考中…」占位 reasoning 节点。 一次修好两件事:气泡有了开场,后续工具节点也有了父节点。判据就是
lastReasoningId未设置,因此真实 thinking 或旁白领头的 turn 永远看不到它,同一 turn 内也只会插入一次(跨增量更新同样成立)。占位复用 index 0 的 id——恰好在这种情况下它是空闲的,也正是 prologue 的REASONING_START开启该段落时用的 id。测试
test/cot-message.test.ts新增 4 例:叙述条目按转写顺序渲染成 reasoning 节点、纯工具开局插入占位且所有工具节点挂同一父节点、真实 thinking 领头时不插占位、跨增量更新只插一次。test/thinking-transcript.test.ts的两条旧断言写死「text 块不提取」,随行为更新,并补一条空白 text 块被跳过的用例。🤖 Generated with Claude Code
https://claude.ai/code/session_01EXTEzQtcEcQkJzv1gNnxfa