系列目标:30 天从 LangChain 入门到企业级部署
今日任务:掌握异步 Chain 调用 → 实现 Token 级流式输出 → 构建 FastAPI 流式聊天接口!
⚡ 一、为什么需要流式响应?
传统 AI 调用是“全有或全无”:
- 用户提问 → 等待 5 秒 → 一次性返回全部答案
问题:
- 用户焦虑:“AI 是不是卡了?”
- 长回答体验差(如生成 500 字文章)
- 无法实现“打字机效果”
解决方案:
✅ 流式响应(Streaming) —— 模型每生成一个 token,就立即推送给前端!
✅ 异步(Async) —— 不阻塞主线程,支持高并发!
💡 今天,我们就用 LangChain + Ollama + FastAPI,打造一个支持 SSE(Server-Sent Events)的流式聊天机器人!
🧱 二、核心概念
表格
| 技术 | 作用 |
|---|---|
astream() | 异步流式调用 Chain/Agent,返回 token 生成器 |
AsyncCallbackHandler | 异步回调(监听流式事件) |
SSE (Server-Sent Events) | 前端接收流式数据的标准协议(比 WebSocket 简单) |
FastAPI | 支持异步和流式响应的 Python Web 框架 |
🔔 注意:并非所有 LLM 都支持流式!
✅ Ollama(含 Qwen)✅ 支持!
❌ 某些 API 模型可能不支持(需查文档)
🛠️ 三、动手实践 1:基础流式输出(控制台版)
步骤 1:启用 LLM 流式模式
# day15_streaming.py
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
# 关键:设置 streaming=True
llm = ChatOllama(model="qwen:7b", temperature=0, streaming=True)
prompt = ChatPromptTemplate.from_template("请写一首关于{topic}的五言诗")
chain = prompt | llm
步骤 2:使用 astream() 异步流式调用
import asyncio
async def stream_poem():
print("🤖 AI 正在作诗:", end="", flush=True)
# astream() 返回异步生成器
async for chunk in chain.astream({"topic": "春天"}):
print(chunk.content, end="", flush=True)
print("\n✅ 完成!")
# 运行
asyncio.run(stream_poem())
▶️ 输出效果(逐字打印):
🤖 AI 正在作诗:春风吹绿柳,
细雨润花红。
燕语穿林过,
莺歌入梦中。
✅ 完成!
✅ 实现了“打字机效果”!用户不再干等。
🛠️ 四、动手实践 2:自定义流式 Callback(记录首 token 时间)
from langchain_core.callbacks import AsyncCallbackHandler
class StreamingCallback(AsyncCallbackHandler):
def __init__(self):
self.first_token_time = None
async def on_llm_start(self, serialized, prompts, **kwargs):
print("⏳ LLM 开始生成...")
async def on_llm_new_token(self, token, **kwargs):
if self.first_token_time is None:
self.first_token_time = asyncio.get_event_loop().time()
print(f"\n⏱️ 首个 token 延迟: {self.first_token_time - start_time:.2f}s")
print(token, end="", flush=True)
💡 可用于监控“首 token 延迟”——衡量系统响应速度的关键指标!
🌐 五、动手实践 3:FastAPI 流式聊天接口(生产级)
步骤 1:安装依赖
pip install fastapi uvicorn sse-starlette
步骤 2:创建流式 API
# day15_fastapi_streaming.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from sse_starlette.sse import EventSourceResponse
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
import asyncio
app = FastAPI()
# 全局 LLM(生产环境建议用连接池)
llm = ChatOllama(model="qwen:7b", temperature=0, streaming=True)
prompt = ChatPromptTemplate.from_template("{input}")
chain = prompt | llm
async def event_generator(user_input: str):
"""生成 SSE 事件流"""
async for chunk in chain.astream({"input": user_input}):
yield {"data": chunk.content}
yield {"data": "[DONE]"} # 结束标记
@app.get("/stream")
async def stream_chat(q: str):
return EventSourceResponse(event_generator(q))
步骤 3:启动服务
uvicorn day15_fastapi_streaming:app --reload
步骤 4:前端测试(浏览器控制台)
// 打开浏览器控制台执行
const evtSource = new EventSource("http://localhost:8000/stream?q=你好");
evtSource.onmessage = (event) => {
if (event.data === "[DONE]") {
console.log("🔚 结束");
evtSource.close();
} else {
process.stdout.write(event.data); // 实际前端应 append 到 DOM
}
};
✅ 前端将实时收到每个 token,实现丝滑打字效果!
⚠️ 六、注意事项 & 最佳实践
表格
| 问题 | 建议 |
|---|---|
| 模型不支持 streaming | 检查 LLM 是否实现 BaseChatModel.stream() 方法 |
| 中文 token 分割奇怪 | Ollama 对中文流式支持良好,Qwen 表现优秀 |
| 高并发下内存溢出 | 限制最大并发;使用连接池管理 LLM 实例 |
| 需要中断生成? | 目前 LangChain 不支持“取消流式”,需自行管理超时 |
| 安全性 | 流式接口同样需做输入校验 + 敏感词过滤(结合 Day 14 Callback) |
💡 生产建议:
- 使用
uvicorn+gunicorn部署- 添加请求限流(
slowapi)- 日志记录完整会话(非仅流)
📦 七、配套代码结构
langchain-30-days/
└── day15/
├── streaming_console.py # 控制台流式输出
└── fastapi_streaming_api.py # FastAPI 流式聊天接口
📝 八、今日小结
- ✅ 理解了流式响应对用户体验的价值
- ✅ 学会了用
astream()实现 Token 级流式输出 - ✅ 掌握了
AsyncCallbackHandler监听流式事件 - ✅ 构建了基于 FastAPI + SSE 的生产级流式接口
- ✅ 知道了流式在高并发下的优化方向
🎯 明日预告:Day 16 —— 文档加载器(Document Loaders)大合集!PDF、Word、网页、数据库一键读取!