Files
Jyotisha/docs/tasks/TASK-rectification-collect-direction-20260904.md
T

15 KiB
Raw Blame History

任务书 · 采集阶段第三件事没有方向:盘外核对抢跑到已拒答领域,问题不指向任何领域(2026-09-04)

基线:origin/staging 代码 45bdb63e(文档头 45e00f46)。本单只改 frontend/src/lib/rectification-agentic/v9/method-followup.tsspoken-prompt.tsrectification-v9-tools.ts(读盘投影 + set-focus 校验)与 agentic-rectification.ts 提示词,以及对应测试。不改 Python 引擎、不改 decideRectification 的采用/确认门、不动 Skill 版本。

TASK-report-sensitivity-float-crash-20260904.md 无文件交集,可并行。

0. 用户可感知的现象

staging45bdb63e)真实会话,出生窗 30 分钟,账本里 2 条带日期事件(事业 1、感情 1)加 1 条只有年份没有日期的感情事:

用户 助手 问题
1 家人某年采集题答「没有」 「你平时主要做什么工作?」 可评分事件 2 < 3,缺的是带年份的第三件事;职业备注没有日期,答完也不推进
2 说了职业 「当前范围还在继续收窄中,接下来我们继续。」+「除了工作这条线,还有哪件事你能记起大概的年份?」 第一句是空话(没有任何数值收窄);第二句没有指向任何领域,用户不知道该往哪想

第 2 轮服务端落下的焦点是 oos_blind:holdoutintent=out_of_sample_checkdomain=family——用户上一轮刚拒答的领域。Agent 拿到的写作提示是「校时还没用过家人这条线……」,它知道家人刚被否,于是把问题改写成不指向任何领域的泛问。

1. 事故实证

用该 Case 的 receipt 形状(9 个候选、2 条可评分事件、event_quality.passed=false, minimum=3, scoreable_event_count=2oos_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: nullspokenFollowupForUserGENERIC_COLLECT_QUESTION(「从你最容易想起来的一件事开始就好……」)。也就是说,两条路径都不会把用户引到一个具体领域。

代码定位(method-followup.ts @45bdb63e,按符号):

  • holdoutValidationStatus / canAskHoldoutdecision-from-dossier.ts):只要引擎给了 oos_blind_prompts 就判 not_started,不看 event_quality
  • buildMethodFollowupPlanif (!next && input.holdoutValidation === "not_started") 分支:直接取 input.oosBlindPrompts?.[0],不过 declined,不看 meetsAcceptanceEventQualitysessionOutcome === "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. 根因

  1. 盘外核对没有前置门。 BUG-396 已钉死「2 条继续收集,训练门只计 training 事件」,但计划层的 OOS 分支只看 holdoutValidation === "not_started",而这个状态只要引擎输出了提示就成立。于是训练门都没开就开始「盘外核对」,并且用的是被 declinedDomainsBUG-520)明确排除的领域。
  2. 计划里没有「补第三件带年份的事」这一步。 方法覆盖轮转的四个领域里有三个已覆盖或拒答,剩下的职业不带日期;带年份的学业/财务/搬家/健康只存在于穷尽路径。
  3. Agent 的写作提示与用户上下文矛盾,校验器又不要求方向。 提示说问家人,用户刚否掉家人,Agent 只能写泛问;服务端没有任何一条规则要求采集题写出领域。

3. 决策记录(产品负责人 2026-09-04)

  1. 可评分事件不足 3 条时,不得进入盘外核对(OOS / holdout 提问)。 计划的两处 OOS 分支都加 meetsAcceptanceEventQuality 前置;不满足就当作没有 OOS 提示。这不推翻 BUG-463(holdout 不绑采用门)——本单只管「什么时候问」,不碰 canAdopt / canConfirmExactMinute
  2. OOS 提示必须跳过已拒答领域declinedDomains,沿用 BUG-520 的 intent 口径)。全部被拒则退到带年份的 holdout 事件,再没有就 null
  3. 缺第三件事时,先补带年份的领域,再问职业。 顺序:家人 → 学业 → 财务 → 搬家 → 健康/压力 → 事业 → 感情,跳过已拒答与已有确认证据的领域;全部走完才轮到职业,职业也关了才用泛问。计划主流程与 exhaustionSpokenCollectFollowup 共用同一份顺序,不得再复制一份。这条只在 !meetsAcceptanceEventQuality 时生效,不改 BUG-442/472 关于职业覆盖不挡采用的结论。
  4. 采集题必须指向领域,方向由服务端定、话由 Agent 说。 这是 Agent 产品,题干继续由 Agent 用自己的话写,但服务端校验题干必须包含目标领域的至少一个关键词;不合格返回 invalid_spoken_prompt: domain_missing,两次不合格后服务端用 USER_COLLECT_QUESTION[domain] 落焦点(现有 spokenPromptFailures 机制)。领域为 other 的泛问只允许在 §3.3 全部走完之后出现。
  5. 进度句必须有数字,没数字就不说。 「当前范围还在继续收窄中」这种没有数据支撑的句子不得出现。服务端在读盘投影里给出 collection_progress = { scoreable, minimum, missing }(来自 receipt event_quality),提示词要求:下一问是采集题时,先用一句话说明还差几件带时间的事,数字只来自这个字段;没有这个字段就不说进度。

