一句「你好」走完整窗口排盘 + 400 字判词 + 扣 1 点。三层实证与不扣点 通道缺口写进任务书;产品拍板不扣点不排盘回一句,分流禁用正则, BUG-922/923 的每轮必调合同不动。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
11 KiB
TASK · 普通对话寒暄轮不再走全量排盘(快速通道)
- 基线 commit:
origin/staging09b41009(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. 硬红线
- 不改
index.ts两处every turn句、不改consultationNatalPrepareStep/consultationWindowPrepareStep、不改contractReady()。consultation-birth-time-mode.test.ts与consultation-workflow-contract.test.ts里 BUG-922 的四条断言必须保持绿。 - 代码里不得出现寒暄关键词表、匹配寒暄的正则、按长度判定的阈值。(工具名/JSON 解析用的既有正则不在此列。)
- fail-open 是硬要求:任何异常路径都必须落回完整咨询路径,不得静默回一句话了事。
- 寒暄路径不得调用任何排盘工具,不得发 activity 步骤,不得写 workflow receipt 或技法审计行。
- 寒暄路径的回复不得包含任何盘上主张。实现方式是能力剥夺——这次调用不绑 Skill、不给工具、不注入任何星盘上下文,它手里根本没有盘——不是事后检查输出。
- 迁移只加函数,不动表结构;不得放宽任何计费校验、幂等或锁。
- 隐私:不得把用户问题原文写进除既有 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 --noEmit0;npm run lint0 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. 让步顺序
- 无 Docker → 5.3 的自动化验收降级为 staging 实测清单 +
BLOCKED.md,其余照做。 - 免费完成函数在评审中被判定计费一致性风险过高 → 退到「寒暄轮照常扣点但不排盘、只回一句」,并立即回报产品:这会推翻 D1,必须产品二次确认后才算数。
- 分流调用实测 P50 超过 1.5 s → 功能保留,超时上限降到 1.5 s,并在进度记录里给出实测数字。
- 分流准确率不满意时,不得加关键词表或正则来补——宁可漏判(漏判的代价是照常花一次钱,用户无感)。
- 误判成本口径(写进进度记录):把咨询误判成寒暄 = 用户收到一句「在,想看哪一块?」,再问一次即可;这比 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