聊聊Web开发里的流式数据 从原理到FastAPI实战

打开一些人工智能大模型网页版,你会发现文字是一个字一个字蹦出来的,而不是等上五六秒后一整段话突然出现。这背后就是流式数据在起作用。传统的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...

相关推荐
SMF19191 小时前
【PyCharm】让 PyCharm 使用 .venv 虚拟环境
ide·python·pycharm
Scene2161 小时前
AgentScope 2.0:4. Message & Event —— 消息模型与事件流深度解析
后端
元界metalite1 小时前
MyBatis 字段改名为何查询不报错?MetaLite ORM 如何做到类型安全?
后端
修远客1 小时前
感知模块:Agent的眼睛和耳朵 — 三层降级策略让Agent永不"失明"
python·agent
Csvn1 小时前
🐍 Day 2 :Python 变量与数据类型 — 一切皆对象
后端
Csvn2 小时前
📊 SQL 入门 Day 17:数据更新与删除
后端·sql
秋天的一阵风2 小时前
🔥 Network 里那坨 "data:" 我真看吐了,自制开源 Chrome 插件,AI 流式调试直接开挂
前端·人工智能·后端
circuitsosk2 小时前
Prompt Engineering进阶:面向复杂业务场景的模板化管理与动态注入策略
python·langchain·prompt·跨境电商·rag·上下文管理·动态注入
IT_陈寒2 小时前
Python的GIL让我深夜加班,这破锁到底怎么折腾的
前端·人工智能·后端