硬编码盘点新发现的两处定位错误:opening 轮双重提问去重(结构化 判定,不用正文正则);引擎 user_meaning 作为素材分层,指令语气 不得直出用户。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
11 KiB
任务书 · Agent 输出人味修复:语气是产品的一部分,不是正确性的牺牲品(2026-09-01)
基线:origin/staging @ ff97de5e。
0. 问题定性
用户反馈:Agent 输出"没有人味"。经排查这是结构性的,三个来源,全部在产品侧,均可在不动任何真实性红线的前提下修复:
- 咨询系统提示配比失衡(
mastra/index.ts+mastra/product-voice.ts):约 60 条指令里 ~90% 是禁令与合同(Never/Do not/Treat as final),语气指引只有三句零星短语,无 persona、无共情原则、无正面语料示例。模型被挤成审计报告腔。 - Level 2 模板把温度压到最后(
product-voice.ts):本命问题强制以"## 统一参数与原始结构"的度数表开场,"## 现代生活"在技法审计表之后垫底。用户问"我婚姻怎么样",第一眼是岁差和上升度数。 - 校正 agent 限一句话 + 硬编码文案无语气(
mastra/agentic-rectification.ts指令"每轮正文只输出一句确认或承接";method-followup.ts/answer-choice.ts/rectification-decision.ts/ route.ts 里的服务端直出字符串):用户在校正里读到的话大半是修 BUG-456/D-1 那几轮为正确性手写的字符串,公文腔("按同一规则计入当前区分题")、审讯腔("有没有记得住时间的收入变化、大笔支出或欠债?")、法务腔边界句每轮重复;答完 5 题把 30 分钟缩到 7 分钟,没有任何进度反馈。
关键边界:skill 10.0.14 文本只要求"正文只做承接与解释",并未限制一句话——"一句"是产品 prompt 自加的。问题槽服务端唯一所有、正文不得出题/复述题干、candidate/accepted/confirmed 语言边界、不得宣称唯一分钟:这些红线全部保留,本任务只改语气与节奏。
1. 硬红线
- 真实性与合同边界一字不放宽:技法审计表仍在正文文末;不得虚构;
REPRESENTATIVE_MINUTE_DISCLAIMER的语义必须保留(措辞可重写、频率可调,但交付轮必须完整出现一次"不是已确认的唯一出生分钟"语义);answer_policy / precise-timing 守卫不动。 - 问题槽归属不动:服务端 focus 仍是唯一问题源;正文仍不得提问、复述、改写题干;选择卡仍由结构化槽位承载。放宽的只是承接正文的长度与温度(一句 → 2-4 句,可含进度感),不是职能。
- 不改 skill 包(10.0.14 文本已兼容本任务;如发现某条硬编码文案被 skill 逐字锁定,先报告再动)。
- 禁止空洞暖场:人味 ≠ 客套话轰炸。不得加"亲爱的""感谢你的信任"式填充;不得每轮重复问候;不得用 emoji。人味的定义见 §voice 原则。
- 既有测试断言若逐字锁定旧文案,允许同步更新断言文本,但断言的语义约束(如"必须含不可分区间语义""不得出现唯一分钟措辞")必须保留并改为语义级匹配;在 PR 里逐条列出改动的文案断言。
cd frontend && ./node_modules/.bin/tsc --noEmit通过(不要用npx tsc);rectification-*/consultation-*/consult-*/chat-*测试 fail=0。
2. Voice 原则(写进代码库作为持久参照)
新建 frontend/docs/VOICE.md(或 frontend/src/lib/voice-guide.ts 中的文档常量),至少包含:
- 人设:一位懂行、可靠、说人话的占星师朋友——不是客服,不是法务,不是表单。
- 五条原则:先回应人再展开方法;说进度("我们已经把范围从 30 分钟缩到 7 分钟了");解释为什么问(一句就够);同一约束换着说(边界句不逐字复读);口语化但不轻佻。
- 好/坏对照各 ≥5 组,取材自真实文案(见 §任务 C 清单)。
任务 0(门控)· 快照当前腔调,先立对照
把事故 case f83d9b42 的完整 assistant 输出串(route 直出 + 硬编码文案)按当前代码复排一份存入 PR 描述,作为"改前"样本。改后用同一形状复排"改后"样本。这是本任务书唯一的"红→绿":主观质量靠对照呈现,不靠断言。
任务 A(P0)· 咨询 prompt 重构:voice 置顶,禁令收纳
mastra/index.ts / mastra/product-voice.ts:
productConversationVoice开头新增 voice 段(人设 + 五原则 + 2 组好/坏对照),放在所有合同条款之前。- Level 2 骨架顺序不动,但明确允许并要求:在
## 统一参数与原始结构之前写一段 3-6 句、无标题的直接回应——先用人话回答用户问的事(方向性结论 + 一句共情/背景承接),再进入骨架。相应更新 "Do not shorten…" 措辞为"骨架不可省略,但必须以直接回应开场"。 - 禁令清单归并去重(内容不变,压缩语言密度),把散落的三句语气指令并入 voice 段。
generalJyotishInstructions/ onboarding 指令同样加 voice 段。
任务 B(P0)· 校正 agent 松绑到"2-4 句自然承接"
mastra/agentic-rectification.ts:
- "每轮正文只输出一句确认或承接" → "每轮正文 2-4 句:确认收到什么、(可选)一句为什么有用或当前进度、自然过渡到界面上的下一步。不提问、不复述题干、不预告选项。"
- 新增进度感指引:当 dossier 显示候选区间较开场收窄时,允许且鼓励用一句自然语言说出(数字来自服务端投影,不得自算)。
- 加 2 组好/坏对照(如 坏:"好的,记下了。" 好:"2016 年 9 月上大学,记下了——这类有明确月份的节点对校正特别有用。")。
任务 C(P1)· 硬编码文案人味重写(集中 + 逐条)
先把散落的用户可见字符串集中到一个 copy 模块(如 lib/rectification-agentic/v9/user-copy.ts),再逐条重写。清单(行号是线索,按符号定位;语义约束在括号里,必须保留):
| 现文案 | 位置 | 问题 | 方向 |
|---|---|---|---|
| "请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。" | method-followup.ts GENERIC_COLLECT_QUESTION |
公文枚举 | "从你最容易想起来的开始就好——比如哪年上的大学、哪年换的工作,记得大概年份就行。" |
| "有没有记得住时间的收入变化、大笔支出或欠债?" 等 USER_COLLECT_QUESTION 表 | method-followup.ts |
审讯式 | 每条加一层自然引子,去掉罗列感 |
| "请点选下面的选项作答。点选和一句话回答会按同一规则计入当前区分题。" | route.ts | 内部规则外漏 | "直接点下面的选项就行,打字回答也一样算数。" |
| "我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。" | route.ts | 生硬 | "我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。" |
| "当前可信区间是 X–Y,代表分钟 Z。代表分钟只是代表性候选,不是已确认的唯一出生分钟。" | rectification-decision.ts nonConvergingRangeNarration |
法务腔+每轮复读 | 交付轮完整版带进度感("从最初的 30 分钟缩到了 X–Y 这 7 分钟");中间轮允许省略边界句或用短版(语义约束:交付/采用轮必须含完整边界语义) |
| "…可以从下面的时间里选一个采用。" | answer-choice.ts |
生硬 | 与上一条合并重写 |
| "当前问题已更新,请刷新后重新作答。" | route.ts | 机器腔 | 口语化 |
| "接下来请点选下面这一问。" 类占位 | turn-exit/答题链 | 占位残留 | 确认 Task C(上轮)已替换为真实题干后删除死文案 |
UNVERIFIED_BIRTH_TIME_NOTICE 等咨询侧 notice |
consultation-birth-time-mode.ts |
与 TASK-consultation-birth-accuracy 的任务 D 协同,避免双改冲突 | 以该任务书为准,本轮跳过 |
重写要求:每条在 PR 里"旧 → 新 → 保留的语义约束"三栏列出。
任务 E(P1)· 定位错误的硬编码:开场去重 + user_meaning 分层
硬编码全量盘点(2026-09-01)发现两处不是"文案难听"而是"定位错误",重写解决不了,需要结构处理:
E-1 · 开场双重提问去重
事故 case 转写第一屏:opening 轮模型正文已自然包含收集引导("就从你最容易想起来的一件事开始……你先随便说一件吧"),紧接着 finalizeSuccessfulTurnExit 把服务端 focus 的 GENERIC_COLLECT_QUESTION 作为第二条独立 assistant 消息写进 turn 历史,用户连续看到两条语义重复、语气断裂的消息。
修法(保持问题槽红线):focus 仍照常持久化(UI 问题槽渲染不变);但当本轮模型正文存在且该 focus 是 opening 轮的 collect 类 focus 时,不再把题干作为独立 turn 消息追加。判定条件必须是结构化的(action === opening && focus.intent 为 collect 且本轮已有非空 assistant 正文),不得用正文字符串匹配(红线:不得靠正文正则判断"是否已问过"——BUG-456 教训的反面同样成立:这里判断的是"本轮有无正文"这个结构事实,不是正文内容)。非 opening 轮的问题写入历史(上轮任务 C 的成果)保持不变。
E-2 · 引擎 user_meaning 分层:素材,不是用户文案
Python 引擎的 user_meaning 字段(refinement_packet.py / decision_policy.py / confirmation-gate.ts 等,如"主限更偏向 05:00,分盘大运更偏向 04:54。冲突时不能按更高把握收口。""不得把它描述为已确认的唯一出生分钟")设计定位是给模型/内部的解释素材,但存在被 UI 或确定性叙述字段直出给用户的路径——用户会读到混着内部指令语气的句子。
修法:
- 盘点所有
user_meaning的消费点,分成两栏:进模型上下文(保留原样)vs 直出用户(逐个处理)。 - 直出用户的点:改为消费专门的展示字段(新增
display_copy或由 TS copy 模块按结构字段生成),或改为模型转述路径;指令语气句("不得…")一律不得出现在用户可见文本。 - 引擎侧字段语义不变、不删字段(模型和审计仍在用);这是消费端分层,不是引擎改造。
- 在 PR 里贴出直出点清单与前后对照。
任务 D(P2)· 防回归
- copy 模块导出集中后,加一条"机器腔词表"测试:用户可见文案不得包含 "按同一规则计入"、"请刷新后重新作答"、"当前状态为" 等词条(词表放测试文件,≥8 条,含本次清理的全部旧词)。
- 交付/采用轮边界语义测试改为语义级:断言包含"不是已确认的唯一出生分钟"或等义白名单短语之一,而非逐字全句。
验收标准
- 任务 0 的改前/改后对照(事故 case 形状全程文案 + 一次婚姻主题咨询的开场三段)贴 PR;对照中开场不得再出现语义重复的两连问(任务 E-1)。
tsc --noEmitexit 0;四组前端测试 fail=0;改动的文案断言逐条三栏说明。- 红线 1/2 的证明:交付轮输出仍含边界语义;正文仍不含题干(现有 server-focus/question-ownership 测试全绿)。
VOICE.md落库;机器腔词表测试落库。- 未在真实环境验证的部分如实注明。