Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
13 KiB
13 KiB
TASK · 证据轮模型无正文被判整轮失败:证据与下一问都已落库,却只剩"没有拿到下一个问题";答题旁白只会说"范围没变"(2026-09-10)
- 基线:
origin/staging@719ff55a(BUG-631/632 已合入并部署) - 分支:
codex/rectification-evidence-turn-empty-answer-20260910,基于origin/staging - 串行:在
TASK-rectification-skipped-health-deadend-20260909.md(BUG-626/627,改answer-choice.ts/turn-exit.ts/ 客户端"重新加载")之后开工;若那份尚未领取,本单先做,那份的决策 5 改为在本单的客户端改动上叠加。 - 执行方:coding agent;验收:Claude
- 涉及文件:
frontend/src/lib/rectification-agentic/v9/agent-run.ts(RETRYABLE_ERROR_CODESL144、streamAttempt收尾 L960–995、成功收尾 L560–620)、v9/run-diagnostic.ts、v9/answer-choice.ts(composeChoiceNarration调用 L551/582/603)、v9/choice-action.ts(composeChoiceNarrationL98–123)、v9/probe-explain.ts(explainScoreMovementL215)、frontend/src/components/rectification-agentic-chat.tsx(run.failed分支 L888、失败收尾 L970–985、liveQuestionOnMessagesL1474、questionGapL1486、RectificationReadonlyRangeL178–187、输入框占位 L1776–1782)、frontend/src/lib/rectification-surface-state.ts(rectificationQuestionGapStateL278) - BUG 编号起点:BUG-633(
docs/BUG_HISTORY.md当前最大 BUG-632) - 优先级:BUG-633 P1(证据轮直接断流,用户只能刷新或新建);BUG-634 P2(五题答完每次都是"范围没变",用户以为坏了)
1. 事故实证(staging 2026-09-10 10:04 +0800,Skill 10.0.21;只写结构)
顺序:开场 → 学业两件 → 感情两件 → 三道带年月选择题(事业 / 迁居 / 事业)→ D9 风格题 → 家人题「记不得」→ 财务一件 → 事业两件 → D10 风格题 → 迁居采集题 → 用户答"某年某月搬到某市" → 用户气泡下面直接是"没有拿到下一个问题。",再下面是"本轮没有生成可展示的回复,状态已记录。"
Case JSON 里这一轮:
| 项 | 值 |
|---|---|
用户 turn status |
failed;phases = run.started, skill.bound, case.loaded, intent.classified, evidence.proposed, run.failed;没有 answer.composed |
tool_activities |
rectification-read-case completed、rectification-record-evidence-batch completed(22 073 ms,13 个方法);没有 rectification-set-focus |
evidence |
迁居事件已 confirmed,sourceTurnId = 这一轮 |
latest_result |
新的 resultId,createdAt 在 batch 结束同一秒;inference_state 5 轮答题全部重放(BUG-587/594 正常) |
current_question |
collect:health_pressure:collect_method_evidence,kind=collect_spoken,question_source=focus——下一问已经落库 |
turns[].question |
这一轮为 null(没有 assistant 行可挂) |
| 客户端 | 失败消息文案对应 run-diagnostic.ts 的 empty_stream;问题缺口显示"没有拿到下一个问题"+"重新加载";输入框占位是"请回答上面的问题…",而上面没有问题 |
2. 根因
2.1 BUG-633(P1):模型在 batch 之后没写正文,服务端把整轮判失败,不回捞已落库的东西
record-evidence-batch工具内部已完成:写证据 → 重算(带previous重放)→persistPlanFocus落下健康采集焦点(askedTurnId= 本轮)。工具返回后模型既没调set-focus写spokenPrompt,也没写"记下了:…"一句,直接以stop结束。streamAttemptL993!answerText.trim()→empty_stream。empty_stream不在RETRYABLE_ERROR_CODES(fe87a9ec于 2026-08-24 移除,原因是重试会重放证据)。于是finalizeTurn("failed", null, …),不计费,发run.failed。- 失败路径不经过
finalizeSuccessfulTurnExit/ensureNonTerminalTurnExit(路由 L899 只在result.ok时调),也不会用主持人旁白把 batch 已返回的事件复述写成 host turn。证据、评分、焦点三样都已提交,唯独用户看不到。 - 客户端
run.failed后不调loadCaseSnapshot(L975–990 只在succeeded时刷新);即便刷新,liveQuestionOnMessages要求某条 settled assistant 消息带同focus_id的question,失败轮没有 assistant 行 →questionGap走preparing→ 两次轮询 →unavailable。"重新加载"只是再取一次快照,结果相同。整页刷新同理。 - 模型为什么不写正文,staging 没有本轮的 run diagnostic 可查(
RectificationRunDiagnostic只在内存,未落库),无法从代码侧确定;前三轮同形状(read-case → batch → set-focus → 正文)都成功。已知的候选原因:thinking 打开后模型把承接句只写进reasoning-delta(BUG-359 的镜像);或模型把正文写在调 batch 的同一 step 里,step-answer.ts在非 set-focus 的公开工具调用时retractLive丢掉了它。两种情况都不该让用户承担。
2.2 BUG-634(P2):答题旁白只会说"范围没变",时间线说"还在收窄",两句都没有信息
本例五道选择题答完,旁白五次"已记录,范围没变。",时间线一直"目前范围 04:45–05:15,还在收窄"。算术上这是正确的,但不是用户能理解的:
| 簇 | 答题前 | 答题后 | 强冲突数 |
|---|---|---|---|
| 04:53 | 11 | 15 | 1 |
| 04:50 | 9 | 11 | 1 |
| 05:00 | 13 | 11 | 2 |
| 04:54 | 10 | 10 | 2 |
| 05:06 | 12 | 10 | 2 |
| 05:15 | 10 | 10 | 1 |
| 04:47 | 11 | 9 | 2 |
| 05:08 | 12 | 8 | 2 |
| 05:14 | 12 | 8 | 2 |
- 可信区间规则(
credible-range.ts::unionStillValidRange):落后峰值不足MIN_SEPARATION_LEAD = 8的簇都保留。峰 15,最低 8,差 7,九个簇全留 → 区间等于整个搜索窗。 - 淘汰规则(
apply-probe-outcome.ts):STRONG_CONFLICT_ELIMINATION_COUNT = 3;带年月题 ±2,性格题 ±1 且不计冲突。三道带年月题的答案两两指向不同区段(第一题支持 04:48–04:59+05:15,第二题支持 04:45–04:53,第三题支持 05:00–05:15),没有任何一分钟同时满足三题,每簇最多 2 次冲突,谁也淘汰不了。 - 引擎给的带年月探针一共只有这三道,全部答完;其余全是无年月的分盘对照题(
yearless_ungrounded_contrast,BUG-629 已降级)。从此刻起,选择题已经不可能再收窄区间,只有新的带年月经历能改变分数。 这不是 bug,是数据本身分不开,但产品必须把这件事说出来。 choice-action.ts::composeChoiceNarration收了deltasByCluster,正文却只用explainRangeChange;probe-explain.ts::explainScoreMovement能生成"04:52–04:53 领先,05:08–05:12 落后",从没被说出来。RectificationReadonlyRange的"还在收窄"是写死的。
3. 决策记录(产品负责人 2026-09-10 授权诊断;以下为 Claude 建议,执行前默认生效,用户可否决)
- 证据轮"模型无正文"不是失败。 只要本 attempt 内
rectification-record-evidence-batch已completed(toolTerminalStatus),empty_stream/max_steps/answer_truncated三种收尾都改走主持人兜底:服务端用 batch 已返回的事件复述(display_date_label+ 事件短语)组成"记下了:…。",作为本轮 assistant 正文finalizeTurn("completed", …),正常结算计费(评分和下一问都已经算出来了),再走finalizeSuccessfulTurnExit。焦点已由工具落库,askedTurnId就是本轮,问题自然挂上。 - 没有任何公开写工具完成时,
empty_stream允许重试一次。RETRYABLE_ERROR_CODES不整体放开;在streamAttempt收尾处判断"本 attempt 没有 batch / set-focus / compare 任何一个 completed"才返回retryable,否则维持failed。这样不会重放证据(fe87a9ec的顾虑仍然成立)。 - 失败也刷新快照,问题不靠消息承载。 客户端
run.failed分支之后照样loadCaseSnapshot;rectificationQuestionGapState新增输入questionPersisted(current_question非空且question_source === "focus"):为真时不进preparing / unavailable,改渲染一条主持人问题行(复用 collect_spoken 的问题段样式,focus_id取current_question),输入框占位才允许写"请回答上面的问题…"。这一条同时替代 BUG-626 决策 5 里"修复"按钮的大半场景;那份任务书的repair-exit路由仍保留给"穷尽且无载体"的情况。 - 答题旁白必须带进度。
composeChoiceNarration在appliedInference且范围不变时,输出explainScoreMovement(deltasByCluster)的领先/落后句,格式"已记录,范围没变;04:52–04:53 领先,05:08–05:12 落后。";连领先/落后都没有(全零)才只说"范围没变"。 - 时间线不得写死"还在收窄"。 文案由快照决定:
discriminating_event_probes为空且candidate_contrast_packet.probes里没有带年月项 → "目前范围 X–Y,选择题已问完,再补带年月的经历才会变";否则 "目前范围 X–Y,还在核对"。不写"收窄"。 RectificationRunDiagnostic落日志。 每个 attempt 收尾把 diagnostic(finishReason、stepCount、lastCompletedTool、stateMutationCommitted、tokens)写一条console.infoJSON,便于下次从 staging 容器日志定位模型侧原因。不落库、不加迁移。- 不动采用门、确认门、
MIN_SEPARATION_LEAD、STRONG_CONFLICT_ELIMINATION_COUNT、PROBE_WEIGHT、minute_step=1指纹;Skill 不 bump。
4. 硬红线
- 主持人兜底正文只能来自 batch 工具的返回(事件复述),不得让服务端另起模型调用补写,不得引用用户原话之外的年份。
- 兜底轮的 receipt 必须能看出是兜底:
phases加answer.host_fallback,公开 receipt 里answer_origin = "host_fallback"(message-origin.ts已有 origin 枚举,扩一个值)。 - 重试只在"零公开写工具完成"时发生;有任何一个写工具完成而重试,即视为 BUG-186 复发。
- 不得回到"问题靠正则从正文猜"的路子;主持人问题行的
focus_id只能来自current_question。 - 时间线与旁白的数字只来自快照 / receipt,不得自算。
5. 任务分解
5.1 BUG-633 服务端(决策 1、2、6)
- 验收(
frontend/tests/rectification-v9-stream.test.ts扩):(a) 模拟 stream:read-case → batch completed(返回两件事件复述)→finish(stop)无 text → 结果ok=true,assistant 正文等于"记下了:<复述>。",phases含answer.host_fallback,计费complete被调用一次;(b) 同形状但 batch 未调用 → 第一 attemptretryable,第二 attempt 正常;(c) batch completed 后finish(length)→ 同 (a);(d) 现有三条errorCode === "empty_stream"断言逐条写"原值 / 新值 / 原因"。 rectification-agentic-entry.test.tsL273 的源码断言按新路径改写,说明三栏。
5.2 BUG-633 客户端(决策 3)
- 验收:
rectification-surface-state用例——questionPersisted=true、无消息承载 → 状态persisted_question,不是preparing;组件合同——失败轮之后快照带current_question(kind=collect_spoken)时,DOM 出现主持人问题行且输入框占位为"请回答上面的问题…";快照无问题时占位不得写"上面的问题"。
5.3 BUG-634(决策 4、5)
- 验收:
composeChoiceNarration用例——范围不变、deltas 非零 → 含"领先";全零 → "已记录,范围没变。";RectificationReadonlyRange合同——探针空时文案含"选择题已问完",否则含"还在核对",两种都不含"收窄"。 agent-voice-copy-contract把"还在收窄"列入禁用词。
5.4 记录
docs/BUG_HISTORY.mdBUG-633(关联 BUG-186、BUG-359、BUG-558、BUG-627;说明fe87a9ec为何把empty_stream移出可重试,以及本次为何只在零写工具时放回)、BUG-634(关联 BUG-593、BUG-629);CHANGELOG.md;PROGRESS-rectification-evidence-turn-empty-answer-20260910.md;docs/testing/rectification-scenarios-20260907.md加"证据轮无正文时必须出现『记下了』兜底句和下一问"。
6. 让步顺序
5.1 决策 1 先做、当天部署;5.2 必做;5.1 决策 2 与 5.3 可后置一天;5.4 不可省。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-evidence-turn-empty-answer-20260910 .worktrees/rectification-evidence-turn-empty-answer-20260910 origin/staging
cd .worktrees/rectification-evidence-turn-empty-answer-20260910
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"