打开一些人工智能大模型网页版,你会发现文字是一个字一个字蹦出来的,而不是等上五六秒后一整段话突然出现。这背后就是流式数据在起作用。传统的Web请求更像是寄快递,你下单后要等包裹打包完成、贴好标签才能发货;而流式传输更像是直播,服务器一边生成内容一边往外送,用户不用死等。
这篇文章会先讲清楚流式数据的原理和适用场景,再用FastAPI的官方文档为线索,把从基础用法到进阶技巧一次性讲透。
什么是流式数据
在普通的HTTP交互里,服务器要把完整的响应内容准备好(比如渲染完一个JSON对象、生成完一张图片),才会把数据打包发送给客户端。客户端也是收到全部数据后才开始处理。这种模式叫一次性响应。
流式数据打破了这种等待关系。服务器一旦生成了一部分数据,就立刻发出去,客户端边接收边处理,不用等整个任务跑完。技术上这依赖HTTP协议里的分块传输编码(Transfer-Encoding: chunked),服务器不需要提前告诉客户端总共有多少字节,而是像挤牙膏一样一段一段挤出来 。
用一张图对比会更直观:

什么情况需要用流式数据
不是所有接口都适合流式化,用错了地方反而增加复杂度。以下几种场景是流式传输真正能发挥价值的地方。
大语言模型的逐字输出。像ChatGPT这类应用,生成一整段回答可能要花上十几秒,如果等全部生成完再返回,用户体验会很差。用流式传输,模型生成一个token就往前端推一个token,用户几乎立刻就能看到反馈 。
大文件或视频的分块传输。如果你要返回一个几个GB的视频文件,把它整个读入内存再发送既浪费内存又拖慢首字节时间。流式传输可以做到边读边发,内存占用始终维持在很低的水平,客户端也能更快开始播放 。
实时数据推送 。股票行情、日志监控、进度条更新这类需要服务器主动、持续向客户端推送信息的场景,也天然适合流式模型,其中最标准化的实现方式就是服务器发送事件(Server-Sent Events,简称SSE)。
逐行处理的结构化数据。如果响应内容本质上是一系列独立的JSON对象(比如批量任务的进度反馈),可以用JSON Lines格式一行一行地流式发送,而不必等所有对象都生成完再打包成一个大数组 。
反过来说,如果你的响应本身很小(比如一个简单的用户信息JSON),或者数据必须整体校验后才能返回(比如支付结果),流式传输就没什么意义,老老实实用普通响应就好。
FastAPI中如何实现流式数据
FastAPI基于Starlette构建,天然支持流式响应,核心工具是StreamingResponse类。下面按照使用场景由浅入深地拆解。
用生成器函数构建StreamingResponse
最基础的用法是写一个生成器函数(用yield而不是return),然后把它传给StreamingResponse 。
python
import time
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
def generate_numbers():
for i in range(10):
yield f"数字 {i}\n"
time.sleep(0.5) # 模拟耗时的生成过程
@app.get("/stream")
async def stream_numbers():
return StreamingResponse(generate_numbers(), media_type="text/plain")
这里的关键点在于,generate_numbers函数每次执行到yield就会暂停并把当前的值交出去,FastAPI收到后立刻把这段内容发送给客户端,而不是等整个循环跑完。
关于同步与异步的选择 。生成器函数既可以是普通的def(同步生成器),也可以是async def(异步生成器)。如果你的耗时操作是CPU密集型的,或者调用的是同步阻塞库,用同步生成器配合def路径操作函数即可;如果是IO密集型(比如调用外部API、读写数据库),推荐用async def异步生成器,配合await asyncio.sleep()这类异步等待,能更好地利用事件循环、避免阻塞其他请求 。
流式传输原始字节数据
除了文本,StreamingResponse同样能处理二进制字节流,这在传输图片、音视频或者任意二进制文件时很常用 。
python
from fastapi.responses import StreamingResponse
def generate_bytes():
for chunk in [b"chunk-1-", b"chunk-2-", b"chunk-3"]:
yield chunk
@app.get("/bytes")
async def stream_bytes():
return StreamingResponse(generate_bytes(), media_type="application/octet-stream")
自定义响应子类
如果你需要频繁流式返回某种特定格式的数据(比如始终是PNG图片),可以创建StreamingResponse的子类,把媒体类型固定下来,减少重复代码 。
python
from fastapi.responses import StreamingResponse
class PNGStreamingResponse(StreamingResponse):
media_type = "image/png"
@app.get("/image")
async def get_image():
def generate_png_chunks():
# 这里用实际的图片数据生成逻辑替换
yield b"\x89PNG\r\n\x1a\n"
yield b"...更多的PNG字节..."
return PNGStreamingResponse(generate_png_chunks())
这种做法的好处是,response_class参数可以在OpenAPI文档中正确标注Content-Type,前端调用方一看文档就知道该怎么处理这个响应 。
模拟文件读取的流式传输
对于本地大文件,更实用的写法是打开文件后按块读取并yield出去,配合yield from可以把这个过程写得非常简洁 。
python
def iter_file(path: str, chunk_size: int = 1024 * 1024):
with open(path, "rb") as f:
while chunk := f.read(chunk_size):
yield chunk
@app.get("/download")
async def download_file():
return StreamingResponse(
iter_file("large_video.mp4"),
media_type="video/mp4"
)
需要注意的是,如果文件IO是同步阻塞的,且你的路径操作函数是async def,最好把文件读取放到线程池里执行(比如用run_in_threadpool),否则会阻塞整个事件循环,拖慢其他并发请求的响应速度 。
服务器发送事件 SSE
当你需要服务器持续、主动地向前端推送消息(而不是客户端每次都要发起新请求去轮询),SSE是比原始StreamingResponse更规范的选择。FastAPI从较新版本开始原生支持SSE,格式遵循text/event-stream标准,每条消息以data:开头 。
python
import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def event_generator():
for i in range(5):
yield f"data: 进度 {i * 20}%\n\n"
await asyncio.sleep(1)
yield "data: 完成\n\n"
@app.get("/progress")
async def progress():
return StreamingResponse(event_generator(), media_type="text/event-stream")
浏览器端可以用原生的EventSource API直接消费这个接口,不需要额外的库,非常适合做进度条、通知推送这类轻量级实时功能 。
大模型场景中的实战代码
结合前面提到的LLM流式输出场景,一个典型的FastAPI转发OpenAI流式响应的写法大概是这样 :
python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import openai
app = FastAPI()
async def ask_model(query: str):
stream = await openai.ChatCompletion.acreate(
model="gpt-4",
messages=[{"role": "user", "content": query}],
stream=True,
)
async for chunk in stream:
delta = chunk["choices"][0]["delta"]
if "content" in delta:
yield delta["content"]
@app.post("/chat")
async def chat(query: str):
return StreamingResponse(ask_model(query), media_type="text/plain")
常见的坑 别踩
流式接口调通了却发现浏览器还是一次性收到全部内容,这是最常见的抱怨,原因通常出在下面几个地方 。
| 问题来源 | 典型表现 | 解决办法 |
|---|---|---|
| 反向代理缓冲 | Nginx默认会缓冲响应体,导致流式效果消失 | 在Nginx配置里关闭proxy_buffering |
| 生成器混用同步阻塞调用 | 异步路径函数里用了同步的time.sleep等待 |
改用asyncio.sleep,或把阻塞逻辑丢进线程池 |
| ASGI服务器本身的缓冲 | 某些服务器或中间件默认会攒够一定字节数才发送 | 检查uvicorn/gunicorn配置,必要时降低缓冲阈值 |
| 客户端库不支持增量读取 | 前端用了会自动等待整个响应完成的请求库 | 改用支持流式读取的fetch/EventSource/httpx等 |
排查这类问题时,最直接的方法是用curl -N命令行工具去请求接口,-N参数会关闭curl自身的缓冲,能最真实地反映服务器端到底有没有真正做到流式发送。
写在最后
流式数据本质上是用渐进式交付 替代一次性交付 ,核心价值在于降低用户感知到的延迟,尤其在内容生成耗时较长或数据量很大的场景中效果明显。FastAPI凭借Starlette的异步能力,把这件事做得相当优雅------一个生成器函数配合StreamingResponse就能搞定大部分需求,遇到实时推送场景再上SSE,遇到大文件再配合分块读取,工具箱基本齐全了。
真正上手写的时候,记得把"是否真的需要流式"这个问题先问一遍自己,别为了炫技给一个返回几十字节JSON的接口套上流式的外壳,那样反而增加了不必要的复杂度。
参考资料
FastAPI Documentation, Stream Data , fastapi.tiangolo.com/advanced/st...
FastAPI Documentation, Custom Response - HTML, Stream, File, others , fastapi.tiangolo.com/advanced/cu...
Stack Overflow, FastAPI StreamingResponse not streaming with generator function , stackoverflow.com/questions/7...
FastAPI Documentation, Server-Sent Events (SSE) , fastapi.tiangolo.com/tutorial/se...
Medium, Low Latency Video Streaming Application with FastAPI , medium.com/@praveen060...