Files
Jyotisha/docs/tasks/PROGRESS-consult-answer-truncation-20260926.md
T

8.3 KiB
Raw Blame History

PROGRESS · 普通咨询回答被掐断仍扣点(BUG-1051)· 2026-09-26

  • 执行方式:直接执行(产品负责人授权子代理执行;Claude 事后独立验收)
  • 基线:origin/staging e53052a2 开工;提交前变基到 16f4600d(BUG-1049/1050 校正开场单,只与本单的文档文件重叠,已并存)
  • 分支:codex/consult-answer-truncation-20260926(本地提交,未推送)
  • BUG 编号:1051(1049/1050 由并行的校正开场单占用)
  • 复发自:BUG-305;相关 BUG-944 / BUG-612 / BUG-280

结论

项 结果
根因 确认,三层:① 工具循环与写回答共用一个 110 秒 AbortSignal;② Mastra 1.50.1 超时不抛错,发 abort 块 + finish(tripwire) 后正常关流,结算段只认抛错和 length;③ content-filter / tool-calls / other / unknown / error 等非 stop 结束也被当完成
D1 写回答自有时钟 完成:CONSULTATION_COMPOSE_TIMEOUT_MS = 70_000,首用才起算,compose / 续写 / Pass 4 重写 / 回答重试共用
D2 非 stop 不结算 完成:run.failed / answer_truncated,不调 onComplete、账务走 cancel,记 compose-abort / tool-abort 与 answer-truncated 步;被掐时不冲出 Pass 4 半句;length 续写不变
D3 观测 完成:composeFinishReason、composeAborted、answerVisibleChars,只有枚举 / 布尔 / 计数;公开回执不变
超时层逐层核对 无阻塞项,见下表;未改 deploy/ 与 .gitea/workflows

为什么是 70 秒

依据 数字
staging 同供应商默认模型的实测吞吐(PROGRESS-report-writer-failure-20260902.md Run A telemetry,端到端含首字延迟) 560 tok / 5.7 s、1240 / 13.6 s、816 / 8.1 s、759 / 7.4 s → 约 90–100 tok/s
70 秒可写 按实测 ≈ 6,300–7,000 可见 token;按一半吞吐仍 ≈ 3,000 token
BUG-305 保留的可见预算 8,192 token(现 compose 上限 16,384,thinking 关闭)
一次四标题回答的常见长度 约 1,500–2,500 token,70 秒有 3–4 倍余量
最坏总等待 工具循环 110 s + 写回答 70 s = 180 s(产品接受「约 3 分钟」)

写不完 70 秒的回答不会再被当成完成:按截断提示、不扣点。实际 compose 耗时分布需部署后看 composeFinishReason / run.total 复核;若 tripwire 频繁出现,再按数据调整这个值。

会不会有别的层先掐断 3 分钟的流

层 核对结果 处理
路由 maxDuration Next 16 文档写明它只给部署平台读(node_modules/next/dist/docs/.../route-segment-config/maxDuration.md);生产是自托管 node server.js(deploy/railway-web.Dockerfile),不执行 120 → 240,与校正路由同值,作上限说明;源码合同锁 110 + 70 < 240
Node HTTP 服务器 server.timeout 默认 0;requestTimeout(300 s)只管接收请求体;Next 只设 keepAliveTimeout(连接空闲,不影响进行中的响应) 无需改
Caddy(deploy/Caddyfile.*) 只有 reverse_proxy web:3000,未配任何 timeout;Caddy 默认无响应读 / 写超时。先例:校正路由 maxDuration = 240、整轮预算 210 s 经同一条链路运行 无需改,未动 deploy/
CDN AGENTS §1:公网边缘只有 Caddy,无 CDN —
客户端 use-consultation-run.ts 的 AbortController 只在用户取消时触发,无超时;恢复轮询 consultation-recovery-poll.ts 每 1.75 s 一次、无上限;/api/consult/status 无时间阈值 无需改
账务预扣 cancel_consultation_credit / 完成 RPC 没有分钟级过期;过期清理是 1 天 无需改
NDJSON 心跳 写回答阶段一直有 answer.delta;长时间无字节只可能在工具计算阶段,本单未改变该阶段时长 无需改
断线 三条路径都是 continueAfterDisconnect: true,客户端断开后服务端继续写完并结算 不变

