把一个走 OpenAI 兼容接口的客户端切到本地 Ollama,第一件事就会撞上流式的差异。界面上的表现完全一样,一个字一个字往外蹦。协议底下,两边的帧长得不一样。
云端那套是 SSE,每条消息带 data: 前缀,靠空行分帧,之前写 ChatServer 那篇时拆过四个细节。Ollama 的 /api/chat 不是 SSE。它按换行符切帧,一行一个独立 JSON,没有 data: 前缀,也没有事件名。默认就开着流式,端口 11434。
bash
curl -s -X POST "http://127.0.0.1:11434/api/chat" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek-r1:1.5b","stream":true,
"messages":[{"role":"user","content":"你是谁"}],
"options":{"temperature":0.7,"num_ctx":2048}}'
跑一下能看到,每一行都是完整 JSON,message.content 是这一片的增量文本。这带来三个客户端要处理的事。
一、按行切,别按 SSE 解析
复用 SSE 解析器是第一个本能反应,然后就会在 data: 前缀上卡住。Ollama 的行里没有这个前缀,按 SSE 的逻辑剥离前缀会报错或者丢字。
正确的做法是维护一个缓冲区,每收到一段字节就追加进去,按换行符切出完整行,剩下的不完整的留在缓冲区里。切出来的每一行扔给 JSON 解析,解析失败说明这行还没收全,退回去继续等。这就是半包处理,TCP 不保证一次到达的数据正好是一行。
python
buf = ""
for chunk in resp.iter_content(chunk_size=None, decode_unicode=True):
buf += chunk
while "\n" in buf:
line, buf = buf.split("\n", 1)
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue # 半包或脏行,丢弃并继续收
handle(obj)
那段 except 里的 continue 别改成报错断连。本地模型偶尔会有一行异常,断连的代价是整轮回答重来。
二、结束帧里藏着监控要的数字
流什么时候结束,看 done 字段。done 为 false 的时候,content 是增量,往里拼。为 true 的时候,这一帧的 content 是空的,但带了完整的统计,总耗时、模型加载耗时、prompt 的 token 数、生成的 token 数、各自的耗时。
这些数字别扔。云端接口里拿它们要额外调用量接口,本地接口白送。把 eval_count 除以 eval_duration 就是生成速度,load_duration 长说明模型刚被换进内存,这正好解释了为什么第一问慢第二问快。做监控的时候按这几个字段出曲线,本地服务的性能问题一眼就能定位,比看日志猜快得多。
结束原因在 done_reason,一般是 stop。拿到非 stop 的值要单独记,那意味着流被截断了,上层拼出来的回答是不完整的。
三、num_ctx 不是 max_tokens,设小了会静默截断
参数表里最容易误读的是 num_ctx。它是上下文窗口的总容量,把 system 提示词、多轮历史和本次输出全算在内,作用等价于别家的 max_tokens,但语义是窗口不是单次输出上限。
坑在超限的行为上。窗口装不下的时候,老的历史会被挤掉,服务不报错,模型就是突然忘了前几轮说过什么。表象和记忆丢失一模一样,第一次遇到会往提示词上查,查一天。
两个对策。发请求前自己粗算 token 数,接近窗口就先做一轮压缩,之前写上下文压缩那篇的方法直接搬过来用。同时把每次响应里的 token 计数记下来,观察实际占用离窗口上限有多远,贴得太近就提前扩窗口或者缩历史。
| 差异点 | 云端 SSE | Ollama 流式 |
|---|---|---|
| 分帧 | data: 前缀加空行 |
换行符,一行一个 JSON |
| 结束标志 | [DONE] 标记 |
done 字段为 true 的帧 |
| 用量统计 | 要额外请求 usage | 结束帧自带 token 与耗时 |
| 输出上限 | max_tokens 参数 |
num_ctx 窗口,含全部历史 |
| 超限行为 | 报错或截断输出 | 挤掉旧历史,不报错 |
一句提醒收尾。本地接口的价值是可控和便宜,代价是没人替你兜底。云端客户端里那些被 SDK 藏起来的细节,缓冲、半包、窗口、结束原因,到了本地都得自己经手一遍。经手过一次,再回头看云端的行为,也会明白得多。
这一批还有两篇,一篇讲 Ollama v0.40.15 的升级解读,决策模型适合接进路由和护栏;另一篇讲 LLM 输出怎么落成正式的 Word 文档。感兴趣的关注一下。