Files
Jyotisha/docs/tasks/TASK-rectification-adopt-narration-fix-20260904.md
T

8.8 KiB
Raw Blame History

修复单 · 采用旁白 Agent 在点选入口从不运行(2026-09-04)

基线:origin/staging 0aaa0d70TASK-rectification-adopt-narration-20260904.md 的实现)。本单只改 answer-choice.ts 与新测试,不动 Agent 提示词以外的决策逻辑。

0. 验收结论(对照原任务书)

结论 证据
4.1.1 nakshatra 计划/决策同口径,无 frame 的 distinguish 不进 deferred_followup 通过 rectificationFollowupCataloginspectDiscriminatorProbes;计划层兜底丢进 dropped_probes(not_renderable)
4.1.2 isRemainingDiscriminatorFollowup / interviewToPersist 忽略无 frame distinguish 通过 代码 + 14 探针 fixture
4.1.3 persistableFocusDomain 非白名单返回 nullappearance 零 RPChorary 断言三栏说明 通过 rectification-server-focusrectification-collect-stall
4.1.4 采用旁白 Agent + 模板兜底,三条入口都接 未通过(P1 见 §1;点选入口 Agent 从不运行,且模板改用了过期决策
4.1.5 采用轮 current_question 为 null、零次 set_focus 通过 新测试 assertNoFocusWrite
4.2 区分题答否不关整条领域 通过 declinedDomains 按 intentdossierWithClosedFocus 带 intentBUG-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, ... });

点选路径(applyRectificationChoicepersistApplied)传进来的 input.dossier点选前加载的 dossier,其 latestResult.decisionReceipt.inference_state 还是上一轮;本轮新状态在 input.decisionState,本轮决策在 input.nextActionpersistApplied 手里还有完整的 nextDecision)。函数开头已经把两者合成了 latest,但早退分支没有用它,而是从旧 dossier 重新 decideFromDossier

  • 旧 dossier 的决策是「还有一道题要问」(theme_refine / ask_candidate_discriminatorprobe 非空),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 全部沿用。追加:

  1. persistNextInterviewAfterChoice 内不得再调用 decideFromDossier;决策由调用方传入,或用 decideAfterInferenceChange({ dossier, state: input.decisionState })persistApplied 保持同一入口。
  2. 「三条入口」测试的点选用例必须用点选前 dossier + 答完后 decisionState + 答完后 nextAction 的形状;不得再用已答完的 dossier 冒充。

5. 任务分解

5.1 P1 · 早退分支用本轮决策(BUG-521)

  1. persistNextInterviewAfterChoice 新增可选入参 decision?: RectificationDecision;三条调用方都传:persistAppliednextDecisionpersistNextInterviewIfIdle 传已算好的 decisionapplyCollectFocusDenial 传它用来生成 nextAction 的那份决策。未传时用 decideAfterInferenceChange({ dossier: input.dossier, state: input.decisionState, userStopped: false, birthDate }),不得用 decideFromDossier(input.dossier)
  2. adoptDeliveryFactsadoptHostNarration 的 dossier 参数改传合成后的 { ...input.dossier, latestResult: latest }(函数开头已有的 latest),保证活跃候选与相对支持度是本轮的。
  3. persistAppliedadoptionFacts = adoptDeliveryFacts(nextDecision, input.dossier) 同样改用合成 receipt。

验收:

  • rectification-adopt-narration-20260904.test.ts「three adopt entry points」点选用例改成 §1 的形状:dossier receipt 为「最后一道题未答、分数 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 模板里的范围与代表分钟来自 nextAction05:0005: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 路径在真实环境的命中率会被这条拉低而且没人知道。

  1. 提示词改成「不要出现『确认』『精确』这两个词」(与校验器同口径)。
  2. 增加一条可观测:Agent 路径的结果写进本轮 run diagnostic / 服务端日志一个枚举字段 adopt_narration: "agent" | "template:<validate reason>" | "template:model_error" | "template:not_ready",不写模型原文。没有这条,部署后没人能证明 Agent 到底有没有写。
  3. 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. 让步顺序

  1. 5.1 必做,否则 4.1.4 在最常见入口上等于没做。
  2. 5.2 的可观测字段与超时优先于提示词改动。
  3. 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 起;开工时再核对。