Files
Jyotisha/docs/tasks/TASK-consult-smalltalk-fastpath-20260920.md
T
Jesse_ChenandClaude Opus 5 fcad06372e docs(tasks): 普通对话寒暄轮快速通道任务书(BUG-976/977)
一句「你好」走完整窗口排盘 + 400 字判词 + 扣 1 点。三层实证与不扣点
通道缺口写进任务书;产品拍板不扣点不排盘回一句,分流禁用正则,
BUG-922/923 的每轮必调合同不动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-20 10:19:18 +08:00

121 lines
11 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 · 普通对话寒暄轮不再走全量排盘(快速通道)
- 基线 commit:`origin/staging` `09b41009`(2026-09-20 fetch)
- 分支:`codex/consult-smalltalk-fastpath-20260920`
- 工作树:`.worktrees/consult-smalltalk-fastpath-20260920`
- BUG 编号起点:本单成文时 `docs/BUG_HISTORY.md` 最大号 **975**,本单用 **BUG-976 / BUG-977**(开工时重新核对最大号,若已被占用顺延)
- 串行:与 BUG-937/938(`8b11ae7d`,已在 staging)无冲突。**同期若有别的单改 `frontend/src/app/api/consult/route.ts` 或 `frontend/src/mastra/index.ts`,必须串行,本单在后。**
---
## 1. 事故实证
真机(申报出生窗口账号,普通对话会话):用户整轮只发两个字「你好」。
返回的是:活动面板「已完成 4 步」→ `## 先回答你的问题` → 约 400 字的完整判词(「上亮下软」的反差 → 谁在推谁在修 → 别去应白羊的「抢」去扮演土星的「守」→ 三条短行动)→ 结尾「要落到具体年份和月份,还需要一个出生分钟」。并按一轮咨询扣 1 点。
这不是模型发挥失常,三层都是我们自己写死的(行号随代码漂移,**以符号名定位**):
| # | 位置(符号) | 事实 |
| --- | --- | --- |
| 1 | `frontend/src/mastra/index.ts` · `windowJyotishInstructions` | 「Call run-jyotish-window-consultation before answering **every turn, including short follow-ups, clarifications, and complaints**」。`jyotishInstructions`(本命)同款 |
| 2 | `frontend/src/lib/stream-agent-response.ts` · `contractReady()` | `requireTool` 为真时要求本轮**恰好一次成功排盘调用**,否则整轮 `runtime_contract_incomplete`、正文被丢弃。`route.ts` 窗口与本命两条流都是 `requireTool: true` |
| 3 | `frontend/src/mastra/product-voice.ts` · `productConversationVoice` | OPENER SHAPE 明写「natal / general / declared-window **三种模式共用**」「开场不是一句判词,是一个形状」,四步强制 |
| 4 | `frontend/src/mastra/product-voice.ts` · `natalSpokenReportContract` 末行 | 唯一的豁免句「Short chit-chat that is not a natal domain claim may stay conversational」**只存在于这个常量**,而它只拼进 `getJyotishAgent`;`getWindowJyotishAgent` / `getGeneralJyotishAgent` 拿不到 → **BUG-977** |
| 5 | `frontend/src/lib/consultation-thinking-plan.ts` · `consultationSpokenHeadingRule("window")` | 窗口模式「描述稳定层就用 `## 先回答你的问题` 开头」,所以标题也是我们要求的 |
| 6 | `frontend/supabase/migrations/20260717000000_consultation_request_lifecycle.sql` · `begin_consultation_credit` | 预留即 `credits - 1`;`cancel_consultation_credit` 只退款、**不写回复消息** |
| 7 | `frontend/supabase/migrations/20260808030000_consultation_stream_recovery.sql` · `complete_consultation_response` | 「把回复写进 `chat_sessions.messages`」与「`complete_usage` 结算扣点」绑在同一次调用 → **今天没有「不扣点但保留对话」的通道** |
## 2. 根因
产品从来没有定义过「非咨询轮」这一类。系统把每一条用户消息都当成一次咨询:先预留点数,再强制排盘,再按开场形状说话。09-17 的语气改造(`84b293fb`)与 BUG-922/923 的「每轮必调排盘」两边单看都对,叠在一起就让一句「你好」花掉一次完整窗口排盘、几十秒和 1 个点。
## 3. 决策记录(产品负责人 2026-09-20 口头拍板)
- **D1**:纯寒暄轮 **不扣点、不排盘、回一句白话**(在 a / b / c 三选中选 a)。
- **D2**:**分流判定不得用关键词表、正则、字数阈值**(产品原话「别整什么正则」)。判定由一次极短的结构化模型调用做出。
- **D3**:该调用**复用本轮已选模型**,不引入便宜模型目录概念,沿用 2026-09-15 性能单已拍板的「分类用贵模型」口径。输入只有本轮可见问题 + 最后一对历史,输出上限极小。
- **D4**:分流发生在**进入 Agent 之前**。BUG-922 的「every turn」两句、BUG-923 的第 0 步 `activeTools` 收窄与 `toolChoice: "auto"`、`contractReady()` 三处**一个字不改**,避免复发。
- **D5**:**fail-open**。分类不确定、超时、报错、输出不合 schema,一律走现有完整咨询路径。宁可多花一次,不可敷衍用户。
- **D6**:寒暄回复由同一次调用直接产出(一次往返既分流又出话),不做固定文案池。
- **D7**:为了「不扣点但保留对话」,**新增**一个免费完成的数据库函数;不改现有 `complete_consultation_response` 的签名与行为。
## 4. 硬红线
1. 不改 `index.ts` 两处 `every turn` 句、不改 `consultationNatalPrepareStep` / `consultationWindowPrepareStep`、不改 `contractReady()`。`consultation-birth-time-mode.test.ts` 与 `consultation-workflow-contract.test.ts` 里 BUG-922 的四条断言必须保持绿。
2. 代码里**不得出现**寒暄关键词表、匹配寒暄的正则、按长度判定的阈值。(工具名/JSON 解析用的既有正则不在此列。)
3. fail-open 是硬要求:任何异常路径都必须落回完整咨询路径,不得静默回一句话了事。
4. 寒暄路径**不得调用任何排盘工具**,不得发 activity 步骤,不得写 workflow receipt 或技法审计行。
5. 寒暄路径的回复**不得包含任何盘上主张**。实现方式是**能力剥夺**——这次调用不绑 Skill、不给工具、不注入任何星盘上下文,它手里根本没有盘——不是事后检查输出。
6. 迁移**只加函数,不动表结构**;不得放宽任何计费校验、幂等或锁。
7. 隐私:不得把用户问题原文写进除既有 observability 字段以外的任何地方。
## 5. 任务分解
### 5.1 分流模块(新文件 `frontend/src/lib/consultation-smalltalk.ts`)
`classifyConsultationTurn()`:输入本轮可见问题、最后一对历史、用户称呼;输出 zod `.strict()` 联合 `{ kind: "smalltalk", reply: string } | { kind: "consult" }`。超时上限 3 s,任何失败返回 `{ kind: "consult" }`。
`reply` 的形状(对齐 `starter-greeting.ts` 定稿口径与 `frontend/docs/VOICE.md`):一句白话,≤ 20 字,不以句号结尾,不客服腔(禁「有什么可以帮您」「很高兴为您服务」),不带星月比喻,不追问一串。
- 验收:单测用假模型,不打真实供应商。判 `smalltalk`:你好 / 在吗 / 谢谢 / 晚安 / 哈哈 / 我回来了。判 `consult`:「你好,帮我看看事业」/「?」/「你在说什么鬼」/「今年怎么样」/ 空白追问。异常分支:超时、非法 JSON、`reply` 为空、`reply` 超长 → 全部 `consult`。
### 5.2 `route.ts` 分流接线
在 `resolveConsultationQuestion` 之后、进入 Agent 之前调用,且**仅当** `entrypoint === undefined`(普通对话)。`daily_starlanguage`、`birth_time_rectification`、`guided_topic` 三个入口一律不进分流。命中 `smalltalk` 时走轻量流:发与现在同构的 SSE 事件(`answer.delta` + 结束事件),不发 activity、不发 receipt、不进 `streamAgentResponse` 的 `requireTool: true` 路径。
- 验收:源码合同测试——寒暄分支不引用任何 `run-jyotish-*`;三个入口不进分流;`requireTool: true` 的两处调用点数量不变。
### 5.3 免费完成(新迁移 `frontend/supabase/migrations/<ts>_consultation_free_completion.sql`)
新增 `complete_consultation_free(p_user_id, p_request_id, p_session_id, p_response_message, p_actual_usage)`:复用与 `complete_consultation_response` 相同的 advisory lock 与参数校验;把回复 append 进 `chat_sessions.messages`;把预留的 1 点按 `cancel_consultation_credit` 同一套 `credit_transactions` 记账退回;`consultation_requests.status` 置 `completed`;`complete_usage` 仍照常记账(成本要可观测),但不扣点。幂等:已 `completed` 且 `response_message` 相同 → `success = true`。
- 验收:`npm run test:db --prefix frontend`(需 Docker)。**本机无 Docker 时**:迁移照写,验收降级为部署后由产品在 staging 实测——发一句「你好」,点数不变、刷新后这轮对话还在——并把缺口写进 `BLOCKED.md`,**不得写成「通过」**。
### 5.4 纵深防御(提示词,只是兜底不是主修)
把 `natalSpokenReportContract` 末行的 chit-chat 豁免句搬进 `productConversationVoice`,让三种模式共用,并补一句:用户只是打招呼、道谢、告别时,回一句话,不要套开场形状。
- 注意:这一条**只解决「话说多」,不解决「排盘」**——`requireTool` 仍会强制排盘。主修是 5.1–5.3。
- 验收:`consultation-voice-contract.test.ts` 新增断言——本命 / 窗口 / 无生时三种模式的指令字符串都含该豁免句;既有 OPENER SHAPE 断言保持绿。
### 5.5 前端
寒暄轮不显示思考面板与活动步骤,不显示技法证据面板,点数显示不变。
- 验收:`tsc --noEmit` 0;`npm run lint` 0 error;`npm test` 失败清单与基线 `09b41009` 逐条一致、0 新红;`next build` 后 `/` 仍 `○ Static`;首屏 gzip ±2%。
### 5.6 记录
- `docs/BUG_HISTORY.md`:**BUG-976**(普通对话寒暄轮被当咨询轮:全量排盘 + 400 字判词 + 扣 1 点)、**BUG-977**(chit-chat 豁免句只发给本命 Agent,窗口与无生时模式拿不到)。两条都要写明与 BUG-922 / BUG-923 的关系:不是复发,是那两单的边界之外。
- `CHANGELOG.md`:一行「打招呼不再触发整盘计算,也不扣点」。
- `frontend/docs/VOICE.md`:加一条「寒暄只回一句」。
## 6. 让步顺序
1. 无 Docker → 5.3 的自动化验收降级为 staging 实测清单 + `BLOCKED.md`,其余照做。
2. 免费完成函数在评审中被判定计费一致性风险过高 → 退到「寒暄轮照常扣点但不排盘、只回一句」,**并立即回报产品**:这会推翻 D1,必须产品二次确认后才算数。
3. 分流调用实测 P50 超过 1.5 s → 功能保留,超时上限降到 1.5 s,并在进度记录里给出实测数字。
4. 分流准确率不满意时,**不得**加关键词表或正则来补——宁可漏判(漏判的代价是照常花一次钱,用户无感)。
5. 误判成本口径(写进进度记录):把咨询误判成寒暄 = 用户收到一句「在,想看哪一块?」,再问一次即可;这比 BUG-922 的整轮失败轻,可接受。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/consult-smalltalk-fastpath-20260920 \
.worktrees/consult-smalltalk-fastpath-20260920 origin/staging
grep -n "## BUG-922\|## BUG-923\|## BUG-937\|## BUG-938" docs/BUG_HISTORY.md # 四条全文读完再动手
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sed 's/## BUG-//' | sort -n | tail -1 # 核对最大号
```
前端门禁(改完逐条跑,结果进 `PROGRESS-consult-smalltalk-fastpath-20260920.md`):
```bash
cd frontend
./node_modules/.bin/tsc --noEmit
npm run lint
npm test
npm run build
```