各种结束方式的行为

写回答的最后一个流 有正文 无正文
stop run.completed,扣点 既有 answer-retry → 仍空则 empty_answer
length 续写一次;续写 stop → 完成;续写仍 length、被掐或其他 → answer_truncated 同左
Mastra abort + finish(tripwire) answer_truncated,记 compose-abort,半句不外发 empty_answer 路径(不扣点)
content-filter / tool-calls / other / unknown / error answer_truncated empty_answer 路径
供应商没发 finish(Mastra 补 reason 为空 → unknown) answer_truncated empty_answer 路径
流里完全没有 finish 块(未在 Mastra 观测到,防传输提前关闭) answer_truncated(composeFinishReason = missing) empty_answer 路径

工具循环(本命路径里被「drain」、不进正文的那一段)自己的结束原因不参与结算判定;它被掐时记 tool-abort,写回答照常在自己的时钟上进行。

改动文件

文件 改动
frontend/src/mastra/consultation-tools.ts CONSULTATION_COMPOSE_TIMEOUT_MS、createConsultationAnswerClock();运行态加三项观测字段并进 consultationModelStepTelemetry
frontend/src/app/api/consult/route.ts answerPhaseSignal;compose、3 处续写、3 处回答重试改用;maxDuration 240
frontend/src/lib/stream-agent-response.ts 每次 attempt 的结束记录、abort 块留痕、结算判定、被掐不冲半句、观测写回
frontend/src/lib/agent-observability.ts 严格 schema 加三项字段
frontend/tests/consult-answer-truncation-20260926.test.ts 新增 15 条,真实 Mastra Agent + 假模型
frontend/tests/consultation-agentic-runtime.test.ts BUG-305 旧用例加三栏说明 + abort 步断言;11 条 fixture 补 finish(stop)

既有测试改动(三栏)

位置 原值 新值 原因
「a timeout after partial visible text…」(BUG-305) 手工 throw DOMException("TimeoutError") 代表超时半截,是唯一超时回归 保留,只代表「真的抛出」的 catch 分支,补断言 abort 运行步;真实超时形状由新文件覆盖 手造形状不是 Mastra 行为,旧测试一直绿而线上照样扣点(§7.4)
11 条既有 fixture(contract/degraded/window precompute/answer-retry/reasoning 等) 生成器只 yield text-delta 就结束 末尾补 yield STOP_FINISH(Mastra 正常结束必有的 finish(stop)) 无 finish 的流现在按截断处理;断言一条未改

验证(Node 22.14)

项 结果
新回归文件 15 / 15 通过,连跑 3 次稳定;修复前代码上 11 / 15 失败
tsc --noEmit 0 错
npm run lint 0 error(128 warning;本单涉及文件里的 2 条 warning 均为基线已有)
npm test 全量 变基前(e53052a2 上)4060 条,pass 4008 / fail 24 / skip 28;变基后(16f4600d 上)4069 条,pass 4017 / fail 24 / skip 28。对照基线 cs-test.log 4045 条、fail 25 / skip 28:失败名单 0 新增;基线的「Gitea quality gate validates before publishing an immutable ACR manifest」通过(基线日志早于 e53052a2 恢复 upload-artifact action);其余 24 条逐条一致(均为 Docker / DB)。测试名 0 消失;新增 24 条 = 本单 15 + 校正开场单 9
Python 门禁集 948 passed / 1 skipped(与基线一致)
npm run build -- --webpack / 仍 ○ Static;rootMainFiles gzip 130,933 B(基线 130,933,0.000%)
stray frontend/frontend/ 已删除

让步与未做

  • 活动区标题「已完成 N 步」按时间线行数计,截断回复上仍这样写;未完成的信号是输入框上方的提示。改标题属于 UI 改动(要同步 DESIGN.md),不在本单范围,建议另开小单。
  • 真实供应商下的 compose 耗时分布、tripwire 占比需部署后看 [agent-observability] 复核(环境缺口:本地无模型凭据)。
  • 真机走查见 docs/testing/consult-answer-truncation-20260926.md(环境缺口:无登录态)。
  • 无出生分钟路径里,若模型正文被 Pass 4 全部拦下、服务端补拒答句,而写正文的流恰好被掐,本单会按截断处理(显示拒答句 + 未完成提示、不扣点)。概率很低,保持诚实优先。