返回 AICAP-180
B8 · Day 73真 API + 流式 + Docker

模型流式接入

Day 72 用硬编码的 3 帧把 SSE 管子打通了,但管子里流的是假 token。今天在 B1→B18 曲线上把假 token 换成真模型:

阶段: B8 · 真 API + 流式 + Docker(Day 71-80) 标签: #ttft #streaming #deepseek-v4 #backpressure

今日导引(由浅入深)

Day 72 用硬编码的 3 帧把 SSE 管子打通了,但管子里流的是假 token。今天在 B1→B18 曲线上把假 token 换成真模型:

  • 给 provider 传 stream: true
  • 逐帧解析 DeepSeek 的 SSE delta。
  • 把首 token 延迟(TTFT)测出来。

这是「流式」从协议演练变成真实产品体验的一步:TTFT 决定用户「等了多久才看到第一个字」,是流式 UX 的核心指标。明天(Day 74)会处理这条真流式管子在 upstream 故障时如何 typed 地重试 / 终止。

最小可判定产出/api/chat 接 DeepSeek 真流式,记录一组 TTFT(ms) 进 worklog。

1. 机理精读

1.1 真流式接入要处理三件事,第一是 provider 的流式参数

DeepSeek(OpenAI 兼容协议)开流式只要在请求体加 stream: true,响应就从一次性 JSON 变成 text/event-stream

  • 服务端把生成切成一串 data: {chunk} 帧。
  • 每帧带一小段 token delta。
  • 最后以 data: [DONE] 收尾。

这和 Day 72 手搓的哨兵帧结构同构——区别只是 token 来自真模型而非硬编码。

1.2 第二是不同模型的 SSE 帧结构差异

这是真流式最容易翻车的点:DeepSeek 与 Qwen3 的 delta 字段命名、usage 帧出现的位置不一样:

  • OpenAI 兼容流里增量内容在 choices[0].delta.content
  • 但有的实现把 usage 放在最后一帧、有的全程为 null 只在末帧给、有的需要请求时显式 stream_options: {include_usage: true} 才回传 usage。

解析器必须对「字段可能缺失 / 位置不固定」鲁棒,不能假设每帧结构一致——否则换个模型(Day 76 接 Qwen3)就崩。

1.3 第三是背压与超时

流式是「上游持续产、下游消费」的生产者-消费者问题:

  • 如果下游(浏览器 / 客户端)慢消费,上游 token 会在中间缓冲堆积,吃内存。
  • 同时要有整体 wall-clock 超时——单帧不卡不代表整体不卡,一个一直吐但永远不结束的流必须被超时干净终止(对应 Day 74 的 timeout(504) error code)。

背压的最小处理是依赖 ReadableStream 的拉取式背压(下游不读、上游不产),但仍需顶层超时兜底。

1.4 核心指标是 TTFT(Time To First Token)

流式 UX 的价值几乎全在 TTFT——它是「点了发送到屏幕上冒出第一个字」的延迟,决定产品「感觉快不快」。整段生成的总时长(end-to-end latency)对体验反而次要,因为用户从第一个 token 起就在读了。

所以本日要测的不是总时长而是 TTFT 分布。对照 Anthropic Messages API streaming(2026-01)的 content_block_delta 帧节奏:它的流式同样是「先 message_start 快速回、再持续 content_block_delta」,把感知延迟压在第一帧。

2. 推导 / 手算 / 代码走读

2.1 走读 src/agent/eval/stats.tssummarizeLatency

TTFT 是延迟样本,应按尾部分位数而非均值看。本日记录 TTFT 复用其思路:

  • summarizeLatency(samplesMs):先 [...samplesMs].sort((a, b) => a - b),再算 p50/p95/p99/mean,返回 LatencySummary{ n, p50, p95, p99, mean })。
  • 关键设计理由(代码注释原话):p95/p99 surface the tail that the mean hides——TTFT 的均值会被少数快请求拉低,真正伤体验的是慢尾,所以必须看 p95/p99。
  • percentile(sorted, p):对已排序数组做线性插值取分位,idx = (p/100)*(len-1)lo/hi 两端按权重 w = idx - lo 加权——这是连续分位估计,不是粗暴取整。
  • 用法:把每次请求的 TTFT(ms) 收集成数组传给 summarizeLatency,得到 { n, p50, p95, p99, mean } 写进 worklog。

2.2 手算一个 TTFT 例子

