Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
129 lines
13 KiB
Markdown
129 lines
13 KiB
Markdown
# 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 秒答题预算,可能多出不扣点的截断)。
|