Files
Jyotisha/docs/tasks/PROGRESS-consult-single-pass-answer-20260927.md
T

12 KiB
Raw Blame History

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 行审计表。