04|Langgraph | 从入门到实战 | 进阶篇 | 流式传输

一、简介

本系列为Langgraph文章,最终以实现企业级项目。

💟 我们已经上线了一版本初稿,欢迎各位体验 https://ai.sofreight.com/ 💟

💟 本文教学带上了该初版Agent的部分业务教学,欢迎阅览 💟

该系列文章,以官方文档路径撰写,深入浅出并配以自己理解,配以GIF动图演示、适当扩展延伸官方案例以及源码讲解

当然如果你需要你也可以查看官方文档

最终实战项目目标:构建一个Agents Framework(智能代理框架) 多智能体协作企业系统
本文如若有错误地方,烦请指正,另外方便的话,麻烦点个赞关注一下,谢谢

  • 📊 Agent-Graph:每个业务 Agent 以状态图形式编排节点、条件边与循环边;
  • 🔧 工具体系:自动发现与注册工具,支持函数调用(tool calling),可扩展MCP服务;
  • 🗄️ 记忆/持久化:使用 Postgres 作为 LangGraph 的 checkpointer 与 store,Redis缓存prompt;
  • 📋 统一注册中心:自动发现、注册并预编译 Agent 图与工具;
  • 💪 滚动窗口摘要算法压缩上下文,用户画像,短期记忆,长期记忆混合
  • 🌐 API 网关:FastAPI 路由聚合,提供通用 chat、agents、session 等接口;
  • 🔄 可插拔 LLM:通过模型工厂与配置驱动,统一管理多厂商 LLM。
  • 🌀 prompt缓存:redis加载prompt,prompt-web热更新管理
  • 👁️‍🗨️ RAG 向量数据库,与工程结合,结构化,非结构化管理检索,召回
  • 🥰 下一步引导功能,猜你想要功能


❤本系列文章,配套项目源码地址❤
https://github.com/wenwenc9/langgraph-tutorial-wenwenc9

Langgraph系列文章
01|Langgraph | 从入门到实战 | 基础篇
02|Langgraph | 从入门到实战 | workflow与Agent
03|Langgraph | 从入门到实战 | 进阶篇 | 持久化

langchain的系列文章(相信我把Langchain全部学一遍,你能深入理解AI的开发)
01|LangChain | 从入门到实战-介绍
02|LangChain | 从入门到实战 -六大组件之Models IO
03|LangChain | 从入门到实战 -六大组件之Retrival
04|LangChain | 从入门到实战 -六大组件之Chain
05|LangChain | 从入门到实战 -六大组件之Memory
06|LangChain | 从入门到实战 -六大组件之Agent

二、Langgraph 流式传输意义

本章节对应的项目地址吗,希望能给我一个小星星,欢迎各位交流:

https://github.com/wenwenc9/langgraph-tutorial-wenwenc9/tree/main/Langgraph_Learning/2-进阶

流式传输(Streaming)在 LangGraph 中具有重要的实际意义,主要体现在以下几个方面

维度 核心价值
🎯 用户体验 即时反馈,降低感知延迟
性能优化 降低内存占用,提升并发能力
🔍 可观测性 实时监控,快速调试
💰 成本控制 节省 API 调用,优化资源使用
🚀 生产就绪 支持大规模、长时运行的应用

用一个简单的代码来体验流式传输:

  • 使用 stream 方法调用图
  • 创建了两个节点refine_topicgenerate_joke ,串行执行
python 复制代码
from typing import TypedDict
from langgraph.graph import StateGraph, START, END


class State(TypedDict):
  topic: str
  joke: str


def refine_topic(state: State):
    return {"topic": state["topic"] + " and cats","name":"wenwenc9"}


def generate_joke(state: State):
    return {"joke": f"This is a joke about {state['topic']}"}

graph = (
  StateGraph(State)
  .add_node(refine_topic)
  .add_node(generate_joke)
  .add_edge(START, "refine_topic")
  .add_edge("refine_topic", "generate_joke")
  .add_edge("generate_joke", END)
  .compile()
)

