docs(tasks): adopt narration written by agent, template as fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
| `TASK-rectification-adopt-flow-20260902.md` | — | 采用流程整治 | 已验收 | `35e5781e`(BUG-497~500) |
|
||||
| `TASK-rectification-adopt-flow-fix-20260903.md` | — | 采用流程修复单(含删「用这个时间看盘」) | 已验收 | `e8c98c37`(BUG-501/502) |
|
||||
| `TASK-rectification-uncertainty-stop-20260903.md` | — | 不确定度停止规则加样本下限 | 已验收 | `0c0df426`(BUG-503) |
|
||||
| `TASK-rectification-adopt-narration-20260904.md` | — | 探针池耗尽时采用卡旁白「继续往下收」+ 已丢弃探针绕过 BUG-472 早退 + 区分题答否关线 | 待执行 | 基线 `4d0a67a2`,BUG-516 起 |
|
||||
| `TASK-rectification-adopt-narration-20260904.md` | — | 探针池耗尽时采用卡旁白改 Agent 生成 + 已丢弃探针绕过 BUG-472 早退 + 区分题答否关线 | 待执行 | 基线 `4d0a67a2`,BUG-516 起 |
|
||||
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 任务书 · 采集题答「没有」后旁白说「继续往下收」却直接出采用卡(2026-09-04)
|
||||
|
||||
基线:`origin/staging` `4d0a67a2`。本单只改 TypeScript 决策/落焦点层与测试,不改 Python 引擎、不改 Skill 包、不动确认门。与 `TASK-rectification-uncertainty-stop-20260903.md`(BUG-503,已合入)无文件级冲突。
|
||||
基线:`origin/staging` `4d0a67a2`。本单只改 TypeScript 决策/落焦点层、新增一个只写旁白的小 Agent,以及测试;不改 Python 引擎、不改 Skill 包、不动确认门。与 `TASK-rectification-uncertainty-stop-20260903.md`(BUG-503,已合入)无文件级冲突。
|
||||
|
||||
## 0. 真实环境复现(用户 09-04 staging 反馈,事实)
|
||||
|
||||
@@ -41,8 +41,9 @@
|
||||
## 2. 决策记录(产品授权)
|
||||
|
||||
1. **采用门语义不变。** 候选分不开、探针池空时先出范围 + 代表分钟 + 采用卡,采用后再核对(BUG-463 / BUG-472 / BUG-501 的既定设计)。本单**不**把 holdout / OOS 提前到采用前——引擎剩余探针与保留事件都不能把相邻 6 分钟分开,提前问只是表演。若产品负责人希望改成「采用前先问保留事件」,另开任务书并在此处覆盖本条。
|
||||
2. 用户看到的应当是 **BUG-472 设计的交付旁白**(`deliveryAdoptNarration`:「已经从最初的 … 收到 05:00–05:06 … 代表分钟是 05:00 … 可以从下面选一个先用着。」),并且在同一句里说清**为什么停**:剩下的问题分不开 05:00 和 05:06。
|
||||
3. 「没有」= 对这一道题的否定,不等于「这条线以后都别问」。仅当焦点本身是**采集题**(`collect_method_evidence`)时,否定才算拒答该领域;对计分区分题的否定只计分、不关线。这条覆盖 BUG-440 里「答 C declined 计分」的表述中「declined 即覆盖」的那一半:D12 同领域卡「没有」仍计分,但 career/relocation 等**有年份**的区分题答「没有」不再写成领域拒答。
|
||||
2. 用户看到的旁白必须说清**为什么停**(剩下的问题分不开 05:00 和 05:06)、**现在的范围与代表分钟**、**采用后拿什么核对**;BUG-472 的 `deliveryAdoptNarration` 模板句降为兜底,见第 3 条。
|
||||
3. **交付旁白由 Agent 写,不再是模板句**(产品负责人 09-04 拍板)。用户「没有」这句已经是模型分类器(`classifyRectificationTurnIntent`)判的,判得也对;问题出在分类**之后**的确定性快路径:`decideFromDossier` 算完就把一句固定文案经 `persistV9DeterministicTurn` 写回并返回,Agent 没上场,所以「不明不白」。改为:决策层仍是唯一权威(分不分得开、能不能采用、范围与代表分钟、采用后拿什么核对,全部来自 receipt,模型不得参与也不得改写),但把这些**结构化事实交给一个只写旁白的小 Agent**,让它用自己的话解释「为什么停、现在的范围、采用后怎么核对」。模板句降级为模型失败 / 超时 / 校验不过时的兜底。多一次模型调用、多 1–2 秒等待是接受的代价。
|
||||
4. 「没有」= 对这一道题的否定,不等于「这条线以后都别问」。仅当焦点本身是**采集题**(`collect_method_evidence`)时,否定才算拒答该领域;对计分区分题的否定只计分、不关线。这条覆盖 BUG-440 里「答 C declined 计分」的表述中「declined 即覆盖」的那一半:D12 同领域卡「没有」仍计分,但 career/relocation 等**有年份**的区分题答「没有」不再写成领域拒答。
|
||||
|
||||
## 3. 硬红线
|
||||
|
||||
@@ -51,6 +52,8 @@
|
||||
3. 不得为了让 `collect:appearance` 能落库而放宽 DB check;`appearance / marks / horary / nakshatra` 这类非证据领域不得成为口述采集焦点。
|
||||
4. 不得再新增一处「先落焦点、失败了再兜底旁白」的路径;早退判定只在 `shouldSkipFollowupPersist` 一处。
|
||||
5. 测试总数不得低于 `origin/staging` `4d0a67a2` 的实测;改既有断言写「原值 / 新值 / 原因」。
|
||||
6. 旁白 Agent 只写话,不做决定:不能拿到工具、不能改焦点、不能改 sessionOutcome;输入只有服务端给的结构化事实,输出经校验后才落库。它说出的每一个 `HH:MM`、每一个年份、每一个「相对支持度」数字都必须能在输入事实里找到,否则整段作废走模板兜底,不得部分采用。
|
||||
7. 模型原文不得进 `docs/BUG_HISTORY.md`、进度记录或测试 fixture;测试用 fake model 返回固定文本。
|
||||
|
||||
## 4. 任务分解
|
||||
|
||||
@@ -59,11 +62,19 @@
|
||||
1. `buildMethodFollowupPlan` 的 nakshatra 分支必须与决策层同一口径:只有 `nakshatraProbeIfFollowupCanAsk` 会选中的探针才产出 followup。实现方式二选一,实现者定并写进 BUG 条目:(a) `rectificationFollowupCatalog` 传入的 `nakshatraProbe` 先过 `inspectDiscriminatorProbes`(同 `askedKeys / topCandidateTimes`),不可渲染即传 null;(b) 分支内 `makeFollowup(..., true, true)` 产出无 `choice_frame` 时直接丢弃并进 `dropped_probes`。**两种都要保证** `plan.deferred_followup` 不再含无 `choice_frame` 的 `distinguish_candidates`。
|
||||
2. `answer-choice.ts` `isRemainingDiscriminatorFollowup`:无 `choice_frame` 的 distinguish followup 不算「剩余区分题」(它本来就渲染不出来)。`interviewToPersist` 对 `deferred_followup` 只在其可渲染时才取。
|
||||
3. `server-focus.ts` `persistableFocusDomain`:非白名单领域返回 `null`(不得返回 `appearance / horary / nakshatra`);`persistSpokenChoiceFallback` 只允许 `collectQuestionDomain` 后仍在白名单内的领域,否则返回 `status: "skipped"` 且**不**触发 `persistFocusAfterChoice` 的二次重试。既有断言 `persistableFocusDomain("horary") === "horary"` 改为 `null`:原值 `"horary"` / 新值 `null` / 原因:DB check 不含 horary,原断言锁的是一条必然写库失败的路径。
|
||||
4. 交付旁白带停止原因:`adoptHostNarration` / `deliveryAdoptNarration` 在 `stopReason` 为空但 `nextAction.type === "offer_provisional_range"` 且决策层 `droppedProbes` 非空、`probe` 为 null 时,在进度句之前加一句(放 `user-copy.ts`,进 `listUserVisibleCopy`):`剩下的问题已经分不开 05:00 和 05:06,先给范围。`(分钟由 `credible_range` 填入;两端相同则只说代表分钟)。`decisionReceipt` 已有 `dropped_probes`,不需要新字段;若需要在 `publicNextAction` 增加只读 `exhausted_probe_pool: boolean`,允许。
|
||||
4. **交付旁白改为 Agent 生成,模板兜底。** 新增 `v9/adopt-narration-agent.ts`(模式照抄 `turn-intent-classifier.ts`:单次 `Agent.generate`,无工具,`abortSignal` 透传,超时上限与分类器同级):
|
||||
- 输入 `AdoptDeliveryFacts`(服务端从 `decideFromDossier` 结果 + receipt 拼,纯函数 `adoptDeliveryFacts(decision, dossier)`,可单测):`credible_range`(起止分钟)、`representative_minute`、活跃候选与相对支持度、`opening_window`(最初范围,`deliveryAdoptNarration` 已在用)、`answered_rounds`、`stop_facts`(本轮为何停:`dropped_probes` 按 reason 归并成用户能懂的几类——「剩下的题分不开 A 和 B」「没有年份的分盘题不再问」「婚恋题只覆盖窗口两端」;每类附计数,不附探针 hash)、`post_adopt_verification`(预留 holdout 事件的领域与年份、OOS 三条线的领域名;来自 `holdout_validation_probes` / `oos_blind_prompts`,不含用户原文)。
|
||||
- 指令:只写 2–4 句中文旁白,对用户说清三件事:为什么这一轮不再往下问、现在给的范围与代表分钟是什么、采用之后会用哪些事核对;最后一句固定接 `RECTIFICATION_USER_COPY.adoptCue`(「可以从下面选一个先用着。」)由服务端拼,不让模型写。不得出现输入里没有的时间、年份、数字;不得承诺「确认」「精确」;不得再提问。
|
||||
- 校验(服务端,`validateAdoptNarration(text, facts)`):所有 `\d{1,2}:\d{2}` 必须 ∈ facts 的分钟集合;所有四位年份必须 ∈ facts 的年份集合;不含「?」结尾的句子;长度 ≤ 240 字。不过就整段丢弃,用 `deliveryAdoptNarration` 模板(模板本身按本单 4.1.4 旧方案补一句停止原因,作为兜底也要能解释)。
|
||||
- 接入点:**只在采用卡首次打开的那一轮**调用,三条入口都要接——点选区分题后(`answer_choice` 动作,`applied.narration`)、采集题否定后(`applyCollectFocusDenial` → `nextInterview.hostNarration`)、`persistNextInterviewIfIdle` 兜底。判定条件:`decision.precisionStage === "ready_to_adopt"` 且本轮无新焦点。模型来自 route 已解析的 `resolvedModel`,`answer-choice.ts` / `server-focus.ts` 保持零模型依赖,Agent 调用放在 route 层或一个薄的 `deliverAdoptNarration({ model, facts, fallback })`。
|
||||
- 计费:与分类器相同口径(若分类器不计费,这一次也不计费;若计费,写进 `CHANGELOG.md`)。
|
||||
- `decisionReceipt` 已有 `dropped_probes` / `holdout_validation_probes` / `oos_blind_prompts`,不需要新字段;若需要在 `publicNextAction` 增加只读 `exhausted_probe_pool: boolean`,允许。
|
||||
5. 采用卡出现的那一轮,`current_question` 必须为 null 且不得有任何 `set_agentic_rectification_conversation_focus` 调用(BUG-472 断言原样搬到新 fixture)。
|
||||
|
||||
验收:
|
||||
- 新测试 `frontend/tests/rectification-adopt-narration-20260904.test.ts`:fixture 复刻本案形状——五件已确认证据(education×1 作 holdout、relationship×2、career×2)、`inference_state` 含 14 条探针(其中 nakshatra 一条、五条 yearless varga、一条 dasha_activation、两条 candidate_ids 不含活跃候选的 known_event_quality)、5 条 answered、活跃 {05:00, 05:06, 04:53} 分数 21/18/9、receipt 含 `window_scan.d*_candidates_differ=true`、`declined_skipped_topics=[family]`。断言:`decideFromDossier` 为 `offer_provisional_range / adopt_representative / canAdopt=true / probe=null`;`plan.next_followup` 与 `plan.deferred_followup` 均为 null 或均带 `choice_frame`;`persistNextInterviewAfterChoice` 返回 `persisted=false`、`choiceReady=false`、`hostNarration` 匹配 `/分不开 05:00 和 05:06/` 且 `/可以从下面选一个先用着/`、不匹配 `/继续往下收/`;`accounting.calls` 无 `set_agentic_rectification_conversation_focus`。
|
||||
- 新测试 `frontend/tests/rectification-adopt-narration-20260904.test.ts`:fixture 复刻本案形状——五件已确认证据(education×1 作 holdout、relationship×2、career×2)、`inference_state` 含 14 条探针(其中 nakshatra 一条、五条 yearless varga、一条 dasha_activation、两条 candidate_ids 不含活跃候选的 known_event_quality)、5 条 answered、活跃 {05:00, 05:06, 04:53} 分数 21/18/9、receipt 含 `window_scan.d*_candidates_differ=true`、`declined_skipped_topics=[family]`。断言:`decideFromDossier` 为 `offer_provisional_range / adopt_representative / canAdopt=true / probe=null`;`plan.next_followup` 与 `plan.deferred_followup` 均为 null 或均带 `choice_frame`;`persistNextInterviewAfterChoice` 返回 `persisted=false`、`choiceReady=false`、`hostNarration`(模板兜底路径)匹配 `/分不开 05:00 和 05:06/` 且 `/可以从下面选一个先用着/`、不匹配 `/继续往下收/`;`accounting.calls` 无 `set_agentic_rectification_conversation_focus`。
|
||||
- 同一 fixture 的 `adoptDeliveryFacts`:`representative_minute="05:00"`、范围 05:00–05:06、`stop_facts` 含「分不开」一类且计数 ≥ 1、`post_adopt_verification` 含 education 2016 与 family/finance/health 三条线名。
|
||||
- `adopt-narration-agent` 测试(fake model):(a) 模型返回含事实内分钟与年份的正常文本 → 采用,末尾拼上 `adoptCue`;(b) 模型返回含 `04:58`(不在事实里)→ 整段丢弃,返回模板;(c) 模型返回以「?」结尾 → 丢弃;(d) 模型抛错 / abort → 模板,且不抛出到 route;(e) 三条入口各一条 route 级测试,断言只在 `ready_to_adopt` 首轮调用一次模型,采用后再刷新不再调用。
|
||||
- 同一 fixture 走 `applyCollectFocusDenial`(family 采集焦点 + 否定)得到同样旁白。
|
||||
- `rectification-server-focus`:`persistableFocusDomain("appearance") === null`、`("horary") === null`、`("nakshatra") === null`;对 `domain="appearance"` 的 distinguish followup(无 frame)调用 `persistServerOwnedFocus` 返回 `skipped` 且零次 RPC。
|
||||
- `rectification-collect-stall` 的「live five-evidence case」fixture 补上 nakshatra 探针与 `window_scan`,原断言不变仍需通过(这是回归锁)。
|
||||
@@ -84,7 +95,7 @@
|
||||
|
||||
## 5. 让步顺序
|
||||
|
||||
1. P0 全做。若 4.1.4 的停止原因句与 BUG-503 已加的 `stopReasonPrefix` 排版冲突,先保留 BUG-503 的原因句,新句放其后,不得删任何一句。
|
||||
1. P0 全做。若 4.1.4 的旁白 Agent 本轮做不完,最低交付是模板句补上停止原因(4.1.4 里「模板本身也要能解释」那一半),并在 BUG-516 条目与进度记录里写明「Agent 旁白未做」,不得把模板版写成完成。模板句若与 BUG-503 已加的 `stopReasonPrefix` 排版冲突,先保留 BUG-503 的原因句,新句放其后,不得删任何一句。
|
||||
2. P2 若时间不够可拆成 `-fix` 单独做,但 BUG-517 条目必须以 `investigating` 状态先写进 `docs/BUG_HISTORY.md`。
|
||||
3. 任何一步需要放宽采用门 / 确认门才能通过测试,立即停手写 `BLOCKED.md`。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user