LangGraph 流式输出(Stream Events)深度解析:异步处理与实时响应的艺术
一、什么是流式输出?
在现代AI应用中,流式输出(Stream Events)指逐步生成和返回结果的技术。LangGraph通过astream_events方法实现了这一能力,特别适合:
- 长时间运行的工作流
- 需要实时反馈的场景
- 复杂多步骤任务的进度追踪
1.1. 流式在干什么?
Agent(LangGraph)里用 astream_events / astream,模型每吐出一小段 token,就会有一段事件。
服务端用 异步生成器:async for ...: yield chunk,不要一次性 return 整段文本。
FastAPI 用 StreamingResponse(生成器, media_type=...),底层是 HTTP 分块传输(chunked),客户端边收边显示,不必等整段结束。
二、核心方法:astream_events
python
from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.tools import tool
from langchain_community.callbacks import get_openai_callback
from langchain.chat_models import init_chat_model
from langchain.agents.middleware import HumanInTheLoopMiddleware
import os
from dotenv import load_dotenv
from langgraph.types import Command
import asyncio
from typing import Any, AsyncIterator, Optional
load_dotenv(override=True)
# 流式打印「放慢」:每个 token / 每条 values 之后睡眠的秒数(调大更慢,0 表示不睡)
STREAM_TOKEN_DELAY_SEC = 0.5
STREAM_VALUES_DELAY_SEC = 0.5
model = init_chat_model(model="qwen2-72b", #
model_provider='openai',
api_key= os.getenv("api_key"),
base_url= os.getenv("base_url"),
temperature=0.3,
max_retries=4,
#max_tokens=10
)
# 1. 系统提示词
system_prompt = """你是多功能助手,可以执行写文件、执行SQL、发送邮件、读取数据等操作。"""
@tool
def write_file(filename: str, content: str) -> str:
"""写入文件"""
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
print("write_file 函数正在执行,并且文件写入成功!")
return f"已写入文件: {filename}"
@tool
def execute_sql(query: str) -> str:
"""执行 SQL 查询"""
# 实际应用中连接数据库
return f"执行 SQL: {query}"
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""发送邮件"""
return f"已发送邮件到 {to}"
@tool
def read_data(source: str) -> str:
"""读取数据(安全操作)"""
return f"读取数据: {source}"
# 4. 添加记忆
checkpointer = InMemorySaver()
# 5. 创建 Agent
agent = create_agent(
model=model,
tools=[write_file, execute_sql,send_email,read_data],
system_prompt=system_prompt,
checkpointer=checkpointer,
# middleware=[
# HumanInTheLoopMiddleware(
# interrupt_on={
# "write_file": True, #写文件需要确认文件名和内容
# "execute_sql": True,# 执行SQL需要确认SQL语句
# "send_email": {
# "allowed_decisions": ["approve", "edit", "reject"],
# "description": "发送邮件需要确认收件人和内容"
# },
# "read_data": False,
# },
# description_prefix="敏感操作需要您的批准",
# ),
# ],
)
# 6. 运行对话
config = {"configurable": {"thread_id": "user-001"}}
# 初始调用
# result = agent.invoke(
# {"messages": [{"role": "user", "content": "在文件 test.txt 中写入内容:Hello, World!"}]},
# config=config
# )
def _run_stream_sync() -> None:
"""同步:stream + 普通 for。"""
events: list = []
for event in agent.stream(
{"messages": [{"role": "user", "content": "在文件 test.txt 中写入内容:Hello, World!"}]},
config=config,
stream_mode="values",
):
events.append(event)
print(events)
def _handle_astream_event_v2(event: dict[str, Any]) -> bool:
"""
astream_events(version='v2') 每条一般是 dict,常见字段:
- event: 事件类型(如 on_chat_model_stream / on_tool_start / on_chain_end)
- name: runnable / 节点名
- data: 负载(流式 token、工具入参、chunk 等)
不同版本字段名可能略有差异,用 .get 兜底。
返回值:本次是否打印了模型流式 token(为 True 时调用方可 await asyncio.sleep 放慢)。
"""
print(f"{event}")
et = event.get("event") or event.get("type") or ""
name = event.get("name", "")
data = event.get("data") or {}
# 1) 模型流式 token(逐字输出给前端时可拼字符串)
if "chat_model_stream" in et or et.endswith("stream"):
chunk = data.get("chunk")
if chunk is not None:
text = getattr(chunk, "content", None)
if text is None and isinstance(chunk, dict):
text = chunk.get("content")
if text:
print(text, end="", flush=True)
return True
return False
# 2) 工具开始 / 结束
if "tool" in et:
print(f"\n[{et}] {name} data={data!r}")
# 3) 链结束、最终消息等(按需打印)
if et in ("on_chain_end", "on_parser_end") or "end" in et:
out = data.get("output")
if out is not None:
print(f"\n[结束片段] {name}: {type(out).__name__}")
return False
async def _run_stream_async() -> None:
"""
异步流式三要点:
1) async for agent.astream_events(...) 在 async def 里
2) 在循环内按 event 类型处理(见 _handle_astream_event_v2)
3) 入口 asyncio.run(_run_stream_async());FastAPI 里用 async def 路由直接 await 本协程或下面 async 生成器
注意:同一次用户请求只应 async for 一种流(events 或 values),不要紧接着再跑一遍,否则会重复调用模型。
"""
user_input = {"messages": [{"role": "user", "content": "在文件 test.txt 中写入内容:Hello, World!"}]}
print("--- astream_events v2(按事件类型处理;每个 token 后可 sleep 放慢)---")
async for raw in agent.astream_events(user_input, config=config, version="v2"):
if isinstance(raw, dict):
if _handle_astream_event_v2(raw) and STREAM_TOKEN_DELAY_SEC > 0:
await asyncio.sleep(STREAM_TOKEN_DELAY_SEC)
else:
print(raw)
async def _run_stream_async_values_only() -> None:
"""只看整图 state 快照(messages 等),事件比 astream_events 粗。"""
user_input = {"messages": [{"role": "user", "content": "你好"}]}
async for snapshot in agent.astream(user_input, config=config, stream_mode="values"):
if not isinstance(snapshot, dict):
print(snapshot)
continue
msgs = snapshot.get("messages") or []
if msgs:
last = msgs[-1]
content = getattr(last, "content", None) or ""
print(f"[values] 最后一条 {type(last).__name__}: {str(content)[:300]}")
if STREAM_VALUES_DELAY_SEC > 0:
await asyncio.sleep(STREAM_VALUES_DELAY_SEC)
async def iter_agent_sse_chunks(
user_input: dict[str, Any],
*,
runnable_config: Optional[dict[str, Any]] = None,
token_delay_sec: float = 0.0,
) -> AsyncIterator[str]:
"""
给 FastAPI StreamingResponse 用:把模型流式 token yield 成字符串。
例:return StreamingResponse(iter_agent_sse_chunks(...), media_type="text/plain")
runnable_config:每次请求传入 {"configurable": {"thread_id": "..."}},勿用模块级单线程 config。
token_delay_sec:每个片段 yield 后暂停的秒数,便于本地演示「打字机」节奏(生产可传 0)。
"""
cfg = runnable_config if runnable_config is not None else config
async for raw in agent.astream_events(user_input, config=cfg, version="v2"):
if not isinstance(raw, dict):
continue
et = raw.get("event") or ""
data = raw.get("data") or {}
if "chat_model_stream" not in et and not et.endswith("stream"):
continue
chunk = data.get("chunk")
if chunk is None:
continue
text = getattr(chunk, "content", None)
if text is None and isinstance(chunk, dict):
text = chunk.get("content")
if text:
yield text if isinstance(text, str) else str(text)
if token_delay_sec > 0:
await asyncio.sleep(token_delay_sec)
async def _run_ainvoke_async() -> None:
"""单次异步调用,无流式。"""
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "在文件 test.txt 中写入内容:Hello, World!"}]},
config=config,
)
print(result['messages'][-1].content)
if __name__ == "__main__":
# 同步:_run_stream_sync()
# 异步:必须 asyncio.run(...) 启动事件循环
asyncio.run(_run_stream_async())
#asyncio.run(_run_ainvoke_async())
下面是打印的部分事件:可以看到不同的事件对应不同的操作;
| 字段 | 描述 |
|---|---|
event |
事件名称,格式为 `on_[runnable_type]_(start |
name |
生成事件的 Runnable 名称 |
run_id |
当前执行实例的唯一 ID |
parent_ids |
父级 Runnable 的 ID 列表(从根到直接父级) |
data |
与事件关联的具体数据(内容取决于事件类型) |
tags/metadata |
关联的标签和元数据 |
json
{'event': 'on_chain_start',
'data': {'input': {'messages': [{'role': 'user', 'content': '在文件 test.txt 中写入内容:Hello, World!'}]}},
'name': 'LangGraph',
'tags': [],
'run_id': '019d529c-9287-7a43-a2dd-47902a384aad', 'metadata': {'thread_id': 'user-001'},
'parent_ids': []}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='我', additional_kwargs={}, response_metadata={'model_provider': 'openai'}, id='lc_run--019d52a1-2631-7de3-93af-4f0eacfb588a', tool_calls=[], invalid_tool_calls=[], tool_call_chunks=[])}, 'run_id': '019d52a1-2631-7de3-93af-4f0eacfb588a', 'name': 'ChatOpenAI', 'tags': ['seq:step:1'], 'metadata': {'thread_id': 'user-001', 'langgraph_step': 1, 'langgraph_node': 'model', 'langgraph_triggers': ('branch:to:model',), 'langgraph_path': ('__pregel_pull', 'model'), 'langgraph_checkpoint_ns': 'model:e802fae0-b546-0423-ee13-0d359ba3ee83', 'checkpoint_ns': 'model:e802fae0-b546-0423-ee13-0d359ba3ee83', 'ls_provider': 'openai', 'ls_model_name': 'qwen2-72b', 'ls_model_type': 'chat', 'ls_temperature': 0.3}, 'parent_ids': ['019d52a1-2624-7580-a84a-673165776bba', '019d52a1-2628-7e00-8050-1ea59acdc05f']}
{'event': 'on_chat_model_end', 'data': {'output': AIMessage(content='我将为您在文件 test.txt 中写入内容 "Hello, World!"。这是个简单的文件操作,用于创建或更新一个文本文件。\n', additional_kwargs={}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'qwen-flash', 'model_provider': 'openai'}, id='lc_run--019d52a1-2631-7de3-93af-4f0eacfb588a', tool_calls=[{'name': 'write_file', 'args': {'filename': 'test.txt', 'content': 'Hello, World!'}, 'id': 'call_37bcb4d67a71483aa1a91e', 'type': 'tool_call'}], invalid_tool_calls=[]), 'input': {'messages': [[SystemMessage(content='你是多功能助手,可以执行写文件、执行SQL、发送邮件、读取数据等操作。THINK:告诉用户你为什么要这么做', additional_kwargs={}, response_metadata={}), HumanMessage(content='在文件 test.txt 中写入内容:Hello, World!', additional_kwargs={}, response_metadata={}, id='94b27274-d43c-4081-a6d2-5ee0a4cdb778')]]}}, 'run_id': '019d52a1-2631-7de3-93af-4f0eacfb588a', 'name': 'ChatOpenAI', 'tags': ['seq:step:1'], 'metadata': {'thread_id': 'user-001', 'langgraph_step': 1, 'langgraph_node': 'model', 'langgraph_triggers': ('branch:to:model',), 'langgraph_path': ('__pregel_pull', 'model'), 'langgraph_checkpoint_ns': 'model:e802fae0-b546-0423-ee13-0d359ba3ee83', 'checkpoint_ns': 'model:e802fae0-b546-0423-ee13-0d359ba3ee83', 'ls_provider': 'openai', 'ls_model_name': 'qwen2-72b', 'ls_model_type': 'chat', 'ls_temperature': 0.3}, 'parent_ids': ['019d52a1-2624-7580-a84a-673165776bba', '019d52a1-2628-7e00-8050-1ea59acdc05f']}
{'event': 'on_tool_start', 'data': {'input': {'filename': 'test.txt', 'content': 'Hello, World!'}}, 'name': 'write_file', 'tags': ['seq:step:1'], 'run_id': '019d52a1-2a90-7a82-929c-8a7a1b04f657', 'metadata': {'thread_id': 'user-001', 'langgraph_step': 2, 'langgraph_node': 'tools', 'langgraph_triggers': ('__pregel_push',), 'langgraph_path': ('__pregel_push', 0, False), 'langgraph_checkpoint_ns': 'tools:2b7b9b2b-4fa4-5ece-1345-62b120d20e76', 'checkpoint_ns': 'tools:2b7b9b2b-4fa4-5ece-1345-62b120d20e76'}, 'parent_ids': ['019d52a1-2624-7580-a84a-673165776bba', '019d52a1-2a8f-74f1-bb53-1ba00cf2f3e3']}
[on_tool_end] write_file data={'output': ToolMessage(content='已写入文件: test.txt', name='write_file', id='537e937e-da05-4b48-9ecb-75c68e60d558', tool_call_id='call_37bcb4d67a71483aa1a91e'), 'input': {'filename': 'test.txt', 'content': 'Hello, World!'}}
事件类型 (event) |
组件名称 (name) |
数据块 (chunk) (流式传输中) |
输入数据 (input) (开始时) |
输出数据 (output) (结束时) |
|---|---|---|---|---|
on_chat_model_start 聊天模型启动 |
'[模型名称]' |
(无) | {"messages": [[系统消息, 用户消息]]} (包含完整的消息历史) |
(无) |
on_chat_model_stream 聊天模型流式传输 |
'[模型名称]' |
AIMessageChunk(content="你好") (当前的文本片段) |
(无) | (无) |
on_chat_model_end 聊天模型结束 |
'[模型名称]' |
(无) | {"messages": [[系统消息, 用户消息]]} |
AIMessageChunk(content="你好世界") (最终生成的完整回复) |
on_llm_start 大语言模型启动 |
'[模型名称]' |
(无) | {'input': '你好'} (原始文本输入) |
(无) |
on_llm_stream 大语言模型流式传输 |
'[模型名称]' |
'你好' (当前的文本片段) |
(无) | (无) |
on_llm_end 大语言模型结束 |
'[模型名称]' |
(无) | '你好人类!' |
(无) |
on_chain_start 链启动 |
'format_docs' (文档格式化链) |
(无) | (无) | (无) |
on_chain_stream 链流式传输 |
'format_docs' |
'你好世界!, 再见世界!' (处理后的文本片段) |
(无) | (无) |
on_chain_end 链结束 |
'format_docs' |
(无) | [Document(...)] (输入的文档列表) |
'你好世界!, 再见世界!' (拼接后的最终字符串) |
on_tool_start 工具启动 |
'some_tool' |
(无) | {"x": 1, "y": "2"} (工具调用的参数) |
(无) |
on_tool_end 工具结束 |
'some_tool' |
(无) | (无) | {"x": 1, "y": "2"} (工具执行的结果) |
on_retriever_start 检索器启动 |
'[检索器名称]' |
(无) | {"query": "你好"} (搜索关键词) |
(无) |
on_retriever_end 检索器结束 |
'[检索器名称]' |
(无) | {"query": "你好"} |
[Document(...), ..] (检索到的文档列表) |
on_prompt_start 提示词模板启动 |
'[模板名称]' |
(无) | {"question": "你好"} (填入模板的变量) |
(无) |
on_prompt_end 提示词模板结束 |
'[模板名称]' |
(无) | {"question": "你好"} |
ChatPromptValue(messages: [系统消息, ...]) (填充后的完整提示词对象) |
| 事件名称 | 角色 (谁在干活) | 阶段 (什么时候触发) | 核心含义 | 餐厅类比 |
|---|---|---|---|---|
on_chain_start |
工作流/链 (Chain) | 开始 | 整个流程或某个步骤刚开始,准备接收输入。 | 厨师长接到单子,喊了一声"开始做这道菜"。 |
on_chat_model_start |
大脑 (AI 模型) | 开始 | AI 模型刚开始思考或生成,还没吐出字。 | 厨师拿起锅铲,准备开始炒菜(但还没炒)。 |
on_chain_stream |
工作流/链 (Chain) | 进行中 | 链内部产生了中间结果并流式输出(较少见,通常用于复杂链)。 | 厨师长每隔一会把切好的配菜递给下一个人。 |
on_tool_start |
手脚 (工具) | 开始 | 准备调用外部功能(搜索、计算器、API)。 | 厨师准备去冰箱拿食材,或者去洗碗(干杂活)。 |
假设你问:"帮我查一下北京现在的天气。"
on_chain_start
含义:你的主程序(链)启动了。
内心独白:"收到任务,开始干活。"
on_chat_model_start
含义:主程序把问题扔给 AI 大脑。
内心独白:"AI 开始思考了。"
on_chat_model_stream (省略,这是吐字过程)
内心独白:"我想... 我... 需要... 查... 天气..."
on_chat_model_end
含义:AI 思考完毕,决定调用工具。
内心独白:"我想完了,我要调用天气工具。"
on_tool_start
含义:程序准备调用天气 API。
内心独白:"正在打开天气插件,参数是'北京'。"
on_tool_end
含义:天气 API 返回了结果(25度)。
内心独白:"工具用完了,拿到了数据。"
on_chain_stream (可选)
含义:如果你的链设计是流式地把工具结果传回给用户,这里会触发。
内心独白:"正在把中间结果递出去。"
on_chain_end
含义:整个流程结束。
内心独白:"任务全部搞定。"
总结
看到 _start:关注 data.input,看它接收了什么。
看到 _stream:关注 data.chunk,看它吐出了什么(实时内容)。
看到 Chain:这是流程/容器。
看到 Model:这是AI 思考。
看到 Tool:这是外部操作。