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

106 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_CODES` L144、`streamAttempt` 收尾 L960995、成功收尾 L560620)、`v9/run-diagnostic.ts``v9/answer-choice.ts``composeChoiceNarration` 调用 L551/582/603)、`v9/choice-action.ts``composeChoiceNarration` L98123)、`v9/probe-explain.ts``explainScoreMovement` L215)、`frontend/src/components/rectification-agentic-chat.tsx``run.failed` 分支 L888、失败收尾 L970985、`liveQuestionOnMessages` L1474、`questionGap` L1486、`RectificationReadonlyRange` L178187、输入框占位 L17761782)、`frontend/src/lib/rectification-surface-state.ts``rectificationQuestionGapState` L278
- BUG 编号起点:**BUG-633**`docs/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` | `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` completed22 073 ms13 个方法);**没有 `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-633P1):模型在 batch 之后没写正文,服务端把整轮判失败,不回捞已落库的东西
1. `record-evidence-batch` 工具内部已完成:写证据 → 重算(带 `previous` 重放)→ `persistPlanFocus` 落下健康采集焦点(`askedTurnId` = 本轮)。工具返回后模型既没调 `set-focus``spokenPrompt`,也没写"记下了:…"一句,直接以 `stop` 结束。`streamAttempt` L993 `!answerText.trim()``empty_stream`
2. `empty_stream` 不在 `RETRYABLE_ERROR_CODES``fe87a9ec` 于 2026-08-24 移除,原因是重试会重放证据)。于是 `finalizeTurn("failed", null, …)`,不计费,发 `run.failed`
3. 失败路径不经过 `finalizeSuccessfulTurnExit` / `ensureNonTerminalTurnExit`(路由 L899 只在 `result.ok` 时调),也不会用主持人旁白把 batch 已返回的事件复述写成 host turn。**证据、评分、焦点三样都已提交,唯独用户看不到。**
4. 客户端 `run.failed` 后不调 `loadCaseSnapshot`L975990 只在 `succeeded` 时刷新);即便刷新,`liveQuestionOnMessages` 要求某条 settled assistant 消息带同 `focus_id``question`,失败轮没有 assistant 行 → `questionGap``preparing` → 两次轮询 → `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_contrast`,BUG-629 已降级)。**从此刻起,选择题已经不可能再收窄区间,只有新的带年月经历能改变分数。** 这不是 bug,是数据本身分不开,但产品必须把这件事说出来。
- `choice-action.ts::composeChoiceNarration` 收了 `deltasByCluster`,正文却只用 `explainRangeChange``probe-explain.ts::explainScoreMovement` 能生成"04:5204:53 领先,05:0805:12 落后",从没被说出来。`RectificationReadonlyRange` 的"还在收窄"是写死的。
## 3. 决策记录(产品负责人 2026-09-10 授权诊断;以下为 Claude 建议,执行前默认生效,用户可否决)
1. **证据轮"模型无正文"不是失败。** 只要本 attempt 内 `rectification-record-evidence-batch``completed``toolTerminalStatus`),`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` 分支之后照样 `loadCaseSnapshot``rectificationQuestionGapState` 新增输入 `questionPersisted``current_question` 非空且 `question_source === "focus"`):为真时不进 `preparing / unavailable`,改渲染一条主持人问题行(复用 collect_spoken 的问题段样式,`focus_id``current_question`),输入框占位才允许写"请回答上面的问题…"。这一条同时替代 BUG-626 决策 5 里"修复"按钮的大半场景;那份任务书的 `repair-exit` 路由仍保留给"穷尽且无载体"的情况。
4. **答题旁白必须带进度。** `composeChoiceNarration``appliedInference` 且范围不变时,输出 `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_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) 模拟 streamread-case → batch completed(返回两件事件复述)→ `finish(stop)` 无 text → 结果 `ok=true`assistant 正文等于"记下了:<复述>。"`phases``answer.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.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. 开工前置命令
```bash
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)"
```