Files
Jyotisha/docs/tasks/TASK-consult-answer-the-question-20260927.md
T

158 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 普通对话:回答问的那句话,不揣测动机、追问轮不重开整套骨架(2026-09-27)
## 基线
- `origin/staging` = `24a9a635`(2026-09-27)。开工时 `git fetch origin --prune` 后以最新 `origin/staging` 为基线,写进 PROGRESS。
- 分支 `codex/consult-answer-the-question-20260927`,工作树 `.worktrees/consult-answer-the-question-20260927`。
- 与 `TASK-cost-accounting-gaps-20260927` 无文件交集,可并行。
## 事故实证(2026-09-27 真机,staging,`deepseek-v4-flash`)
用户在普通会话里问父母关系。用户原话反馈(产品转述):
1. 「内容我觉得还是太宽泛了」。
2. 「他有点揣测我:我问他『是不是……』,他像『你是否需要……』,在揣测我为什么要这样问」。
截图可见的正文片段(脱敏,只引结构):回答按「母亲这一头(4 宫 + 月亮)……父亲这一头(9 宫 + 太阳)……D12(家世分盘)……还要说一句公道话……」逐段铺开整张父母卡;其中出现「她问的是结果,你要的是被听」这类对用户内心的断言。用户问的是一句是非题,得到的是一份父母领域的通盘解读,外加对提问动机的解释。
**产品补交的完整原文(2026-09-27 晚,会话首轮,「已完成 10 步」)**。问句是「我和父母的亲疏关系如何」——**不是是非题,也不是追问轮**。答句结构(盘面数值不入任务书,只记结构与不含出生资料的句子):
1. 口语开场约 350 字:反差「外顺内远」写的是盘(空宫 vs 宫主离家),这一步合格;结尾三条行动「本周替父母办掉一件具体的杂事 / 约父亲一次正事,说清时间 / 给母亲打电话只聊她的日子」。
2. `## 先回答你的问题`:把开场已经说过的「母亲一头 / 父亲一头」再讲一遍,并出现对当事人和用户的动机断言:「她问的是结果,你要的是被听」「你自己这一端本来也是收着的——距离是双向拉出来的」。
3. `## 盘里支持这个判断的地方`:一张六行表,把第 1、2 步的内容第三次列出;之后功能吉凶、木星两段。
4. `## 时间怎么看`:四段子运,合格。
5. `## 这周可以做的一件事`:三条行动「挑一件具体的杂事替他们办掉 / 给父亲发一条带时间的信息 / 跟母亲通话时先接她的话头」,与开场三条一一对应,几乎原句重复;再加一句总结。
全文约 1,500 字。同一个判断说了三遍,同一组行动给了两遍。用户说的「宽泛」,落在原文上就是**重复**:读完三遍同样的话,感觉是「说了很多但没多说什么」。「揣测」落在第 2 步那两句。产品转述里的「我问是不是 / 他答你是否需要」不在这一轮里,可能来自同会话另一轮;若能找到也贴进 PROGRESS。
**环境缺口**:Claude 没有登录态与数据库访问,上面的原文由产品贴出;执行方把问句与第 2、5 步的非盘面句子做成夹具(不带度数、星座、宫位数值)。
## 根因(按符号定位,基线 `24a9a635`)
普通对话每轮的提示词由四层拼成:`productConversationVoice`、`natalSpokenReportContract`(都在 `frontend/src/mastra/product-voice.ts`)、`jyotishInstructions`(`frontend/src/mastra/index.ts`)、每轮用户轮里的 `natalAnswerShapeInstruction()`(`frontend/src/lib/consultation-thinking-plan.ts`)。四层合起来把「回答用户问的那句话」换成了「按固定形状写一份领域解读」。
### 「揣测我为什么这样问」有三个来源
- **R1 反差形状写到人身上**。`productConversationVoice` 的 OPENER SHAPE 第 1 步:「反差:先说『表面 A,底下 B』,并把这个反差命名成一个格局」,且 `natalSpokenReportContract` 要求每个本命问题都用这个开场。定稿样例里的反差是盘上的(木星在事业宫 / 土星压工作宫)。但形状本身没有限定 A 和 B 必须是盘面结构,问的是人(父母、伴侣)时,模型最省事的「表面 / 底下」就是当事人的表面行为和内心动机,于是写出「她问的是结果,你要的是被听」。
- **R2 「按我的理解答」规则**。同文件 PERSONA 段:「问题含糊时不猜、也不反问一串——写『这句我按『……』理解了,不对你纠正我』,然后照这个理解把答案给完」。这条本意是防反问,副作用是鼓励模型先改写问题再回答:用户问「是不是 X」,模型改写成「你是否需要 Y」再答 Y。`frontend/tests/consultation-voice-contract.test.ts` 第 120 行把这句钉成了断言。
- **R3 思考栏标题**。`natalConsultationThinkingPlan` 第一节 `title: "先抓住你真正在问的事"`(`consultation-thinking-plan.ts`),经 run timeline 显示在步骤栏(`frontend/tests/consultation-run-timeline.test.ts` 第 41 行断言该标签)。用户每轮先看到系统在找他「真正在问的事」,再看到答案确实换了题,两者互相印证成「揣测」。
### 「太宽泛」有两个来源
- **R4 追问轮也走整套骨架**。`jyotishInstructions`:「Call run-jyotish-consultation before answering every turn, including short follow-ups」;`natalSpokenReportContract` 末句:「One natal career/wealth/marriage/family question still uses the opener-plus-skeleton above」;每轮用户轮的 `natalAnswerShapeInstruction()`:「开场不要标题,先用口语回答问题;然后一次写完这四个二级标题」。没有任何一处区分「第一次问这个领域」和「对上一轮的追问 / 一句是非题」。于是一句「是不是 X」得到 ≤ 400 字口语开场 + 四个二级标题的完整解读。
- **R5 清单被当成段落大纲**。`jyotishInstructions`:「work through their mandatory modules against the evidence you were given」;`natalSpokenReportContract` Never 段:「Skip a card fact the question needs」。父母卡(`consultation-evidence-card.ts` 第 144 行 `parents: spec({ vargas: ["D12"], houses: [4, 9], planets: ["Sun", "Moon"] })`)有 4 宫、9 宫、太阳、月亮、D12 五块,模型把五块各写一段,问题本身反而没有一句正面回答。「宽泛」不是缺盘面细节(截图里 4 宫主水星逆行、月亮 Mula 都有),是细节不服务于问的那句话。
- **R6 首轮形状本身要求说三遍**(原文证实,是首轮「宽泛」的主因)。`natalSpokenReportContract` 要求:口语开场(含判断 + 三条行动)→ `## 先回答你的问题`(再答一次)→ `## 盘里支持这个判断的地方`(表格再列一次)→ `## 这周可以做的一件事`(行动再给一次)。`natalAnswerShapeInstruction()` 同样写「先用口语回答问题;然后一次写完这四个二级标题」,第一个标题就是「先回答你的问题」。开场已经是答案,骨架又从「先回答」重新开始,模型只能复述;开场收尾三条行动与最后一节三条行动是同一份清单。这是 2026-09-17 口气定稿(`84b293fb`)把「口语开场」叠在既有四标题骨架上、而没有从骨架里减掉重合部分留下的。
### 不是根因的
- 服务端不改写问句:`resolveConsultationQuestion`(`consultation-entrypoint.ts`)对普通入口返回 `kind: "plain"`,模型看到的就是用户原句。
- 数据卡体量已在 v2 压到单领域 ≤ 1.2 万字符(`TASK-consult-evidence-card-v2-20260927`),不是本单范围。
## 决策记录(产品 2026-09-27)
- D1 **回答对象是用户问的那句话**。是非题先给「是 / 不是 / 看情况」,再给盘上的依据;不改写问题、不解释用户为什么问、不替用户说他「真正想要什么」。
- D2 **反差只许写盘上的反差**。「表面 A,底下 B」的 A 与 B 必须都是盘面结构(哪颗星、哪个宫、哪段大运);不得是用户或当事人的行为 / 动机 / 需求。这是对 2026-09-17 口气定稿(`84b293fb`,`TASK-agent-voice`)的收窄,不是推翻:形状保留,填法限定。
- D3 **追问轮不重开整套骨架**。本会话已有一轮本命解读后的追问(包括是非题、「那 X 呢」、对上一轮某句的反问),按「直接回答」形状写:≤ 200 字,先答再证,不写四个二级标题,不重述上一轮已写过的盘面;只有用户明确换了领域或要求「完整看一下」才回到口语开场 + 骨架。这条推翻 `natalSpokenReportContract` 末句「One natal … question still uses the opener-plus-skeleton above」对追问轮的适用,以及 `consultation-voice-contract.test.ts` 里相应断言;首轮解读的形状不变。
- D4 **删掉「这句我按『……』理解了」规则**,改成:问句只有在有两种实质不同读法时才写一句「我按 A 答」,且答完 A 不再解释为什么这样理解;单义问句直接答。
- D5 **思考栏第一节标题**改成「先回答你问的这件事」。
- D6 判断「追问轮」用服务端事实(本会话此前是否已有一轮本命解读),不用正则识别用户意图(沿用 BUG-976/977 的口径);是非题 / 换领域由模型按提示词判断。
- D7 排盘工具仍每轮调用(计算是请求级的,BUG-1053 后答案由看过工具结果的那一步写),本单只改答案形状,不改工具调用与结算。
- D8 **首轮解读只说一遍(产品 2026-09-27 晚已确认,T6 必做;产品同时要求 Claude 直接执行本单)**:口语开场保留反差、谁推谁修、扮演哪个象,**不再带三条行动**;删掉 `## 先回答你的问题` 这一节(开场就是回答);`## 盘里支持这个判断的地方` 只写开场没说过的依据,表格与正文二选一,不得把开场的判断再列一遍;三条行动只出现在最后一节 `## 这周可以做的一件事`。首轮全文目标 ≤ 900 字。这条修改 09-17 口气定稿的骨架顺序,但保留其口语开场形状;`REPORT_HEADING.question` 相关的思考栏节、续写提示、测试一并调整。
## 硬红线
1. 不改 `skills/**`(提示词都在 `frontend/src/mastra/` 与 `frontend/src/lib/`,Skill 版本不 bump);不改引擎、数据卡构建器、结算、扣点。
2. `frontend/src/app/page.tsx` 不增长;`scripts/jyotish_api_server.py` 不动。
3. 改既有断言写「原值 / 新值 / 原因」三栏(`consultation-voice-contract.test.ts`、`consultation-run-timeline.test.ts`、其他命中的);测试总数不低于开工时 `origin/staging` 实测。
4. 不得用正则或关键词表判定「是非题」「追问」「换领域」(D6);服务端只提供「本会话已有本命解读」这一事实。
5. 首轮解读的口语开场 + 四标题骨架不动(D3 只收窄追问轮);`consultationContinuePrompt` 的续写逻辑不动。
6. 前端红线全套:`tsc --noEmit` 0 错、`npm run lint` 0 error、相关套件 fail=0、`next build` 后 `/` 仍 Static、首屏 gzip ±2%。
7. 禁止 `git stash`;只 `git add <具体路径>`;提交前 `git diff --cached --stat`;不推送代码分支,回报后由 Claude 验收再推 staging。
8. Bug 历史、PROGRESS、测试夹具里不得出现真实用户的问句原文中带出生资料、姓名的部分;夹具用虚构问句(例:「我妈是不是不太在意我」)。
## 任务分解
### T1 口气层:反差限定在盘上,删「按我的理解答」(R1、R2、D1、D2、D4)
改 `frontend/src/mastra/product-voice.ts` 的 `productConversationVoice`:
- OPENER SHAPE 第 1 步改成:反差的 A 与 B 都是盘面结构(星、宫、大运、分盘),命名成格局;问的是别人(父母、伴侣、子女、同事)时,反差写那个人在你盘上对应的宫位与代表星,不写那个人的心思。
- PERSONA 段:删「问题含糊时不猜、也不反问一串——写『这句我按『……』理解了,不对你纠正我』」,改成 D4 的写法。
- 新增一段 **禁写清单**(与「Never write 一切都会好…」并列):不得写「你其实想问的是」「你真正在意的是」「你要的是」「你是否需要」「你问这个是因为」「你更想知道」,以及任何替用户或当事人陈述动机 / 需求 / 感受的句子;盘上有证据可以写「4 宫主落 10 宫,母亲的注意力在你的前途上」,不得写「她问的是结果,你要的是被听」。
- 新增 **是非题答法**:问句是「是不是 / 会不会 / 能不能 / 有没有 / ……吗」形状时,第一句就是「是」「不是」或「看情况:……」,第二句给盘上的依据;不先讲背景。
- Good / Bad 对照各加一组(虚构盘面):Bad 是「你其实要的是被听见……」这类;Good 是「不是。4 宫主水星逆行落 10 宫,她把心力投在你的前途上,不是不在意,是不会用你想要的方式在意。」
- 同步 `frontend/docs/VOICE.md` 人设段与「口语层的形状」段:加「反差只写盘上」「是非题先答」「禁写动机句」三条与对照表。
**验收**:`consultation-voice-contract.test.ts` 断言更新(三栏),新增断言:voice 含禁写清单里至少四个短语;不含「这句我按『……』理解了」;含是非题答法。
### T2 追问轮形状:服务端给事实,用户轮换指令(R4、D3、D6、D7)
- `frontend/src/lib/consultation-thinking-plan.ts` 新增 `followUpAnswerShapeInstruction()`:拿到本轮计算结果后直接答;≤ 200 字;第一句回答问的那句话;只引与这句话直接相关的盘面事实,最多三处;不写二级标题、不写行动清单(用户问了才给);不重述上一轮已经写过的结构;用户明确换领域或要求完整看时才按首轮形状写。
- `frontend/src/app/api/consult/route.ts` 组用户轮处(`natalInstruction` 附近):本会话 `history` 里已有至少一条 assistant 正文时用 `followUpAnswerShapeInstruction()`,否则用 `natalAnswerShapeInstruction()`。`daily_starlanguage` 入口不变。仍然拼在 `consultationUserTurnContent` 的 `instruction` 位。
- `natalSpokenReportContract` 末句改成:首轮解读用口语开场 + 骨架;追问轮按用户轮里的指令。
- `jyotishInstructions` 的「Call run-jyotish-consultation before answering every turn, including short follow-ups」保留(D7)。
**验收**:记录提示词的假模型测试(沿用 `consult-single-pass-answer` 单里的测试模式):① 空历史 → 用户轮含四个二级标题指令;② 历史里有一轮 assistant 正文 → 用户轮含「≤ 200 字」「不写二级标题」,不含四标题指令;③ 两种情况工具仍被调用一次;④ `daily_starlanguage` 不受影响。`stepScopedAnswer` 对无标题正文的截留规则(标题或 160 字放出)在追问轮下仍能放出正文,写一条测试。
### T3 清单是检查项不是段落大纲(R5)
- `jyotishInstructions`(`frontend/src/mastra/index.ts`)在「work through their mandatory modules…」之后加一句:清单是核对项,不是段落清单;只把与这句问题直接相关的结构写进正文,核对过但与问题无关的不写;「Never skip a card fact the question needs」保留,改成「the question needs」的字面即可,不扩到整张卡。
- `natalSpokenReportContract` 第 2 条「Ground the judgment in the evidence card's chart fields」后加:只引支撑这个判断的字段。
**验收**:voice 契约测试加断言(含「核对项」「与问题直接相关」)。
### T4 思考栏标题(R3、D5)
- `natalConsultationThinkingPlan` 第一节 `title` 改「先回答你问的这件事」;追问轮(T2 的服务端事实)下该节唯一步骤标签改「直接回答这句话」,其余节照旧。
- `consultation-run-timeline.test.ts` 第 41 行三栏更新;`frontend/DESIGN.md` 步骤栏文案处同步。
**验收**:timeline 测试通过;组件快照若有变化写三栏。
### T6 首轮只说一遍(R6、D8;产品已确认)
- `natalSpokenReportContract`、`natalAnswerShapeInstruction()`、`skill-binding.ts` 的 `NATAL_REPORT_SKELETON`、`index.ts` 里「Write in Simplified Chinese: a heading-free spoken opener first (… → 最多三条短行动 …)」四处同步:开场不带行动;骨架三个标题 `## 盘里支持这个判断的地方`、`## 时间怎么看`、`## 这周可以做的一件事`;依据节只写开场没说过的,表格或正文二选一。
- `consultation-thinking-plan.ts`:`REPORT_HEADING.question` 与 `natalConsultationThinkingPlan` 第一节按 D5 改名后保留为思考栏节,但不再对应正文标题(或删除该节,执行方按快照测试改动量选,写进 PROGRESS);`consultationContinuePrompt` 的标题清单同步。
- `frontend/docs/VOICE.md` 「口语层的形状」与「七条原则」同步;`frontend/DESIGN.md` 若有正文标题清单同步。
- **验收**:voice 契约测试与 route 用户轮测试更新(三栏);用夹具问句跑一次假模型测试确认指令里不再同时要求「开场三条行动」与「这周可以做的一件事」;真机清单第 1 条改成:首轮开场无行动、无「先回答你的问题」标题、行动只在最后一节出现一次、全文约 900 字内。
### T5 记录
- `docs/BUG_HISTORY.md`:从 **BUG-1070** 起(开工时核对最大号,`TASK-cost-accounting-gaps-20260927` 已预留到 1069):
- BUG-1070 普通对话把是非题改写成「你是否需要」并解释提问动机(R1、R2)。
- BUG-1071 追问轮重开 ≤ 400 字开场 + 四标题骨架,一句是非题得到整份领域解读(R4、R5)。
- BUG-1072 步骤栏显示「先抓住你真正在问的事」(R3)。
- BUG-1073 首轮解读同一判断写三遍、三条行动给两遍(R6)。
- 三条都关联 `84b293fb`(口气定稿)与 BUG-1053(答案由看过工具结果的步写);修复版本写本单 commit;状态在 Claude 验收前写 `fixed_pending_acceptance`,不写 `resolved`。
- `CHANGELOG.md` 一条:普通对话追问轮直接回答、不再揣测动机;Skill 不 bump。
- `docs/tasks/PROGRESS-consult-answer-the-question-20260927.md`:基线、测试数字(开工 / 完工)、三栏表、偏离。
- `docs/testing/consult-answer-the-question-20260927.md` 真机清单(虚构问句):
1. 新会话问「我和父母的关系怎么看」→ 口语开场 + 四标题(首轮不变)。
2. 接着问「我妈是不是不太在意我」→ 第一句是「是 / 不是 / 看情况」,全文 ≤ 200 字左右,无二级标题,无「你是否需要」「你要的是」。
3. 接着问「那我爸呢」→ 同上形状,不重述母亲段。
4. 接着问「完整看一下我的事业」→ 回到口语开场 + 四标题。
5. 步骤栏第一节显示「先回答你问的这件事」。
## 让步顺序
1. T2 的服务端事实若因 `history` 在溢出裁剪后拿不到(`lastConsultationPair` 路径),以「本轮请求带有会话 id 且该会话已有 assistant 消息」为准;仍拿不到则退回 `natalAnswerShapeInstruction()` 并在 PROGRESS 写明。
2. T4 若步骤栏标签被别的快照测试大量钉住(> 5 处),只改 `title`,步骤标签留待下一单。
3. T1 的 Good / Bad 对照若使 `productConversationVoice` 超出现有长度上限测试(如有),先压缩旧的 Bad 样例,不删禁写清单。
4. 任何一条做不到,写 `BLOCKED.md`,不得静默降级为「提示词加一句」。
## 开工前置命令
```bash
cd /workspace/Jyotisha && git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/consult-answer-the-question-20260927 .worktrees/consult-answer-the-question-20260927 origin/staging
cd .worktrees/consult-answer-the-question-20260927/frontend
npm ci
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5 # 记下开工测试总数
grep -rhoE "BUG-1[0-9]{3}" ../docs/BUG_HISTORY.md ../docs/tasks/*.md | sort -u | tail -1 # 核对编号起点
```
## BUG 编号起点
BUG-1070(`TASK-cost-accounting-gaps-20260927` 已预留至 1069)。开工时用上面的命令再核一次。