LangGraph 高级特性实战:流式执行、子图与六大 Agent 工作流设计模式

前面学习 LangGraph 时,我们已经能用 StateGraph 定义状态、节点和边,也能处理条件分支、记忆和中断。

继续往真实项目走,还需要掌握三个能力:

流式执行:图还没跑完,就把中间结果实时交给前端
子图:把复杂流程拆成可以复用的小流程
设计模式:根据任务特点选择合适的图结构

这篇文章围绕这三部分展开,目标不是背 API,而是知道在什么场景下该怎样组织 LangGraph。

一、流式执行:为什么不只用 invoke()?

平时最常见的写法是:

res = graph.invoke({"topic": "布偶猫"})
print(res)

invoke() 会一直等待,直到整个图执行结束,再一次性返回最终状态。

这对短任务没有问题,但如果图中包含多个 LLM 调用、工具调用或耗时节点,用户会长时间看不到任何反馈。

流式执行(Streaming Execution)就是:图一边执行,一边把状态变化、消息增量或自定义进度事件交给调用方。

invoke:图全部结束 -> 返回最终结果
stream:图执行过程中 -> 持续产出 chunk

1. stream() 与 astream()

LangGraph 提供两套流式 API:

API 使用方式 适合场景
stream() for chunk in ... 普通 Python 脚本、命令行程序
astream() async for chunk in ... FastAPI、WebSocket、异步服务

两者关注的内容和 stream_mode 基本一致,主要区别是调用方式:

# 同步流式输出
for chunk in graph.stream(input_data, stream_mode="updates"):
    print(chunk)
# 异步流式输出
async for chunk in graph.astream(input_data, stream_mode="updates"):
    print(chunk)

可以先这样理解:stream 是同步版,astream 是异步版。

2. stream_mode:决定想看到什么

stream_mode 决定每一个 chunk 里输出哪一种数据。

模式 输出内容 常见用途
values 每个超步之后的完整状态 看完整状态快照
updates 每个节点刚写入的增量更新 观察节点执行结果
messages 消息增量,通常是模型逐字输出 聊天页面打字机效果
checkpoints 检查点事件 监控持久化和恢复
tasks 任务的开始、结束、错误 运行观测与排错
debug 检查点与任务调试信息 开发阶段排查问题
custom 节点主动写出的自定义数据 进度条、阶段通知

3. values:每一步的完整状态

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class OverAllState(TypedDict):
    initial_state: str
    node_a_output: str
    node_b_output: str

def node_a(state: OverAllState):
    return {"node_a_output": "节点 A 的输出"}

def node_b(state: OverAllState):
    return {"node_b_output": "节点 B 的输出"}

