模型流式接入
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.ts 的 summarizeLatency
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 = 380ms。 p50:idx=0.5*4=2→sorted[2]=260ms。p95:idx=0.95*4=3.8→lo=3, hi=4, w=0.8→290*0.2 + 900*0.8 = 58 + 720 = 778ms。- 解读:mean=380 看着还行,但 p95=778 暴露了一条慢尾——这正是
summarizeLatency强调看尾部的原因。
(以上为手算演示,非实测 TTFT。)
2.3 无 key 与需 key 的边界
注意:src/agent/eval/stats.ts 是纯函数、RNG 可注入、无 key 可单测;但真实 TTFT 数字需要带 key 跑真流式才能产出。即「分位数算法」可离线验证,「TTFT 数值」必须在线测。
3. 今日实战
- 把
/api/chat(Day 72 的 SSE route)接到 DeepSeek 真流式:请求体设model=deepseek-v4-flash、stream: true,走 provider-agnostic runner(默认 deepseek)。 - 逐帧解析 DeepSeek SSE:取
choices[0].delta.content增量,遇data: [DONE]收尾;对usage帧位置 / 缺失做鲁棒处理。 - 在收到第一帧非空 delta 的时刻打点,减去请求发出时刻 = TTFT(ms)。
- 多跑几次收集 TTFT 数组,用
src/agent/eval/stats.ts的summarizeLatency思路算 p50/p95/p99/mean,记入 worklog。 - key 现已配置——可直接跑真流式。
4. 今日实测 / 产出
- 待跑(需 key 跑,key 已配置)——TTFT 数字 (ms) 记入 worklog。
- 可引用的已落地相邻真值(同模型成本参照,非本日新测):V4-Flash completion 79.3%、cost $0.0139/run。
src/agent/eval/stats.ts:已构建(summarizeLatencytested,纯函数无 key),本日复用其分位数思路记录 TTFT,未改动。
5. 常见误区 / 陷阱
- 假设所有模型 SSE 帧结构一致:DeepSeek 与 Qwen3 的 delta 字段命名 / usage 帧位置不同,解析器必须对缺失 / 位移鲁棒,否则 Day 76 换模型即崩。
- 用 mean 报 TTFT:均值掩盖慢尾,必须报 p95/p99(
summarizeLatency的设计意图)。 - 没有顶层 wall-clock 超时:单帧不卡 ≠ 整体不卡,一个永不结束的流必须被超时干净终止(对应 Day 74 的
timeout)。 - 硬编码 legacy 模型 id:
deepseek-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.ts(summarizeLatencyp50/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。
- legacy
- 下次复查点: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 故障时如何分类、退避、干净终止)。