6 LangGraph的stream流式输出

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:这是外部操作。

相关推荐
qq_5470261796 小时前
LangChain 工具调用(Tool Calling)
java·大数据·langchain
AI大模型..7 小时前
数据洞察加速器:LLM Copilot 如何让 SQL 查询效率提升 50% 以上?
人工智能·langchain·llm·agent·llama
new Object ~1 天前
LangChain的短期记忆存储实现
python·langchain
liu****1 天前
LangChain-AI应用开发框架(六)
人工智能·python·langchain·大模型应用·本地部署大模型
java资料站1 天前
第07章:LangChain使用之Agents
langchain
小驴程序源1 天前
【OpenClaw 完整安装实施教程(Windows + Ollama 本地模型)】
gpt·langchain·aigc·embedding·ai编程·llama·gpu算力
Trouvaille ~1 天前
零基础入门 LangChain 与 LangGraph(三):环境搭建、包安装与第一个 LangChain 程序
python·ai·chatgpt·langchain·大模型·openai·langgraph
西西弗Sisyphus1 天前
大模型运行的 enforce_eager 参数
langchain·prompt·transformer·vllm·enforce_eager
花千树-0101 天前
Java 实现 ReAct Agent:工具调用与推理循环
java·spring boot·ai·chatgpt·langchain·aigc·ai编程