Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
13 KiB
任务书 · 咨询追问轮不调排盘工具即整轮失败:提示词与运行合同对齐(2026-09-17)
0. 基线
- 基线 commit:
1b508d5e(origin/staginghead)。 - 分支:
codex/consult-followup-tool-contract-20260917,git worktree add -b codex/consult-followup-tool-contract-20260917 .worktrees/consult-followup-tool-contract-20260917 origin/staging。 - 范围:只有前端(
frontend/src/mastra/index.ts、frontend/src/mastra/consultation-tools.ts、frontend/src/app/api/consult/route.ts与对应测试)。不动 Python、不动迁移、不动 Skill、不动生时校正 Agent。 - BUG 段:BUG-922 起(基线
docs/BUG_HISTORY.md最大号 BUG-921,开工时再核对一次)。
1. 事故实证
产品负责人 2026-09-17 在 staging 一个申报出生时段(declared_birth_window)会话里连发三轮,三轮都没有得到回答。第三轮的事件流原样如下(已去掉会话与用户标识):
| 事件 | 内容 |
|---|---|
| 请求 | consultationMode: declared_birth_window,theme: general,question: 你在说什么鬼;服务端历史只有两条用户消息「请帮我看看我家庭关系的整体模式和特点」「?」,没有任何助手消息 |
skill.started / skill.completed |
jyotish-vedic-astrology |
activity |
phase: loading-method,「正在补齐方法与计算步骤」 |
run.failed |
code: runtime_contract_incomplete,「Agent 未完成必要的方法与计算步骤,本次不会扣点。」 |
回执 steps |
只有两步:skill 与 validation: runtime-contract-retry,没有任何 tool 步骤 |
回执 workflow |
route: declared-birth-window,status: blocked,techniqueTruth: declared-window |
代码定位(按符号):
- 门禁:
frontend/src/lib/stream-agent-response.ts的contractReady()。requireTool为真时要求consultationToolCompleted && consultationToolSuccessCount === 1。第一轮不满足则调用options.retry()再跑一轮,仍不满足即throw new Error("runtime_contract_incomplete"),模型已写出的文字全部丢弃(consumeAttempt在契约未绿时不释放文本,见 BUG-286 与测试「text written before the contract completes is dropped」)。 - 申报时段路线:
frontend/src/app/api/consult/route.ts中shouldRunDeclaredWindowWorkflow分支调用streamAgentResponse({ requireTool: true, retry, ... }),retry的追问原文是「运行合同不完整:本次尚未取得声明窗口计算结果。请调用 run-jyotish-window-consultation 完成计算,再据此回答」。 - 窗口 Agent 系统指令:
frontend/src/mastra/index.ts的windowJyotishInstructions,含这一句:「For questions that need personal chart structure, call run-jyotish-window-consultation before answering. Simple conversational follow-ups may use the existing packet.」 - 本命 Agent 系统指令:同文件
jyotishInstructions(第 17 行起,含natalSpokenReportContract之后的第 6 行)含同构的一句:「For questions that require a new chart claim, call run-jyotish-consultation before answering. Simple conversational follow-ups may use the existing context.」 - 工具选择:两条路线都是
toolChoice: "auto"。本命路线consultationNatalPrepareStep(frontend/src/mastra/consultation-tools.ts)在第 0 步只把activeTools收窄到排盘工具,但没有强制调用;申报时段路线用的是不带prepareStep的streamOptions。 - 历史为什么只剩用户消息:用户消息在扣点预留 RPC 里就写库(
route.ts的p_question_message: { role: "user", text: visibleQuestion }),运行失败不补助手消息。所以前两轮「家庭关系」「?」也都失败了。
2. 根因
系统提示词与运行合同互相矛盾。 提示词明确允许模型对「简单的对话式追问」不调排盘工具、复用「已有 packet / context」;服务端合同却要求每一次请求都必须恰好一次成功的排盘工具调用,否则整轮作废。像「?」「你在说什么鬼」这类追问,模型按提示词判断为对话式追问、直接用文字回答,门禁判定合同未完成;服务端追问一句后模型仍按同一套系统指令行事,第二轮也不调工具,于是 runtime_contract_incomplete。
提示词里的「existing packet / existing context」还是事实错误:排盘结果只在单次请求内缓存(createWindowConsultationTools / createConsultationTools 的闭包变量 calculation),跨请求根本没有可复用的 packet。模型信了这句话就会跳过工具。
这两句是 2026-08-21 提交 9958e00a 写进去的,门禁是 2026-08-17 提交 7886629b 定的,两者从未对齐。Bug 历史里 runtime_contract_incomplete 的既有记录(BUG-205 缓存拒绝、BUG-214 失败次数计入门禁、BUG-286 契约未绿文本丢弃)都不是这个原因,防复发措施均仍在,本单是新问题,不是复发。
第一轮「请帮我看看我家庭关系的整体模式和特点」是正经解盘问题,它为什么失败本单不下结论,见 T4。
3. 决策记录
- 产品负责人 2026-09-17 拍板 方案 1:删掉「简单追问可以不调工具」的例外,每一轮回答前都必须调用排盘工具。理由:工具在单次请求内有缓存、代价小;每轮都有当轮证据,符合「解盘必须有原始数据」红线。
- 否决方案 2(保留提示词、让没调工具的轮次走不扣点的纯对话回复):追问轮会没有星盘证据,与 AGENTS.md Part B 的证据要求冲突。
- 不推翻任何既有红线:
contractReady()的「恰好一次成功调用」口径不放宽;BUG-214 允许失败尝试后重试成功的语义保持;BUG-286 契约未绿即丢弃文本的语义保持。 - 生时校正 Agent(
agentic-rectification.ts)不在本单范围,它有自己的合同。
4. 硬红线
- 不改
contractReady();不改retry/retryForAnswer的触发条件;不给失败轮扣点。 page.tsx与jyotish_api_server.py不动(AGENTS.md §6)。- 既有测试断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。
- 不顺手改 Skill 文件、不 bump Skill 版本(本单只改 Agent 系统指令与 SDK 调用参数)。
- Bug 历史与进度记录不得写入会话 ID、用户 ID、出生资料。
5. 任务分解
T1 删掉两处「简单追问可跳过工具」例外(BUG-922)
windowJyotishInstructions:把「For questions that need personal chart structure, call run-jyotish-window-consultation before answering. Simple conversational follow-ups may use the existing packet.」改为「Call run-jyotish-window-consultation before answering every turn, including short follow-ups, clarifications, and complaints; the packet is request-scoped and is never carried over from an earlier turn.」jyotishInstructions:同样改「For questions that require a new chart claim… Simple conversational follow-ups may use the existing context.」为「Call run-jyotish-consultation before answering every turn, including short follow-ups; the calculation is request-scoped and is never carried over from an earlier turn.」- 全文再 grep 一次
follow-ups may use,确认frontend/src无残留。 - 验收:
- 在
frontend/tests/consultation-birth-time-mode.test.ts(它已经读窗口 Agent)或consultation-agentic-runtime.test.ts加契约:两段指令doesNotMatch(/follow-ups may use the existing/)且match(/before answering every turn/)。 - 既有「personal Agent exposes the Jyotish Skill and named server tool」等测试原样通过。
- 在
T2 第 0 步强制调用排盘工具(BUG-923)
提示词只是软约束,门禁是硬约束,中间应该有一层确定性的 SDK 参数把二者钉在一起。
consultationNatalPrepareStep:第 0 步在activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID]之外把toolChoice改为"required";后续步骤保持"auto"。- 申报时段路线:新增
consultationWindowPrepareStep(同文件、同形状:第 0 步activeTools: ["run-jyotish-window-consultation"]+toolChoice: "required"),并在route.ts的申报时段分支把streamWithOverflowRetry(agent)与retry改为使用带该prepareStep的选项对象(照本命分支natalStreamOptions的写法建windowStreamOptions)。 retryForAnswer/continueAfterLength/composeSection不带强制(它们是在合同已绿之后跑的,本命分支的composeSection明确toolChoice: "none",不动)。- 开工先确认本仓 Mastra / AI SDK 版本的
prepareStep接受toolChoice: "required"(查node_modules/@mastra/core与ai的类型定义,写进进度记录)。 - 验收:
consultation-agentic-runtime.test.ts既有「natal first step exposes only the chart calculation tool」扩成断言第 0 步toolChoice === "required"、第 1 步"auto";为窗口prepareStep加对称的一条。- 加一条
route.ts源码契约:申报时段分支的agent.stream(调用与retry使用带prepareStep的选项(对齐consultation-stream-recovery.test.ts那类结构断言的写法)。 - 已有的「a calculation that succeeds only after failed attempts still satisfies the contract」「text written before the contract completes is dropped」原样绿。
T3 记录
docs/BUG_HISTORY.md新增 BUG-922、BUG-923:现象、触发条件(申报时段 / 本命模式下发短追问)、根因(提示词例外 vs 合同)、修复、验证、防复发(契约测试 + 第 0 步 required)、关联 BUG-205 / 214 / 286。CHANGELOG.md一条:追问、反问、短句也会先重新取回本轮星盘证据再回答,不再整轮失败。docs/tasks/PROGRESS-consult-followup-tool-contract-20260917.md:测试数字、失败清单与基线逐条比对、gzip 前后、prepareStep类型核对结果。
T4 第一轮失败原因取证(不占 BUG 号,写进进度记录)
第一轮是正经解盘问题,不能用 T1 的根因解释。回执只写到 web 容器控制台(frontend/src/lib/agent-observability.ts 的 console.info("[agent-observability]", …)),不进数据库。执行方做不了(无 VPS 登录态),把下面的取证方法原样写进进度记录,留给产品负责人或部署侧:
# 在 VPS 上,以 deploy 用户
cd /opt/jyotisha-production
docker compose -f deploy/docker-compose.server.yml logs web --since 2026-09-17T00:00:00 \
| grep agent-observability | grep '<该会话的 sessionId>' | head
三条 run.failed 各自的 steps 若都没有 tool 步骤,即同一根因、本单覆盖;若第一轮有 tool 步骤且 status: failed,另立单,不得并进本单。
6. 让步顺序
- T2 若本仓 SDK 版本的
prepareStep不接受toolChoice: "required"(类型或运行时报错),退而只做 T1,并在进度记录与BLOCKED.md写明版本与报错原文;不得顺手升级依赖。 - T2 若
required让本命第 0 步在generalDailyContext(首页今日)路径误触发(那条路径requireTool: false,用的是getGeneralJyotishAgent,理论上不经过natalStreamOptions),先用测试证明,再决定是否只对申报时段路线加 required。 - T1、T3 不可让步。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/consult-followup-tool-contract-20260917 .worktrees/consult-followup-tool-contract-20260917 origin/staging
cd .worktrees/consult-followup-tool-contract-20260917/frontend
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20 # 记下基线总数与失败清单
grep -o "BUG-[0-9]\+" ../docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 BUG-921
grep -rn "follow-ups may use" src # 应恰好 2 处
验收口径:tsc 0 错;lint 0 error;npm test 失败清单与基线逐条一致(无 Docker 的那组不算新增)且总数不减;next build 后 / 仍 Static;首屏 gzip ±2%(本单不动客户端 bundle,应为 0 变化)。
8. 环境缺口(留给产品负责人真机)
部署后(先核对 /api/health 的 deployment.gitCommit 等于本单合入 staging 的 SHA)在申报时段模式的会话里:
- 先问一个正经问题(例如「看看我事业上的整体特点」),应正常出回答。
- 紧接着发「?」,再发「你在说什么鬼」。两轮都应先出现「正在比较声明出生窗口内的稳定层」再出回答,不得出现「Agent 未完成必要的方法与计算步骤」。
- 换到本命模式(有出生分钟)的会话重复第 2 步,同样不得整轮失败。
- 同一会话刷新页面后,历史里应能看到助手回复,不再是只有用户消息。