Files
Jyotisha/docs/tasks/TASK-rectification-evidence-turn-empty-answer-20260910.md
T

13 KiB
Raw Blame History

TASK · 证据轮模型无正文被判整轮失败:证据与下一问都已落库,却只剩"没有拿到下一个问题";答题旁白只会说"范围没变"2026-09-10

  • 基线:origin/staging @ 719ff55aBUG-631/632 已合入并部署)
  • 分支:codex/rectification-evidence-turn-empty-answer-20260910,基于 origin/staging
  • 串行:在 TASK-rectification-skipped-health-deadend-20260909.mdBUG-626/627,改 answer-choice.ts / turn-exit.ts / 客户端"重新加载")之后开工;若那份尚未领取,本单先做,那份的决策 5 改为在本单的客户端改动上叠加。
  • 执行方:coding agent;验收:Claude
  • 涉及文件:frontend/src/lib/rectification-agentic/v9/agent-run.tsRETRYABLE_ERROR_CODES L144、streamAttempt 收尾 L960995、成功收尾 L560620)、v9/run-diagnostic.tsv9/answer-choice.tscomposeChoiceNarration 调用 L551/582/603)、v9/choice-action.tscomposeChoiceNarration L98123)、v9/probe-explain.tsexplainScoreMovement L215)、frontend/src/components/rectification-agentic-chat.tsxrun.failed 分支 L888、失败收尾 L970985、liveQuestionOnMessages L1474、questionGap L1486、RectificationReadonlyRange L178187、输入框占位 L17761782)、frontend/src/lib/rectification-surface-state.tsrectificationQuestionGapState L278
  • BUG 编号起点:BUG-633docs/BUG_HISTORY.md 当前最大 BUG-632
  • 优先级:BUG-633 P1(证据轮直接断流,用户只能刷新或新建);BUG-634 P2(五题答完每次都是"范围没变",用户以为坏了)

1. 事故实证(staging 2026-09-10 10:04 +0800Skill 10.0.21;只写结构)

顺序:开场 → 学业两件 → 感情两件 → 三道带年月选择题(事业 / 迁居 / 事业)→ D9 风格题 → 家人题「记不得」→ 财务一件 → 事业两件 → D10 风格题 → 迁居采集题 → 用户答"某年某月搬到某市" → 用户气泡下面直接是"没有拿到下一个问题。",再下面是"本轮没有生成可展示的回复,状态已记录。"

Case JSON 里这一轮:

用户 turn status failedphases = run.started, skill.bound, case.loaded, intent.classified, evidence.proposed, run.failed;没有 answer.composed
tool_activities rectification-read-case completed、rectification-record-evidence-batch completed22 073 ms13 个方法);没有 rectification-set-focus
evidence 迁居事件已 confirmedsourceTurnId = 这一轮
latest_result 新的 resultIdcreatedAt 在 batch 结束同一秒;inference_state 5 轮答题全部重放(BUG-587/594 正常)
current_question collect:health_pressure:collect_method_evidencekind=collect_spokenquestion_source=focus——下一问已经落库
turns[].question 这一轮为 null(没有 assistant 行可挂)
客户端 失败消息文案对应 run-diagnostic.tsempty_stream;问题缺口显示"没有拿到下一个问题"+"重新加载";输入框占位是"请回答上面的问题…",而上面没有问题

2. 根因

2.1 BUG-633P1):模型在 batch 之后没写正文,服务端把整轮判失败,不回捞已落库的东西

  1. record-evidence-batch 工具内部已完成:写证据 → 重算(带 previous 重放)→ persistPlanFocus 落下健康采集焦点(askedTurnId = 本轮)。工具返回后模型既没调 set-focusspokenPrompt,也没写"记下了:…"一句,直接以 stop 结束。streamAttempt L993 !answerText.trim()empty_stream
  2. empty_stream 不在 RETRYABLE_ERROR_CODESfe87a9ec 于 2026-08-24 移除,原因是重试会重放证据)。于是 finalizeTurn("failed", null, …),不计费,发 run.failed
  3. 失败路径不经过 finalizeSuccessfulTurnExit / ensureNonTerminalTurnExit(路由 L899 只在 result.ok 时调),也不会用主持人旁白把 batch 已返回的事件复述写成 host turn。证据、评分、焦点三样都已提交,唯独用户看不到。
  4. 客户端 run.failed 后不调 loadCaseSnapshotL975990 只在 succeeded 时刷新);即便刷新,liveQuestionOnMessages 要求某条 settled assistant 消息带同 focus_idquestion,失败轮没有 assistant 行 → questionGappreparing → 两次轮询 → unavailable。"重新加载"只是再取一次快照,结果相同。整页刷新同理。
  5. 模型为什么不写正文,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-634P2):答题旁白只会说"范围没变",时间线说"还在收窄",两句都没有信息

