Ollama 的流式接口不是 SSE,客户端要改三处

把一个走 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 文档。感兴趣的关注一下。

相关推荐
n112126 天前
Ollama 接 Open WebUI 中文界面:我踩的 4 个坑
云原生·eureka·ollama·本地大模型·open webui·私有部署·中文界面
一枕槐安4877 天前
开源大模型 Llama3 本地部署全过程,Ollama 上手
开源·本地部署·大模型推理·ollama·llama3·modelfile
孤狼GPT7 天前
ChatGPT、Codex工程实战:Spring Boot的SSE一直在推送,为什么经过Nginx后前端才一次性显示?
spring boot·chatgpt·sse·codex·chatgptplus
一枕槐安4878 天前
大模型 API 调用封装:统一适配 GPT、文心一言、通义千问
人工智能·gpt·文心一言·大模型api·流式输出·统一封装·多模态路由
死磕音视频9 天前
Open WebUI 入门:用 Docker 装上本地对话界面并连接 Ollama
docker·ollama·本地大模型·open webui
上火的金鱼妹10 天前
关于多进程sse连接通知的实战
javascript·状态模式·sse
xzlAwin11 天前
Win10 安装 Ollama 本地AI
ai·ollama
Flynn Liang13 天前
Spring AI ChatClient 详解:system/user/assistant 角色、call 与 stream 到底怎么选
java·人工智能·spring·springai·流式输出·chatclient·java接入大模型
还卿一钵无情泪13 天前
PEFT-Transformers、Unsloth、LLaMA-Factory 微调框架 横向对比
深度学习·大模型·llama·ollama