fix(rectification): give the opening collect question concrete examples
The other-domain fallback copy was being used as the first question. Opening now uses GENERIC_COLLECT_QUESTION; after-evidence other copy stays. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7761,3 +7761,19 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-504 | 开场采集题用「再说一件」措辞,用户不知从何说起
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-03
|
||||
- 最近更新:2026-09-03
|
||||
- 影响面:`spokenFollowupForUser`、`GENERIC_COLLECT_QUESTION`
|
||||
- 用户现象:新用户开场招呼后看到「也可以再说一件你记得大概时间的事。」不知道该写什么;招呼段按设计不提问、不举例。
|
||||
- 触发条件:零证据开场,`OPENING_COLLECT_DOMAIN = other`,followup `source=method_coverage`。
|
||||
- 根因:`e8c98c37` 把开场 domain 改成 `other` 后,`spokenFollowupForUser` 按 domain 查 `USER_COLLECT_QUESTION`。`other` 是给已有证据后的补问兜底(「也可以再说一件」),真正开场文案 `GENERIC_COLLECT_QUESTION` 只在 domain 查不到时才用。domain→文案查表把兜底文案当开场。
|
||||
- 修复:`source === method_coverage && domain === other && collect_retry !== true` 时用 `GENERIC_COLLECT_QUESTION`。文案补上「一件事」和「哪年搬的家」。`oos_blind` 兜底与 `collect_retry` 的 `other` 仍用原补问。不改 `OPENING_COLLECT_DOMAIN` / questionId / 记账。
|
||||
- 验证:`rectification-server-focus` 零证据开场 spoken prompt 等于 `GENERIC_COLLECT_QUESTION`,不含「也可以再」,含「比如」;exhaustion `oos_blind` 与 `collect_retry` 仍走 `USER_COLLECT_QUESTION.other` / retry。源码锁至少两个具体例子。
|
||||
- 防复发:开场题文案必须带具体例子;`USER_COLLECT_QUESTION.other` 只用于已有证据后的补问。
|
||||
- 相关记录:BUG-501
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export type CollectDomain =
|
||||
| "health_pressure"
|
||||
| "other";
|
||||
|
||||
export const GENERIC_COLLECT_QUESTION = "从你最容易想起来的开始就好——比如哪年上的大学、哪年换的工作,记得大概年份就行。";
|
||||
export const GENERIC_COLLECT_QUESTION = "从你最容易想起来的一件事开始就好——比如哪年上的大学、哪年换的工作、哪年搬的家,记得大概年份就行。";
|
||||
|
||||
export const USER_COLLECT_QUESTION: Readonly<Record<string, string>> = {
|
||||
relationship: "感情这边,还记得哪年认真在一起、分开,或结婚吗?有年份就很好。",
|
||||
|
||||
@@ -978,9 +978,14 @@ export function spokenFollowupForUser(followup: MethodFollowup | null): string |
|
||||
if (followup.choice_frame) return serverOwnedChoiceCopy(followup.choice_frame)?.prompt ?? null;
|
||||
if (followup.intent !== "collect_method_evidence") return null;
|
||||
const domain = followup.domain ?? "";
|
||||
const openingOther = followup.source === "method_coverage"
|
||||
&& domain === "other"
|
||||
&& followup.collect_retry !== true;
|
||||
const base = followup.collect_retry === true
|
||||
? (USER_COLLECT_QUESTION_RETRY[domain] ?? USER_COLLECT_QUESTION[domain] ?? GENERIC_COLLECT_QUESTION)
|
||||
: (USER_COLLECT_QUESTION[domain] ?? GENERIC_COLLECT_QUESTION);
|
||||
: openingOther
|
||||
? GENERIC_COLLECT_QUESTION
|
||||
: (USER_COLLECT_QUESTION[domain] ?? GENERIC_COLLECT_QUESTION);
|
||||
const period = followup.year_label
|
||||
?? (followup.probe_year && followup.probe_year > 0 ? `${followup.probe_year} 年前后` : null);
|
||||
return period ? `${period},${base}` : base;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
@@ -13,10 +14,16 @@ import {
|
||||
import { buildChoiceFrame, serverOwnedChoiceCopy } from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||||
import {
|
||||
buildMethodFollowupPlan,
|
||||
exhaustionSpokenCollectFollowup,
|
||||
spokenCollectFallbackFollowup,
|
||||
spokenFollowupForUser,
|
||||
type MethodFollowup,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import {
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
USER_COLLECT_QUESTION,
|
||||
USER_COLLECT_QUESTION_RETRY,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import type { EventProbeStyleOption } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import { fakeAccounting, CASE_ID, FOCUS_ID, USER_ID, activeFocusFixture } from "./rectification-v9-test-support.ts";
|
||||
|
||||
@@ -512,8 +519,14 @@ test("zero-evidence opening persists the server-owned first question", async ()
|
||||
// 新:开场是「随便一件带时间的经历」,domain = other。
|
||||
// 保留语义:仍是 collect_spoken,仍由服务端 persist,Agent 不自拟开场题,id 不含 unknown。
|
||||
assert.equal(followup.domain, "other");
|
||||
assert.equal(followup.source, "method_coverage");
|
||||
assert.doesNotMatch(stableFollowupQuestionId(followup), /unknown/);
|
||||
const expected = spokenFollowupForUser(followup);
|
||||
assert.equal(stableFollowupQuestionId(followup), "collect:other:collect_method_evidence");
|
||||
// 旧:开场 domain=other 后查表落到「也可以再说一件」→ 新:method_coverage + other 且非 retry 用 GENERIC_COLLECT_QUESTION → 保留 开场题由服务端出、带具体例子
|
||||
assert.equal(spokenFollowupForUser(followup), GENERIC_COLLECT_QUESTION);
|
||||
assert.doesNotMatch(spokenFollowupForUser(followup) ?? "", /^也可以再/);
|
||||
assert.match(spokenFollowupForUser(followup) ?? "", /比如/);
|
||||
const expected = GENERIC_COLLECT_QUESTION;
|
||||
|
||||
const accounting = fakeAccounting({
|
||||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||||
@@ -551,6 +564,58 @@ test("zero-evidence opening persists the server-owned first question", async ()
|
||||
openKind: "collect_spoken",
|
||||
openPrompt: expected,
|
||||
});
|
||||
assert.equal(persisted.focus?.targetDomain, "other");
|
||||
});
|
||||
|
||||
test("exhaustion other fallback and other retry keep the after-evidence copy", () => {
|
||||
const dated = (domain: string, year: string, extra: { eventKind?: string } = {}) => ({
|
||||
status: "confirmed" as const,
|
||||
domain,
|
||||
datePrecision: extra.eventKind ? "unknown" as const : "year" as const,
|
||||
occurredFrom: extra.eventKind ? null : `${year}-01-01`,
|
||||
occurredTo: null,
|
||||
...(extra.eventKind ? { eventKind: extra.eventKind } : {}),
|
||||
});
|
||||
const exhausted = exhaustionSpokenCollectFollowup({
|
||||
evidence: [
|
||||
dated("education", "2016"),
|
||||
dated("career", "2020"),
|
||||
dated("relationship", "2018"),
|
||||
dated("finance", "2024"),
|
||||
dated("relocation", "2022"),
|
||||
dated("health_pressure", "2021"),
|
||||
dated("occupation", "2020", { eventKind: "occupation_note" }),
|
||||
],
|
||||
declinedTopics: [{ target_domain: "family", status: "declined" }],
|
||||
});
|
||||
assert.equal(exhausted?.domain, "other");
|
||||
assert.equal(exhausted?.source, "oos_blind");
|
||||
assert.equal(spokenFollowupForUser(exhausted), USER_COLLECT_QUESTION.other);
|
||||
|
||||
const retry: MethodFollowup = {
|
||||
method_id: "dasha_events",
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: "dated_event",
|
||||
domain: "other",
|
||||
kind_hint: null,
|
||||
user_prompt_hint: "retry",
|
||||
must_not_label: false,
|
||||
choice_frame: null,
|
||||
source: "method_coverage",
|
||||
collect_retry: true,
|
||||
};
|
||||
assert.equal(spokenFollowupForUser(retry), USER_COLLECT_QUESTION_RETRY.other);
|
||||
});
|
||||
|
||||
test("GENERIC_COLLECT_QUESTION keeps concrete opening examples", () => {
|
||||
const source = readFileSync(new URL("../src/lib/rectification-agentic/user-copy.ts", import.meta.url), "utf8");
|
||||
assert.match(GENERIC_COLLECT_QUESTION, /比如/);
|
||||
assert.ok(
|
||||
[...GENERIC_COLLECT_QUESTION.matchAll(/上的大学|换的工作|搬的家/g)].length >= 2,
|
||||
GENERIC_COLLECT_QUESTION,
|
||||
);
|
||||
// 旧:「…开始就好——比如哪年上的大学、哪年换的工作…」→ 新:加「哪年搬的家」→ 保留 开场题必须带具体例子、由服务端出
|
||||
assert.match(source, /GENERIC_COLLECT_QUESTION = "从你最容易想起来的一件事开始就好/);
|
||||
});
|
||||
|
||||
test("degraded spoken collect does not keep discriminator identity or block a later card", async () => {
|
||||
|
||||
Reference in New Issue
Block a user