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

11 KiB
Raw Blame History

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. 开工前置命令

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):

cd frontend
./node_modules/.bin/tsc --noEmit
npm run lint
npm test
npm run build