docs(rectification): add uncertainty-stop task brief — one unsure answer must not end the interview (BUG-503)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# 任务书 · 一道题选「一时说不好」就交付:不确定度停止规则缺样本下限(2026-09-03)
|
||||
|
||||
基线:`origin/staging` `1d77b5b1`。本单独立于 `TASK-rectification-adopt-flow-fix-20260903.md`(BUG-501/502),但建议由同一实现者**串行**做:若 `codex/rectification-adopt-flow-fix-20260903` 尚未合入 staging,就在该分支之上继续,避免 `docs/BUG_HISTORY.md` 与测试文件冲突。
|
||||
|
||||
## 0. 真实环境复现(用户 09-03 反馈,事实)
|
||||
|
||||
用户走完开场、给了几件带日期的事之后,**整场只收到一道选择题**:
|
||||
|
||||
> 说到这几段短暂的恋情,回想一下你自己在亲密关系里更接近哪一种相处方式?
|
||||
> A. 把日子过稳当,也在意关系里的安全感 / B. 爱聊、爱换话题,新鲜感一过就容易飘 / C. 把对方当家人养,情绪来了也藏不住 / D. 一时说不好
|
||||
|
||||
用户选 **D**。系统回复"已记录你的选择,并更新了候选比较。"随后直接交付:
|
||||
|
||||
> 已经从最初的 60 分钟收到 14:02–14:43 这 41 分钟。代表分钟是 14:43。当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。
|
||||
|
||||
用户原话:"41 分钟跟没做有什么区别"、"全程就反馈给我一个问题,我选择了'一时说不好'就直接给我定了时间"。
|
||||
|
||||
## 1. 根因(静态核对,不是猜)
|
||||
|
||||
`frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts` `uncertaintyHighFromAnswers`(~529):
|
||||
|
||||
```ts
|
||||
const userAnswers = answers?.filter((item) => item.classified_from === "choice" || item.classified_from === "declined") ?? [];
|
||||
if (userAnswers.length === 0) return false;
|
||||
const uncertain = userAnswers.filter((item) => item.answer_class === "unsure" || item.classified_from === "declined").length;
|
||||
return uncertain >= Math.ceil(userAnswers.length / 2);
|
||||
```
|
||||
|
||||
**没有样本下限**:1 道题选 D → `1 >= ceil(1/2) = 1` → `userUncertaintyHigh = true` → `core/rectification-decision.ts` `classifyStop` 返回 `{ kind: "exhausted", reason: "user_uncertainty_too_high" }`(~214)→ `decideRectification` 在 `!separation.sufficient` 之前就命中 `completeWithRange(…, "exhausted")`(~297)→ `canAdopt` 为真时 `sessionOutcome = adopt_representative` → `answer-choice.ts` `persistApplied` 用 `deliveryAdoptNarration` 出交付话术,`shouldContinueAfterStructuredChoice` 因 nextAction 不是 `ask_*` 而不续跑。**第一道题选"说不好"就等于按了"先这样"。**
|
||||
|
||||
同一方向还有一条会在第二道题触发:`apply-probe-outcome.ts` 把 `unsure` 记成 `low_information` 轮(~55),`decisionBudgetFromInference` 连续 `low_information` 计入 `plateauRounds`,`maxPlateauRounds = 2`(`references/rectification_policy.v1.json`)→ `budgetExhausted` → 同样 `completeWithRange`。即**连着两道选 D 也立刻交付**。
|
||||
|
||||
这条规则来自 `TASK-rectification-convergence-20260830.md` 任务 6 引入的上游 Stop Rule "user uncertainty is too high to map support/conflict reliably"(`3a4396a4`)。上游语义是"样本够多且大半答不上来",本仓实现成了"任意一道答不上来"。
|
||||
|
||||
两个旁证:
|
||||
- D 答案 `deltas` 全 0,却仍走 `appliedInference: true` 的旁白"已记录你的选择,并更新了候选比较。"——没更新任何比较。
|
||||
- 交付话术不说明**为什么停**(`user-copy.ts` `nonConvergingRangeNarration` / `deliveryAdoptNarration` 不接 `stopReason`),用户只看到"收到 41 分钟",不知道是自己的"说不好"把访谈停掉的。
|
||||
|
||||
## 2. 修法
|
||||
|
||||
### 2.1 P1 · 不确定度停止加样本下限(BUG-503)
|
||||
|
||||
1. `references/rectification_policy.v1.json` 增加 `"minUncertaintyAnswers": 3`(`scripts/rectification_policy.py` 读同一文件,加键不影响它;Python 侧不必读这个键)。
|
||||
2. `uncertaintyHighFromAnswers`:`userAnswers.length < RECTIFICATION_POLICY.minUncertaintyAnswers` → 返回 `false`;达到下限后沿用现有"≥ 一半"判定。`classified_from === "declined"` 继续计入不确定,但同样受下限约束。
|
||||
3. 平台期:`decisionBudgetFromInference` 的 `plateauRounds` 只在 `userAnswers.length >= minUncertaintyAnswers` 后才对 `unsure` 轮计数——实现方式二选一,实现者定并写进 BUG 条目:(a) 在 `decisionBudgetFromInference` 里按 `answered_probes` 的用户答题数做同样的下限门;(b) `apply-probe-outcome.ts` 对 `unsure` 返回新 kind `"unsure"`,`plateauRounds` 只数 `low_information`(即真正"答了但分不开"的轮),并把 `ROUND_KINDS` / `compose-receipt.ts` / `core/types.ts` 同步。**不得**改 `maxPlateauRounds = 2` 本身。
|
||||
4. `answer-choice.ts` `composeChoiceNarration`:`answer_class === "unsure"` 时旁白改为"已记录。这题先不计分,换一件事问。"——`choice-action.ts` 加 `unsure` 分支,`appliedInference` 语义不变(仍持久化 inference 行,只是话术不说"更新了比较")。
|
||||
|
||||
### 2.2 P2 · 交付时说出停止原因
|
||||
|
||||
`persistApplied` 的 `adoptionNarration` / `completedRangeNarration` 在 `stopReason` 非空时,在 `progressClause` 之前加一句原因(放 `user-copy.ts`,进 `listUserVisibleCopy`):
|
||||
- `user_uncertainty_too_high`:`前面几道题你多半选了"说不好",再问下去也分不开,先停在这里。`
|
||||
- `tied_first`:`几个候选打成平手,问题已经分不开它们。`
|
||||
- `insufficient_dated_events` / `insufficient_domains` 走的是 `collect`,不在此列。
|
||||
`decisionReceipt` 已有 `stopReason`,前端不需要新字段;`nextAction` 公开 `stop_reason` 若尚未暴露则加上(只读字段,`publicNextAction`)。
|
||||
|
||||
## 3. 硬红线
|
||||
|
||||
1. 采用门 / 确认门语义不动:`publicCanAdopt`、`sessionOutcomeAllowsAdopt`、确认门 fail-closed 一律不改;本单只改"什么时候算 exhausted"。
|
||||
2. `maxPlateauRounds`、`DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8`、`EFFECTIVE_ANSWER_SAFETY_CAP = 10` 三个外层熔断不动。
|
||||
3. 用户主动「先这样」(`userStopped`)仍立即交付,不受本单影响。
|
||||
4. 选项、answer_class、计分服务端所有;不改 Python 引擎;Skill 10.0.14 不 bump。
|
||||
5. `./node_modules/.bin/tsc --noEmit` 通过(不用 `npx tsc`);`npm run lint --prefix frontend` 0 错误;`rectification-*` / `consultation-*` / `consult-*` / `chat-*` 测试 fail=0;改动的既有断言逐条三栏说明(旧 → 新 → 保留语义)。
|
||||
6. 无凭据不得声称已真实环境验证。
|
||||
|
||||
## 4. 测试
|
||||
|
||||
- `rectification-convergence-budget.test.ts` "dossier and post-inference decisions share the half-uncertain stop rule"(~338):现在的 `halfUncertain`(2 答 1 unsure)与 `halfDeclined`(2 答 1 declined)断言 `user_uncertainty_too_high` → 改为 `stopReason ?? null === null`(旧:2 答 1 不确定即停 → 新:不足 3 答不停 → 保留语义:**达到下限后**一半不确定仍停)。补三条:1 答 1 unsure → 不停且 nextAction 仍是 `ask_candidate_discriminator`(给一个可用 probe);3 答 2 unsure → `user_uncertainty_too_high`;4 答 2 unsure → 停(一半规则不变)。`evidenceDoesNotDilute` 用例改成 3 答 2 unsure + 3 条 evidence 行,语义保留(evidence 行不稀释)。
|
||||
- 平台期:`rectification-inference-machine.test.ts` 或新用例——连续 2 道 unsure、用户答题数 < 3 → `budgetExhausted` 为假、决策仍出 probe;答题数 ≥ 3 后连续 2 道 unsure → 停。
|
||||
- `rectification-answer-choice.test.ts`:unsure 的旁白为"已记录。这题先不计分,换一件事问。",`appliedInference` 仍为 true。
|
||||
- 用户可见文案:新增两句进 `listUserVisibleCopy`,`rectification-user-copy` 相关测试(含"不得/请写成"指令腔过滤)通过。
|
||||
- 用 §0 场景写一条端到端形状测试(fake accounting):3 条带日期证据、1 个可用 probe、用户答 D → 返回体 `nextAction.type === "ask_candidate_discriminator"` 或至少不是 `adopt_representative`,且持久化了下一道题。
|
||||
|
||||
## 5. 开工前置
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
# 若 codex/rectification-adopt-flow-fix-20260903 未合入,则以它为起点:
|
||||
git worktree add -b codex/rectification-uncertainty-stop-20260903 \
|
||||
../.worktrees/rectification-uncertainty-stop-20260903 origin/codex/rectification-adopt-flow-fix-20260903
|
||||
# 否则以 origin/staging 为起点。
|
||||
```
|
||||
|
||||
读 `TASK-rectification-convergence-20260830.md` 任务 6(停止规则来源)、`references/upstream/evidence_thresholds.md`、`docs/BUG_HISTORY.md`(BUG-463 起的收敛链)。BUG 编号 **BUG-503**(复发自:无;引入于 `3a4396a4`);§2.2 可并入 503 或独立成条。**行号是线索,按符号名定位。**
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
1. §0 形状:1 道题选 D → 不交付,继续出下一道题;连续 2 道 D 且答题数 < 3 → 仍不交付。
|
||||
2. 3 道题 2 道 D → 交付,且交付话术第一句说明"多半选了说不好"。
|
||||
3. D 答案旁白不再说"更新了候选比较"。
|
||||
4. `minUncertaintyAnswers` 只在 `references/rectification_policy.v1.json` 定义一处。
|
||||
5. tsc + lint + 四组测试 fail=0;BUG_HISTORY 追加 BUG-503;三栏说明齐。
|
||||
6. 真实环境人工清单(部署后):开场给 3 件带日期的事 → 第一道选择题选 D → 应继续出题而不是交付;连选 3 道 D → 交付并写明原因。
|
||||
Reference in New Issue
Block a user