Files
Jyotisha/TASK-agent-voice-20260901.md
T
Jesse_Chen 8617eb565c
Independent Staging Quality Gate / validate (push) Failing after 15m32s
Independent Staging Quality Gate / publish (push) Has been skipped
docs(voice): add task E — opening dedupe and user_meaning layering
硬编码盘点新发现的两处定位错误:opening 轮双重提问去重(结构化
判定,不用正文正则);引擎 user_meaning 作为素材分层,指令语气
不得直出用户。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-01 15:40:51 +00:00

11 KiB
Raw Blame History

任务书 · Agent 输出人味修复:语气是产品的一部分,不是正确性的牺牲品(2026-09-01)

基线:origin/staging @ ff97de5e

0. 问题定性

用户反馈:Agent 输出"没有人味"。经排查这是结构性的,三个来源,全部在产品侧,均可在不动任何真实性红线的前提下修复:

  1. 咨询系统提示配比失衡mastra/index.ts + mastra/product-voice.ts):约 60 条指令里 ~90% 是禁令与合同(Never/Do not/Treat as final),语气指引只有三句零星短语,无 persona、无共情原则、无正面语料示例。模型被挤成审计报告腔。
  2. Level 2 模板把温度压到最后product-voice.ts):本命问题强制以"## 统一参数与原始结构"的度数表开场,"## 现代生活"在技法审计表之后垫底。用户问"我婚姻怎么样",第一眼是岁差和上升度数。
  3. 校正 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. 硬红线

  1. 真实性与合同边界一字不放宽:技法审计表仍在正文文末;不得虚构;REPRESENTATIVE_MINUTE_DISCLAIMER语义必须保留(措辞可重写、频率可调,但交付轮必须完整出现一次"不是已确认的唯一出生分钟"语义);answer_policy / precise-timing 守卫不动。
  2. 问题槽归属不动:服务端 focus 仍是唯一问题源;正文仍不得提问、复述、改写题干;选择卡仍由结构化槽位承载。放宽的只是承接正文的长度与温度(一句 → 2-4 句,可含进度感),不是职能。
  3. 不改 skill 包(10.0.14 文本已兼容本任务;如发现某条硬编码文案被 skill 逐字锁定,先报告再动)。
  4. 禁止空洞暖场:人味 ≠ 客套话轰炸。不得加"亲爱的""感谢你的信任"式填充;不得每轮重复问候;不得用 emoji。人味的定义见 §voice 原则。
  5. 既有测试断言若逐字锁定旧文案,允许同步更新断言文本,但断言的语义约束(如"必须含不可分区间语义""不得出现唯一分钟措辞")必须保留并改为语义级匹配;在 PR 里逐条列出改动的文案断言。
  6. 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

  1. productConversationVoice 开头新增 voice 段(人设 + 五原则 + 2 组好/坏对照),放在所有合同条款之前
  2. Level 2 骨架顺序不动,但明确允许并要求:在 ## 统一参数与原始结构 之前写一段 3-6 句、无标题的直接回应——先用人话回答用户问的事(方向性结论 + 一句共情/背景承接),再进入骨架。相应更新 "Do not shorten…" 措辞为"骨架不可省略,但必须以直接回应开场"。
  3. 禁令清单归并去重(内容不变,压缩语言密度),把散落的三句语气指令并入 voice 段。
  4. 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 或确定性叙述字段直出给用户的路径——用户会读到混着内部指令语气的句子。

修法:

  1. 盘点所有 user_meaning 的消费点,分成两栏:进模型上下文(保留原样)vs 直出用户(逐个处理)。
  2. 直出用户的点:改为消费专门的展示字段(新增 display_copy 或由 TS copy 模块按结构字段生成),或改为模型转述路径;指令语气句("不得…")一律不得出现在用户可见文本。
  3. 引擎侧字段语义不变、不删字段(模型和审计仍在用);这是消费端分层,不是引擎改造。
  4. 在 PR 里贴出直出点清单与前后对照。

任务 DP2)· 防回归

  • copy 模块导出集中后,加一条"机器腔词表"测试:用户可见文案不得包含 "按同一规则计入"、"请刷新后重新作答"、"当前状态为" 等词条(词表放测试文件,≥8 条,含本次清理的全部旧词)。
  • 交付/采用轮边界语义测试改为语义级:断言包含"不是已确认的唯一出生分钟"或等义白名单短语之一,而非逐字全句。

验收标准

  1. 任务 0 的改前/改后对照(事故 case 形状全程文案 + 一次婚姻主题咨询的开场三段)贴 PR;对照中开场不得再出现语义重复的两连问(任务 E-1)。
  2. tsc --noEmit exit 0;四组前端测试 fail=0;改动的文案断言逐条三栏说明。
  3. 红线 1/2 的证明:交付轮输出仍含边界语义;正文仍不含题干(现有 server-focus/question-ownership 测试全绿)。
  4. VOICE.md 落库;机器腔词表测试落库。
  5. 未在真实环境验证的部分如实注明。