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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a7fd9abb45
commit
61d6f248e4
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-07 — 生时校正也换成知心姐姐口吻,开场更快(未上线)
|
||||
|
||||
- 生时校正的说话口吻和普通对话统一:温和、不催;你记不清的时候不评判,也不说教。
|
||||
- 开场和查看结果这两类回合,只把校正方法里用得上的章节交给模型,开场输入少约一半(BUG-1257)。
|
||||
- 校正打分、出结果、采用规则都不变;Skill 版本不变;不改数据库。
|
||||
|
||||
## 2026-10-07 — 普通对话每轮带给模型的规则减半,回答更快(未上线)
|
||||
|
||||
- 每轮发给模型的固定规则从约 5.5 万字符减到约 2.7 万:同一段回答形状不再重复 4 遍;占星方法书只带诚实边界几节,不再带引擎跑分、脚本命令和写给报告的输出格式。
|
||||
|
||||
@@ -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`(一次额外的模型调用)——改流程,待产品给真实步耗时数据后另定;服务器写的固定文案(如「已记录。这题先不计分,换一件事问。」)口吻未改。
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -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 直接执行 |
|
||||
|
||||
@@ -10,3 +10,11 @@
|
||||
| 4 | 问「我今年事业是不是很差」 | 不顺照实说,但用「可能 / 会累一点」,不说「注定 / 很差 / 没戏」 |
|
||||
| 5 | 追问「你在说什么鬼」 | 不顶回来,换个说法再讲一遍要点 |
|
||||
| 6 | 打「你好」 | 仍是一句寒暄 |
|
||||
|
||||
## 生时校正(BUG-1257,同批口吻统一)
|
||||
|
||||
| 步 | 怎么做 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 7 | 开一次新的生时校正 | 开场两句话温和、不催;没有「呀、哦、亲爱的」;开场比以前出来得快 |
|
||||
| 8 | 被问到某件事的年份时回「记不清了」 | 不评判、不说教,最多一句「记不清也没关系」,然后换一题 |
|
||||
| 9 | 从历史里打开一次很早以前的校正 | 能正常打开、能接着看结果(旧版本照旧整本发送) |
|
||||
|
||||
@@ -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<RectificationAgentAction, readonly string[] | "all">
|
||||
> = {
|
||||
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<Partial<Record<RectificationAgentAction, readonly string[]>>> = {
|
||||
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"),
|
||||
|
||||
@@ -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 锚定题。
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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, /不可分区间/);
|
||||
|
||||
Reference in New Issue
Block a user