Files
PolyGateway/research-wiki/designs/2026-09-11-streaming-stall-design.md
T
iomgaa 03cbf4c8db docs: add approved streaming stall measurement and mid-stream hedging design
Findings: industry paradigm research (watchdog-retry is the norm,
mid-stream hedging has no public precedent; pi-tps threshold-based
stall deduction; Gregg off-CPU paired reporting; vLLM exclusion
clauses). Design: generation_ms redefined as net generation time
(wall minus all inter-chunk stalls over threshold), CallStats gains
stall_ms/stall_count, HEDGE__STALL_AFTER_S mid-stream hedge
independent of HEDGE__AFTER_S. Issues #27 #28.
2026-09-13 22:53:00 -04:00

211 lines
21 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.
# 流式中段沉默:计量口径与对冲(已批准设计)
- 状态: **已批准(2026-09-11 人类拍板 §9 五项,见 §9 决策记录)**;本文件只做设计,不含实现
- 基线: main `a28d944` / 1.3.7(已发布,含单路并发对冲与 `CallStats.generation_ms/hedges/hedge_won`)
- 输入: `findings/2026-09-11-streaming-stall-hedging-research.md`(业界范式与来源,下称"调研",引用处标注 §x.y);下游 Terminal-Bench 实测 4407 次成功调用遥测(需求来源);1.3.7 设计 `designs/2026-09-10-24-hedged-requests-design.md`;远端 issue #27(P1)/#28(P2)
- 关联: ARCH §6.4 取消语义、§7.2 重试;既有流式看门狗 `streaming.py`;1.3.7 对冲机制(本设计直接站在其编排与记账语义上)
## 1. 背景与问题陈述
两个问题都是**口径/能力缺口,不是实现 bug**:1.3.7 行为与其设计文档(§4.5 H8、对冲触发判据)完全一致,但下游实测暴露了设计时未覆盖的病态形态。
### P1:中段沉默计入 `generation_ms`,"裸生成时间"名不副实(issue #27)
`CallStats.generation_ms` 的口径是赢家 transport 调用的完整墙钟:`middleware/retry.py:~335` `gen_started` 紧贴 `transport.complete()` 调用前,`:~346` sink 在返回后立即记录。对流式,`complete()` 返回 = 整个流读完,故中段沉默全部计入——这是 1.3.7 设计明写的口径,但"裸生成时间"这个名字误导了下游把它当活跃生成时间用。
下游实测(Terminal-Bench,4407 次成功调用):
| 指标 | 数值 | 含义 |
| --- | --- | --- |
| ttft 中位 | 1.63s(仅 3 次 >20s) | 首 token 几乎从不慢 |
| 慢调用 `generation_ms` 中位 | 69.4s | 墙钟长尾严重 |
| 其中 `max_inter_token_ms` 中位 | 54.9s | 长尾的主体是**一次中段沉默** |
| 沉默占全部模型时间 | 71.3% | "在等不在生成"是主要成本 |
| 沉默与输出/输入量相关性 | r = 0.05 / −0.06 | 沉默与生成量无关 |
| reasoning_tokens 上报次数 | 129 / 4407 | 沉默**不是**模型在思考,是渠道/服务端排队(调研 §A4:prefill/decode 互踩是结构性现象) |
结论:下游需要一个"扣除沉默段的活跃生成时间"口径,且带归因(沉默几次、共多长),不是只给一个净数(Gregg off-CPU 分解原则,调研 §B2)。
### P2:中段沉默无对冲,现有两道闸门都接不住(issue #28)
| 既有机制 | 证据 | 为何接不住中段沉默 |
| --- | --- | --- |
| 首 token 对冲 `HEDGE__AFTER_S` | `middleware/retry.py:542-562` `_past_hedge_window` 返回 `not primary.done() and not first_token.is_set()`;非对冲模式 `__call__` 传 `first_token_event=None` 根本不观测 | 首 token 到达后对冲窗**永不打开**;实测 ttft 中位 1.63s,`AFTER_S=20` 对流式调用恒不触发 |
| 看门狗 `INTER_TOKEN_TIMEOUT_S` | `streaming.py`(budget = ttft_s 首 chunk / inter_token_s 后续);`openai_compat.py:573` `inter_token_s = source.inter_token_timeout_s or source.timeout_s`;配置 `config.py:46` | 能掐中段沉默,但**掐掉即丢弃全部已生成内容**、从头重试;重试可能再卡;每步成本 = 一次完整重新生成 |
调研结论(§A3):业界对中段沉默的落地形态收敛为"间隔看门狗 → 抛错 → 重试",**"中段沉默触发并发副本"在任何主流网关/库中均无公开先例**——本设计的 P2 方案 2B 属超业界现状能力,须诚实标注。
## 2. 业界范式摘要(压缩自调研,不重查网络)
| 范式 | 出处(调研节) | 对本设计的对应 |
| --- | --- | --- |
| 中段沉默对冲**无公开先例**;主流 = 看门狗 → Transient → 重试 | 调研 §A3(LangChain `stream_chunk_timeout` 默认 120s 已合并;Codex CLI 静默 300s 整路重试;LiteLLM 提案默认关) | P2 的 2A = 业界基线;2B 若做须自证语义 |
| 沉默扣除唯一先例 pi-tps:gap ≥500ms 判 stall,**减所有超阈值段**,连续停顿合并一次,样本不足返回 null | 调研 §B3 | P1 的 `stall_ms/stall_count` 口径直接照此;"只减最大一段"无任何先例,否决 |
| Gregg off-CPU:墙钟分解须**成对报告 + 归因**,只报净数无法行动 | 调研 §B2 | 1B' 下由 `generation_ms`(净)+ `stall_ms`/`stall_count` 成对报告满足,墙钟 = `generation_ms + stall_ms` 可还原 |
| NVIDIA TTFT/ITL/E2EL:排队计入 TTFT,基准口径**不扣除**沉默 | 调研 §B1 | 扣沉默是库自造口径,必须显式声明、不与基准口径混写 |
| vLLM `generation_time_ms` 排除条款:token 数 <2 时间隔类字段 null;`n>1` 无法归因即抑制 | 调研 §B3 | 三新字段的 None 条款照抄:内容 chunk <2 / 非流式 → None |
| Envoy/gRPC:判赢在**响应头**即 commit 不回头;per_try_timeout 只在响应开始前有效,中段停顿不重新对冲 | 调研 §A2 | P2 赢家裁定与"不回头"语义的参照;约束未来流式透传(§5.2) |
另:配置命名先例对照见调研 §3.3,本设计键名与其对齐。
## 3. 共享观测原语:内容 chunk 单调时间戳序列
P1(计量)与 P2(中段触发)共用同一份底层观测,避免两套计时逻辑漂移:
| 项 | 设计 |
| --- | --- |
| 记录点 | transport 层(`openai_compat.py:605` 现算 `max_gap` 处已逐 chunk 循环,时间戳序列在此追加记录,增量成本近零) |
| 记录内容 | 每个**内容 chunk** 的单调钟时间戳;空 chunk / SSE keepalive 注释**不计时**(LangChain 语义,调研 §A3——keepalive 字节会重置 httpx read timeout,对"有字节无内容"的卡顿失效,我们的序列天然免疫) |
| 派生原则 | 一切计量(`stall_ms` 等)与中段触发信号(§5.2)都从此序列/其增量事件派生,**不在热路径做判定**(调研 §3.2-1);热路径只做"记一个时间戳 + set 一个 Event" |
| chunk ≠ token | chunk 边界是传输/解析缓冲产物,非 token 边界(vLLM #27485 陷阱,调研 §B1);一切间隔类口径按 chunk 声明,不假装是 ITL |
P2 触发所需的活体信号由此派生:transport 每记一个内容 chunk 即刷新一个 `asyncio.Event`(语义 = "自上一内容 chunk 以来未再沉默"),编排侧以"等该事件超时"实现 stall 计时器——与 1.3.7 首 token 事件同构,零新机制类别。
## 4. P1 方案:沉默计量口径(1B' 已批准)
### 4.1 方案 1A:不改库,下游自算 —— 已否决
| 项 | 内容 |
| --- | --- |
| 语义 | 下游用既有 `LLMResponse.max_inter_token_ms`(`types.py:417-418`)自算 `generation_ms − max_inter_token_ms`,即**只减最大一段**;库仅澄清文档 |
| 改动面 | README/CHANGELOG 口径澄清,零代码 |
| 优点 | 零实现、零契约变更;`max_inter_token_ms` 已在遥测表(`telemetry/schema.py:43/85`),存量数据即可分析(本次实测正是用它定位的病) |
| 缺点 | 多段沉默场景**系统性高估**活跃时间(调研 §B3:只减最大一段无任何先例);口径责任散落每个下游,各家算法漂移后跨项目不可比;无 `stall_count`,违反 Gregg 归因原则 |
**否决理由(人类裁决,2026-09-11)**:口径必须库内收敛,不得散落各下游自算。
### 4.2 方案 1B'(已批准):`generation_ms` 口径重定义
人类原话:"如果 generation_ms 与 max_inter_token_ms 本质是一样的那能不能改一下 generation_ms,减去所有大于 stall_ms 的中段沉默得到一个纯净的时间"。PM 澄清两字段本质不同(完整墙钟 vs 单次最大沉默段)后,人类意图收敛为:原草案 1B 的 `active_generation_ms` 净口径**直接写进 `generation_ms` 本身**,不新增派生字段。
| 项 | 内容 |
| --- | --- |
| 语义 | `generation_ms` 重定义为**净生成时间** = 赢家 transport 墙钟 − Σ(所有 > 阈值的相邻**内容 chunk** 沉默,pi-tps 式口径);`CallStats` 新增两归因字段(默认 None):`stall_ms: int \| None`(沉默总额)、`stall_count: int \| None`(超阈值段数,连续停顿合并一次);**不新增 `active_generation_ms`**——`generation_ms` 本身就是净口径 |
| 归因与还原 | 墙钟 = `generation_ms + stall_ms`;净数与沉默总额/次数成对报告,Gregg 成对报告原则(调研 §B2)在净口径下依然满足 |
| 排除条款(照 vLLM,与原案一致) | 非流式 / 内容 chunk <2 → `stall_ms`/`stall_count` 为 None 且 `generation_ms` **不退减**(= 原墙钟);对冲输家的沉默**不进**赢家快照(输家数据留在其 attempt 遥测行,可 join 还原) |
| 阈值 | 新配置 `{SCOPE}__STREAM__STALL_THRESHOLD_MS`,**默认 1000ms**(§9-③ 拍板);人类附加要求"需要是一个可以调整的参数,对外提供调整接口"——调整接口 = 既有装配路径(env 键 / `GatewaySettings` 字段 / `GatewayClient` 构造 kwarg 三层均可调),**不提供 per-call 覆盖**(§8 非目标 F);**不做自适应**(防与对冲阈值耦出反馈环,调研 §3.2-3) |
| 遥测 | 遥测表新增 `stall_ms`/`stall_count` 两列(§9-⑤ 拍板):`telemetry/schema.py` PG/SQLite 各一次可空 DDL + recorder/`middleware/telemetry.py` 透传;`generation_ms` **仍不进遥测列**(YAGNI——下游要墙钟用 `latency_ms` 或 `generation_ms + stall_ms` 还原) |
| 改动面 | `types.py:417-418` 邻域(`CallStats` 增两字段);`ports.py` `TransportResult` 增 `stall_ms`/`stall_count: float \| None` 默认 None;`openai_compat.py:605` 邻域从 chunk 时间戳序列计算两值(沿用 §3 原语);`middleware/retry.py:~335/~346` 邻域(赢家记账改为 `record_generation`(净值) + context 存 stall 两值进快照,口径与 H8 同款);`telemetry/schema.py:43/85` 邻域(PG/SQLite 各一次可空 DDL)+ recorder + `middleware/telemetry.py` 透传;`config.py:46` 邻域(新键 + loader);`client.py` 构造透传 |
#### 契约变更的正当性与披露
语义突变在一般情形下违反"只增不删不改名"精神(§4.3 拒绝理由),本次由人类以"窗口期最小代价"裁决放行,依据:
| 依据 | 事实 |
| --- | --- |
| 字段极新 | `generation_ms` 是 1.3.7(2026-09-10)引入的新字段,发布仅 2 天 |
| 消费者唯一 | 唯一消费者是提出本需求的下游 Terminal-Bench;三参考项目均未消费该字段 |
| 名实自相矛盾 | 字段名"裸生成时间"原本承诺的就是净口径,现实现(墙钟)与名字矛盾,本次重定义是**兑现名字**而非推翻承诺 |
| 无账本断裂 | 遥测 DB 从未存过 `generation_ms`(已验证 `telemetry/schema.py` 与 `middleware/telemetry.py` 均无此字段),库内无历史账本纵向比较断裂 |
披露义务:CHANGELOG 设独立"行为变更"段显著标注 1.3.7→1.3.8 口径变化 + README 口径表更新 + 本设计存档。
### 4.3 方案 1C:改 `generation_ms` 口径为扣沉默 —— 原拒绝理由被人类裁决覆盖(留档)
| 理由 | 依据 |
| --- | --- |
| 已发布契约突变:1.3.7 设计 §4.5 H8 明写墙钟口径并已被下游消费 | 铁律"字段只增不删不改名";语义突变等同改名 |
| 历史账本不可比:遥测 DB 存量 `generation_ms` 全是墙钟,口径切换后纵向比较断裂 | 遥测必录铁律的对账前提 |
| NVIDIA/vLLM 基准口径均不扣沉默(调研 §B1),库把墙钟改掉会与业界定义脱钩 | 口径分层应靠新增字段,不是靠改旧字段 |
**人类裁决(2026-09-11)**:上述理由在一般情形下成立,但人类以"窗口期最小代价"覆盖之(字段发布仅 2 天、消费者唯一、遥测无账本,见 §4.2 正当性表)——净口径以 1B' 形式落地;原 1B 三字段并存案随之被取代。
### 4.4 P1 对比与决策
| 维度 | 1A 下游自算(已否决) | 1B 原三字段案(已被取代) | **1B' 口径重定义(已批准)** | 1C 改原口径 |
| --- | --- | --- | --- | --- |
| 多段沉默准确性 | 差(系统性高估) | 好(减所有超阈段) | 好(减所有超阈段) | 好 |
| 归因能力 | 无(只有最大段) | 有(总量 + 次数) | 有(`stall_ms`/`stall_count` 成对) | 无(只有净数) |
| 口径唯一性 | 散落各下游 | 库内收敛 | 库内收敛 | 库内但破坏存量 |
| 契约风险 | 零 | 只增字段 + 遥测 DDL | 语义突变,经人类以"窗口期最小代价"裁决放行(§4.2 正当性) | **违反只增不删精神,拒绝** |
| 实现量 | 零 | 中(五文件邻域) | 中(六文件邻域,含端口) | 小但不可接受 |
**决策:1B'(§9-① 拍板)**;1A 因"口径必须库内收敛"被人类否决;1B 原案被 1B' 取代;1C 拒绝理由被人类裁决覆盖(留档 §4.3)。
## 5. P2 备选方案:中段沉默的应对
### 5.1 方案 2A:不做中段对冲,看门狗链 + 文档
| 项 | 内容 |
| --- | --- |
| 语义 | 维持业界主流:中段沉默超 `INTER_TOKEN_TIMEOUT_S` → 掐流 → Transient → 既有重试/换源链;库补文档与配置指引(各源默认值、与 `timeout_s` 的回落关系 `openai_compat.py:573`、建议按源沉默分布取值) |
| 改动面 | README/wiki 配置指引,零代码 |
| 优点 | 零实现零契约变更;有 LangChain(默认 120s)与 Codex CLI(300s)生产先例(调研 §A3);掐死即 Transient,四分类不新增语义 |
| 缺点 | 掐掉即**丢弃全部已生成内容**、从头重试;重试可能再卡同一类排队;每步成本 = 一次完整重新生成;实测沉默 >20s 占 19.0%,这条链会频繁点火 |
### 5.2 方案 2B(已批准):中段沉默对冲
| 项 | 内容 |
| --- | --- |
| 触发 | 新配置 `{SCOPE}__HEDGE__STALL_AFTER_S`(**独立于 `HEDGE__AFTER_S` 生效**,§9-④ 拍板"否,独立生效";调研 §3.3 命名);语义:主路**相邻内容 chunk 间隔**超此阈值 → 触发。机制 = §3 原语派生的"内容 chunk Event 等待超时",与首 token 事件同构;`_past_hedge_window`(`retry.py:542-562`)扩为"首 token 未至走 AFTER_S,首 token 已至走 STALL_AFTER_S" |
| 对冲执行 | 触发后主路**不取消**;并发异源副路走**完整准入 + 熔断**,无配额静默放弃(1.3.7 设计 §4.3 语义原样复用);`FIRST_COMPLETED` 赛跑;输家取消,est 保留不喂熔断、遥测 `hedge_cancelled`(1.3.7 设计 §4.4/§4.5 全部复用,零新记账语义) |
| 赢家计量 | 赢家路为准:`generation_ms`(1B' 净口径)/`stall_ms`/`stall_count`(1B' 同批落地,§9-①)全部取赢家路;`hedge_won` 语义不变(赢家是否对冲路) |
| 无拼接污染 | `chat()` 是**库内聚合流**——调用方拿完整 `LLMResponse`,流在库内消费完毕才返回;中段切换赢家时两路内容从未交付调用方,**无拼接问题**。这与调研 §3.1 基于"流透传"假设的 commit 语义不同。**约束明写**:未来若做流式透传,commit 点须重审——一旦任一路首 chunk 已透传下游,即按 Envoy"判赢不回头"锁定该路,中段对冲对该次调用永久关闭(§8 非目标:本版不做透传) |
| 与看门狗的交互 | 两参数语义独立:若主路沉默超 `inter_token_timeout_s` 被看门狗掐死,**对冲路已在跑 → 主路之死不算失败,等副路**;副路未起 → 照旧 Transient 重试。装配守卫:`stall_after_s ≥ inter_token_timeout_s` 时 warning(对冲窗永不开,形同虚设);建议 `stall_after_s < inter_token_timeout_s` |
| 成本披露 | 实测沉默 >20s 占 **19.0%**——触发率远高于首 token 对冲(ttft 仅 3/4407 超 20s);每触发一次最多 +1 并发请求 + 一份 est 预扣滞留(1.3.7 §4.4 口径);**默认必须关闭**,文档明写"对冲 = 用配额换延迟" |
| 改动面 | `retry.py:542-562` 邻域(双阈值触发窗 + 与看门狗掐死的汇合规则);`ports.py` + `openai_compat.py:569-575/605` 邻域(`complete` 加内容 chunk Event 参数——公共端口签名第二次变更,须进批准项);`config.py` 邻域(新键 + 交叉守卫);`client.py` 透传;遥测复用 `hedge_cancelled`,零新列 |
**方案 2C 不单列,作为 2B 的裁定细节**:看门狗掐死瞬间原子地并发补一路(掐与对冲合一)。评估:省一个参数但把"观测阈值"与"掐死阈值"耦合,掐死时副路从零开始(TTFT 全付),总延迟 = 沉默阈值 + 完整生成,严格劣于 2B 的"沉默中提前并发";且与 2A 链叠加时行为难推理。否决,仅留档。
### 5.3 P2 对比与决策
| 维度 | 2A 看门狗链 + 文档 | **2B 中段沉默对冲(已批准)** |
| --- | --- | --- |
| 已生成内容 | 掐掉全丢 | 主路成果保留到最后一刻,输家才弃 |
| 长尾延迟 | 沉默阈值 + 完整重试耗时 | ≈ 沉默阈值 + min(两路剩余) |
| 业界先例 | 有(LangChain/Codex CLI) | **无公开先例**(调研 §A3),属超业界现状能力 |
| 成本 | 每次沉默 = 一次完整重新生成 | 触发率 19% 量级 × 一次额外请求 |
| 公共面变更 | 零 | 端口签名再变 + 新配置键 |
| 实现量 | 零(文档) | 大(但编排/记账/遥测全复用 1.3.7) |
**决策:2B(§9-② 拍板,独立于 `HEDGE__AFTER_S` 生效)**。诚实标注保留:它超出业界现状能力(调研 §A3),人类已明确接受;2A 链(看门狗 → Transient → 重试)作为不启用对冲时的默认行为继续存在,文档指引义务不变;P1(1B')同批落地后下游可直接量化沉默代价。
## 6. 配置汇总(新增两键,默认关闭/合理默认)
| 键 | 值域 | 缺省 | 守卫与既有键关系 |
| --- | --- | --- | --- |
| `{SCOPE}__STREAM__STALL_THRESHOLD_MS` | 正整数毫秒 | **1000**(§9-③ 拍板,可调) | 仅计量口径,不改任何行为;不设 = 用缺省;调整接口 = env 键 / `GatewaySettings` 字段 / `GatewayClient` 构造 kwarg 三层,无 per-call 覆盖;命名对齐调研 §3.3(pi-tps 500ms 先例,但场景不同) |
| `{SCOPE}__HEDGE__STALL_AFTER_S` | 有限正数秒(复用 `ensure_call_deadline` 同款校验) | **未设 = 关闭** | 独立于 `HEDGE__AFTER_S`;`stall_after_s ≥ inter_token_timeout_s` → 装配 warning(§5.2);单源 scope 设阈值 → warning 允许(1.3.7 §5 同款);与 `call_deadline_s` 组合守卫沿用 1.3.7 §5/§6 |
`STREAM` 段须加入 `_RESERVED_SEGMENTS`(`config.py:57` 邻域,1.3.7 为 `HEDGE` 做过同款);3 段键天然跳过 `_load_sources`(`config.py:401-407`)。装配路径与 1.3.7 对冲同款:`GatewaySettings` 字段 + `__post_init__` 守卫 + `GatewayClient.__init__` keyword-only 入口即校;不提供 per-call 覆盖(阈值是源/渠道特性,1.3.7 §5 同款判据)。
## 7. 与既有契约的关系
| 契约 | 处理 |
| --- | --- |
| `CallStats`/`LLMResponse` 只增不删不改名 | 两新字段(`stall_ms`/`stall_count`)全 `int \| None`、默认 None;1.3.7 及以前构造的位置调用不炸(1.3.7 §4.5 同款论证);`generation_ms` 语义重定义为净口径(**已批准例外**,§4.2 正当性与披露),`max_inter_token_ms` 原语义一字不动 |
| 遥测迁移 | `stall_ms`/`stall_count` 两列可空,PG/SQLite 各一次 DDL;旧行 null;迁移纪律照 issue #12/#13/#15 教训与 1.3.7 §4.5 备选调先例;**加列已拍板**(§9-⑤) |
| 公共端口签名 | `Transport.complete` 随 2B 第二次加参(内容 chunk Event,必填 kw),与 1.3.7 `first_token_event` 同款约定;另 `TransportResult` 随 1B' 增 `stall_ms`/`stall_count`(默认 None) |
| 文档登记义务 | README 能力表 + CHANGELOG + ARCHITECTURE.md(口径段与对冲触发段)三处同步,发版按 CLAUDE.md §4.4.1 清单执行 |
| 版本归口 | **1.3.8 确认**(人类拍板 P1 + P2 同批发);P1 含 `generation_ms` 口径变更,披露义务照 §4.2;P2 纯增量(新键 + 默认关闭行为) |
## 8. 非目标
| # | 不做 | 理由 |
| --- | --- | --- |
| A | 不做墙钟/净双口径并存字段(如 `active_generation_ms`) | 1B' 拍板净口径直接写进 `generation_ms`;墙钟由 `generation_ms + stall_ms` 还原(§4.2),并存字段徒增口径漂移面 |
| B | 不做自适应分位对冲阈值(bhope/hedge DDSketch) | 列未来方向:per-source 滚动分布跨进程要进 Redis,且与沉默计量阈值耦合有反馈环风险(调研 §3.2-3);本版绝对阈值已够用 |
| C | 不做流式透传 | 透传引入 commit 点与"判赢不回头"约束(§5.2),是独立大设计 |
| D | 不做内容拼接/续写 | 增量文本不可拼接,缝合即结果污染(调研 §3.1) |
| E | embedding/OCR 不动 | 无流式中段概念,沿用 1.3.7 非目标 A |
| F | 不做 per-call 沉默阈值覆盖 | 人类拍板(§9-③):调整接口走装配层(env 键 / `GatewaySettings` 字段 / `GatewayClient` 构造 kwarg 三层),per-call 覆盖超本版范围 |
## 9. 决策记录(2026-09-11 全部拍板)
| # | 决策点 | 拍板结果 | 人类原话 / 选项 |
| --- | --- | --- | --- |
| ① | P1 选型 | **1B'**:`generation_ms` 口径重定义为净时间(原 1B 的 `active_generation_ms` 口径直接写进 `generation_ms` 本身,不新增派生字段);新增 `stall_ms`/`stall_count` 归因字段 | "如果 generation_ms 与 max_inter_token_ms 本质是一样的那能不能改一下 generation_ms,减去所有大于 stall_ms 的中段沉默得到一个纯净的时间"(PM 澄清两字段本质不同后,意图收敛为口径重定义) |
| ② | P2 选型 | **2B**:中段沉默对冲 | 选定 2B(已明确接受"超业界现状能力"标注) |
| ③ | `STREAM__STALL_THRESHOLD_MS` 默认值 | **1000ms,可调** | "需要是一个可以调整的参数,对外提供调整接口"——接口 = 装配三层(env 键 / `GatewaySettings` 字段 / `GatewayClient` 构造 kwarg),不提供 per-call 覆盖 |
| ④ | 中段对冲是否以 `HEDGE__AFTER_S` 已启用为前提 | **否,独立生效** | 原话"否,独立生效" |
| ⑤ | 遥测是否加 `stall_ms`/`stall_count` 两列 | **加** | 选定"加"(归因进 DB,可对账) |
另(§9 之外拍板):P1 + P2 **同批发 1.3.8**(§7)。
## 10. 自查记录
- 行数 210 ≤400;无超 15 行代码块(全文无代码块,仅表格与伪语义表述)。
- 方案改动面均列具体文件:行号(1B'、2B);行号沿用 PM 已验证事实,未重新验证。
- 调研结论引用处标注(调研 §x.y);未重查网络。
- 2026-09-11 批准确认:§9 五项 + 同批发 1.3.8 全部落地本文;1A/1C 保留为否决留档,1B 原案标注被 1B' 取代。