Files
Jyotisha/docs/tasks/PROGRESS-consult-single-pass-answer-20260927.md
T
Jesse_ChenandClaude Opus 5.5 3b67d8e945
Independent Staging Quality Gate / validate (push) Successful in 11m39s
Independent Staging Quality Gate / publish (push) Canceled after 3m31s
docs(tasks): consult single-pass acceptance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-27 01:12:51 +08:00

129 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · 普通咨询回答改由看过星盘的那一步写(BUG-1053)· 2026-09-27
- 执行方式:直接执行(产品负责人授权子代理执行;Claude 事后独立验收)
- 基线:`origin/staging` `5f2e007f` 开工;提交前变基到 `03c7c0a9`(BUG-1052 首页落点单与数据卡调研任务书,只与本单的 `CHANGELOG.md` / `docs/BUG_HISTORY.md` / `docs/tasks/README.md` / `frontend/DESIGN.md` 重叠,已并存)
- 分支:`codex/consult-single-pass-answer-20260927`(本地提交,未推送)
- BUG 编号:1053(开工与提交时核对,1052 由并行的首页落点单占用)
- 引入:`04463e9a`(2026-08-23);相关 BUG-612 / BUG-942–944 / BUG-1051
## 结论
| 项 | 结果 |
| --- | --- |
| 缺陷复核 | 确认。用同一套真实 `getJyotishAgent` + 记录提示词的假模型跑修复前代码:3 次模型调用,只有第 2 次(主循环工具结果之后)的提示里有工具结果和 golden 盘面数值;用户看到的正文是第 3 次(compose)写的,它的提示里两者都没有 |
| D1 删掉第三段 | 完成:删 `composeAnswer` / `interpretFindings` / `publishFindings` / `composeOnce` / `drainSpoken` / `ThinkFinding`、`consultationComposePrompt` / `consultationSectionPrompt`、`AGENT_SLICE_MAX_STEPS`;主循环最后一步的正文就是回答 |
| 取答边界 | `stepScopedAnswer`:按步扣住正文,这一步调用了工具就整段丢;见下文 |
| 写作要求 | 搬进主循环看到的用户轮:`natalAnswerShapeInstruction()` / `dailyAnswerShapeInstruction()` |
| 续写有据 | `continueAfterLength(output, evidence)` + `consultationContinueMessages()`,三条路线共用一个构造函数 |
| 时间预算 | `createConsultationRunClock()`:工具 110 秒不变,循环拿到计算结果后交给 70 秒答案钟;最坏 180 秒 |
| BUG-1051 | 保持:结算只认写回答那一步的 `stop`;`length` 续写;其余截断不扣点 |
| D2 观测 | 名字不变、含义改为写回答那一步(见下文) |
| D3 | 未碰:工具结果瘦身、家庭拆分、审计表、Python 引擎、Skill 路由 |
## 回答怎么和工具前后的过程说明隔开
Mastra 的 `fullStream` 每个模型步都有 `step-start` → `text-delta…` → `tool-call`(可选)→ `tool-result` → `step-finish(reason)`。`consumeAttempt` 在 `stepScopedAnswer` 下:
| 情况 | 处理 |
| --- | --- |
| 计算结果到手之前的正文 | 与原来一样:不进回答,只存进 `uncontractedText` 作降级材料(BUG-961) |
| 计算结果到手之后,这一步的正文 | 先扣住;出现 Markdown 标题(`#`–`###`)或满 `ANSWER_RELEASE_CHARS = 160` 字才放出,之后本步照常流式(Pass 4 按句放行) |
| 这一步出现 `tool-call` | 扣住的正文整段丢弃,本步后续正文也不收 |
| `step-finish` 时还扣着 | 结束原因不是 `tool-calls` 就放出(短回答),是就丢 |
| 残余风险 | 一步先写了 ≥160 字或一个标题、再调用工具:那段已放出、无法撤回。系统提示要求不重读已交付的方法段,实测形状里过程说明都是一句话,未见此情况;部署后留意 |
160 字的依据:工具前的过程说明通常一句(「我再读一下参考」);本命开场规定 3–6 句、≤400 字,160 字大约是开场的两三句,第一段字晚一两秒出现。
## 写作要求现在在哪里
| 位置 | 内容 |
| --- | --- |
| 系统提示(未改) | `productConversationVoice`、`natalSpokenReportContract`、VOICE §7 开场形态、`consultationSpokenHeadingRule("natal")` 四个 H2 |
| 用户轮 `natalInstruction`(改) | 原句「事业/财富/婚恋/家庭先给口语开场,再按四个标题写结论…」换成 `natalAnswerShapeInstruction()`:拿到本轮计算结果后直接写回答、只用结果里的盘面事实、工具前后不写过程说明;开场无标题;一次写完四个 `##` 标题;不写统一参数 / 技法审计表 / 思考过程清单 / 内部 JSON。仍以「如需新的个人星盘结论,必须调用服务器绑定的排盘工具」开头,不与工具调用要求冲突(旧 compose 提示的「不要再调用排盘工具」没有搬过来) |
| 「深入看今日」入口 | `dailyAnswerShapeInstruction()`:三节写法原文保留,加同一句取证要求。旧 compose 提示强制本命四标题,与入口三节矛盾,这次一并消除 |
## 时间预算与每一层超时
| 层 | 修复前 | 修复后 |
| --- | --- | --- |
| 工具 / 申报时段预计算 | `agentAbortSignal` 110 秒 | 不变:`runClock.toolSignal` 110 秒;每领域 31 秒、领域墙钟 65 秒不变 |
| 主循环模型流 | 110 秒(与工具共用) | `runClock.loopSignal`:计算结果到手前跟随工具阶段 110 秒;到手后(`onAnswerPhase`)只受 70 秒答案钟约束 |
| 写回答 | compose 另起 70 秒答案钟(第一次写回答流打开时起算) | 同一只 70 秒答案钟,从计算结果到手那一刻起算,覆盖主循环剩余步骤 |
| 续写 / 回答重试 | 共用答案钟 | 不变 |
| 竞态 | — | 计算刚在 110 秒前完成、消费端还没读到 `tool-result` 时,`answerReady()`(`state.consultationToolCompleted`)让循环交接而不是被掐 |
| 最坏总等待 | 110 + 70 = 180 秒 | 仍是 180 秒:计算必须在 110 秒内完成,答案钟最晚 110 秒起算 |
| 路由 `maxDuration` | 240 | 240 不变(源码合同锁 110 + 70 < 240 且 ≤ 180) |
| Node / Caddy / 客户端 / 账务 / 断线 | BUG-1051 已逐层核对(`PROGRESS-consult-answer-truncation-20260926.md`) | 本单没有拉长最坏时长,结论不变;未改 `deploy/`、`.gitea/workflows` |
为什么选「计算结果到手时起算的答案钟」而不是「整条主循环 180 秒」:两者都保证写回答至少 70 秒,但整条 180 秒时,主循环若到 180 秒才停在 `length`,续写再拿 70 秒会到 250 秒,超过 `maxDuration`;答案钟方案让写回答、续写、重试共享同一个 70 秒,总时长封顶 180 秒。
写回答那一步开着 thinking(它是主循环的一步),思考 token 也从这 70 秒里出;修复前主循环工具结果后那一步同样在 110 秒里思考并写完一遍(然后被丢弃),再由 compose 重写。现在少一次整篇写作,正常情况应更快。
申报时段路线:预计算完成即合同就绪,写回答从流开始就走答案钟(修复前是 110 秒减去预计算的剩余时间)。无出生分钟路线:没有工具、不交接,主流仍 110 秒,续写 / 重试 70 秒,行为不变。
## 各种结束方式的行为
结束原因取「最后一个写出正文的步」的 `step-finish` 原因;没有分步信息时取整条流的 `finish`。原因:Mastra 循环在一步以 `other` / `unknown` / 无工具调用的 `tool-calls` 结束后会再跑一步,如果那一步什么也没写、以 `stop` 结束,整条流的 `finish` 就是 `stop`,半截回答会被当成完成(测试里实测到)。
| 写回答的那一步 | 有正文 | 无正文 |
| --- | --- | --- |
| `stop` | `run.completed`,扣点一次 | `answer-retry`(保留工具、命中同请求缓存,不重复宣布计算)→ 仍空 `empty_answer` |
| `length` | 续写一次,带计算结果;续写 `stop` → 完成;续写仍 `length` / 被掐 / 其他 → `answer_truncated` | 同左 |
| 答案钟掐断(Mastra `abort` + `finish(tripwire)`) | `answer_truncated`,记 `compose-abort`,半句不外发 | 不扣点 |
| `content-filter` / `tool-calls` / `other` / `unknown` / `error` | `answer_truncated`,不扣点 | `empty_answer` 路径 |
| Pass 4 把整篇都拒了 | — | `answer-retry` 带 `PASS4_RETRY_HINT` |
| Pass 4 拒了部分句子 | 就地丢弃,不重写(BUG-950) | — |
计算结果到手之前被掐记 `tool-abort`,之后记 `compose-abort`(名字沿用 BUG-1051,现指写回答那一步)。
## D2 观测字段
| 字段 | 修复前含义 | 修复后含义 |
| --- | --- | --- |
| `composeFinishReason` | compose 流的结束原因 | 写回答那一步的结束原因(续写 / 重试时取最后一次写回答的流) |
| `composeAborted` | compose 流是否被掐 | 写回答那一步是否被掐 |
| `answerVisibleChars` | 回答字数 | 不变 |
| `modelFinishReason`(只进日志) | 最后一个流的 `finish` | 不变(整条流的 `finish`,可能与上面不同) |
名字保留是为了和 BUG-1051 刚部署的埋点连续;仍只有枚举 / 布尔 / 计数,公开回执不带。
## 删掉了什么
| 删除 | 说明 |
| --- | --- |
| `composeAnswer`(路由 + 流选项) | 看不到计算结果的第二个流 |
| `interpretFindings` / `publishFindings` / `ThinkFinding` | 只回计划 id、文本恒空;`think.step` 只是把行从 running 翻到 done。计划行现在由第一条 `answer.delta` 或 `run.*` 收口;事件 schema 与客户端 reducer 不动(兼容) |
| `composeOnce` 与 `phase.started/completed`(interpret / compose) | 同上 |
| `drainSpoken` | 丢弃主循环正文的开关 |
| `consultationComposePrompt`、`consultationSectionPrompt`、`ConsultationSectionPromptReason` | 前者被写作要求取代;后者自 BUG-944 起已是死代码 |
| `AGENT_SLICE_MAX_STEPS` | 只有 compose 用 |
| `createConsultationAnswerClock`、`CONSULTATION_COMPOSE_TIMEOUT_MS` | 并进 `createConsultationRunClock`,常量改名 `CONSULTATION_ANSWER_TIMEOUT_MS`(值 70 秒不变) |
未删:`think-step-gate.ts`(`public-thinking.test.ts` 与 `rectification-step-answer.test.ts` 仍在用);`consultationSliceGenerationSettings` 与 `sliceAddedVisibleText`(开工前就已无调用,不在本单范围)。
## 测试
| 项 | 结果 |
| --- | --- |
| 新回归 `consult-single-pass-answer-20260927.test.ts` | 15 条全绿:真实 `getJyotishAgent`(真实 skill 绑定、真实计算工具、真实 prepareStep)+ 记录提示词的假模型,计算数据来自 `fixtures/consultation-workflow-report-blocked-repairs-golden.json`(引擎真实输出,虚构身份) |
| 修复前对照 | 同一套真实 Agent 跑修复前的 `stream-agent-response` + compose 接线:3 次调用,工具结果与 golden 月亮度数只出现在第 2 次提示里,用户正文来自第 3 次 |
| 改写的既有用例 | 全部附「原值 / 新值 / 原因」:`consultation-agentic-runtime` 6 条(其中 5 条改名,旧名写在原值栏)、`consult-answer-truncation-20260926` 全文件(名字不变,文件头统一说明 + 3 条单独说明)、`consultation-thinking-plan` 2 条(名字保留)、`consultation-workflow-contract` 3 处、`consultation-stream-recovery` 1 处、`application-billing-contract` 1 处 |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error(127 warning,均为既有;本单改动文件里的 2 条 warning 开工前就有) |
| 全量 `npm test`(Node 22.14,变基前) | 4084 条,fail 24 / skip 28;失败名单与基线 `ct-test.log`(4069 / 24 / 28)逐条一致,0 新增;消失的 5 个名字均为上面改名的用例(原值栏记旧名),新增 20 个名字 |
| 全量 `npm test`(变基到 `03c7c0a9` 后) | 4093 条(多出的 9 条是 BUG-1052 单自带的),fail 24 / skip 28;失败名单与基线逐条一致,0 新增 |
| Python 门禁集 | 948 passed / 1 skipped(与基线一致;本单未改 Python,也没有 Python 合同读这几份前端文件) |
| `npm run build -- --webpack` | 通过,`/` 为 `○` Static |
| 首屏 gzip(rootMainFiles) | 131,253 B,基线 130,933 B,+0.24%(±2% 内) |
| 杂项 | 已删构建留下的 `frontend/frontend/` |
## 没做 / 留给真机
- 真实供应商下:写回答那一步的耗时、`composeFinishReason` 分布、引用的上升 / 月亮 / 宫位是否与星盘页一致——无模型凭据,按 `docs/testing/consult-single-pass-answer-20260927.md` 部署后走。
- 已知取舍:第一段字要等标题或 160 字;「一步先写长段再调工具」时那段会留在正文里(见上文残余风险)。
- D3 范围外(另行决策):约 4 万 token 工具结果瘦身与按领域选技法、家庭拆父母 / 子女、43 行审计表。
## 验收(Claude,2026-09-27)
独立复跑(Node 22.14,基于 `03c7c0a9`):tsc 0;lint 0 error;`npm test` 4093 / 24 fail / 28 skip,失败名单与 home-landing 分支逐条一致;消失的 5 个测试名即 compose 时代的 5 条改名(原名在原值注释里);Python 门禁集退出 0;`/`、`/chart`、`/ephemeris`、`/people` ○ Static;rootMainFiles gzip(level 9)130933 B,±0%。未碰 `.gitea/`、`vendor/`、`deploy/`,无删除文件。代码抽查:答案写作指令移入本命用户轮(`natalAnswerShapeInstruction`),「深入看今日」三节不再被强加四标题;按步截留(有标题或满 160 字才放出,调工具的步整段丢弃);续写带上计算结果。部署后要看:真机引用的上升/月亮/宫位与星盘页是否一致;`composeFinishReason` 分布(思考 token 计入 70 秒答题预算,可能多出不扣点的截断)。