From 61d6f248e4acb38da9d30d9f91f0ce02b965d049 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 7 Oct 2026 11:08:25 +0800 Subject: [PATCH] feat(rectification): warm older-sister voice; opening and read-only turns get only the Skill sections they use (BUG-1257) Both rectification prompts now share RECTIFICATION_VOICE_HEAD, the same warm voice as the consultation answer (no mind-reading here: these turns collect facts). Opening and read-only turns used to send the whole bound Skill (11,812 characters for 10.0.33); they now send the sections they act on, plus the product-identity section when the bound Skill has it. 9.0.0 still goes out whole (BUG-621). Rule bodies, scoring, cards and adoption are unchanged. Opening input 7,433 -> 3,892 tokens in a DeepSeek replay. Full suite 4970 / fail 24, identical to c76fdda7; build keeps / Static. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8 --- CHANGELOG.md | 6 ++++ docs/BUG_HISTORY.md | 15 ++++++++ docs/tasks/README.md | 1 + docs/testing/consult-voice-sister-20261007.md | 8 +++++ .../rectification-agentic/v9/skill-slice.ts | 35 +++++++++++++++---- frontend/src/mastra/agentic-rectification.ts | 15 +++++--- ...fication-grounding-prompt-20260927.test.ts | 23 ++++++++++-- frontend/tests/rectification-v9-agent.test.ts | 8 ++++- 8 files changed, 97 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a1aed3..3ace78be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-10-07 — 生时校正也换成知心姐姐口吻,开场更快(未上线) + +- 生时校正的说话口吻和普通对话统一:温和、不催;你记不清的时候不评判,也不说教。 +- 开场和查看结果这两类回合,只把校正方法里用得上的章节交给模型,开场输入少约一半(BUG-1257)。 +- 校正打分、出结果、采用规则都不变;Skill 版本不变;不改数据库。 + ## 2026-10-07 — 普通对话每轮带给模型的规则减半,回答更快(未上线) - 每轮发给模型的固定规则从约 5.5 万字符减到约 2.7 万:同一段回答形状不再重复 4 遍;占星方法书只带诚实边界几节,不再带引擎跑分、脚本命令和写给报告的输出格式。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b7f4b3e0..98f82672 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16823,3 +16823,18 @@ - 防复发:测试锁形状摘要只在合同出现、方法块不再含共享方法且小于 4,000 字符、方法块不含「强制工作流 / 五层硬约束 / MEVG / Transit Actionable Output」等章节、边界要点存在。 - 相关记录:BUG-1253(清单原句被念给用户,同属「给模型分析用的文字进了说话层」)、BUG-1255。 - 修复版本:staging `c76fdda7`(2026-10-07 09:08 部署核对:health gitCommit=c76fdda7、各项 ok,`/api/account` 401,`/login` 200;门禁 run 1753 一次通过)。真机未走,状态保持 fixed-pending-verify + +## BUG-1257 | 生时校正口吻与普通对话不一致;开场 / 只读轮整本 Skill 进模型 + +- 状态:fixed-pending-verify(分支 `codex/rectification-voice-slim-20261007`;部署与真机前不标 resolved) +- 首次发现 / 最近更新:2026-10-07 / 2026-10-07 +- 来源:产品要求把生时校正也按普通对话的方式优化、口吻统一由 Claude 直接做。 +- 影响面:`mastra/agentic-rectification.ts` 两版系统提示的开头;`v9/skill-slice.ts` 的按轮次切片。不改校正打分、出卡、采用门、工具、服务器写的固定文案、Skill 文件。 +- 现象:普通对话口吻已改为知心姐姐(BUG-1255),校正仍是「懂行、可靠、说人话」;两版系统提示各写一份相同的身份与口吻。开场轮、只读轮把整本 Skill(10.0.33 为 11,812 字符)发给模型,含长会话摘要字段说明(只在 full_diagnostics 里有)、上游同步、触发条件等本轮用不上的章节。 +- 根因:证据轮切片(TASK-rectification-grounding-20260927 T6)只覆盖了证据轮;口吻随普通对话改动时校正侧未同步。 +- 修复:身份与口吻提为共用的 `RECTIFICATION_VOICE_HEAD`(知心姐姐;不催、不评判记不清,「记不清也没关系」最多一句;不说教、无语气词;校正不读心)。开场轮只附「必须先读、OpeningPolicy、ConversationFocus、可调用工具」,只读轮只附「必须先读、Case 状态、可调用工具、候选语言、输出与停止」,两者有「持久产品身份」章节时一并附上(可选章节缺失不迫使发全文);必需章节缺失的旧版(9.0.0)照旧整本发送(BUG-621)。按分钟 / 按盘型两版规则正文不动。 +- 验证:全部 35 个历史 Skill 版本切片检查:10.0.x 全部按章节切,9.0.0 整本;10.0.33 开场 11,812 → 4,576、只读 → 6,571 字符。DeepSeek 模拟(deepseek-flash,手工构造的工具结果,不入库):开场轮输入 7,433 → 3,892 token;证据轮基本不变;「记不清」场景两版都温和、无语气词;开场重复题干例子改前 1/7、改后 1/7,判为模型随机性。全量 4970 项 fail 24,与 `c76fdda7` 逐条一致;tsc 0、lint 0 error;build `/` Static,首屏 gzip 不变。改断言 2 个文件(三栏写在测试里)。 +- 防复发:测试锁两版共用口吻头、开场 / 只读切片章节与长度、旧版整本发送。 +- 相关记录:BUG-1255(普通对话口吻)、BUG-1256(普通对话提示词精简)、BUG-621(历史版本须可打开)。 +- 未做:每轮强制第一步 `rectification-read-case`(一次额外的模型调用)——改流程,待产品给真实步耗时数据后另定;服务器写的固定文案(如「已记录。这题先不计分,换一件事问。」)口吻未改。 +- 修复版本:待发布 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 316962b2..e36b92e7 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -430,3 +430,4 @@ | —(直接执行,无任务书) | —(记录在 BUG-1254) | **门禁偶发红:数据库测试排队等槽位超时**:46 个 fixture 文件抢 2 个槽位、固定等 5 分钟,排在后面的随机失败;改为槽位易手即续期、连续 10 分钟无进展才失败(BUG-1254) | 已验收 | Claude 直接执行;门禁实际效果待看 | | —(直接执行,无任务书) | —(记录在 BUG-1255) | **回答口吻改知心姐姐、放开读心**(10-07 产品决定):人设统一;读心守三条(先答再读、可能/多半一句、当事人要有依据且未受冲);删冲突的旧规矩。推翻 BUG-1070 读心部分 | 已验收 | Claude 直接执行;真机清单 `docs/testing/consult-voice-sister-20261007.md` 待产品 | | —(直接执行,无任务书) | —(记录在 BUG-1256) | **普通对话提示词减半**(10-07 产品授权 + 临时 key 对比):形状说明 4 份合 1、方法书只摘诚实边界、共享方法不再进聊天、边界要点改写补回;系统提示 55,242 → 26,591 字符,模型对比无退化 | 已验收 | Claude 直接执行;对比记录 `docs/testing/prompt-slim-20261007-model-runs.md` | +| —(直接执行,无任务书) | —(记录在 BUG-1257) | **生时校正口吻统一 + 开场 / 只读轮按章节附 Skill**:两版系统提示共用知心姐姐口吻头;开场 11,812 → 4,576 字符;读档案那一步(每轮多一次模型调用)待真实步耗时数据后另定 | 已验收 | Claude 直接执行 | diff --git a/docs/testing/consult-voice-sister-20261007.md b/docs/testing/consult-voice-sister-20261007.md index a2c52fdd..70039085 100644 --- a/docs/testing/consult-voice-sister-20261007.md +++ b/docs/testing/consult-voice-sister-20261007.md @@ -10,3 +10,11 @@ | 4 | 问「我今年事业是不是很差」 | 不顺照实说,但用「可能 / 会累一点」,不说「注定 / 很差 / 没戏」 | | 5 | 追问「你在说什么鬼」 | 不顶回来,换个说法再讲一遍要点 | | 6 | 打「你好」 | 仍是一句寒暄 | + +## 生时校正(BUG-1257,同批口吻统一) + +| 步 | 怎么做 | 通过标准 | +| --- | --- | --- | +| 7 | 开一次新的生时校正 | 开场两句话温和、不催;没有「呀、哦、亲爱的」;开场比以前出来得快 | +| 8 | 被问到某件事的年份时回「记不清了」 | 不评判、不说教,最多一句「记不清也没关系」,然后换一题 | +| 9 | 从历史里打开一次很早以前的校正 | 能正常打开、能接着看结果(旧版本照旧整本发送) | diff --git a/frontend/src/lib/rectification-agentic/v9/skill-slice.ts b/frontend/src/lib/rectification-agentic/v9/skill-slice.ts index ba9b0188..13b8cfd2 100644 --- a/frontend/src/lib/rectification-agentic/v9/skill-slice.ts +++ b/frontend/src/lib/rectification-agentic/v9/skill-slice.ts @@ -18,18 +18,36 @@ */ import type { RectificationAgentAction } from "./step-budget"; -/** Heading titles per action; evidence = §5 and §7 of the 10.0.x layout. */ +/** + * Heading titles per action. Evidence = §5 and §7 of the 10.0.x layout. + * Opening and read-only used to send the whole body (11.8K characters for + * 10.0.33); they now send the sections they act on (BUG-1257). Never sent to a + * turn: §1 trigger / method ownership, §6 the long-session summary (its fields + * exist only in full_diagnostics), §11 upstream sync. The agent never runs + * rescore / accept / confirm as its own action (the route maps every typed + * message to evidence), so those keep the whole body. + */ export const RECTIFICATION_SKILL_SECTIONS_BY_ACTION: Readonly< Record > = { - opening: "all", - read_only: "all", + opening: ["必须先读与服务器权威", "OpeningPolicy", "ConversationFocus 与意图承接", "可调用工具与输入边界"], + read_only: ["必须先读与服务器权威", "Case 状态与只读边界", "可调用工具与输入边界", "candidate / accepted / confirmed 语言边界", "输出与停止条件"], evidence: ["ConversationFocus 与意图承接", "批量证据与日期真实性"], rescore: "all", accept: "all", confirm: "all", }; +/** + * Sent ahead of the wanted sections when the bound Skill has them; a missing + * optional section never forces the whole body. The product-identity section + * exists from 10.0.33 on and carries a version suffix in its title. + */ +const OPTIONAL_SECTION_PREFIXES_BY_ACTION: Readonly>> = { + opening: ["持久产品身份与盘型交付"], + read_only: ["持久产品身份与盘型交付"], +}; + export type RectificationSkillSlice = Readonly<{ text: string; /** Heading lines sent, or "all" when the whole body went out. */ @@ -58,11 +76,16 @@ export function sliceRectificationSkill( if (sections.length > 0) sections.at(-1)!.lines.push(line); else preamble.push(line); } - const picked = wanted.map((title) => sections.find((section) => section.title === title)); - if (picked.some((section) => !section)) return { text: instructions, sections: "all" }; + const required = wanted.map((title) => sections.find((section) => section.title === title)); + if (required.some((section) => !section)) return { text: instructions, sections: "all" }; + const optional = (OPTIONAL_SECTION_PREFIXES_BY_ACTION[action] ?? []) + .map((prefix) => sections.find((section) => section.title.startsWith(prefix))) + .filter((section): section is { title: string; lines: string[] } => Boolean(section)); + const picked = [...optional, ...required]; const title = preamble.find((line) => line.startsWith("# ")) ?? ""; const headings = picked.map((section) => section!.lines[0]!.trim()); - const note = `(本轮是证据轮:以下只附本轮适用的 Skill 章节「${picked.map((section) => section!.title).join("」「")}」;其余章节不适用于本轮,按服务器投影与工具说明执行。)`; + const turnName = action === "evidence" ? "证据轮" : action === "opening" ? "开场轮" : "只读轮"; + const note = `(本轮是${turnName}:以下只附本轮适用的 Skill 章节「${picked.map((section) => section!.title).join("」「")}」;其余章节不适用于本轮,按服务器投影与工具说明执行。)`; const body = picked.map((section) => section!.lines.join("\n").trim()).join("\n\n"); return { text: [title, note, body].filter(Boolean).join("\n\n"), diff --git a/frontend/src/mastra/agentic-rectification.ts b/frontend/src/mastra/agentic-rectification.ts index c5f2500a..d834ddfe 100644 --- a/frontend/src/mastra/agentic-rectification.ts +++ b/frontend/src/mastra/agentic-rectification.ts @@ -35,8 +35,16 @@ export { * jyotish-birth-time-rectification Skill; this prompt must never re-implement * gate → scan → score → diagnostics. */ -const agenticRectificationInstructions = `你是 Jyotisha,只服务当前绑定 jyotish-birth-time-rectification Skill 的生时校正 Case。方法以绑定 Skill 为准,不在系统提示中重写。 -懂行、可靠、说人话:先回应人,再展开方法。范围由系统说:范围变化、没重新比较、比较没跑成这几句由服务器接在你的正文后面;除出牌轮外你不写时刻(HH:MM)、时间区间和百分比;任何一句里的时刻或百分比与服务端当轮事实不符,整句不会落库。下一问是采集题时,先承接用户刚说的事;进度数字只用 collection_progress,没有该字段不说进度。不得写「范围在收窄」「继续收窄」这类没有数字的进度句。 +/** + * Identity and voice shared by both rectification prompts (BUG-1257). The voice + * is the same warm, knowing older sister as the consultation answer (BUG-1255); + * it was 「懂行、可靠、说人话」 here while the chat side changed. Mind-reading is + * not part of rectification: these turns collect and confirm facts. + */ +const RECTIFICATION_VOICE_HEAD = `你是 Jyotisha,只服务当前绑定 jyotish-birth-time-rectification Skill 的生时校正 Case。方法以绑定 Skill 为准,不在系统提示中重写。 +像一个懂行、说话温和的姐姐:先回应人,再展开方法;语气放软,但不含糊。用户想不起来、说不准时不催、不评判,「记不清也没关系」这类话最多一句;不说教,不写「呀」「哦」「抱抱」「宝」「亲爱的」,不叠字,不卖萌,不用表情。`; + +const agenticRectificationInstructions = `${RECTIFICATION_VOICE_HEAD}范围由系统说:范围变化、没重新比较、比较没跑成这几句由服务器接在你的正文后面;除出牌轮外你不写时刻(HH:MM)、时间区间和百分比;任何一句里的时刻或百分比与服务端当轮事实不符,整句不会落库。下一问是采集题时,先承接用户刚说的事;进度数字只用 collection_progress,没有该字段不说进度。不得写「范围在收窄」「继续收窄」这类没有数字的进度句。 1. 第一步调用 rectification-read-case。服务器是事实、焦点、权限与终态的唯一权威。 2. 事实只能来自用户原话;复述日期必须用 display_date_label。不得虚构事件、候选或出生分钟。 3. 新事件走 rectification-record-evidence-batch。用户更正之前说过的某件事(如「刚才 2028 打错了,是 2018」)走 rectification-revise-evidence,evidenceId 取 relevant_evidence_summary 里那一条的 evidence_id,不要再记一条新的。工具执行保持静默;思考用简体中文写在思维链;对用户说的话必须自己写在正文里,不叙述工具或内部状态。 @@ -46,8 +54,7 @@ const agenticRectificationInstructions = `你是 Jyotisha,只服务当前绑 坏:「记下了:2016 年 9 月入学、2020 年 6 月毕业、2021 年换工作……」(逐条复述是服务器的事) 坏:「范围还在收。」好:「2016 年 9 月入学记下了。还有吗?比如第一份工作、搬到别的城市。」`; -const segmentProductInstructions = `你是 Jyotisha,只服务当前绑定 jyotish-birth-time-rectification Skill 的生时校正 Case。方法以绑定 Skill 为准,不在系统提示中重写。 -懂行、可靠、说人话:先回应人,再展开方法。服务器持久产品身份是唯一交付口径;目标是判断用哪张盘解读,不承诺缩到某一分钟。没有摘要仍是新产品,不能退回分钟优先。 +const segmentProductInstructions = `${RECTIFICATION_VOICE_HEAD}服务器持久产品身份是唯一交付口径;目标是判断用哪张盘解读,不承诺缩到某一分钟。没有摘要仍是新产品,不能退回分钟优先。 1. 第一步调用 rectification-read-case。服务器是事实、焦点、权限与终态的唯一权威;只解释当前服务器 product_delivery 与 segment_summary,不从历史正文或代表候选猜盘型。 2. 事实只能来自用户原话;日期只用 display_date_label。新事件走 rectification-record-evidence-batch;工具执行保持静默,对用户说的话必须自己写在正文里。 3. 用 rectification-set-focus 的 spokenPrompt 写出服务端给你的下一问:问同一个年份/期间、同一个事件家族,不改选项、不合并两题。正文只承接,不提问、不复述题干。没有下一问就不自拟问题。职业题只问平时做什么;开始年份只能走服务器 career 锚定题。 diff --git a/frontend/tests/rectification-grounding-prompt-20260927.test.ts b/frontend/tests/rectification-grounding-prompt-20260927.test.ts index ab3ecc83..3ff4d30a 100644 --- a/frontend/tests/rectification-grounding-prompt-20260927.test.ts +++ b/frontend/tests/rectification-grounding-prompt-20260927.test.ts @@ -203,10 +203,25 @@ test("segment-product real model calls keep target identity in the evidence slic test("T6: the slice rule is a code constant; other actions and unknown layouts get the whole Skill", () => { assert.deepEqual(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.evidence, ["ConversationFocus 与意图承接", "批量证据与日期真实性"]); - assert.equal(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.opening, "all"); - assert.equal(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.read_only, "all"); + // 原值: opening / read_only 都是 "all",开场轮发整本 Skill + // 新值: 开场轮只附「必须先读、OpeningPolicy、ConversationFocus、可调用工具」,只读轮只附「必须先读、Case 状态、可调用工具、候选语言、输出与停止」,两者有「持久产品身份」时一并附上 + // 原因: 2026-10-07 校正口吻统一为知心姐姐、开场 / 只读轮只附用得上的 Skill 章节(BUG-1257) + assert.deepEqual(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.opening, ["必须先读与服务器权威", "OpeningPolicy", "ConversationFocus 与意图承接", "可调用工具与输入边界"]); + assert.deepEqual(RECTIFICATION_SKILL_SECTIONS_BY_ACTION.read_only, ["必须先读与服务器权威", "Case 状态与只读边界", "可调用工具与输入边界", "candidate / accepted / confirmed 语言边界", "输出与停止条件"]); const body = activeSkillBody(); - assert.equal(sliceRectificationSkill(body, "opening").text, body); + const opening = sliceRectificationSkill(body, "opening"); + assert.ok(opening.text.length < body.length / 2, `${opening.text.length} of ${body.length}`); + assert.match(opening.text, /本轮是开场轮/); + // Dropped as sections (their names may still be mentioned inside kept text). + for (const dropped of ["CaseConversationSummary 与长会话记忆", "上游同步边界", "触发条件与方法学归属"]) { + const heading = new RegExp(`^## (?:\\d+\\. )?${dropped}`, "m"); + assert.doesNotMatch(opening.text, heading, dropped); + assert.doesNotMatch(sliceRectificationSkill(body, "read_only").text, heading, dropped); + } + if (body.includes("持久产品身份与盘型交付")) { + assert.match(opening.text, /持久产品身份与盘型交付/); + assert.match(sliceRectificationSkill(body, "read_only").text, /持久产品身份与盘型交付/); + } const evidence = sliceRectificationSkill(body, "evidence"); assert.deepEqual(evidence.sections, ["## 5. ConversationFocus 与意图承接", "## 7. 批量证据与日期真实性"]); assert.ok(evidence.text.length < body.length / 2); @@ -222,4 +237,6 @@ test("T6: the slice rule is a code constant; other actions and unknown layouts g // 9.0.0 numbers its sections differently (its §5 is the candidate language): // no title match, so the whole bound body goes out unchanged. assert.equal(sliceRectificationSkill(oldestBody, "evidence").text, oldestBody); + assert.equal(sliceRectificationSkill(oldestBody, "opening").text, oldestBody); + assert.equal(sliceRectificationSkill(oldestBody, "read_only").text, oldestBody); }); diff --git a/frontend/tests/rectification-v9-agent.test.ts b/frontend/tests/rectification-v9-agent.test.ts index 40043be1..14c5fb52 100644 --- a/frontend/tests/rectification-v9-agent.test.ts +++ b/frontend/tests/rectification-v9-agent.test.ts @@ -58,7 +58,10 @@ const agentSource = readFileSync( ); test("system prompt carries only high-priority boundaries, never the method copy", () => { - const promptStart = agentSource.indexOf("const agenticRectificationInstructions"); + // 原值: 从 const agenticRectificationInstructions 起截取 + // 新值: 从共用的 RECTIFICATION_VOICE_HEAD 起截取(身份与口吻两版共用一份) + // 原因: 2026-10-07 校正口吻统一为知心姐姐、开场 / 只读轮只附用得上的 Skill 章节(BUG-1257) + const promptStart = agentSource.indexOf("const RECTIFICATION_VOICE_HEAD"); const promptEnd = agentSource.indexOf("export function getRectificationV9Agent"); const prompt = agentSource.slice(promptStart, promptEnd); assert.doesNotMatch(prompt, /rectification-gate[\s\S]*rectification-scan/); @@ -68,6 +71,9 @@ test("system prompt carries only high-priority boundaries, never the method copy assert.doesNotMatch(prompt, /run the required gate/); assert.doesNotMatch(prompt, /candidate_range/); assert.match(prompt, /jyotish-birth-time-rectification/); + assert.match(prompt, /像一个懂行、说话温和的姐姐/); + assert.doesNotMatch(prompt, /懂行、可靠、说人话/); + assert.equal(prompt.match(/\$\{RECTIFICATION_VOICE_HEAD\}/g)?.length, 2, "both prompts share the head"); assert.match(prompt, /display_date_label/); assert.match(prompt, /rectification-record-evidence-batch/); assert.match(prompt, /不可分区间/);