builder = StateGraph(OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile()

for mode, chunk in graph.stream(
    {"initial_state": "初始状态"},
    stream_mode=["values"],
):
    print(mode, chunk)

输出状态会逐渐变完整:

初始状态
初始状态 + node_a_output
初始状态 + node_a_output + node_b_output

所以 values 很适合观察“此刻整个图的状态是什么”。缺点是状态很大时,每次传完整状态会比较冗余。

4. updates:只看谁改了什么

把上例改成:

for mode, chunk in graph.stream(
    {"initial_state": "初始状态"},
    stream_mode=["updates"],
):
    print(mode, chunk)

你会得到类似:

("updates", {"node_a": {"node_a_output": "节点 A 的输出"}})
("updates", {"node_b": {"node_b_output": "节点 B 的输出"}})

这里最关键的是:

values:给你当前完整状态
updates:只给你本次节点产生的变化

实际项目中,如果前端只需刷新局部内容,通常 updates 更省数据,也更容易定位“哪个节点做了更新”。

5. messages:实现模型逐字输出

对于聊天机器人,最常用的是 messages

from langgraph.graph import StateGraph, START, END, MessagesState
from langchain.messages import HumanMessage

def llm_node(state: MessagesState):
    response = model.invoke(state["messages"])
    return {"messages": [response]}

builder = StateGraph(MessagesState)
builder.add_node("llm_node", llm_node)
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", END)
graph = builder.compile()

for mode, chunk in graph.stream(
    {"messages": [HumanMessage(content="你好")]},
    stream_mode=["messages"],
):
    message_chunk, metadata = chunk
    print(message_chunk.content, end="", flush=True)

message_chunk.content 是本次新增的文本片段;metadata 里常有当前节点名、图步骤、模型信息等运行元数据。

注意:想真正获得模型的逐 token 输出,底层模型集成也必须支持流式响应。

6. custom:节点主动报告进度

有些信息不是状态,也不是模型消息,例如:

正在读取 PDF...
已完成向量检索...
正在生成报告第 2 部分...

这类内容适合 custom。节点通过 stream_writer 写出自定义事件,前端可以据此展示进度。

from langgraph.config import get_stream_writer

def process_node(state):
    writer = get_stream_writer()
    writer({"stage": "retrieval", "message": "正在检索知识库"})

    # 执行真正业务逻辑
    result = "检索完成"

    writer({"stage": "retrieval", "message": "检索完成"})
    return {"result": result}

调用方监听:

for mode, chunk in graph.stream(input_data, stream_mode=["custom"]):
    print(chunk)

7. astream_events:看 Runnable 级别事件

stream/astream 主要关注图的执行结果和状态变化。

astream_events() 则更偏底层运行追踪:它可以输出 Runnable 的启动、结束、输入、输出以及父子调用关系。做复杂 Agent 调试、链路追踪或接入可观测平台时更有价值。

async for event in graph.astream_events(input_data, version="v2"):
    print(event["event"], event.get("name"))

日常页面流式回复优先用 astream;需要分析内部调用链时,再看 astream_events

二、子图:把大图拆成可复用模块

当一个 Agent 的流程越来越长,所有节点都堆在一个 StateGraph 中会很难维护。

例如一个“智能学习助手”可能包含:

主图
├── 用户意图识别
├── RAG 检索子图
├── 练习题生成子图
├── 答案审核子图
└── 汇总输出

子图(Subgraph)本质上就是:一个已经 compile() 的 LangGraph 图,被父图当成流程的一部分使用。

1. 两种嵌入方式

方式一:在父图节点函数里调用子图
subgraph = sub_builder.compile()

def call_subgraph_node(state: ParentState):
    sub_res = subgraph.invoke({"topic": state["topic"]})
    return {"summary": sub_res["summary"]}

特点:

  • 父节点可以自由转换输入输出
  • 父图与子图不必使用完全相同的状态结构
  • 适合需要做参数适配、数据清洗、结果二次处理的场景
方式二:直接把子图注册为父图节点
subgraph = sub_builder.compile()

parent_builder.add_node("research_subgraph", subgraph)
parent_builder.add_edge(START, "research_subgraph")

特点:

  • 写法更短
  • 父图和子图需要有能对接的共享状态字段
  • 适合“子图就是一个完整节点”的情况

一句话区分:

需要转换数据:在节点函数里 invoke 子图
状态能够直接对接:子图直接作为父图节点

2. 为什么子图很重要

子图解决的不是“能不能跑”,而是复杂系统的组织问题:

  • 复用:同一个检索、审核、工具调用流程可被多个父图使用
  • 隔离:每个模块只关注自己的状态和节点
  • 协作:不同人维护不同子图,降低相互影响
  • 测试:可以单独对一个子图调用 invoke() 和调试

3. 子图持久化:Per-invocation 与 Per-thread

子图和检查点结合时,容易遇到一个关键选择。

策略 含义 适合场景
Per-invocation 每次调用子图都像新任务,不保留子图自身历史 独立、无状态的小任务
Per-thread 同一个 thread 内的子图调用保留历史 多轮对话、可恢复流程
# 默认思路:一次调用对应一次独立的子图执行
subgraph = sub_builder.compile()

# 让子图跟随线程保存自己的检查点与历史
subgraph = sub_builder.compile(checkpointer=True)

如果子图负责多轮对话、interrupt() 后恢复,通常需要 Per-thread

如果一个父图里多次调用同一个有状态子图,也要警惕历史互相影响:前一次子图的上下文可能影响后一次调用。此时需要设计不同的命名空间、不同线程,或者直接选无状态调用。

4. 子图流式输出

父图流式运行时,也可以开启子图输出。此时 chunk 通常会带有命名空间信息,用来标识数据来自哪一层图:

(命名空间, mode, data)

例如命名空间 ("call_subgraph",) 表示:这一段流数据来自父图中名为 call_subgraph 的子图调用。

这对前端很重要,因为它可以区分:

主图的最终回答
子图中的检索进度
子图中的 LLM 消息片段

三、六种常用运行图设计模式

LangGraph 的节点和边可以自由组合,但工程上有一些反复出现的成熟结构。它们不是固定模板,而是帮助你先判断“这个任务本质上是哪一类”。

1. Prompt Chaining:提示词链

核心思路:把一个复杂任务拆成多个顺序清晰的小步骤,前一步输出作为后一步输入。

用户主题 -> 生成提纲 -> 撰写初稿 -> 润色文章 -> 输出

适合:

  • 长文写作
  • 报告生成
  • 数据提取后再总结
  • 先分类、后处理的任务

优点是步骤稳定、容易调试;缺点是路径固定,不能根据中间结果自由调整。

2. Parallelization:并行化

核心思路:多个互不依赖的任务同时执行,最后再汇总。

               -> 生成诗
输入主题 ->    -> 生成笑话  -> 汇总
               -> 生成文案

LangGraph 中可以从同一个上游节点连出多条边:

builder.add_edge(START, "node_poem")
builder.add_edge(START, "node_joke")
builder.add_edge(["node_poem", "node_joke"], "node_output")

这里 node_output 会等待 node_poemnode_joke 都完成后再执行,这就是扇入(Fan-in)。

适合:独立检索、多份内容生成、多路数据校验。

3. Routing:路由

核心思路:先判断任务类型,再走对应节点。

输入 -> 路由判断 -> 写诗节点 / 讲笑话节点 / 兜底节点

常见写法是让模型生成结构化结果,再由条件边决定去向:

def router(state) -> Literal["poem_node", "joke_node", "default_node"]:
    if state["mode"] == "poem":
        return "poem_node"
    if state["mode"] == "joke":
        return "joke_node"
    return "default_node"

builder.add_conditional_edges("router_node", router)

重点:路由判断最好使用 Literal、Pydantic Schema 等结构化输出,不要完全依赖模型生成的自然语言句子。

4. Orchestrator-worker:编排器与工作节点

这个模式由“规划”和“执行”两部分组成:

输入任务 -> Orchestrator 制定计划 -> 动态分发多个 Worker -> 汇总

例如用户要求“写一篇 RAG 教程”,编排器可以先规划:

1. 什么是 RAG
2. 文档加载与切分
3. 向量化与检索
4. 生成回答与评估

接着通过 Send 为每一个章节创建一个 Worker 任务。

from langgraph.types import Send

def router(state):
    return [
        Send("worker_node", {"section": section})
        for section in state["plan"]
    ]

它和普通并行化的区别是:

普通并行:任务数量在写图时就确定
编排器-工作节点:任务数量在运行时由编排器决定

因此它非常适合:自动报告、复杂研究任务、批量处理不定长列表。

5. Evaluator-optimizer:评估器与优化器

核心思路是:生成结果后先评估;不合格就携带反馈重新生成。

Generator -> Evaluator
                 | 合格 -> END
                 | 不合格 + 反馈 -> Generator

例如先生成一个笑话,再让评估器输出结构化结果:

class Feedback(BaseModel):
    grade: Literal["好笑", "不好笑"]
    feedback: str

路由函数根据 grade 决定结束还是重试:

def route_joke(state):
    if state["funny_or_not"] == "好笑":
        return "accept"
    return "reject_and_feedback"

适合:

  • 文案生成与质量检查
  • SQL 生成与语法验证
  • 代码生成与测试
  • 翻译生成与审校

这个模式一定要有最大重试次数或递归限制,否则可能无限循环,持续消耗模型 token。

6. Agent:智能体循环

前面五种模式大多由开发者事先确定主要步骤。Agent 的不同在于:模型会根据当前消息和工具结果,动态决定下一步是否调用工具、调用哪个工具。

最基础的 ReAct 结构是:

用户问题 -> LLM 决策 -> 是否需要工具?
                         | 是 -> Tool -> 回到 LLM
                         | 否 -> END

示例骨架:

from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt.tool_node import ToolNode

def model_node(state: MessagesState):
    response = model_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def router(state: MessagesState):
    last_msg = state["messages"][-1]
    if last_msg.tool_calls:
        return "tool_node"
    return END

builder = StateGraph(MessagesState)
builder.add_node("model_node", model_node)
builder.add_node("tool_node", ToolNode(tools=tools))
builder.add_edge(START, "model_node")
builder.add_conditional_edges("model_node", router)
builder.add_edge("tool_node", "model_node")

注意:Agent 不是“比工作流更高级”,而是“把更多执行决策交给模型”。

当步骤明确、风险高、需要稳定执行时,固定工作流通常更合适;当工具选择和执行顺序无法提前确定时,才更适合 Agent。

四、如何选择设计模式?

你的任务特点 优先选择
步骤固定,前一步依赖后一步 Prompt Chaining
多个任务互不依赖 Parallelization
不同意图走不同流程 Routing
运行时才知道要创建多少任务 Orchestrator-worker
需要生成、检查、重试 Evaluator-optimizer
工具和执行顺序无法预先确定 Agent

真实项目经常是组合使用:

Routing 判断用户意图
  -> RAG 子图检索资料
  -> Orchestrator-worker 并行生成报告章节
  -> Evaluator-optimizer 检查质量
  -> stream 把生成过程实时发送给前端

五、总结

LangGraph 高级能力的价值,不在于把图画得多复杂,而在于把复杂任务拆得更可控。

最后用几句话记住本篇重点:

stream:图执行时实时拿结果
subgraph:把复杂图拆成可复用模块
Prompt Chaining:固定顺序的步骤链
Parallelization:独立任务同时跑
Routing:根据状态选择路径
Orchestrator-worker:运行时动态分发任务
Evaluator-optimizer:不合格就带反馈重试
Agent:让模型决定是否调用工具和下一步行动

掌握这些模式后,再写 RAG、工具调用、多轮对话或数字人智能体时,就不再只是“把代码接起来”,而是能先设计出合理、可维护、可观察的执行流程。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