4. 硬红线

  1. 不改 holdoutValidationStatus 的返回语义、不改 decideRectification / deliveryCapability 的采用与确认门(BUG-463 防复发条款)。修法在计划层的分支条件,不在决策层。
  2. 不改 declinedDomains 的 intent 口径(BUG-520)。
  3. 顺序只能有一处定义;exhaustionSpokenCollectFollowup 与计划主流程必须调用同一个函数。
  4. validateSpokenPrompt 的领域关键词表放在 user-copy.tsagent-voice-lexicon.ts 旁边,每个领域至少 3 个日常词(例:education → 上学/大学/毕业/考试/学业;finance → 收入/买房/贷款/欠债/钱;relocation → 搬家/住/外地;health_pressure → 生病/受伤/住院/压力;family → 家里/父母/家人/添丁;career → 工作/入职/换工作;relationship → 交往/分手/结婚/感情)。关键词命中是 includes,不做分词。不得把这个校验套到区分题(choice_frame 非空)或 OOS 题上——它们已有年份校验。
  5. 既有断言若要改,写「原值 / 新值 / 原因」三栏。预计要动的:rectification-server-focus 中「exhaustion oos_blind 仍走 USER_COLLECT_QUESTION.other」相关用例(若受顺序合并影响);rectification-yearless-ungroundedvalidate_holdout 用例需要补足 4 条带日期证据才能维持 oos_blind 结论(它现在的 evidence 是 4 条 year 精度 + 1 条职业,meetsAcceptanceEventQuality 是否满足由执行方实测后写明)。
  6. 任务书、进度记录、Bug 历史、测试 fixture 不得出现该 Case 的 ID、日期、职业描述;fixture 用虚构年份。
  7. 进度记录必须贴 npm test 摘要六行(tests/pass/fail/cancelled/skipped/todo)与退出码,以及无 Docker 基线失败清单比对(BUG-523 教训)。

5. 任务分解

5.1 P0 · 盘外核对加前置门并跳过拒答领域(BUG-524)

  1. 新增 holdoutFollowupFor(input, declined)meetsAcceptanceEventQuality(input.evidence) 不满足 → null;否则取 oosBlindPrompts 中第一个 !declined.has(domain) 的提示 → holdoutAskFields;没有则退到带年份的 holdout 事件;再没有 → null
  2. 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_followupoos_blinddomain === "education"
  • 同上但三个领域全部拒答、无带年份 holdout 事件 → 不出 OOSsessionOutcome: "validate_holdout"next_followup === null

5.2 P0 · 补第三件带年份的事进入计划主流程(BUG-525)

  1. exhaustionSpokenCollectFollowup 里的领域顺序抽成 datedCollectOrder(§3.3 顺序,含跳过规则),穷尽路径与计划主流程共用。
  2. buildMethodFollowupPlan:在方法覆盖轮转之前、OOS 分支之后加一步:!next && !meetsAcceptanceEventQuality(input.evidence) → 走 datedCollectOrder,取到带领域的 datedCollectFollowup;没有再进原轮转(职业)与原泛问分支。
  3. 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: financefamily/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-stallrectification-eight-method 全绿;BUG-442/472 的采用出口用例不变)。

5.3 P1 · 采集题干必须指向领域(BUG-526)

  1. validateSpokenPrompt:当 followup.intent === "collect_method_evidence"followup.domain 在关键词表内且 !followup.choice_frame 时,题干必须命中该领域至少一个关键词,否则 { ok: false, reason: "domain_missing" }
  2. rectification-set-focus 描述加一句:「采集题必须写出服务端给你的领域(学业/家里/钱/搬家……),不要写成『随便哪件事』」。
  3. 第二次仍不合格时的服务端兜底沿用现有 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)

  1. 读盘投影(rectification-read-case 与 route 的 interview 投影)加 collection_progress: { scoreable, minimum, missing },来自 decisionReceipt.gates.event_quality;缺字段时为 null
  2. agentic-rectification.ts 提示词:「下一问是采集题时,先用一句说明还差几件带时间的事,数字只用 collection_progress;没有该字段不说进度。不得写『范围在收窄』『继续收窄』这类没有数字的进度句。」把「继续收窄」「范围还在」加进 MACHINE_VOICE_LEXICON 只对旁白校验生效(若旁白无校验,则只改提示词并在进度记录写明)。

验收:投影测试锁 collection_progress 取值(2/3/1)与缺 gate 时 null;提示词测试断言含「collection_progress」与「不得」句。

6. 让步顺序

  1. 5.1 与 5.2 必做,缺一条用户仍然拿到没方向的问题。
  2. 5.3 可以退到只加提示词 + 兜底题干为领域题,但校验器改动是防复发的钉子,不做要在进度记录写明理由。
  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 → 5245.2 → 5255.3 → 5265.4 → 527);开工时再核对 docs/BUG_HISTORY.md 最大号。BUG-524 关联 BUG-396(训练门未开不得进入 holdout)与 BUG-520(拒答口径);BUG-525 关联 BUG-442/472(职业覆盖)与 BUG-426(计划回落到带年份的采集题)。