假设跑 5 次得 [210, 240, 260, 290, 900] ms:

  • 排序后即原序;mean = (210+240+260+290+900)/5 = 380 ms。
  • p50:idx=0.5*4=2sorted[2]=260 ms。
  • p95:idx=0.95*4=3.8lo=3, hi=4, w=0.8290*0.2 + 900*0.8 = 58 + 720 = 778 ms。
  • 解读:mean=380 看着还行,但 p95=778 暴露了一条慢尾——这正是 summarizeLatency 强调看尾部的原因。

(以上为手算演示,非实测 TTFT。)

2.3 无 key 与需 key 的边界

注意:src/agent/eval/stats.ts 是纯函数、RNG 可注入、无 key 可单测;但真实 TTFT 数字需要带 key 跑真流式才能产出。即「分位数算法」可离线验证,「TTFT 数值」必须在线测。

3. 今日实战

  1. /api/chat(Day 72 的 SSE route)接到 DeepSeek 真流式:请求体设 model=deepseek-v4-flashstream: true,走 provider-agnostic runner(默认 deepseek)。
  2. 逐帧解析 DeepSeek SSE:取 choices[0].delta.content 增量,遇 data: [DONE] 收尾;对 usage 帧位置 / 缺失做鲁棒处理。
  3. 在收到第一帧非空 delta 的时刻打点,减去请求发出时刻 = TTFT(ms)。
  4. 多跑几次收集 TTFT 数组,用 src/agent/eval/stats.tssummarizeLatency 思路算 p50/p95/p99/mean,记入 worklog。
  5. key 现已配置——可直接跑真流式。

4. 今日实测 / 产出

  • 待跑(需 key 跑,key 已配置)——TTFT 数字 (ms) 记入 worklog。
  • 可引用的已落地相邻真值(同模型成本参照,非本日新测):V4-Flash completion 79.3%、cost $0.0139/run
  • src/agent/eval/stats.ts:已构建(summarizeLatency tested,纯函数无 key),本日复用其分位数思路记录 TTFT,未改动。

5. 常见误区 / 陷阱

  • 假设所有模型 SSE 帧结构一致:DeepSeek 与 Qwen3 的 delta 字段命名 / usage 帧位置不同,解析器必须对缺失 / 位移鲁棒,否则 Day 76 换模型即崩。
  • 用 mean 报 TTFT:均值掩盖慢尾,必须报 p95/p99(summarizeLatency 的设计意图)。
  • 没有顶层 wall-clock 超时:单帧不卡 ≠ 整体不卡,一个永不结束的流必须被超时干净终止(对应 Day 74 的 timeout)。
  • 硬编码 legacy 模型 iddeepseek-chat/deepseek-reasoner 将于 2026-07-24 退役,新代码勿硬编码。

6. 学习资源(每条带 YYYY-MM)

  • Anthropic, "Messages API streaming"(官方文档,2026-01;content_block_delta 帧节奏范本)。
  • DeepSeek API 文档, "Streaming / Chat Completions"(官方文档,2026 现行;stream: true、SSE delta、stream_options.include_usage)。
  • 本仓代码:src/agent/eval/stats.tssummarizeLatency p50/p95/p99 + mean,B3)。
  • "Latency vs. throughput in LLM serving"(综述类,2025-2026 现行讨论;TTFT 为流式 UX 核心指标的论据)。

SOTA检查 (2026-06 更新)

  • 当前主流:DeepSeek 用 deepseek-v4-flash / deepseek-v4-pro 新 id(runner 默认直连 deepseek)。SSE token 流 + TTFT 分位监控是当下流式服务标准实践。
  • 是否仍 SOTA:是。但模型 id 有硬退役日。
  • 过时黑名单
    • legacy deepseek-chat / deepseek-reasoner 将于 2026-07-24 退役,切勿在新代码硬编码。
    • OpenRouter 路由仍可用,但 runner 默认直连 deepseek。
  • 下次复查点:2026-07-24(DeepSeek legacy id 退役)核对所有硬编码 id;Day 76 接 Qwen3 时复验其 SSE delta 帧结构与 DeepSeek 的差异。

衔接

  • 昨天:Day 72 — SSE 流式协议(硬编码 3 帧验证 text/event-stream 可读)。
  • 今天:把假 token 换成 DeepSeek 真流式,逐帧解析 delta,测 TTFT(key 已配置)。
  • 明天:Day 74 — typed 错误 + 重试(这条真流式管子遇 upstream 故障时如何分类、退避、干净终止)。