输出内容如下:

python 复制代码
{'refine_topic': {'topic': 'ice cream and cats'}}
{'generate_joke': {'joke': 'This is a joke about ice cream and cats'}}

三、流式中的不同模式

我们在前面使用了stream方法,传入了stream_mode="updates" ,我们还有别的模式方法

将以下一个或多个流模式作为列表传递给 streamastream 方法

模式 描述
values 在图的每一步之后流式传输状态的全部值。
updates 在图的每一步之后流式传输状态的更新。如果在同一步骤中进行了多个更新(例如,运行了多个节点),则这些更新将分别流式传输。
custom 从你的图节点内部流式传输自定义数据。
messages 从任何调用 LLM 的图节点流式传输 2 元组(LLM 令牌、元数据)。
debug 在整个图执行过程中尽可能流式传输信息。

LangGraph 的 stream_mode 参数让你选择性地获取图执行过程中的不同类型数据,支持单模式和多模式组合

下面我将逐一的讲解,依然使用上面那个基础的图graph 图代码

1、updates模式

  • 流式传输每一步节点返回的状态更新。
  • 流式传输的输出包括节点的名称以及更新内容。
  • 具体是每个节点为对象
    其实就是打印每个节点,对于操作base_state对应键
python 复制代码
for chunk in graph.stream(
    {"topic": "ice cream"},
    stream_mode="updates",
):
    for node, update in chunk.items():
        print(f"节点 {node} 更新: {update}")

输出

python 复制代码
节点 refine_topic 更新: {'topic': 'ice cream and cats'}
节点 generate_joke 更新: {'joke': 'This is a joke about ice cream and cats'}

我们前面创建了 两个节点

  • refine_topic 更新状态机的 joke 跟一个 name
  • generate_joke 更新状态机的 joke

所以看到了具体每个节点操作状态机字段的更新内容,其中name并非在状态机是无法看到的

2、values模式

可以在每一步之后流式传输图的全状态,每个节点涉及到 base_state 更新的都会打印出来
其实也就是,整个base_state的变化监控

python 复制代码
for chunk in graph.stream(
    {"topic": "ice cream"},
    stream_mode="values",
):
    print(chunk)

输出

python 复制代码
{'topic': 'ice cream'}
{'topic': 'ice cream and cats'}
{'topic': 'ice cream and cats', 'joke': 'This is a joke about ice cream and cats'}

从输入到,2个节点处理后的内容都看到

3、custom模式

前面咱了解了updatesvalues,这些都是对base_state状态机的操作打印

在真实的业务中,我们时常会让AI调用XX工具,然后用户等待过程不会那么枯燥

在Langgraph中,有custom自定义的模式,我们配合get_stream_writer 方式可以实现

python 复制代码
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.config import get_stream_writer
import time

class State(TypedDict):
    messages: list[str]

def node1(state: State):
    writer = get_stream_writer()
    writer("节点1: 开始处理...船期业务")      # 自定义流数据
    time.sleep(3) # 模拟业务耗时操作
    writer("节点1: 船期业务处理完成!")
    return {"messages": state["messages"] + ["节点1已处理"]}

def node2(state: State):
    writer = get_stream_writer()
    writer("节点2:生成回复中...")     # 自定义流数据
    return {"messages": state["messages"] + ["节点2已处理"]}

# 构建最小图
graph = (
    StateGraph(State)
    .add_node("node1", node1)
    .add_node("node2", node2)
    .add_edge(START, "node1")
    .add_edge("node1", "node2")
    .add_edge("node2", END)
    .compile()
)
graph

创建了2个节点,并且模拟业务的部分,并且模拟业务耗时3s

我们单独使用custom方式

python 复制代码
inputs = {"messages": ["你好世界"]}

# 同时流式传输更新和自定义数据
for chunk in graph.stream(inputs, stream_mode="custom"):
    # print(f"模式: {mode}")
    print(f"数据: {chunk}")
    print("---")

输出内容如下

