Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
8.8 KiB
修复单 · 采用旁白 Agent 在点选入口从不运行(2026-09-04)
基线:origin/staging 0aaa0d70(TASK-rectification-adopt-narration-20260904.md 的实现)。本单只改 answer-choice.ts 与新测试,不动 Agent 提示词以外的决策逻辑。
0. 验收结论(对照原任务书)
| 项 | 结论 | 证据 |
|---|---|---|
4.1.1 nakshatra 计划/决策同口径,无 frame 的 distinguish 不进 deferred_followup |
通过 | rectificationFollowupCatalog 过 inspectDiscriminatorProbes;计划层兜底丢进 dropped_probes(not_renderable) |
4.1.2 isRemainingDiscriminatorFollowup / interviewToPersist 忽略无 frame distinguish |
通过 | 代码 + 14 探针 fixture |
4.1.3 persistableFocusDomain 非白名单返回 null,appearance 零 RPC;horary 断言三栏说明 |
通过 | rectification-server-focus、rectification-collect-stall |
| 4.1.4 采用旁白 Agent + 模板兜底,三条入口都接 | 未通过(P1) | 见 §1;点选入口 Agent 从不运行,且模板改用了过期决策 |
4.1.5 采用轮 current_question 为 null、零次 set_focus |
通过 | 新测试 assertNoFocusWrite |
| 4.2 区分题答否不关整条领域 | 通过 | declinedDomains 按 intent;dossierWithClosedFocus 带 intent;BUG-520 |
rectification-collect-stall live fixture 补 nakshatra + window_scan 后原断言仍过 |
通过 | 本地重跑 |
| tsc / lint / 定向套件 | 通过 | tsc 0 错;lint 0 error 74 warning(既有);rectification-* / agentic-rectification-* / birth-time-* 869 条 fail=0 |
全量 tests/*.test.ts |
环境缺口 | 2638 条,fail 25 全部为 docker ENOENT 的 DB / 部署 / 备份套件,与无 Docker 基线一致;无一条校正相关 |
| BUG-519 / BUG-520、CHANGELOG、docs/testing 清单 | 通过 | 无案例 ID、无用户资料 |
1. 事故实证(P1 · 点选入口 Agent 从不运行)
answer-choice.ts persistNextInterviewAfterChoice 的采用早退分支:
const decision = decideFromDossier(input.dossier, { birthDate });
const facts = adoptDeliveryFacts(decision, input.dossier);
const fallback = adoptHostNarration({ dossier: input.dossier, decision, ... });
点选路径(applyRectificationChoice → persistApplied)传进来的 input.dossier 是点选前加载的 dossier,其 latestResult.decisionReceipt.inference_state 还是上一轮;本轮新状态在 input.decisionState,本轮决策在 input.nextAction(persistApplied 手里还有完整的 nextDecision)。函数开头已经把两者合成了 latest,但早退分支没有用它,而是从旧 dossier 重新 decideFromDossier:
- 旧 dossier 的决策是「还有一道题要问」(
theme_refine/ask_candidate_discriminator,probe非空),precisionStage !== "ready_to_adopt"→shouldWriteAdoptNarration为 false →narrateAdopt直接返回 fallback,模型一次都没调。 - fallback 模板的
credibleRange / representativeTime / stopReason也来自这份旧决策,而不是本轮的nextAction。改造前模板用的是nextAction.credible_range,这是回归。
本地复现(用 14 探针 fixture,把 dossier 里的 receipt 回退成「最后一道题未答、分数 23/16/7」,decisionState 用答完后的状态,nextAction 用答完后的决策):
pre-click decision: theme_refine, probe=true
agent called: false
narration: <模板>
fresh nextAction: offer_provisional_range [05:00, 05:06] 05:00
新测试「three adopt entry points call the model once」没拦住,是因为它给点选入口传的 dossier 已经是答完后的状态(receipt 与 decisionState 相同),与生产的点选路径不一致。
用户可感知:点最后一道区分题(采用卡最常见的出现方式)时,旁白仍是模板句;只有采集题答「没有」和空闲兜底两条入口才会走 Agent。
2. 根因
早退分支把「决策」当成可以从 dossier 随手再算一遍的东西,违反 BUG-440 防复发条款「同一次调用里出题层与决策层必须用同一个 sessionOutcome」——这里是决策层自己算了两遍、用了两份不同输入。
3. 决策记录
原任务书决策不变。本单不新增产品决策。
4. 硬红线
原任务书 §3 全部沿用。追加:
persistNextInterviewAfterChoice内不得再调用decideFromDossier;决策由调用方传入,或用decideAfterInferenceChange({ dossier, state: input.decisionState })与persistApplied保持同一入口。- 「三条入口」测试的点选用例必须用点选前 dossier + 答完后
decisionState+ 答完后nextAction的形状;不得再用已答完的 dossier 冒充。
5. 任务分解
5.1 P1 · 早退分支用本轮决策(BUG-521)
persistNextInterviewAfterChoice新增可选入参decision?: RectificationDecision;三条调用方都传:persistApplied传nextDecision,persistNextInterviewIfIdle传已算好的decision,applyCollectFocusDenial传它用来生成nextAction的那份决策。未传时用decideAfterInferenceChange({ dossier: input.dossier, state: input.decisionState, userStopped: false, birthDate }),不得用decideFromDossier(input.dossier)。adoptDeliveryFacts与adoptHostNarration的 dossier 参数改传合成后的{ ...input.dossier, latestResult: latest }(函数开头已有的latest),保证活跃候选与相对支持度是本轮的。persistApplied里adoptionFacts = adoptDeliveryFacts(nextDecision, input.dossier)同样改用合成 receipt。
验收:
rectification-adopt-narration-20260904.test.ts「three adopt entry points」点选用例改成 §1 的形状:dossierreceipt 为「最后一道题未答、分数 23/16/7、revision 5」,decisionState = fourteenProbeState(),nextAction = publicNextAction(decideFromDossier(caseDossier()))。断言:模型被调用 1 次;传给模型的facts.representative_minute === "05:00"、facts.active_candidates的分数为 21/18/9(不是 23/16/7)、facts.precision_stage === "ready_to_adopt"。- 同一形状下让 fake model 返回非法文本,断言 fallback 模板里的范围与代表分钟来自
nextAction(05:00–05:06 / 05:00),并含「分不开 05:00 和 05:06」。 - 加一条源码级断言:
persistNextInterviewAfterChoice函数体内不出现decideFromDossier(。 docs/BUG_HISTORY.md新增 BUG-521,复发自 BUG-440(决策层重复计算)。
5.2 P2 · 校验器把「不是确认」也打回(BUG-522,可与 5.1 同批)
validateAdoptNarration 的 /确认|精确/ 是全文匹配;提示词只说「不得承诺确认或精确」,模型很自然会写「这不是确认的分钟」——会被整段打回、静默退到模板,Agent 路径在真实环境的命中率会被这条拉低而且没人知道。
- 提示词改成「不要出现『确认』『精确』这两个词」(与校验器同口径)。
- 增加一条可观测:Agent 路径的结果写进本轮 run diagnostic / 服务端日志一个枚举字段
adopt_narration: "agent" | "template:<validate reason>" | "template:model_error" | "template:not_ready",不写模型原文。没有这条,部署后没人能证明 Agent 到底有没有写。 agent.generate加超时:AbortSignal.any([request.signal, AbortSignal.timeout(8000)]),超时走模板。分类器没有超时是既有缺口,不在本单修,写进BLOCKED.md或进度记录。
验收:fake model 返回「这不是确认的分钟,先用 05:00。」→ 仍打回(校验器不变),但提示词测试断言含「不要出现」;诊断字段在三种结果下各有一条测试;超时用 fake generateText 挂起 + 短 timeout 断言走模板且不抛。
5.3 P3 · stopFactsFromDropped 的「婚恋题只覆盖窗口两端」标签
insufficient_candidates || not_renderable 一律标成婚恋题,而 not_renderable 现在也用于 nakshatra。改成按 semantic_key 前缀分桶(relationship. → 婚恋,其它 → 「有几道题渲染不出来」),或去掉领域词。可推迟。
6. 让步顺序
- 5.1 必做,否则 4.1.4 在最常见入口上等于没做。
- 5.2 的可观测字段与超时优先于提示词改动。
- 5.3 可推迟到下一轮。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-adopt-narration-fix-20260904 .worktrees/rectification-adopt-narration-fix-20260904 origin/staging
cd .worktrees/rectification-adopt-narration-fix-20260904/frontend
./node_modules/.bin/tsc --noEmit
npx tsx --test tests/rectification-adopt-narration-20260904.test.ts tests/rectification-answer-choice.test.ts tests/rectification-collect-stall.test.ts
8. BUG 编号起点
截至本单:BUG-520。本单从 BUG-521 起;开工时再核对。