Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
15 KiB
任务书 · 采集阶段第三件事没有方向:盘外核对抢跑到已拒答领域,问题不指向任何领域(2026-09-04)
基线:origin/staging 代码 45bdb63e(文档头 45e00f46)。本单只改 frontend/src/lib/rectification-agentic/v9/method-followup.ts、spoken-prompt.ts、rectification-v9-tools.ts(读盘投影 + set-focus 校验)与 agentic-rectification.ts 提示词,以及对应测试。不改 Python 引擎、不改 decideRectification 的采用/确认门、不动 Skill 版本。
与 TASK-report-sensitivity-float-crash-20260904.md 无文件交集,可并行。
0. 用户可感知的现象
staging(45bdb63e)真实会话,出生窗 30 分钟,账本里 2 条带日期事件(事业 1、感情 1)加 1 条只有年份没有日期的感情事:
| 轮 | 用户 | 助手 | 问题 |
|---|---|---|---|
| 1 | 家人某年采集题答「没有」 | 「你平时主要做什么工作?」 | 可评分事件 2 < 3,缺的是带年份的第三件事;职业备注没有日期,答完也不推进 |
| 2 | 说了职业 | 「当前范围还在继续收窄中,接下来我们继续。」+「除了工作这条线,还有哪件事你能记起大概的年份?」 | 第一句是空话(没有任何数值收窄);第二句没有指向任何领域,用户不知道该往哪想 |
第 2 轮服务端落下的焦点是 oos_blind:holdout、intent=out_of_sample_check、domain=family——用户上一轮刚拒答的领域。Agent 拿到的写作提示是「校时还没用过家人这条线……」,它知道家人刚被否,于是把问题改写成不指向任何领域的泛问。
1. 事故实证
用该 Case 的 receipt 形状(9 个候选、2 条可评分事件、event_quality.passed=false, minimum=3, scoreable_event_count=2、oos_blind_prompts=[family, education, finance]、7 条 varga 对照探针全部 year=0 被丢成 yearless_ungrounded_contrast)本地重放,隐私数据不入库:
1a. 工具路径(Agent 轮)选中已拒答领域的盘外核对。 buildMethodFollowupPlan 传入 holdoutValidation: "not_started"(与 rectification-v9-tools.ts followupPlanForParsed / 读盘投影一致):
declined=[family] → next = { method: oos_blind, intent: out_of_sample_check, domain: family, frame: false }
无论账本里有没有职业备注、家人有没有拒答,三种形状结果相同。
1b. 决定路径(答「没有」那轮)问的是不带日期的职业。 persistNextInterviewAfterChoice 不传 holdoutValidation,计划走方法覆盖轮转 relationship → career → family(拒答视作 covered)→ occupation。学业、财务、搬家这些带年份的领域根本不在这条轮转里。
1c. 决定路径再走一轮也是泛问。 职业答完后若仍走决定路径,计划落到 !meetsAcceptanceEventQuality 分支:domain: null → spokenFollowupForUser 给 GENERIC_COLLECT_QUESTION(「从你最容易想起来的一件事开始就好……」)。也就是说,两条路径都不会把用户引到一个具体领域。
代码定位(method-followup.ts @45bdb63e,按符号):
holdoutValidationStatus/canAskHoldout(decision-from-dossier.ts):只要引擎给了oos_blind_prompts就判not_started,不看event_quality。buildMethodFollowupPlan内if (!next && input.holdoutValidation === "not_started")分支:直接取input.oosBlindPrompts?.[0],不过declined,不看meetsAcceptanceEventQuality。sessionOutcome === "validate_holdout"早退分支同样不过declined。- 方法覆盖轮转(
!relationshipCovered→!careerCovered→!familyCovered→!occupationCovered)之后才是!meetsAcceptanceEventQuality的泛问分支,且该分支domain: null。 exhaustionSpokenCollectFollowup已经有正确的顺序(family → education → finance → occupation → health/relocation/career/relationship → other)和按领域的datedCollectFollowup,但只在拒答/穷尽路径被调用,计划主流程不用它。validateSpokenPrompt:对采集题只查长度、选项字面、内部 token、domain_mismatch(仅当 Agent 显式传了不同targetDomain),不要求题干提到领域。Agent 把家人题改写成泛问,服务端照收。
2. 根因
- 盘外核对没有前置门。 BUG-396 已钉死「2 条继续收集,训练门只计 training 事件」,但计划层的 OOS 分支只看
holdoutValidation === "not_started",而这个状态只要引擎输出了提示就成立。于是训练门都没开就开始「盘外核对」,并且用的是被declinedDomains(BUG-520)明确排除的领域。 - 计划里没有「补第三件带年份的事」这一步。 方法覆盖轮转的四个领域里有三个已覆盖或拒答,剩下的职业不带日期;带年份的学业/财务/搬家/健康只存在于穷尽路径。
- Agent 的写作提示与用户上下文矛盾,校验器又不要求方向。 提示说问家人,用户刚否掉家人,Agent 只能写泛问;服务端没有任何一条规则要求采集题写出领域。
3. 决策记录(产品负责人 2026-09-04)
- 可评分事件不足 3 条时,不得进入盘外核对(OOS / holdout 提问)。 计划的两处 OOS 分支都加
meetsAcceptanceEventQuality前置;不满足就当作没有 OOS 提示。这不推翻 BUG-463(holdout 不绑采用门)——本单只管「什么时候问」,不碰canAdopt/canConfirmExactMinute。 - OOS 提示必须跳过已拒答领域(
declinedDomains,沿用 BUG-520 的 intent 口径)。全部被拒则退到带年份的 holdout 事件,再没有就null。 - 缺第三件事时,先补带年份的领域,再问职业。 顺序:家人 → 学业 → 财务 → 搬家 → 健康/压力 → 事业 → 感情,跳过已拒答与已有确认证据的领域;全部走完才轮到职业,职业也关了才用泛问。计划主流程与
exhaustionSpokenCollectFollowup共用同一份顺序,不得再复制一份。这条只在!meetsAcceptanceEventQuality时生效,不改 BUG-442/472 关于职业覆盖不挡采用的结论。 - 采集题必须指向领域,方向由服务端定、话由 Agent 说。 这是 Agent 产品,题干继续由 Agent 用自己的话写,但服务端校验题干必须包含目标领域的至少一个关键词;不合格返回
invalid_spoken_prompt: domain_missing,两次不合格后服务端用USER_COLLECT_QUESTION[domain]落焦点(现有spokenPromptFailures机制)。领域为other的泛问只允许在 §3.3 全部走完之后出现。 - 进度句必须有数字,没数字就不说。 「当前范围还在继续收窄中」这种没有数据支撑的句子不得出现。服务端在读盘投影里给出
collection_progress = { scoreable, minimum, missing }(来自 receiptevent_quality),提示词要求:下一问是采集题时,先用一句话说明还差几件带时间的事,数字只来自这个字段;没有这个字段就不说进度。
4. 硬红线
- 不改
holdoutValidationStatus的返回语义、不改decideRectification/deliveryCapability的采用与确认门(BUG-463 防复发条款)。修法在计划层的分支条件,不在决策层。 - 不改
declinedDomains的 intent 口径(BUG-520)。 - 顺序只能有一处定义;
exhaustionSpokenCollectFollowup与计划主流程必须调用同一个函数。 validateSpokenPrompt的领域关键词表放在user-copy.ts或agent-voice-lexicon.ts旁边,每个领域至少 3 个日常词(例:education → 上学/大学/毕业/考试/学业;finance → 收入/买房/贷款/欠债/钱;relocation → 搬家/住/外地;health_pressure → 生病/受伤/住院/压力;family → 家里/父母/家人/添丁;career → 工作/入职/换工作;relationship → 交往/分手/结婚/感情)。关键词命中是includes,不做分词。不得把这个校验套到区分题(choice_frame非空)或 OOS 题上——它们已有年份校验。- 既有断言若要改,写「原值 / 新值 / 原因」三栏。预计要动的:
rectification-server-focus中「exhaustionoos_blind仍走USER_COLLECT_QUESTION.other」相关用例(若受顺序合并影响);rectification-yearless-ungrounded的validate_holdout用例需要补足 4 条带日期证据才能维持oos_blind结论(它现在的 evidence 是 4 条 year 精度 + 1 条职业,meetsAcceptanceEventQuality是否满足由执行方实测后写明)。 - 任务书、进度记录、Bug 历史、测试 fixture 不得出现该 Case 的 ID、日期、职业描述;fixture 用虚构年份。
- 进度记录必须贴
npm test摘要六行(tests/pass/fail/cancelled/skipped/todo)与退出码,以及无 Docker 基线失败清单比对(BUG-523 教训)。
5. 任务分解
5.1 P0 · 盘外核对加前置门并跳过拒答领域(BUG-524)
- 新增
holdoutFollowupFor(input, declined):meetsAcceptanceEventQuality(input.evidence)不满足 →null;否则取oosBlindPrompts中第一个!declined.has(domain)的提示 →holdoutAskFields;没有则退到带年份的 holdout 事件;再没有 →null。 sessionOutcome === "validate_holdout"早退与holdoutValidation === "not_started"分支都改用它。
验收(新文件 frontend/tests/rectification-collect-direction-20260904.test.ts,fixture 用 §1 的形状但年份虚构):
- 2 条可评分事件 +
oosBlindPrompts=[family, education, finance]+ family 拒答 +holdoutValidation: "not_started"→next_followup.intent !== "out_of_sample_check"。 - 4 条可评分事件(3 个领域)+ family 拒答 +
holdoutValidation: "not_started"且无可渲染区分探针 →next_followup为oos_blind且domain === "education"。 - 同上但三个领域全部拒答、无带年份 holdout 事件 → 不出 OOS;
sessionOutcome: "validate_holdout"时next_followup === null。
5.2 P0 · 补第三件带年份的事进入计划主流程(BUG-525)
- 把
exhaustionSpokenCollectFollowup里的领域顺序抽成datedCollectOrder(§3.3 顺序,含跳过规则),穷尽路径与计划主流程共用。 buildMethodFollowupPlan:在方法覆盖轮转之前、OOS 分支之后加一步:!next && !meetsAcceptanceEventQuality(input.evidence)→ 走datedCollectOrder,取到带领域的datedCollectFollowup;没有再进原轮转(职业)与原泛问分支。spokenFollowupForUser对这些 followup 必须返回USER_COLLECT_QUESTION[domain](不是GENERIC_COLLECT_QUESTION),这是决定路径的题干。
验收:
- §1 形状、family 拒答、
holdoutValidation: "not_started"→next_followup = { intent: collect_method_evidence, domain: education },spokenFollowupForUser含「上学」或「升学」。 - 再把 education 也拒答 →
domain: finance;family/education/finance/relocation/health_pressure 全拒答且 career/relationship 已有确认证据 →domain: occupation;职业也关闭 → 才出domain: other。 persistNextInterviewAfterChoice(答「没有」路径,形状同rectification-adopt-narration-20260904.test.ts的 denial 用例)在 2 条可评分事件下落下的焦点target_domain为 education 且 prompt 为USER_COLLECT_QUESTION.education;零次decideFromDossier(沿用 BUG-521 源码断言)。- 4 条可评分事件时本分支不触发(
rectification-collect-stall、rectification-eight-method全绿;BUG-442/472 的采用出口用例不变)。
5.3 P1 · 采集题干必须指向领域(BUG-526)
validateSpokenPrompt:当followup.intent === "collect_method_evidence"且followup.domain在关键词表内且!followup.choice_frame时,题干必须命中该领域至少一个关键词,否则{ ok: false, reason: "domain_missing" }。rectification-set-focus描述加一句:「采集题必须写出服务端给你的领域(学业/家里/钱/搬家……),不要写成『随便哪件事』」。- 第二次仍不合格时的服务端兜底沿用现有
spokenPromptFailures;确认兜底题干是USER_COLLECT_QUESTION[domain]而不是泛问。
验收:rectification-spoken-prompt(或新文件):education 题写「除了工作,还有哪件事记得年份?」→ domain_missing;写「上学那会儿,哪年升学或大考还记得吗?」→ ok;区分题与 OOS 题不受影响;rectification-set-focus 集成用例两次泛问后焦点 prompt 为 USER_COLLECT_QUESTION.education。
5.4 P2 · 进度句有数字才说(BUG-527)
- 读盘投影(
rectification-read-case与 route 的 interview 投影)加collection_progress: { scoreable, minimum, missing },来自decisionReceipt.gates.event_quality;缺字段时为null。 agentic-rectification.ts提示词:「下一问是采集题时,先用一句说明还差几件带时间的事,数字只用collection_progress;没有该字段不说进度。不得写『范围在收窄』『继续收窄』这类没有数字的进度句。」把「继续收窄」「范围还在」加进MACHINE_VOICE_LEXICON只对旁白校验生效(若旁白无校验,则只改提示词并在进度记录写明)。
验收:投影测试锁 collection_progress 取值(2/3/1)与缺 gate 时 null;提示词测试断言含「collection_progress」与「不得」句。
6. 让步顺序
- 5.1 与 5.2 必做,缺一条用户仍然拿到没方向的问题。
- 5.3 可以退到只加提示词 + 兜底题干为领域题,但校验器改动是防复发的钉子,不做要在进度记录写明理由。
- 5.4 可推迟。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-collect-direction-20260904 .worktrees/rectification-collect-direction-20260904 origin/staging
cd .worktrees/rectification-collect-direction-20260904/frontend
./node_modules/.bin/tsc --noEmit
npx tsx --test tests/rectification-eight-method.test.ts tests/rectification-yearless-ungrounded.test.ts tests/rectification-server-focus.test.ts tests/rectification-collect-stall.test.ts tests/rectification-adopt-narration-20260904.test.ts tests/rectification-answer-choice.test.ts
记下开工时这批套件的 tests/pass/fail 数与全量 npm test 的失败清单(无 Docker 基线 25 条),交付时逐条比对。
8. BUG 编号起点
截至本单:BUG-523。本单从 BUG-524 起(5.1 → 524,5.2 → 525,5.3 → 526,5.4 → 527);开工时再核对 docs/BUG_HISTORY.md 最大号。BUG-524 关联 BUG-396(训练门未开不得进入 holdout)与 BUG-520(拒答口径);BUG-525 关联 BUG-442/472(职业覆盖)与 BUG-426(计划回落到带年份的采集题)。