python 复制代码
数据: 节点1: 开始处理...船期业务
---
数据: 节点1: 船期业务处理完成!
---
数据: 节点2:生成回复中...
---

现在我们将指定模式为多模式方式 stream_mode=["updates","values","custom"]

python 复制代码
inputs = {"messages": ["你好世界"]}

# 同时流式传输更新和自定义数据
for chunk in graph.stream(inputs, stream_mode=["updates","values","custom"]):
    # print(f"模式: {mode}")
    print(f"数据: {chunk}")
    print("---")

输出内容如下

python 复制代码
数据: ('custom', '节点1: 开始处理...船期业务')
---
数据: ('custom', '节点1: 船期业务处理完成!')
---
数据: ('updates', {'node1': {'messages': ['你好世界', '节点1已处理']}})
---
数据: ('values', {'messages': ['你好世界', '节点1已处理']})
---
数据: ('custom', '节点2:生成回复中...')
---
数据: ('updates', {'node2': {'messages': ['你好世界', '节点1已处理', '节点2已处理']}})
---
数据: ('values', {'messages': ['你好世界', '节点1已处理', '节点2已处理']})
---

可以看到,多模式的列表传输下,响应的chunk为元组,并且能看到更多功能

4、messages模式

聊天界面常用、逐字显示AI回复

使用 messages 流模式,从您的任何部分(包括节点、工具、子图或任务)流式传输大型语言模型(LLM)输出字节串 。

这个功能要求为langchain所定义的 messages 标准消息载体对象,自己点开代码看看
langchain_core.messages

下面案例创建2个节点,来模拟业务

python 复制代码
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage
import time

class State(TypedDict):
    messages: Annotated[list, add_messages]

def node1(state: State):
    """节点1:模拟流式生成分析结果"""
    text = "正在分析船期查询...上海到洛杉矶航线分析完成!"

    # 关键:返回 AIMessage,LangGraph 会自动处理流式
    return {"messages": [AIMessage(content=text)]}

def node2(state: State):
    """节点2:模拟流式生成船期结果"""
    text = """
船期查询结果:
1. 马士基航运 - 1月20日开船
2. 中远海运 - 1月22日开船
3. 长荣海运 - 1月25日开船
查询完成!"""

    return {"messages": [AIMessage(content=text)]}

# 构建图
graph = (
    StateGraph(State)
    .add_node("analyze", node1)
    .add_node("query", node2)
    .add_edge(START, "analyze")
    .add_edge("analyze", "query")
    .add_edge("query", END)
    .compile()
)
inputs = {"messages": [HumanMessage(content="查询上海到洛杉矶船期")]}
current_node = None
for message, metadata in graph.stream(inputs, stream_mode="messages"):
    node_name = metadata.get("langgraph_node", "")
    # 节点切换时显示标题
    if node_name != current_node:
        if current_node:
            print("\n")
        print(f"[{node_name}] ", end="", flush=True)
        current_node = node_name
    # 逐字符模拟流式输出
    if hasattr(message, 'content') and message.content:
        for char in message.content:
            print(char, end="", flush=True)
            time.sleep(0.02)  # 控制输出速度

运行代码,可以看到一个一个字的蹦出来

请注意,base_state定义的状态机代码,你在上个案例运行messages是不会出结果的,因为不是messages 对象

5、debug模式

顾名思义,开发者模式,用于调试使用使用

python 复制代码
import json
for chunk in graph.stream(inputs, stream_mode="debug"):
    print("★",chunk)
    print("\n")

它能得到,图的每一步执行情况

四、综合案例

下面我将用综合案例,来结合多个模式,并且调用模型进行实现

背景:

  • 用户输入一个货物订单号,我们创建一个search_tool节点模拟检索数据库订单运输进度

  • 然后然调用generate_reply_llm 对该订单查询后的结果,美化回复给用户

在这个章节中,演示很早我踩过的坑,真假流式

1、假流式

python 复制代码
from typing import Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.config import get_stream_writer
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from dotenv import load_dotenv, find_dotenv
import os
import time
from typing import TypedDict
load_dotenv(find_dotenv())
modelName = os.getenv('OPENAI_MODEL')

class State(TypedDict):
    messages: Annotated[list, add_messages]
    progress: int

def search_tool(state: State):
    writer = get_stream_writer()
    writer("🔍 正在查询订单数据库...")
    writer("📦 找到用户订单 #12345")
    writer("✅ 查询完成")
    return {"progress": 50}

def generate_reply_llm(state: State):
    writer = get_stream_writer()
    writer("🤖 豆包 AI 正在生成个性化回复...")

    llm = ChatOpenAI(model=modelName)

    prompt = f"""
    用户查询订单状态,已查询到订单#12345
    当前进度: {state['progress']}%

    请生成简短友好的客服回复,中文输出。
    """
    # 返回 AIMessage 对象,而不是字符串
    response = llm.invoke(prompt)
    return {"messages": [response]}  # 注意:返回 AIMessage 对象

# 构建图
graph = (
    StateGraph(State)
    .add_node("search", search_tool)
    .add_node("reply", generate_reply_llm)
    .add_edge(START, "search")
    .add_edge("search", "reply")
    .add_edge("reply", END)
    .compile()
)

运行代码

python 复制代码
start_time = time.time() # 开始时间

# 初始消息也要用 HumanMessage
inputs = {
    "messages": [HumanMessage(content="查询我的订单状态")],
    "progress": 0
}

print("🤖 豆包智能客服启动")
print(f"使用模型: {modelName}")
print("=" * 50)

for mode, chunk in graph.stream(inputs, stream_mode=["custom", "messages", "updates"]):
    if mode == "custom":
        print(f"mode:{mode} , {chunk}")

    elif mode == "updates":
        if isinstance(chunk, dict):
            node_name, update = next(iter(chun![请添加图片描述](https://i-blog.csdnimg.cn/direct/212cf7e3cfd1489d8ae830eb2fbd7306.gif)
k.items()))
            print(f"mode:{mode} ,节点 {node_name} 完成")

    elif mode == "messages":
        # 现在会有数据了!
        msg, metadata = chunk
        if hasattr(msg, 'content') and msg.content:
            print(f"mode:{mode} , {msg.content}")
end_time = time.time() # 结束时间
print("\n" + "=" * 50)
print("🎉 豆包客服会话结束")
elapsed_time = end_time - start_time
print(f"函数耗时: {elapsed_time:.4f} 秒")

可以看到,耗时3秒钟,可以看到一个一个字蹦出来,但是是真的吗?

那么为什么叫假流式呢?,尽管,我们调用图的时候,使用了stream

但是,在replay节点,调用模型是的时候,使用了invoke ,这个是让模型执行完成后整体得到messages

实际上是在节点中,进行response = llm.invoke ,该方法为模型最后的回复,并非真正的流式

只不会在 for chunk 的时候,判断了为messages,然后底层是自动会拆成字节

python 复制代码
# LangGraph 内部伪代码
def _emit_message_events(node_output):
    if "messages" in node_output:
        for msg in node_output["messages"]:
            # 即使是完整消息,也会作为 "流事件" 发出
            yield ("messages", (msg, metadata))

2、真流式

python 复制代码
from typing import Annotated, Generator
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.config import get_stream_writer
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from dotenv import load_dotenv, find_dotenv
import os
from typing import TypedDict
import time
load_dotenv(find_dotenv())
modelName = os.getenv('OPENAI_MODEL')

class State(TypedDict):
    messages: Annotated[list, add_messages]
    progress: int

def search_tool(state: State):
    writer = get_stream_writer()
    writer("🔍 正在查询订单数据库...")
    writer("📦 找到用户订单 #12345")
    writer("✅ 查询完成")
    return {"progress": 50}

def generate_reply_llm(state: State) -> Generator:
    """🔑 真流式节点 - 使用生成器逐令牌输出"""
    writer = get_stream_writer()
    writer("🤖 豆包 AI 正在生成个性化回复...")

    # 🔑 启用流式输出
    llm = ChatOpenAI(model=modelName, streaming=True)

    prompt = f"""
    用户查询订单状态,已查询到订单#12345
    当前进度: {state['progress']}%

    请生成简短友好的客服回复,中文输出。
    """

    # 逐令牌
    full_content = ""
    for chunk in llm.stream(prompt):
        # 每个令牌都作为独立的 AIMessage 发出
        # 注意! 这个会影响sate messages 这个键,最后一次的输出
        # yield {"messages": [AIMessage(content=chunk.content)]}
        # writer({"messages": chunk}) # 走 custom 通道,实时推送
        full_content += chunk.content
        
    return {"messages": [AIMessage(content=full_content)]}


# 构建图
graph = (
    StateGraph(State)
    .add_node("search", search_tool)
    .add_node("reply", generate_reply_llm)  # 生成器节点
    .add_edge(START, "search")
    .add_edge("search", "reply")
    .add_edge("reply", END)
    .compile()
)

对于replay节点进行改造,使用stream方式并且 yield 出去每个 chunk

运行代码

python 复制代码
start_time = time.time() # 开始时间
inputs = {
    "messages": [HumanMessage(content="查询我的订单状态")],
    "progress": 0
}

print("🤖 豆包智能客服启动(真流式版本)")
print(f"使用模型: {modelName}")
print("=" * 50)

for mode, chunk in graph.stream(inputs, stream_mode=["custom", "messages", "updates"]):
    if mode == "custom":
        print(f"mode:{mode} , {chunk}")

    elif mode == "updates":
        if isinstance(chunk, dict):
            node_name, update = next(iter(chunk.items()))
            print(f"mode:{mode} ,节点 {node_name} 完成")

    elif mode == "messages":
        # 🔑 现在会逐令牌输出(打字机效果)
        msg, metadata = chunk
        if hasattr(msg, 'content') and msg.content:
            print(msg.content, end="", flush=True)  # 不换行,逐字输出
end_time = time.time() # 结束时间
print("\n" + "=" * 50)
print("🎉 豆包客服会话结束")
elapsed_time = end_time - start_time
print(f"函数耗时: {elapsed_time:.4f} 秒")

输出,可以看到速度上面,为1S左右,而前面的假流式,为3S左右

在代码中我预留了二个部分,即模型流式生成的时候,将内容发送出去

  • yield 方式,会更新state会更新,但是是最后一条
  • writer 方式,会走custom模式

所以 yield用的不好的人不要用

一般,采用下面2个方式

  • stream_mode模式带上 messages ,你不用做任何yield writer也能看到流式内容
  • stream_mode模式带上 custom ,配合 writer 产生自定义一些内容

只要节点函数里出现了 yield,它就变成生成器节点;这类节点的"最终 return {...}"通常不会被当作一次 state update 合并进 checkpoint。最终落到 get_state() 里的,往往是最后一次 yield 的那条更新,是残缺的messages

五、子图流式

我们前面的章节都是一个图进行流式传输,本节说明图的子图进行流式传输

定义一个父图、定义一个子图,共计4个节点

  • 父图状态机仅有一个键 foo

  • 子图状态机有2个键 foobar

  • 父图有两个节点、node_1node_2

  • 字图有两个节点、subgraph_node_1 subgraph_node_2

为了直观,我们采用updates模式只打印对应节点操作base_state字段的情况

python 复制代码
from langgraph.graph import START, StateGraph
from typing import TypedDict

# ==================== 定义子图 ====================
# 子图状态:定义子图内部节点之间传递的数据结构
class SubgraphState(TypedDict):
    foo: str  # 注意:这个键与父图状态共享(父图也有 foo 字段)
    bar: str  # 子图特有的字段(父图没有)

def subgraph_node_1(state: SubgraphState):
    """
    子图的第一个节点
    功能:初始化 bar 字段
    """
    print("  🔹 [子图节点1] 执行中...")
    return {"bar": "bar"}  # 向状态中添加 bar 字段

def subgraph_node_2(state: SubgraphState):
    """
    子图的第二个节点
    功能:将 foo 和 bar 拼接后更新 foo
    """
    print(f"  🔹 [子图节点2] 接收到状态: foo='{state['foo']}', bar='{state['bar']}'")
    return {"foo": state["foo"] + state["bar"]}  # 更新 foo 字段

# 构建子图
subgraph_builder = StateGraph(SubgraphState)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")  # 子图入口 → 节点1
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")  # 节点1 → 节点2
subgraph = subgraph_builder.compile()  # 编译子图

# ==================== 定义父图 ====================
# 父图状态:只包含 foo 字段
class ParentState(TypedDict):
    foo: str  # 父图的状态字段(会传递给子图)

def node_1(state: ParentState):
    """
    父图的节点1
    功能:在 foo 前面添加 "hi! " 前缀
    """
    print(f"🔷 [父图节点1] 接收到状态: foo='{state['foo']}'")
    return {"foo": "hi! " + state["foo"]}  # 更新 foo 字段

# 构建父图
builder = StateGraph(ParentState)
builder.add_node("node_1", node_1)  # 添加父图节点1
builder.add_node("node_2", subgraph)  # 🔑 添加子图作为节点(子图会被当作一个节点)
builder.add_edge(START, "node_1")  # 父图入口 → 节点1
builder.add_edge("node_1", "node_2")  # 节点1 → 子图节点
graph = builder.compile()  # 编译父图
from IPython.display import Image, display
display(Image(graph.get_graph(xray=True).draw_mermaid_png()))

运行代码

python 复制代码
# ==================== 流式执行 ====================
print("=" * 60)
print("🚀 开始流式执行父图(包含子图)")
print("=" * 60)

for chunk in graph.stream(
    {"foo": "foo"},  # 初始输入:foo="foo"
    stream_mode="updates",  # 流式模式:返回每个节点的状态更新
    subgraphs=True,  # 🔑 关键参数:设置为 True 以流式输出子图内部节点的执行结果
):
    print(chunk)
    print("-" * 60)

print("=" * 60)
print("✅ 执行完成")
print("=" * 60)

可以看到,父子图都能进行流式

实际上关键点是,stream时候指定subgraphs=True

我们也可以,指定subgraphs=False看看输出对比

可以看到,因为是updates模式,很直观的看到关闭的时候,并没有显示子图对于base_state状态机操作的情况,少了2

六、总结

1)流式传输的本质:你到底在"流"什么?

LangGraph 里的 Streaming 不是一个概念,而是几条不同的"数据管道":

  • updates:每个节点对 state 做了哪些更新(最适合看业务步骤、看状态机字段变更)。
  • values:每一步完整 state 快照(最适合整体观察状态如何演进,但输出更大)。
  • custom :节点内部通过 get_stream_writer() 主动推送自定义事件(最适合"工具执行中.../进度条/阶段提示")。
  • messages:LLM 输出的流式消息事件(最适合聊天 UI 的打字机效果)。
  • debug:最全的开发者视角,排查图执行流程非常好用。

2)"真假流式"的工程结论:不要被"看起来在跳字"骗了

很多同学会遇到一种错觉:明明用了 graph.stream(... stream_mode="messages"),也看到文字一段段输出,为什么我还说它是假流式?

关键点是:真流式与否,要看 LLM 是否以 streaming 方式生成,而不是看你在外层是不是用了 stream。

  • 假流式(常见误区)

    • 节点里用的是 llm.invoke(...):模型先完整生成,再一次性返回完整 AIMessage
    • 外层 messages 之所以"像在流",往往只是框架把完整消息当事件发出来(或 UI 自己做了逐字渲染),但这不等于模型端在逐 token 产生。
  • 真流式(正确做法)

    • 节点里用的是 llm.stream(...) / 模型开启 streaming:模型侧就是增量输出,**TTFB(首 token 延迟)**更低,交互体验更顺。

你在综合案例里测到"真流式更快",本质上就是:首 token 更早到达 + UI 更早开始渲染,用户感知速度明显提升。


3)yield 的坑:它适合"输出事件",不适合"维护最终 messages"

你在文中踩到的坑非常典型,也值得强调成结论:

  • 只要节点函数里出现 yield,它就变成生成器节点。
  • 在很多实际运行情况下:生成器节点末尾的 return {...} 不一定会被当作最终 state 更新持久化
  • 结果就是:get_state() 里经常只保留 最后一次 yield 的那条更新,而 LLM 最后一个 chunk 很可能是空字符串/空格/标点,最终 state 就"残缺"。

所以工程上更推荐的模式是:

  • messages 流式给 UI 看 :用 stream_mode="messages"(你不用自己 yield token)
  • custom 流式给 UI 看进度 :用 writercustom
  • 最终对话历史入库 :用普通 return 一次性写入完整 AIMessage

一句话:流式展示 ≠ 流式写 state。把这两件事拆开做,系统最稳。


4)子图流式:subgraphs=True 是关键开关

当你的企业级 Agent 逐渐演进成"父图编排子图"的结构后,流式可观测性会变得更重要。

  • 默认情况下,父图把子图当成一个整体节点,你很难看到子图内部每一步做了什么。
  • 开启:
    • graph.stream(..., stream_mode="updates", subgraphs=True)
  • 就能把子图内部节点的 updates 也流出来,排查问题时非常直观。

这点对"多 Agent 协作""工具链很长"的项目尤其重要:否则你只看到"子图节点执行了",看不到里面到底卡在哪一步。


5)一张表带走:生产环境怎么选(强烈建议收藏)

场景目标 推荐 stream_mode 节点内部推荐写法 是否建议用 yield 更新 messages 结论
调试业务流程 / 看节点改了哪些字段 updates / values 普通 return 可观测性最好
聊天 UI 打字机效果(最常见) messages LLM 用 llm.stream(...),节点只 return 完整消息 体验好、state 干净
工具执行中提示 / 进度条 / 阶段播报 custom(可叠加 updates) writer("...") / writer({"delta": ...}) 工程最友好
父图 + 子图需要"看见内部步骤" updates + subgraphs=True 子图内部保持可观测输出 不建议 排障效率提升
需要"边生成边落盘中间态"(少见) custom / updates 单独 buffer 字段保存进度 谨慎 成本高,非必要不做

6)最终落地建议(企业级项目的写法)

  • UI 流式渲染 :优先吃 messages(LLM 输出)和 custom(工具进度)。
  • 对话历史:只保存"完整的一条 AIMessage",不要保存 token 碎片。
  • 调试与观测 :开发期把 updates/debug 打开,稳定后按需降级。
  • 图复杂化后 :务必用 subgraphs=True 把子图内部执行链路流出来,否则排查成本指数级上升。
相关推荐
WangYaolove13142 小时前
基于自适应svm电影评价倾向性分析(源码+文档)
python·django·毕业设计·源码
黎雁·泠崖2 小时前
Java面向对象:this关键字+构造方法+标准JavaBean
java·开发语言·python
sunfove2 小时前
Python 面向对象编程:从过程式思维到对象模型
linux·开发语言·python
无双@2 小时前
保姆级 安装+使用上 Claude Code
ai·大模型·agent·claude·配置·claude code·skills
Elastic 中国社区官方博客2 小时前
使用 Elasticsearch 管理 agentic 记忆
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
linmoo19863 小时前
Langchain4j 系列之二十二 - Embedding Models
人工智能·langchain·embedding·嵌入模型·langchain4j
沈浩(种子思维作者)3 小时前
什么才叫量子物理学?什么是真正量子计算?
人工智能·python·flask·量子计算
CoderJia程序员甲3 小时前
GitHub 热榜项目 - 日榜(2026-01-17)
ai·开源·大模型·github·ai教程
小小测试开发3 小时前
Python bool 类型常用方法与实战指南:极简类型的高效用法
python