docs: BUG-954 根因由服务端日志定死——窗口 Agent 没绑方法块

日志:input-processor jyotish-skill-bound abort ×2,modelStepCount 0、
0 token、整轮 69ms,模型从未被调用。windowJyotishInstructions 从不含
BOUND_METHOD_MARKER,而 getWindowJyotishAgent 照样 attach 绑定,自
9958e00a(08-21)起申报时段每轮必败。abort 与「模型没调工具」同码,
是它藏四周的原因。初版任务书的「模型没调工具」立论已证伪并改写。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-18 07:50:31 +00:00
co-authored by Claude Opus 5
parent d0317f7f4e
commit e5b2ad14dd
3 changed files with 62 additions and 53 deletions
+15 -13
View File
@@ -12510,22 +12510,24 @@
- 复发自:BUG-947
- 修复版本:`cd4775ae`
## BUG-954 | 申报时段咨询整轮失败:模型没调计算工具,合同判未完成
## BUG-954 | 申报时段咨询自 08-21 起每轮秒败:窗口 Agent 绑了 skill 却没把方法块写进系统提示
- 状态:investigating
- 状态:investigating(根因已由服务端日志确认,修复未做)
- 首次发现:2026-09-18
- 最近更新:2026-09-18
- 影响面:`windowJyotishInstructions`、`consultationWindowPrepareStep`、`consult/route.ts` 申报时段分支、`contractReady()`
- 用户现象:staging(已部署 `9cdcf96b`)申报时段会话问「未来半年我事业如何?」,等待后 `run.failed code=runtime_contract_incomplete`,提示「Agent 未完成必要的方法与计算步骤,本次不会扣点。」公开事件只有 `run.started` → `skill.started/completed` → `activity loading-method` → `run.failed`。
- 触发条件:`consultationMode=declared_birth_window`、`theme=general`、`history=[]` 的首轮提问(问题带应期语义)。
- 已确认事实:
1. 回执 `steps` 只有 `skill` 与 `runtime-contract-retry` 两条,`stepBudget.used=2`,**没有任何 tool 步骤**,`workflow.route=declared-birth-window / status=blocked`,`skill.referenceReads=0`、`methodologySections=0`。
2. 该形状**不是**供应商 error 块那条路:BUG-938 的修复规定 error 块会追加 `validation model-stream-error failed`、不触发合同 retry、公开码为 `calculation_failed`。本次回执无该步、有 retry、公开码是 `runtime_contract_incomplete`,因此两次 attempt 都是「模型没调工具」。
3. `contractReady()`(`stream-agent-response.ts:336-339`)要求 `consultationToolSuccessCount === 1`;未绿时模型正文一律丢弃,所以模型若改成直接写一段文字回答,用户端看到的就是整轮失败而不是那段文字。
4. `run-jyotish-window-consultation` 的 `inputSchema` 只有 `question`(`consultation-tools.ts:765-767`),出生数据全部服务端绑定——由模型决定调不调,对计算结果没有任何信息增益。
- 初判根因(待日志确认):BUG-923 记录过「提示词是软约束,`toolChoice: "auto"` 不能保证第 0 步调用排盘工具」;BUG-937 因为某些 thinking 供应商拒收 `required`,把第 0 步改回 `activeTools` + `auto`,**等于把 BUG-923 的洞原样放回来**,且没有补第三条路。窗口指令同时写着「每轮必须先调工具」和「`can_answer_precise_timing` 恒为 false,不得给出月份 / 日期 / 大运边界」,而本次问题正是应期语义;模型在冲突下选择不调工具的可能性最大。
- 待取证据:staging 该 `requestId` 的服务端日志(`modelFinishReason`、是否有 `[consult-provider-error]`、模型是否产出过被丢弃的正文)、本次使用的模型 id 对应的供应商。没有这条日志之前不得把根因写成 resolved。
- 影响面:`src/mastra/index.ts` `windowJyotishInstructions` / `getWindowJyotishAgent`、`src/mastra/skill-binding.ts` `jyotishSkillBoundProcessor`;`declared_birth_window` 模式的每一轮
- 用户现象:staging 申报时段会话问「未来半年我事业如何?」,`run.failed code=runtime_contract_incomplete`,公开事件只有 `run.started` → `skill.started/completed` → `activity loading-method` → `run.failed`,回执零 tool 步。
- 触发条件:任何 `consultationMode=declared_birth_window` 的请求,与问题内容、模型、供应商无关。
- 根因(服务端日志实证,requestId 脱敏为前八位 `95de38fd`):
1. `docker logs jyotisha-staging-web-1` 命中两条 `[WORKFLOW] Error executing step ... input-processor.step.processor:jyotish-skill-bound: Error: Jyotish skill method is not bound into the system prompt for jyotish-vedic-astrology`(两次 attempt 各一条)。
2. 同一 runId 的 `[agent-observability]`:`toolCalls: []`、`modelStepCount: 0`、`inputTokens: 0`、`outputTokens: 0`、`run.total durationMs: 69`、`retryCount: 1`、`errorCode: runtime_contract_incomplete`。**模型从未被调用**,整轮 69 毫秒结束。
3. 代码对应:`jyotishSkillBoundProcessor`(`skill-binding.ts:143-153`)在输入处理阶段断言系统提示里含 `BOUND_METHOD_MARKER`(`<jyotish-skill name="...">`),缺失即 `abort()`。`jyotishSkillMethodBlock` 只在 `jyotishInstructions`(本命,`index.ts:21`)里插值;`windowJyotishInstructions`(`index.ts:176-190`)从未包含它,而 `getWindowJyotishAgent`(`index.ts:198`)照样 `...jyotishSkillBinding()`。
4. 时间线:处理器由 `d04fc30b`(2026-08-18)引入;窗口 Agent 由 `9958e00a`(2026-08-21)新建,**诞生时就带绑定、不带方法块**。因此申报时段路线自 2026-08-21 起每轮必败,已持续约四周。
- 为什么没被发现:`abort()` 被翻译成与「模型没调工具」相同的公开码 `runtime_contract_incomplete`,与 BUG-922/923/937 的现象完全同名。BUG-937 的记录写的是「本命与申报时段两条路线每一轮都失败」,本命线被 `required` 的修复救活,窗口线的这条根因从未被触及,却因为现象消失一半而被当作同一件事结案。测试侧也没有「凡 attach `jyotishSkillBinding()` 的 Agent,其 instructions 必须含 marker」这条源码合同。
- 修复:见 `docs/tasks/TASK-window-consult-contract-20260918.md`。要点:窗口指令注入带 marker 的方法块(不能直接照抄本命块,它带 Level 2 报告骨架与本命口径);补源码合同测试;处理器 abort 给出独立可见错误码,不再与合同未完成同码。
- 验证:待实现。
- 防复发:待实现。
- 关联记录:BUG-205、BUG-214、BUG-922、BUG-923、BUG-937、BUG-938
- 复发自:BUG-923(`auto` 不保证调用这一条在 BUG-937 修复后回归)
- 复发自:无(与 922/923/937 同现象、不同根因;那三条均未覆盖窗口 Agent 的提示词装配)
- 修复版本:待发布
+1 -1
View File
@@ -138,7 +138,7 @@
| `TASK-consult-three-channels-20260918.md` | `PROGRESS-consult-three-channels-20260918.md` | 咨询运行时三通道:进度 / 思考 / 正文生成时分离。设计分支误用 BUG-939/940/941,落地改号 942–944 | 验收未通过(见修复单) | `94c1e81f` |
| `TASK-consult-three-channels-fix-20260918.md` | `PROGRESS-consult-three-channels-fix-20260918.md` | 验收修复单:领域上限解耦(945)、31s 预算断言(946)、校正思考分片门(947)、Pass 4 按模式分流(948)、容量算术(949) | 已验收通过(Claude:tsc 0 / lint 0 error / npm test 3491 条 31 红且与基线 `742ffbc6` 同一组、原 12 条红全绿 / `/` 仍 Static / 首屏 js gzip 468,388 B 较基线 +0.02%);review 另出 BUG-950~953 见下一行 | `e32ce624` |
| `TASK-consult-pass4-streaming-20260918.md` | `PROGRESS-consult-pass4-streaming-20260918.md` | 验收 review 三轮:Pass 4 一 hold 正文就整段蹦出、逐字流式消失(BUG-950 产品拍板按句放行);无出生分钟模式整段被一句拒绝顶掉、一般知识句一起丢(951 改按句丢弃);该模式下日期不留痕(952);校正流 token 级 thinking 是死链,按 P2 删除并把测试翻转成否定合同(953)。基线 `1e553976` | 待验收 | `cd4775ae` |
| `TASK-window-consult-contract-20260918.md` | — | **P0 线上**:申报时段问「未来半年我事业如何」整轮 `runtime_contract_incomplete`,回执零 tool 步(BUG-954)。根因链 922→923→937:`required` 被供应商拒收后改回 `auto`,「模型可以不调工具」的洞回归;而窗口工具入参只有 question,计算本就不需要模型触发 → 改服务端预跑。另含合同未绿不得静默丢正文(955)、窗口指令应期冲突(956)。基线 `9cdcf96b` | 待领取 | — |
| `TASK-window-consult-contract-20260918.md` | — | **P0 线上**:申报时段模式**自 2026-08-21 起每轮秒败**(69ms、0 token、模型从未被调用)。服务端日志实证根因:窗口 Agent attach 了 `jyotishSkillBinding()`,但 `windowJyotishInstructions` 从来不含方法块 marker,输入处理器直接 abort(BUG-954)。abort 与「模型没调工具」同码,是它藏四周的原因(955);另含合同未绿不得丢正文(956)、计算不该由模型触发(957,须等 954 上线后另轮)、窗口指令应期冲突(958)。基线 `9cdcf96b` | 待领取 | — |
| `TASK-first-paint-dead-screen-fallback-20260917.md` | — | 真机:首页永远停在「正在载入账户」,兜底全在没跑起来的 bundle 里(BUG-936 investigating)。根 layout 加与 bundle 无关的内联兜底 + 去掉本仓正则后行断言 | 待领取 | — |
| `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` |
| `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 |
@@ -1,52 +1,57 @@
# TASK · 申报时段咨询不再靠模型触发计算(2026-09-18 第四轮)
# TASK · 申报时段咨询修复(2026-09-18 第四轮)
> 基线:`origin/staging` @ `9cdcf96b`(已部署,健康检查一致)。
> 事故记录:`docs/BUG_HISTORY.md` BUG-954(investigating)。
> BUG 编号起点:开工时最大号为 **BUG-954**,本单占 **BUG-954(转 resolved)~ BUG-956**。
> 事故记录:`docs/BUG_HISTORY.md` BUG-954(根因已由服务端日志确认)。
> BUG 编号起点:开工时最大号为 **BUG-954**,本单占 **BUG-954 ~ BUG-958**。
>
> **修订说明(2026-09-18 晚)**:本单初版按「模型没调工具」立论,服务端日志已证伪。根因是窗口 Agent 的系统提示里没有 skill 方法块,模型**从未被调用**。原 §3「服务端预跑」降级为 P2 结构性改进(BUG-957),不再是本单的 P0。
## 0. 事故实证
## 0. 事故实证(服务端日志)
staging 真实一轮(`consultationMode=declared_birth_window`、`theme=general`、问题「未来半年我事业如何?」、`history=[]`):
staging 真实一轮(`consultationMode=declared_birth_window`、`theme=general`、问题「未来半年我事业如何?」、`history=[]`),`docker logs jyotisha-staging-web-1`:
```
run.started → skill.started/completed → activity(loading-method) → run.failed
code = runtime_contract_incomplete
receipt.steps = [skill, runtime-contract-retry] ← 没有任何 tool 步
receipt.stepBudget = {planned: 11, used: 2}
receipt.workflow = {route: declared-birth-window, status: blocked, missingLayers: [birth-minute]}
receipt.skill = {loaded: true, referenceReads: 0, methodologySections: 0}
[WORKFLOW] Error executing step ...input-processor.step.processor:jyotish-skill-bound:
Error: Jyotish skill method is not bound into the system prompt for jyotish-vedic-astrology ← 出现两次(两次 attempt)
[agent-observability] toolCalls: [] modelStepCount: 0 inputTokens: 0 outputTokens: 0
run.total durationMs: 69 retryCount: 1 errorCode: runtime_contract_incomplete
```
排除项:这不是供应商 error 块那条路。BUG-938 的修复规定 error 块会写 `validation model-stream-error failed`、**不**触发合同 retry、公开码为 `calculation_failed`。本次三项全不符,所以两次 attempt 都是**模型没有调用 `run-jyotish-window-consultation`**。
**整轮 69 毫秒,模型一次都没被调用,0 token。** 公开事件里的 `skill.started/completed` 是绑定步骤自己报的,不代表方法块真的进了提示。
## 1. 根因链(为什么「修过了还犯」)
## 1. 根因
| 轮次 | 做了什么 | 留下什么 |
| --- | --- | --- |
| BUG-922 | 提示词改成「每一轮都必须先调工具」 | 提示词是软约束 |
| BUG-923 | 第 0 步 `toolChoice: "required"` | 记录里写明「`auto` 不能保证第 0 步调用」 |
| BUG-937 | 某些 thinking 供应商拒收 `required`,改回 `activeTools` + `auto` | **把 BUG-923 的洞原样放回来**,没有补第三条路 |
| 本轮 | —— | 模型不调工具 → 合同未绿 → 正文被丢弃 → 整轮失败 |
| 位置 | 事实 |
| --- | --- |
| `skill-binding.ts:143-153` | `jyotishSkillBoundProcessor` 在输入处理阶段断言系统提示含 `BOUND_METHOD_MARKER` = `<jyotish-skill name="jyotish-vedic-astrology">`,缺失即 `abort()` |
| `index.ts:21` | `${jyotishSkillMethodBlock}` **只**插值进 `jyotishInstructions`(本命) |
| `index.ts:176-190` | `windowJyotishInstructions` 从来没有这个块 |
| `index.ts:198` | `getWindowJyotishAgent` 照样 `...jyotishSkillBinding()` |
叠加因素:`windowJyotishInstructions`(`src/mastra/index.ts:176-190`)同时写着「每轮必须先调工具」和「`can_answer_precise_timing` 恒为 false,不得给出月份 / 日期 / 大运边界」。本次问题(未来半年事业)正是应期语义,模型在两条指令冲突下选择「我答不了、不调工具」是最可能的路径;它若写了解释文字,也会因为 `contractReady()` 未绿被整段丢弃(`stream-agent-response.ts:336-339`)。
时间线:处理器 `d04fc30b`(08-18)引入 → 窗口 Agent `9958e00a`(08-21)新建时就带绑定、不带方法块 → **申报时段路线自 2026-08-21 起每轮必败,已持续约四周**。
**结构缺口一句话**:`run-jyotish-window-consultation` 的 `inputSchema` 只有 `question`(`consultation-tools.ts:765-767`),出生数据完全服务端绑定——**让模型决定调不调,对计算结果没有任何信息增益,只增加一条失败路径。** 本命路线同理。
为什么四周没人发现:`abort()` 被翻成与「模型没调工具」同一个公开码 `runtime_contract_incomplete`。BUG-922/923/937 处理的正是同名现象,本命线被救活后现象消失一半,窗口线这条根因从未被触及。
## 2. 决策记录(待产品确认,实现前必须回填「已授权」)
## 2. BUG-954(P0)窗口 Agent 必须绑定方法块
产品诉求(2026-09-18):只知道出生范围的用户必须能正常使用产品。本单把「模型触发计算」改成「服务端预跑计算」,模型只负责写作。这**推翻**了 BUG-922/923/937 那条「靠提示词 + toolChoice 让模型调工具」的路线,但不推翻它们的目的(每轮必须有本请求内的真实计算)。
1. `windowJyotishInstructions` 注入带 marker 的方法块。**不能直接照抄 `jyotishSkillMethodBlock`**:它带本命 Level 2 报告骨架与「六步宫位 / Yoga 表 / 技法审计表」的输出要求,与窗口口径(无精确应期、只报稳定层、变动层列可能性)冲突。做法二选一,在进度记录里写明选了哪条:
- (a) 抽出 `jyotishSkillMethodBlock` 的「方法主体 + marker」部分,报告骨架段落作为可选后缀,本命加、窗口不加;
- (b) 窗口保留完整方法块,紧随其后用窗口口径逐条覆盖(明确写「以下窗口约束优先于上面方法块里的报告骨架与应期表述」)。
2. 验收标准:
- 新增源码合同测试——**凡 attach `jyotishSkillBinding()` 的 Agent,其 `instructions` 必须包含 `BOUND_METHOD_MARKER`**(遍历 `index.ts` 里的 Agent 工厂,不是逐个手写断言);
- 新增运行测试——构造窗口 Agent 的输入处理,`jyotishSkillBoundProcessor` 不 abort;
- 部署后在 staging 真发一轮申报时段提问,`/api/health` SHA 对得上,回执里有 `run-jyotish-window-consultation` 的 completed 步。
## 3. BUG-954(P0)服务端预跑,模型不再负责触发计算
## 3. BUG-955(P1)处理器 abort 不得与「合同未完成」同码
1. 申报时段路线在进入模型循环**之前**,服务端直接执行窗口计算(现有 `createWindowConsultationTools` 的 `execute` 主体抽成可独立调用的函数),把 evidence packet 作为服务端消息注入上下文。
2. `consultationToolSuccessCount` 由这次服务端执行记账,`contractReady()` 因此在模型开口前就已绿;工具仍然留在 Agent 上(模型想再调一次,命中同一请求内缓存,不重复计算)。
3. 合同 retry 那条路保留,但它现在只可能因为「服务端计算失败」触发,不再因为「模型没调」触发。
4. 验收标准:
- 新增合同测试——申报时段路线在模型**一次工具都不调**的假流下,仍然产出回答、`run.failed` 不出现、回执里有 `run-jyotish-window-consultation` 的 completed 步;
- 现有 `consultationWindowPrepareStep` 的两条断言按新口径改写(三栏说明);
- 不得再出现 `toolChoice: "required"` 或 named toolChoice(BUG-937 防复发条款仍然有效)。
`abort()` 目前落到与「模型没调工具」相同的公开码,是这条 bug 藏四周的直接原因。
## 4. BUG-955(P1)合同未绿时不得静默丢弃模型正文
1. 输入处理器 abort 走独立内部码(例如 `skill_binding_failed`),回执追加 `validation skill-binding-abort failed`,服务端打 `[consult-binding-error]` 日志(requestId + 内部码,不含提示词原文)。
2. 公开层可以继续用现有枚举,但**回执必须能一眼区分**「装配失败」与「模型没调工具」。
3. 参照 BUG-938 对供应商 error 块的同类处置。
4. 验收:假流两条——装配失败 / 模型没调工具,回执步骤名不同;`agent-observability` 两个码都能落日志。
## 4. BUG-956(P1)合同未绿时不得静默丢弃模型正文
`stream-agent-response.ts` 现在在合同未绿时丢弃全部正文,用户只看到「未完成」。即便 BUG-954 落地,这条兜底仍要有:
@@ -54,18 +59,18 @@ receipt.skill = {loaded: true, referenceReads: 0, methodologySections: 0}
2. 模型既没调工具也没写字时,维持现有 `runtime_contract_incomplete`(不扣点)。
3. 验收标准:假流「无工具 + 有正文」→ 用户拿到正文 + 降级说明;假流「无工具 + 无正文」→ 仍是 `runtime_contract_incomplete`。
## 5. BUG-956(P2)窗口指令里「必须调工具」与「不得给应期」的冲突要写清
## 5. BUG-957(P2)结构性改进:计算不该由模型触发
本单初版的主修法,现在降级为独立改进项,与 BUG-954 无因果关系,但值得单独做:`run-jyotish-window-consultation` 的 `inputSchema` 只有 `question`(`consultation-tools.ts:765-767`),出生数据完全服务端绑定——由模型决定调不调,对计算结果没有信息增益,只多一条失败路径(BUG-205/214/922/923/937 都在这条链上)。建议服务端在模型循环前预跑计算并记账,工具仍留在 Agent 上命中同请求缓存。**不要与 BUG-954 同轮做**:先让窗口路线活过来并在真实环境验证,再动运行时结构。
## 6. BUG-958(P3)窗口指令里「必须调工具」与「不得给应期」的冲突要写清
`src/mastra/index.ts:178-190` 补一句显式口径:应期类问题**仍然要先调工具**,再用稳定层给方向性回答,并说明哪部分需要出生分钟;不得因为「精确应期不可用」而跳过计算或拒答整题。验收:`consultation-workflow-contract.test.ts` 加一条源码断言。
## 6. 待取证据(不阻塞开工)
staging 该 `requestId=95de38fd-…` 的服务端日志:`modelFinishReason`、有无 `[consult-provider-error]`、模型是否产出过被丢弃的正文、本次模型 id 的供应商。拿到后回填 BUG-954 的根因段并转 `resolved`;拿不到就保持 `investigating`,但 §3 的结构修法与日志结论无关,可以先做。
## 7. 硬红线
1. `tsc --noEmit` 0 错、`npm run lint` 0 error、`npm test` 失败数不超过基线 `9cdcf96b` 实测的 31 条且清单逐条一致;测试总数不低于 3495。
2. 不得放宽 `contractReady()` 的「每请求一次真实计算」语义——本单是把计算**提前到服务端**,不是取消它。
2. 不得放宽 `contractReady()` 的「每请求一次真实计算」语义。BUG-954 是把方法块补进提示,不是绕过绑定校验;不得用「删掉 `jyotishSkillBinding()`」的方式让窗口路线通过。
3. 不得再写 `toolChoice: "required"` / named toolChoice。
4. 改既有断言写「原值 / 新值 / 原因」三栏。
@@ -79,4 +84,6 @@ cd .worktrees/window-consult-contract-20260918/frontend
npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 开工基线:tests 3495 / pass 3449 / fail 31
```
收工:`docs/tasks/PROGRESS-window-consult-contract-20260918.md` + `docs/BUG_HISTORY.md`(954 转 resolved 或保持 investigating,955/956 新增)+ `CHANGELOG.md`,与代码同一批推 `staging`。
让步顺序:**BUG-954 单独一轮先上**(四周不可用,越快越好,改动面是一段提示词 + 两条测试),955 同轮或紧随;956 次之;957 必须等 954 在真实环境验证通过后另开一轮;958 顺手。
收工:`docs/tasks/PROGRESS-window-consult-contract-20260918.md` + `docs/BUG_HISTORY.md`(954 按证据转 `resolved`,955~958 新增)+ `CHANGELOG.md`(申报时段咨询四周不可用属用户可感知),与代码同一批推 `staging`。