实用指南:LangGraph的stream流式输出与自定义事件
一、背景
LangGraph可以做AI智能体的流程编排,也就是我们通俗说的工作流. 和Dify这些可视化平台本质是一样的,只是Dify将这个编码过程使用图形化的方式给你进行展示了而已。那我们运行工作流的时候,其实不同的应用场景,我们需要观察输出不同节点流程的一些内容,这样便于我们去了解当前工作流的执行情况,还有输出情况。
二、stream流式输出
在 LangGraph 中,stream_mode
是控制工作流输出方式的核心参数,它决定了工作流执行过程中结果的返回形式和时机。根据 LangGraph 的官方文档和实际应用场景,stream_mode
主要有 4 种常用参数配置,分别适用于不同的输出需求。下面详细解析每种参数的作用、使用场景和示例:
1. stream_mode="updates"
(状态值增量流)
- 核心特点:仅输出工作流状态中新增或修改的字段,不包含节点元信息(如节点 ID)。
- 输出形式:每次迭代返回一个字典,仅包含当前步骤中变化的状态字段(例如从检索节点到整理节点时,只返回
organized_summary
字段)。 - 适用场景:前端只需关注 “数据变化”,无需关心具体由哪个节点产生(如轻量渲染、进度条更新)。
2. stream_mode="messages"
(消息流模式)
- 核心特点:以 “消息对象” 格式输出,每个消息包含
type
(类型)、name
(节点名)、content
(内容)等元信息,结构化更强。 主要是输出底层和大模型交换的返回流数据. - 输出形式:迭代器返回标准化消息结构
- 适用场景:多智能体交互、需要明确区分消息来源的场景(如展示 “[检索模块] 已完成”)。
3. stream_mode="values"
(全量状态流)
- 核心特点:每次输出截至当前步骤的完整状态,包含所有已执行节点的累积结果。只输出被更新的state的字段的消息,其它没更新的字段的内容na'bu'dao
- 输出形式:迭代器返回完整状态字典,例如执行到整理节点时,会包含
question
、retrieved_docs
、organized_summary
等所有字段。 - 适用场景:需要实时获取全量状态的场景(如实时仪表盘、基于完整状态的复杂逻辑判断)。
4. stream_mode="custom"
(自定义流模式)
- 核心特点:允许通过
stream_handler
参数自定义输出格式,完全掌控流的内容和结构。 - 使用方式:需配合
stream_handler
函数,对每个节点的输出进行加工后再返回 - 适用场景:需要特殊格式输出的场景(如多模态数据封装、特定协议适配)。
补充说明:默认模式(非流式)
当不设置stream_mode
或设为False
时,属于 “非流式模式”,工作流会在全部执行完成后返回最终状态,本质上是stream_mode
的 “隐性选项”,但通常不归类到上述 4 种流式模式中。
总结:4 种流式模式的核心差异
模式值 | 输出内容特征 | 核心用途 | 灵活性 |
---|---|---|---|
"values" | 全量累积状态 | 全量状态监控、复杂逻辑联动 | 低 |
"messages" | 标准化消息结构(含节点元信息) | 多模块交互、明确消息来源,大模型交换的消息 | 中 |
"updates" | 仅变化的状态字段 | 轻量前端渲染、数据增量更新 | 中 |
"custom" | 完全自定义格式 | 特殊场景适配、个性化输出需求 | 高 |
选择时可根据 “是否需要节点元信息”“是否需要全量状态”“是否需要自定义格式” 三个维度判断,例如多智能体对话场景常用"messages"
,而前端轻量展示常用"updates"
。
1、values 只要有1个节点执行完毕,则返回当前state的全量值
2、updates 哪个节点更新了state的哪个字段, 只返回这个字段更新的信息,其它未更新的字段不会返回
3、messages 底层哪个节点调用了大模型,大模型的流式输出内容会返回
4、custom 用户自定义进行流式输出,可以捕获,并且做自定义输出
三、astream_events事件
除了调用astream的方式获取流,也可以通过监听事件的方式获取工作流的信息.
1、源码注释文档
"""Generate a stream of events.
Use to create an iterator over StreamEvents that provide real-time information
about the progress of the Runnable, including StreamEvents from intermediate
results.
A StreamEvent is a dictionary with the following schema:
- ``event``: **str** - Event names are of the format:
on_[runnable_type]_(start|stream|end).
- ``name``: **str** - The name of the Runnable that generated the event.
- ``run_id``: **str** - randomly generated ID associated with the given
execution of the Runnable that emitted the event. A child Runnable that gets
invoked as part of the execution of a parent Runnable is assigned its own
unique ID.
- ``parent_ids``: **list[str]** - The IDs of the parent runnables that generated
the event. The root Runnable will have an empty list. The order of the parent
IDs is from the root to the immediate parent. Only available for v2 version of
the API. The v1 version of the API will return an empty list.
- ``tags``: **Optional[list[str]]** - The tags of the Runnable that generated
the event.
- ``metadata``: **Optional[dict[str, Any]]** - The metadata of the Runnable that
generated the event.
- ``data``: **dict[str, Any]**
Below is a table that illustrates some events that might be emitted by various
chains. Metadata fields have been omitted from the table for brevity.
Chain definitions have been included after the table.
.. NOTE:: This reference table is for the V2 version of the schema.
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| event | name | chunk | input | output |
+======================+==================+=================================+===============================================+=================================================+
| on_chat_model_start | [model name] | | {"messages": [[SystemMessage, HumanMessage]]} | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_chat_model_stream | [model name] | AIMessageChunk(content="hello") | | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_chat_model_end | [model name] | | {"messages": [[SystemMessage, HumanMessage]]} | AIMessageChunk(content="hello world") |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_llm_start | [model name] | | {'input': 'hello'} | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_llm_stream | [model name] | 'Hello' | | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_llm_end | [model name] | | 'Hello human!' | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_chain_start | format_docs | | | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_chain_stream | format_docs | "hello world!, goodbye world!" | | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_chain_end | format_docs | | [Document(...)] | "hello world!, goodbye world!" |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_tool_start | some_tool | | {"x": 1, "y": "2"} | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_tool_end | some_tool | | | {"x": 1, "y": "2"} |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_retriever_start | [retriever name] | | {"query": "hello"} | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_retriever_end | [retriever name] | | {"query": "hello"} | [Document(...), ..] |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_prompt_start | [template_name] | | {"question": "hello"} | |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
| on_prompt_end | [template_name] | | {"question": "hello"} | ChatPromptValue(messages: [SystemMessage, ...]) |
+----------------------+------------------+---------------------------------+-----------------------------------------------+-------------------------------------------------+
In addition to the standard events, users can also dispatch custom events (see example below).
Custom events will be only be surfaced with in the `v2` version of the API!
A custom event has following format:
+-----------+------+-----------------------------------------------------------------------------------------------------------+
| Attribute | Type | Description |
+===========+======+===========================================================================================================+
| name | str | A user defined name for the event. |
+-----------+------+-----------------------------------------------------------------------------------------------------------+
| data | Any | The data associated with the event. This can be anything, though we suggest making it JSON serializable. |
+-----------+------+-----------------------------------------------------------------------------------------------------------+
Here are declarations associated with the standard events shown above:
`format_docs`:
.. code-block:: python
def format_docs(docs: list[Document]) -> str:
'''Format the docs.'''
return ", ".join([doc.page_content for doc in docs])
format_docs = RunnableLambda(format_docs)
`some_tool`:
.. code-block:: python
@tool
def some_tool(x: int, y: str) -> dict:
'''Some_tool.'''
return {"x": x, "y": y}
`prompt`:
.. code-block:: python
template = ChatPromptTemplate.from_messages(
[("system", "You are Cat Agent 007"), ("human", "{question}")]
).with_config({"run_name": "my_template", "tags": ["my_template"]})
Example:
.. code-block:: python
from langchain_core.runnables import RunnableLambda
async def reverse(s: str) -> str:
return s[::-1]
chain = RunnableLambda(func=reverse)
events = [
event async for event in chain.astream_events("hello", version="v2")
]
# will produce the following events (run_id, and parent_ids
# has been omitted for brevity):
[
{
"data": {"input": "hello"},
"event": "on_chain_start",
"metadata": {},
"name": "reverse",
"tags": [],
},
{
"data": {"chunk": "olleh"},
"event": "on_chain_stream",
"metadata": {},
"name": "reverse",
"tags": [],
},
{
"data": {"output": "olleh"},
"event": "on_chain_end",
"metadata": {},
"name": "reverse",
"tags": [],
},
]
Example: Dispatch Custom Event
.. code-block:: python
from langchain_core.callbacks.manager import (
adispatch_custom_event,
)
from langchain_core.runnables import RunnableLambda, RunnableConfig
import asyncio
async def slow_thing(some_input: str, config: RunnableConfig) -> str:
\"\"\"Do something that takes a long time.\"\"\"
await asyncio.sleep(1) # Placeholder for some slow operation
await adispatch_custom_event(
"progress_event",
{"message": "Finished step 1 of 3"},
config=config # Must be included for python < 3.10
)
await asyncio.sleep(1) # Placeholder for some slow operation
await adispatch_custom_event(
"progress_event",
{"message": "Finished step 2 of 3"},
config=config # Must be included for python < 3.10
)
await asyncio.sleep(1) # Placeholder for some slow operation
return "Done"
slow_thing = RunnableLambda(slow_thing)
async for event in slow_thing.astream_events("some_input", version="v2"):
print(event)
Args:
input: The input to the Runnable.
config: The config to use for the Runnable.
version: The version of the schema to use either `v2` or `v1`.
Users should use `v2`.
`v1` is for backwards compatibility and will be deprecated
in 0.4.0.
No default will be assigned until the API is stabilized.
custom events will only be surfaced in `v2`.
include_names: Only include events from runnables with matching names.
include_types: Only include events from runnables with matching types.
include_tags: Only include events from runnables with matching tags.
exclude_names: Exclude events from runnables with matching names.
exclude_types: Exclude events from runnables with matching types.
exclude_tags: Exclude events from runnables with matching tags.
kwargs: Additional keyword arguments to pass to the Runnable.
These will be passed to astream_log as this implementation
of astream_events is built on top of astream_log.
Yields:
An async stream of StreamEvents.
Raises:
NotImplementedError: If the version is not `v1` or `v2`.
""" # noqa: E501
2、自定义事件(adispatch_custom_event)
除了官方定义的事件,我们还可以自定义事件,自定义事件例如输出流等等,工作流外部就可以进行监听.
官方文档你都找不到,这块是我研究了源代码,通过源代码注释的相关内容才了解到的内容.
代码如下:
import asyncio
from typing import TypedDict
from langchain_core.callbacks import adispatch_custom_event
from langchain_core.runnables import RunnableConfig
from langgraph.constants import END, START
from langgraph.graph import StateGraph
class State(TypedDict):
message: str
async def node1(state: State, config:RunnableConfig) -> State:
await adispatch_custom_event(
"progress_event", #event名称, 自定义
{"message": "第一个步骤Node1"}, # data数据, 字典
config=config
)
return state
async def node2(state: State, config:RunnableConfig):
await adispatch_custom_event(
"progress_event", #event名称, 自定义
{"message": "第二个步骤Node1"}, # data数据, 字典
config=config
)
return state
async def main():
graph = StateGraph(State)
graph.add_node("node1", node1)
graph.add_node("node2", node2)
graph.add_edge(START, "node1")
graph.add_edge("node1", "node2")
graph.add_edge("node2", END)
workflow = graph.compile()
state = State(message="")
events = workflow.astream_events(state)
async for event in events:
event_name = event['event']
if event_name == "on_custom_event":
print(event['data'])
asyncio.run(main())