Ollama 流式输出
模型生成一整段回答可能要好几秒。如果等全部生成完再显示,用户面对的就是一个空白屏加转圈。流式输出(Streaming)把回答拆成一个个小片段,生成一点就推一点,界面能立刻开始渲染。
默认行为有个容易踩的坑:REST API 默认就是流式,而官方 SDK 默认是关的。很多人在 Python 里用 chat() 发现要等很久,就是因为没有显式打开。
版本基线 v0.40.12。字段名与默认值如随版本调整,以官方
docs.ollama.com/capabilities/streaming为准。
一、REST API 与 SDK 的默认值差异
| 调用方式 | 流式默认 | 怎么控制 |
|---|---|---|
REST /api/chat、/api/generate |
开启 | 请求体传 "stream": false 关闭 |
Python ollama 包 |
关闭 | chat(..., stream=True) 开启 |
JavaScript ollama 包 |
关闭 | ollama.chat({ ..., stream: true }) |
用 curl 验证一下:不加 stream 字段,返回的是一串逐行 JSON(NDJSON),每行一个片段,最后一行带 "done": true。
## 默认流式:终端会一段一段吐出 JSON 行
curl http://localhost:11434/api/chat -d '{
"model": "qwen3",
"messages": [
{ "role": "user", "content": "用一句话解释什么是流式输出" }
]
}'
如果只是写脚本做一次性处理,直接把 stream 设成 false 更省事:
## 一次性拿完整 JSON,适合脚本管道处理
curl http://localhost:11434/api/chat -d '{
"model": "qwen3",
"messages": [
{ "role": "user", "content": "用一句话解释什么是流式输出" }
],
"stream": false
}'
二、流式里有三种内容要分别处理
新手常见的写法是只读 message.content,结果发现思考型模型的推理过程"消失"了,或者工具调用发不出去。一个完整的流式循环要认三种片段:
1)正文片段:每个 chunk 的 message.content 是一小段文字,拼起来才是完整回答。界面要做的是追加显示,而不是替换。
2)推理片段:支持思考能力的模型会在每个 chunk 里带上 message.thinking。它和 content 是两套独立字段,可以折叠展示,也可以干脆不显示。
3)工具调用片段:模型决定调工具时,流式里会出现 tool_calls。你需要暂停渲染、执行本地工具、再把结果塞回对话。
三者区分清楚,才谈得上正确的流式渲染。
三、必须累积片段,否则下一轮对话会失忆
这是官方特别强调的一点:流式返回的是残缺片段,你必须自己把它们拼回完整消息,再作为 assistant 消息写进历史。
为什么非拼不可?因为工具调用是三轮接力:模型先"想"(thinking 片段)、再"决定调用"(tool_calls 片段)、本地执行拿到结果,这三样都要回传给模型,它才能继续回答。如果你只把最后那段 content 存进历史,模型下一轮就不知道刚才想过什么、调过什么,回答会明显跑偏。
Python 的完整写法(带逐行注释):
## pip install ollama
from ollama import chat
stream = chat(
model='qwen3',
messages=[{'role': 'user', 'content': '17 × 23 等于多少?'}],
stream=True, # SDK 默认不流式,必须显式打开
)
in_thinking = False # 是否正处于"推理"阶段,用于切换打印标签
content = '' # 累积的正文,最后要拼回历史
thinking = '' # 累积的推理轨迹,同样要拼回历史
for chunk in stream:
# ① 推理片段:先于正文出现
if chunk.message.thinking:
if not in_thinking:
in_thinking = True
print('思考中:\n', end='', flush=True)
print(chunk.message.thinking, end='', flush=True)
thinking += chunk.message.thinking # 关键:累积,不能丢
# ② 正文片段:推理结束后的正式回答
elif chunk.message.content:
if in_thinking:
in_thinking = False # 推理完毕,切回正文
print('\n\n回答:\n', end='', flush=True)
print(chunk.message.content, end='', flush=True)
content += chunk.message.content # 关键:累积,不能丢
## ③ 把拼好的完整消息放回对话历史,供下一轮请求使用
new_messages = [{'role': 'assistant', 'thinking': thinking, 'content': content}]
JavaScript 版本逻辑完全一致,注意用了 process.stdout.write 而不是 console.log——后者每次会换行,流式输出就变成一列歪歪扭扭的字:
// npm install ollama
import ollama from 'ollama'
async function main() {
const stream = await ollama.chat({
model: 'qwen3',
messages: [{ role: 'user', content: '17 × 23 等于多少?' }],
stream: true, // 同样要显式开启
})
let inThinking = false
let content = ''
let thinking = ''
for await (const chunk of stream) {
if (chunk.message.thinking) {
if (!inThinking) {
inThinking = true
process.stdout.write('思考中:\n')
}
process.stdout.write(chunk.message.thinking)
thinking += chunk.message.thinking // 累积推理轨迹
} else if (chunk.message.content) {
if (inThinking) {
inThinking = false
process.stdout.write('\n\n回答:\n')
}
process.stdout.write(chunk.message.content)
content += chunk.message.content // 累积正文
}
}
// 拼好的消息回填历史
new_messages = [{ role: 'assistant', thinking: thinking, content: content }]
}
main().catch(console.error)
四、接入前端时的三个实务建议
节流渲染。 片段可能每秒来几十个,每个都触发一次 DOM 更新会卡。把增量先塞进缓冲区,用 requestAnimationFrame 或 50 毫秒定时器批量刷。
推理区块默认折叠。 thinking 往往比正文长好几倍,全展开会把回答挤出屏幕。做成「正在思考…」的可展开区块,用户想看再点。
错误要能中途处理。 流式连接可能中途断掉。保存已累积的 content,连接异常时把已有部分留在界面上,而不是清空重来——用户至少还能看到半截答案。
想继续深入,下一篇 3.2 Ollama 思考能力 会把 thinking 的开关、等级与默认值讲清楚;如果需要把回答约束成固定结构,看 3.3 Ollama 结构化输出。

TRAE-AI编程


更多建议: