From 6d097de95bfc9110dc6bfe92b64b21fca0249fd0 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 17 Sep 2026 06:10:16 +0000 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20=E5=92=A8=E8=AF=A2=E8=BF=BD?= =?UTF-8?q?=E9=97=AE=E8=BD=AE=E4=B8=8D=E8=B0=83=E6=8E=92=E7=9B=98=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8D=B3=E6=95=B4=E8=BD=AE=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B9=A6=EF=BC=88BUG-922/923=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45 --- docs/tasks/README.md | 1 + ...consult-followup-tool-contract-20260917.md | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docs/tasks/TASK-consult-followup-tool-contract-20260917.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index f658788b..4265f409 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -129,6 +129,7 @@ | `TASK-rectification-tiebreak-card-loss-20260915.md` | `PROGRESS-rectification-tiebreak-card-loss-20260915.md` | **P0**:点卡上「再答两道参考题」交付卡消失(BUG-706);按钮亮但选项建不出变成裸题(BUG-708);旁白写「相对支持度」并与卡上入口打架(BUG-709)。卡上入口删除,出卡前收集,有活题时卡留下、采用置灰 | 待验收 | `codex/rectification-p0-20260915` | | `TASK-rectification-p0-fix-20260915.md` | `PROGRESS-rectification-p0-fix-20260915.md` | **验收修复单**:`f51e494c` 六条缺陷全部实现且方式正确,但 `page.tsx` 从 1951 涨到 1964 行,撞了 `chart-view-route.test.ts` 的 `<= 1951` 上限(AGENTS.md §6 增长冻结)。全量 fail 32→33,就这一条。门禁红很可能是 staging 停在 `2d7698ea`、6 个提交未部署的原因。修法是把 BUG-705 的十来行接线搬出 page.tsx,不放宽上限 | 待验收 | `codex/rectification-p0-fix-20260915` | | `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal` 的 `dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` | +| `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待领取 | — | ### 个人报告 diff --git a/docs/tasks/TASK-consult-followup-tool-contract-20260917.md b/docs/tasks/TASK-consult-followup-tool-contract-20260917.md new file mode 100644 index 00000000..4cdcab36 --- /dev/null +++ b/docs/tasks/TASK-consult-followup-tool-contract-20260917.md @@ -0,0 +1,126 @@ +# 任务书 · 咨询追问轮不调排盘工具即整轮失败:提示词与运行合同对齐(2026-09-17) + +## 0. 基线 + +- 基线 commit:`1b508d5e`(`origin/staging` head)。 +- 分支:`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. 硬红线 + +1. 不改 `contractReady()`;不改 `retry` / `retryForAnswer` 的触发条件;不给失败轮扣点。 +2. `page.tsx` 与 `jyotish_api_server.py` 不动(AGENTS.md §6)。 +3. 既有测试断言不得静默弱化;改任何断言写「原值 / 新值 / 原因」三栏。 +4. 不顺手改 Skill 文件、不 bump Skill 版本(本单只改 Agent 系统指令与 SDK 调用参数)。 +5. 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 登录态),把下面的取证方法原样写进进度记录,留给产品负责人或部署侧: + +```bash +# 在 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. 让步顺序 + +1. T2 若本仓 SDK 版本的 `prepareStep` 不接受 `toolChoice: "required"`(类型或运行时报错),退而只做 T1,并在进度记录与 `BLOCKED.md` 写明版本与报错原文;不得顺手升级依赖。 +2. T2 若 `required` 让本命第 0 步在 `generalDailyContext`(首页今日)路径误触发(那条路径 `requireTool: false`,用的是 `getGeneralJyotishAgent`,理论上不经过 `natalStreamOptions`),先用测试证明,再决定是否只对申报时段路线加 required。 +3. T1、T3 不可让步。 + +## 7. 开工前置命令 + +```bash +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)在申报时段模式的会话里: + +1. 先问一个正经问题(例如「看看我事业上的整体特点」),应正常出回答。 +2. 紧接着发「?」,再发「你在说什么鬼」。两轮都应先出现「正在比较声明出生窗口内的稳定层」再出回答,**不得**出现「Agent 未完成必要的方法与计算步骤」。 +3. 换到本命模式(有出生分钟)的会话重复第 2 步,同样不得整轮失败。 +4. 同一会话刷新页面后,历史里应能看到助手回复,不再是只有用户消息。