Ollama 流式输出

2026-10-29 18:29 更新

模型生成一整段回答可能要好几秒。如果等全部生成完再显示,用户面对的就是一个空白屏加转圈。流式输出(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 结构化输出。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号