在 LangChain 中流式输出
通过 LCEL 链实现令牌流式传输,使应用在每个词到达时立即显示,而不是等待完整响应,从而改善用户感知的延迟。
在 LangChain 中流式输出 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。
流式处理为何重要
如果不使用流式处理,用户只能盯着空白屏幕等待 LLM 完成生成;对于较长的响应,这可能需要 5–30 秒。使用流式处理后,令牌会在生成时逐个显示,从而立即向用户提供反馈,显著提升感知上的响应速度。当您调用 .stream() 时,LangChain 的 LCEL 会自动让流式处理贯穿整个链。
使用 .stream() 实现基本流式处理
每个 LCEL 链都提供 .stream() 方法,该方法会返回一个数据块迭代器。对于以 StrOutputParser 结尾的链,每个数据块都是一个字符串片段。您可以遍历这些数据块,在它们到达时将其打印出来或生成出去。流式传输发生在 HTTP 层:OpenAI API 返回的每个令牌一到达,就会立即经过解析器转发。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
chain = (
ChatPromptTemplate.from_template('Explain {topic} in detail.')
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
# Stream tokens to stdout
for chunk in chain.stream({'topic': 'quantum entanglement'}):
print(chunk, end='', flush=True)
print() # final newline使用 .astream() 实现异步流式处理
.astream() 是 .stream() 的异步版本。它会返回一个异步迭代器,您可以使用 async for 消费它。在 FastAPI、Starlette 以及其他异步 Web 框架中,请求处理器是协程,此时应采用这种方式。在异步处理器中使用同步流式处理会阻塞事件循环。
import asyncio
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
chain = (
ChatPromptTemplate.from_template('Write a poem about {subject}')
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
async def stream_response():
async for chunk in chain.astream({'subject': 'the ocean'}):
print(chunk, end='', flush=True)
asyncio.run(stream_response())在 FastAPI 中使用 StreamingResponse 进行流式传输
在 FastAPI 中,您可以将异步生成器包装在 StreamingResponse 中,并设置 media_type='text/plain',将文本令牌流式传输到浏览器。对于服务器发送事件(SSE),请使用 media_type='text/event-stream',并将每个数据块格式化为 data: ...\n\n。这样,浏览器无需等待完整响应,就能在令牌生成时接收它们。
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def generate_stream(topic: str):
async for chunk in chain.astream({'topic': topic}):
yield chunk
@app.get('/stream')
async def stream_endpoint(topic: str):
return StreamingResponse(
generate_stream(topic),
media_type='text/plain'
)
# SSE format for frontend EventSource
async def sse_stream(topic: str):
async for chunk in chain.astream({'topic': topic}):
yield f'data: {chunk}\n\n'跨越中间步骤的流式处理
LCEL 链会让流式处理贯穿每个支持该功能的步骤。StrOutputParser 支持流式处理,会立即传递数据块。不过,一些解析器(例如 JsonOutputParser)必须先缓存完整输出才能进行解析,这会中断流式处理。LangChain 会清楚地遵循这一点:如果某个步骤不兼容流式处理,它会先累积输出,然后再传递给下游。
from langchain_core.output_parsers import JsonOutputParser
# This chain does NOT stream token by token
# JsonOutputParser must buffer the full response before parsing JSON
json_chain = (
ChatPromptTemplate.from_template('Return JSON: {task}')
| ChatOpenAI(model='gpt-4o-mini')
| JsonOutputParser() # buffers until complete
)
# But partial JSON streaming IS possible with streaming_json_parser
for partial in json_chain.stream({'task': 'list 3 colors'}):
print(partial) # prints partial dict as it fills in使用 astream_events 进行细粒度控制
.astream_events() 提供了更细粒度的流式 API,它会为链中的每个步骤生成事件,而不仅仅是最终输出。每个事件都有一个 kind 字段(on_chain_start、on_llm_stream、on_chain_end)和一个 data 数据载荷。这样,您就可以将工具调用结果、中间推理过程和最终输出分别流式传输到用户界面的不同部分。
async def stream_with_events(question: str):
async for event in chain.astream_events(
{'question': question},
version='v2'
):
kind = event['event']
if kind == 'on_llm_stream':
chunk = event['data']['chunk'].content
print(chunk, end='', flush=True)
elif kind == 'on_chain_end':
print('\n[Done]')
elif kind == 'on_tool_start':
print(f'\n[Tool: {event["name"]}]')缓存流式输出
有时您既需要将令牌流式传输给用户,又需要捕获完整响应,以便进行日志记录或进一步处理。请将 .astream() 与列表累加器结合使用。循环结束后拼接这些数据块,即可得到完整文本。这种模式可以让您实时显示流式输出,同时为分析、缓存或评估保存完整响应。
async def stream_and_capture(question: str) -> str:
full_response = []
async for chunk in chain.astream({'question': question}):
print(chunk, end='', flush=True) # stream to user
full_response.append(chunk) # also collect
print() # newline
complete = ''.join(full_response)
await log_response(question, complete) # log full text
return complete使用工具调用进行流式处理
当模型在流式响应中生成工具调用时,函数参数会以令牌片段的形式到达。您必须先缓存 JSON 参数字符串,等工具调用完成后再执行它。LangChain 会在其代理执行器中自动处理这一过程;但如果您正在构建自定义流式循环,则必须检查 finish_reason,并累积 tool_call.function.arguments 片段。
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def stream_with_tools(prompt: str):
tool_call_buffer = {}
async with client.chat.completions.stream(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
tools=[weather_tool_schema]
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
if idx not in tool_call_buffer:
tool_call_buffer[idx] = ''
if tc.function.arguments:
tool_call_buffer[idx] += tc.function.arguments流式处理中的取消与超时
较长的流式响应需要支持取消。在异步 Python 中,您可以取消包装流的 asyncio.Task。在 FastAPI 中,使用 StreamingResponse 时,框架会自动处理客户端断开连接导致的取消。您可以通过 OpenAI 客户端的 timeout 参数设置超时,也可以使用 asyncio.wait_for() 包装流,使其在达到最大持续时间后中止。
import asyncio
async def stream_with_timeout(question: str, timeout: float = 30.0):
async def _stream():
async for chunk in chain.astream({'question': question}):
yield chunk
try:
async for chunk in asyncio.timeout(_stream(), timeout):
print(chunk, end='', flush=True)
except asyncio.TimeoutError:
print('\n[Stream timed out after 30 seconds]')
except asyncio.CancelledError:
print('\n[Stream cancelled by client disconnect]')使用 JavaScript 在客户端处理 SSE
在前端,浏览器原生的 EventSource API 用于接收服务器发送的事件。当 FastAPI 端点发出 data: token\n\n 数据块时,EventSource 会为每个数据块触发一个 message 事件。每当令牌到达时,就将其追加到 DOM 中,从而实现打字机效果。如需更精细的控制,可以使用 fetch() 配合 response.body.getReader(),获得完整的流式访问能力。
// Frontend JavaScript (not Python)
const source = new EventSource('/stream?topic=quantum+computing');
const outputDiv = document.getElementById('output');
source.onmessage = (event) => {
outputDiv.textContent += event.data;
};
source.onerror = () => {
source.close();
outputDiv.textContent += ' [done]';
};
// Alternative: fetch with ReadableStream
const response = await fetch('/stream?topic=ai');
const reader = response.body.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) break;
outputDiv.textContent += new TextDecoder().decode(value);
}流式处理最佳实践
实现流式处理时,请遵循以下最佳实践:向标准输出打印时始终使用 flush=True,以防止缓冲。如果需要在流式处理期间准确统计令牌数量,请设置 stream_usage=True。在 SSE 流末尾发送 data: [DONE]\n\n 标记,让客户端知道何时关闭连接。使用 curl --no-buffer 测试流式端点,验证令牌是否逐步到达。
# Complete SSE endpoint with DONE sentinel
async def sse_generator(question: str):
try:
async for chunk in chain.astream({'question': question}):
# Escape any newlines in the chunk
safe_chunk = chunk.replace('\n', ' ')
yield f'data: {safe_chunk}\n\n'
finally:
yield 'data: [DONE]\n\n'
@app.get('/chat/stream')
async def chat_stream(question: str):
return StreamingResponse(
sse_generator(question),
media_type='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
)快速检查
测试您对 LangChain 中流式输出的理解。
课程回顾
在本课中,您学到了:stream() 和 astream() 让您能够在令牌生成时遍历令牌数据块,不必长时间等待完整响应;FastAPI 中采用 SSE 格式的 StreamingResponse 可以将令牌实时传送给浏览器客户端;astream_events() 为链中的每个步骤提供细粒度的事件钩子,包括工具调用和中间输出。接下来,我们将学习多轮对话的记忆管理。
常见问题解答
「在 LangChain 中流式输出」课时是免费的吗?
是的 — 「在 LangChain 中流式输出」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。
「在 LangChain 中流式输出」这节课中我会学到什么?
通过 LCEL 链实现令牌流式传输,使应用在每个词到达时立即显示,而不是等待完整响应,从而改善用户感知的延迟。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 AI Engineering Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「在 LangChain 中流式输出」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 AI Engineering Academy 课中编写并运行代码吗?
能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- LangChain 架构与核心抽象
- 使用 LCEL 构建链
- 分支链与并行链
- 在 LangChain 中流式输出