Files
PolyGateway/research-wiki/reviews/2026-09-11-streaming-stall-design-review.md
iomgaa 97de4104c6 docs: finalize streaming stall design with independent review errata
B1 chunk = any delta (content or reasoning), B2 telemetry plumbing
via explicit _emit pass, B3 orchestration entry gate for independent
stall hedging; N1-N9 advisory fixes merged. Human decisions recorded
in design section 9.
2026-09-13 23:20:52 -04:00

14 KiB
Raw Permalink Blame History

核实完毕。所有关键引用已对照源码验证。以下为审查报告全文。


流式中段沉默设计(1.3.8)独立审查报告

  • 审查对象: designs/2026-09-11-streaming-stall-design.md(已批准,1B' + 2B)
  • 对照: findings/2026-09-11-streaming-stall-hedging-research.md、designs/2026-09-10-24-hedged-requests-design.md、main a28d944 源码(retry.py / openai_compat.py / streaming.py / types.py / ports.py / telemetry/schema.py / middleware/telemetry.py / config.py / client.py 全部实读)
  • 立场: 不推翻人类 §9 五项拍板;只找设计与实现层面的漏洞

结论: 有条件实施

总体架构判断正确: 计量与触发共用同一观测原语、复用 1.3.7 准入/记账/取消语义、遥测迁移机制现成,均无方向性问题。但有 3 处阻断级设计缺口(chunk 定义未钉死、遥测透传链路改动面遗漏、独立生效的编排入口遗漏)必须在实施前以设计补遗钉死;另有 9 条建议级修正。均为"补钉"而非"返工"。

九条审查角度逐条裁决

# 角度 裁决 依据(源码实证)
1 沉默计时边界 存疑(N1) 尾部(最后内容 chunk→[DONE]/usage 帧/连接收尾)不是"相邻 chunk 间隔",不会被减,但仍留在墙钟基底里 → 净值含尾部;调研 §B3 的先例(AIPerf 剔除 [DONE]、pi-tps 止于最后 message_update)是"尾部不计",与设计的墙钟基底口径不同,设计未察觉也未声明。TTFT 同理留在净值内(人类原话"减去中段沉默"字面如此,可接受,但须明写)。应然口径: 显式声明"净值 = 墙钟 − 中段沉默,仍含 TTFT 与尾部",README 口径表同步
2 非流式与输家口径 成立(建议 N2) 排除条款(非流式/chunk<2 → None 且不退减)已明写,双口径是设计内选择;建议补一句"非流式 net≈wall(无中段沉默概念),下游预算公式以 stall_ms is None 分支"。输家排除由 _register_hedge 只记赢家 sink 的机制保证(retry.py _register_hedge)✓;但"输家数据留在其 attempt 遥测行可 join 还原"对被取消的输家不成立(无 TransportResult,行是 error 行)→ 见 B2
3 P2 与看门狗竞态 成立(措辞修正 N9) 看门狗掐死抛 StreamLivenessTimeout(streaming.py:24-50),在 transport 边界翻译为 TransientError ... from exc(openai_compat.py:476-477),不是 CancelledError。编排层无需区分两种主路之死: _attempt_hedged Phase 5 的"先了结的是失败则等另一路"分支对任何 _Failed 一律等副路,两败走 _combine_failures(retry.py:171-176)计一次预算——现有机制完整表达设计语义 ✓。但设计"主路之死不算失败"措辞不精确: attempt 级记账照常(breaker record_failure("timeout")、attempt_fails、reasons、遥测 error 行),"不算失败"的只是本轮超级尝试
4 触发信号并发安全 存疑(N3) asyncio.Event 在单循环内 check→clear 之间无 await 即无交错,但超时到期后触发对冲前必须复检 is_set()——否则"chunk 与超时同一事件循环 pass 到达"会误触发。1.3.7 的 _past_hedge_window 是一次性等待(retry.py:542-568),stall 需要循环等-清-重等,设计只写"等该事件超时",未钉循环模式。等待者只有一个(编排),对冲路恒传 None 是 1.3.7 约定(retry.py _attempt_hedged Phase 4),但设计未声明该约定延伸到新的 chunk Event 参数
5 取消穿透 成立(并入 N3) 外部取消到达 → _attempt_hedged 的 except BaseException 两任务同消、尽力收口、原异常上抛,与 1.3.7 逐字一致 ✓;stall 等待循环每轮迭代自建 waiter,须逐字套用 _past_hedge_window 的 waiter 收口 + cancelling() 穿透纪律(retry.py:556-568),设计补一句即可
6 计量正确性 存疑(N4) 分层可行: 沉默在 transport 由 chunk 时间戳序列算出(openai_compat.py:580-590 循环邻域),净值在 retry 由"墙钟−stall"得。但两处的钟不同源: retry 用注入钟 self._now(retry.py:332/346,client.py:242 默认 time.monotonic),transport 硬编码 time.monotonic()(openai_compat.py:562/582)且 transport 不接收注入钟(client.py:508 构造只传 registry/capabilities)。生产同为 monotonic 无事;测试注入 FakeClock 时净值可为负/失真。赢家 sink 与 1.3.7 generation_sink 机制协调成立(改记净值即可)✓
7 遥测 DDL 迁移 成立(编辑点清单见 N7) schema.py 有成熟迁移机制: SQLITE_BACKFILL/_PG_BACKFILL_DECLS 保序追加、recorder "先探测后 ALTER"(auto 档)、telemetry_schema_sql() 给 manual 档同源 SQL。设计引用的迁移纪律(#12/#13/#15)与现状吻合 ✓。"遥测 DB 从未存过 generation_ms"经核实属实(COLUMNS 36 项无此字段)✓
8 配置守卫完备性 存疑(N5) 三段键解析无冲突: _load_sources 以 len(parts) != 4 跳过(config.py:431),STREAM 进 _RESERVED_SEGMENTS(config.py:57)防 4 段 {SCOPE}__STREAM__{N}__* 撞 provider 段,与 HEDGE 先例同型 ✓。但守卫有缺口: ①设计写 stall_after_s ≥ inter_token_timeout_s,未写有效值回落——运行时是 inter_token_timeout_s or timeout_s(openai_compat.py:573),inter_token 未设时守卫对象应是 timeout_s;②严重度与 1.3.7 不一致: 同类"永不触发"条件 hedge_after_s ≥ min(timeout_s) 是 ValueError(config.py:820-824),而 stall 场景非流式无残留收益(1.3.7 ttft warning 留有非流式收益,stall 没有),只给 warning 论证不足;③stall_after_s ≥ call_deadline_s 的 ValueError"沿用 1.3.7"须点名落进 check_hedge_assembly(config.py:772)同函数
9 内部一致性 存疑(N7) 无硬矛盾,但有三处错位: ①§4.2 改动面写 config.py:46 邻域——:46 是 _SOURCE_FIELDS 表(per-source 4 段键注册表),而 §6 明写 {SCOPE}__STREAM__STALL_THRESHOLD_MS 是 3 段 scope 级键,两处矛盾,正确先例是 _load_hedge(config.py:745-769)同款 loader + GatewaySettings 字段;②§4.2 写 types.py:417-418 邻域(CallStats 增两字段)——CallStats 在 types.py:296-324,417-418 邻域是 LLMResponse;③§5.2 写端口加参"须进批准项",但 §9 五项无对应条目(整体批准可视为覆盖,但设计自拟的程序未闭环)

发现列表

阻断级(实施前必须以设计补遗钉死)

B1 | 设计 §3/§4.2/§5.2 | "内容 chunk"定义未钉死,reasoning 增量归属不明

  • 问题: 源码现状是 _iter_sse_deltas 对 content 与 reasoning_content 都产出(openai_compat.py:65-85),看门狗与 first_token_event 都把思考增量当存活证据(openai_compat.py:585-588 注释"思考增量同样是存活证据,与看门狗活性口径一致")。若按"内容 chunk"字面只计 is_content=True 的增量,则 reasoning 模型长 thinking 阶段(只流 reasoning_content)会被误计为沉默: P1 的 stall_ms 虚高、净口径失真;P2 在正常推理中误触发对冲。本库 reasoning 是一等特性,这不是边缘情形。
  • 修法: §3 明写"chunk = 任何增量产出(content 或 reasoning_content),与看门狗/首 token 活性口径一致;仅空 chunk/usage 帧/SSE keepalive 不计时"。排除条款的"内容 chunk <2"同步按此口径。

B2 | 设计 §4.2 改动面/§7 契约表 | 遥测两列的透传链路改动面遗漏,且"输家行可 join"表述不成立

  • 问题: attempt 行的用量字段由 emitter _AttemptUsage.of(response) 从 LLMResponse 读取(middleware/telemetry.py:216-236)。stall 两列要进 attempt 行,必须二选一: (a) LLMResponse 增 stall_ms/stall_count(公共类型,只增带默认值合法,但改动面与 §7 契约表均未列)+ _build_response 搬运;或 (b) _emit 显式传 TransportResult 两值。同时 ports.py 的 TelemetryRecorder.record_llm_call 签名必须加两参——这是第三个端口签名变更,§7 只列了 Transport.complete 与 TransportResult;emitter 的装配闸 signature.bind 以该协议签名为判据(telemetry.py:338),漏改协议直接装配期炸。另外 §4.2"输家数据留在其 attempt 遥测行可 join 还原"对被取消的输家不成立: 取消的输家无 TransportResult,其行是 hedge_cancelled error 行,stall 恒 None;只有"竞速落选但已完成"的输家才有数据。
  • 修法: §4.2/§7 补全链路(选定 (a) 或 (b);ports.py recorder 签名 + emitter + 两 recorder 实现 + schema.py 四处编辑点);修正输家表述为"已完成输家的沉默在其 attempt 行;被取消输家无沉默数据(传输未返回),其存在性由 hedge_cancelled 行表达"。

B3 | 设计 §5.2 改动面 | "独立于 AFTER_S 生效"所需的编排入口改动遗漏

  • 问题: 现状 RetryMW.__call__ 仅在 self._hedge_after_s is not None 时进对冲路径(retry.py __call__ 分支)。§9-④ 拍板独立生效意味着 hedge_after_s=None + stall_after_s=25 必须也能进对冲路径——__call__ 入口门须改为"两阈值任一已设且(对 stall)请求为流式"。同时: ①非流式 + 仅 stall_after_s → 无中段沉默概念,应逐字走单路(等价未配置),设计未声明;②_past_hedge_window 须从一次性等待重构为两阶段(首 token 前 AFTER_S 仅当已设 / 首 token 后 stall 循环),改动面只写"扩为"偏轻;③纯非流式部署设了 stall_after_s 是否给装配 warning,设计未涉及。
  • 修法: §5.2 改动面补 __call__ 入口门与非流式退化声明;§6 配置表补"stall_after_s 仅对流式生效,纯非流式 scope 设置时 warning(或静默,须拍板一句)"。

建议级

N1 | §3/§4.2 | 沉默计时边界未明写(TTFT 与尾部归属)

  • 尾部(末 chunk→[DONE]/usage 帧)不是相邻间隔、不会被减,但留在墙钟基底 → 净值含尾部;TTFT 同理。调研 §B3 先例是"流终止信号后不计",与墙钟基底口径不同。修法: 显式声明"净值 = 墙钟 − 中段沉默,仍含 TTFT 与尾部;尾部归属活跃侧,与 pi-tps'止于最后更新'的差异是有意的(墙钟基底),尾部通常 ≈0";README 口径表同步。

N2 | §4.2 排除条款 | 非流式双口径补一句显式声明

  • "非流式场景 net≈wall(无中段沉默概念),下游预算公式以 stall_ms is None 为分支条件"。

N3 | §5.2 触发机制 | stall Event 循环的并发与取消纪律未钉

  • ①超时到期后触发对冲前复检 is_set();②check→clear 之间不得有 await;③对冲路 chunk Event 恒 None(1.3.7 首 token 约定延伸);④每轮 waiter 收口 + cancelling() 穿透逐字套 _past_hedge_window(retry.py:556-568);⑤等待者唯一(编排),Event 无需多等待者语义。或改用"最后 chunk 时间戳 + 计算剩余时长"模式免 clear 竞态——二选一,设计钉一种。

N4 | §4.2 改动面 | 钟同源纪律与负值防御

  • retry 墙钟(注入 now)与 transport 沉默钟(硬编码 time.monotonic())生产同源、测试分叉。修法: 设计声明"沉默序列与墙钟必须同为 monotonic 语义;net = max(0, wall − stall) clamp;FakeClock 测试用假 transport 自控 stall 值,真实 transport 测试不得注入假钟"。

N5 | §5.2/§6 | 守卫完备性三缺口

  • ①stall_after_s ≥ inter_token_timeout_s 须用有效值 inter_token_timeout_s or timeout_s(openai_compat.py:573 回落同款);②"永不触发"严重度对照 1.3.7 的 ValueError(config.py:820-824)需论证为何只 warning(stall 无非流式残留收益,升 ValueError 的论据其实更强);③stall_after_s ≥ call_deadline_s ValueError 点名落进 check_hedge_assembly。

N6 | §4.2 stall_count 定义 | "连续停顿合并一次"在 gap 口径下无所指

  • 间隔口径里任意两个超阈值 gap 之间至少隔一个 chunk,不存在可合并的"连续停顿";该子句移植自 pi-tps 的事件流语义,在本口径下无操作定义。修法: 直接定义"stall_count = 超阈值相邻间隔个数",删合并子句。

N7 | §4.2/§5.2/§6/§7 | 引用勘误与编辑点清单

  • types.py:417-418 实为 LLMResponse 邻域(max_inter_token_ms 在 :406),CallStats 在 :296-324;config.py:401-407 实为 :427-431;config.py:46 是 _SOURCE_FIELDS(与 §6 三段键矛盾,见裁决 9);openai_compat.py:605 是 max_inter_token_ms 装配处,max_gap 循环在 :580-590;schema.py:43/85 指向 ttft_ms 行,实际编辑点 = DDL 末尾(created_at 之后,schema.py 头注释纪律)+ SQLITE_BACKFILL/_PG_BACKFILL_DECLS 保序对齐 + COLUMNS + 列数断言测试 + telemetry_schema_sql 同源再生成;§5.2"须进批准项"在 §9 无对应条目,补一条批准记录或删该句。

N8 | §4.2 vs §7 | 类型换算点未钉

  • TransportResult stall_ms: float | None vs CallStats int | None: 钉一句换算顺序(建议 retry 层 int(wall_ms) − int(stall_ms) 或先减后取整,二选一),避免各实现 ±1ms 漂移。

N9 | §5.2 与看门狗的交互 | 措辞修正 + 一句哲学论证

  • "主路之死不算失败"改为"本轮超级尝试不因主路之死而败(等副路);attempt 级记账(breaker failure/attempt_fails/遥测 error 行)照常"。并补一句论证: 看门狗掐死喂熔断失败 vs 对冲输家取消不喂熔断,同为源沉默事件却不同处置——前者是既有看门狗语义(1.3.6 起),后者是 1.3.7 已批准语义,本设计不改任一,但运维侧"挂起率"统计须知道这一不对称。

核实记录(引用抽查,全部实读源码)

设计引用 实况 结论
retry.py:542-562 _past_hedge_window :542 起,返回式逐字一致 ✓
retry.py:~335/~346 计时点 :332/:346 ✓(邻域)
openai_compat.py:573 回落 :573 逐字一致 ✓
openai_compat.py:605 max_gap :580-590 循环,:605 装配 偏位(N7)
types.py:417-418 CallStats 在 :296-324 错位(N7)
config.py:46/57/401-407 :46 ✓ :57 ✓ :401-407 实为 :427-431 部分偏位
遥测无 generation_ms 列 COLUMNS 36 项核实 ✓
看门狗抛 TransientError streaming.py + openai_compat.py:476 ✓

结论行: 有条件实施——3 条阻断级(B1 chunk 定义、B2 遥测透传链路、B3 编排入口)+ 9 条建议级(N1-N9),均为设计补遗级修正,无需推翻 §9 拍板。

发现计数: B1-B3(阻断级 3 条),N1-N9(建议级 9 条)。