本例五道选择题答完,旁白五次"已记录,范围没变。",时间线一直"目前范围 04:4505: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:4504:53,第三题支持 05:00–05:15),没有任何一分钟同时满足三题,每簇最多 2 次冲突,谁也淘汰不了。
  • 引擎给的带年月探针一共只有这三道,全部答完;其余全是无年月的分盘对照题(yearless_ungrounded_contrastBUG-629 已降级)。从此刻起,选择题已经不可能再收窄区间,只有新的带年月经历能改变分数。 这不是 bug,是数据本身分不开,但产品必须把这件事说出来。
  • choice-action.ts::composeChoiceNarration 收了 deltasByCluster,正文却只用 explainRangeChangeprobe-explain.ts::explainScoreMovement 能生成"04:5204:53 领先,05:0805:12 落后",从没被说出来。RectificationReadonlyRange 的"还在收窄"是写死的。

3. 决策记录(产品负责人 2026-09-10 授权诊断;以下为 Claude 建议,执行前默认生效,用户可否决)

  1. 证据轮"模型无正文"不是失败。 只要本 attempt 内 rectification-record-evidence-batchcompletedtoolTerminalStatus),empty_stream / max_steps / answer_truncated 三种收尾都改走主持人兜底:服务端用 batch 已返回的事件复述(display_date_label + 事件短语)组成"记下了:…。",作为本轮 assistant 正文 finalizeTurn("completed", …),正常结算计费(评分和下一问都已经算出来了),再走 finalizeSuccessfulTurnExit。焦点已由工具落库,askedTurnId 就是本轮,问题自然挂上。
  2. 没有任何公开写工具完成时,empty_stream 允许重试一次。 RETRYABLE_ERROR_CODES 不整体放开;在 streamAttempt 收尾处判断"本 attempt 没有 batch / set-focus / compare 任何一个 completed"才返回 retryable,否则维持 failed。这样不会重放证据(fe87a9ec 的顾虑仍然成立)。
  3. 失败也刷新快照,问题不靠消息承载。 客户端 run.failed 分支之后照样 loadCaseSnapshotrectificationQuestionGapState 新增输入 questionPersistedcurrent_question 非空且 question_source === "focus"):为真时不进 preparing / unavailable,改渲染一条主持人问题行(复用 collect_spoken 的问题段样式,focus_idcurrent_question),输入框占位才允许写"请回答上面的问题…"。这一条同时替代 BUG-626 决策 5 里"修复"按钮的大半场景;那份任务书的 repair-exit 路由仍保留给"穷尽且无载体"的情况。
  4. 答题旁白必须带进度。 composeChoiceNarrationappliedInference 且范围不变时,输出 explainScoreMovement(deltasByCluster) 的领先/落后句,格式"已记录,范围没变;04:5204:53 领先,05:0805:12 落后。";连领先/落后都没有(全零)才只说"范围没变"。
  5. 时间线不得写死"还在收窄"。 文案由快照决定:discriminating_event_probes 为空且 candidate_contrast_packet.probes 里没有带年月项 → "目前范围 X–Y,选择题已问完,再补带年月的经历才会变";否则 "目前范围 XY,还在核对"。不写"收窄"。
  6. RectificationRunDiagnostic 落日志。 每个 attempt 收尾把 diagnosticfinishReason、stepCount、lastCompletedTool、stateMutationCommitted、tokens)写一条 console.info JSON,便于下次从 staging 容器日志定位模型侧原因。不落库、不加迁移。
  7. 不动采用门、确认门、MIN_SEPARATION_LEADSTRONG_CONFLICT_ELIMINATION_COUNTPROBE_WEIGHTminute_step=1 指纹;Skill 不 bump。

4. 硬红线

  • 主持人兜底正文只能来自 batch 工具的返回(事件复述),不得让服务端另起模型调用补写,不得引用用户原话之外的年份。
  • 兜底轮的 receipt 必须能看出是兜底:phasesanswer.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) 模拟 streamread-case → batch completed(返回两件事件复述)→ finish(stop) 无 text → 结果 ok=trueassistant 正文等于"记下了:<复述>。"phasesanswer.host_fallback,计费 complete 被调用一次;(b) 同形状但 batch 未调用 → 第一 attempt retryable,第二 attempt 正常;(c) batch completed 后 finish(length) → 同 (a)(d) 现有三条 errorCode === "empty_stream" 断言逐条写"原值 / 新值 / 原因"。
  • rectification-agentic-entry.test.ts L273 的源码断言按新路径改写,说明三栏。

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.md BUG-633(关联 BUG-186、BUG-359、BUG-558、BUG-627;说明 fe87a9ec 为何把 empty_stream 移出可重试,以及本次为何只在零写工具时放回)、BUG-634(关联 BUG-593、BUG-629);CHANGELOG.mdPROGRESS-rectification-evidence-turn-empty-answer-20260910.mddocs/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)"