《LangGraph 设计与实现》完整目录
- 前言
- 第1章 为什么需要理解 LangGraph(当前)
- 第2章 架构总览
- 第3章 StateGraph 图构建 API
- 第4章 Channel 状态管理与 Reducer
- 第5章 图编译:从 StateGraph 到 CompiledStateGraph
- 第6章 Pregel 执行引擎
- 第7章 任务调度与并行执行
- 第8章 Checkpoint 持久化
- 第9章 中断与人机协作
- 第10章 Command 与高级控制流
- 第11章 子图与嵌套
- 第12章 Send 与动态并行
- 第13章 流式输出与调试
- 第14章 Runtime 与 Context
- 第15章 Store 与长期记忆
- 第16章 预构建 Agent 组件
- 第17章 多 Agent 模式实战
- 第18章 设计模式与架构决策
第1章 为什么需要理解 LangGraph
本章基于 LangGraph 1.1.6 / langgraph-checkpoint 4.0.1 源码分析。源码路径:
libs/目录。
当 LLM 应用从简单的"一问一答"演进到需要多步推理、工具调用、人机交互的复杂工作流时,一个根本性的工程问题浮出水面:如何可靠地编排有状态的 AI 工作流? LangGraph 正是为回答这个问题而生的。它不是 LangChain 的替代品,而是 LangChain 生态的自然延伸------当 Chain 的线性管道无法表达复杂的控制流时,Graph 提供了循环、分支和持久化状态的能力。
本章将从工程演进的视角出发,讲清楚 LangGraph 的设计动机、核心创新,以及它在多 Agent 框架竞争格局中的独特定位。
:::tip 本章要点
- 从 Chain 到 Graph 的必然演进:理解为什么线性管道不够用
- 有状态工作流的核心需求:循环、条件分支、持久化中断与恢复
- Pregel 模型的灵感来源:Google 的大规模图处理思想如何被借鉴到 AI 工作流中
- 框架对比:LangGraph 与 LangChain/CrewAI/AutoGen 的本质差异
- 为什么要读源码:超越"会调 API"的层面,真正理解运行机制 :::
1.1 从 LangChain Chain 到 LangGraph 的演进
1.1.1 Chain 的黄金时代与局限
LangChain 最早的核心抽象是 Chain------一个将多个步骤串联起来的线性管道。通过 LCEL(LangChain Expression Language)和 Runnable 接口,开发者可以用简洁的管道语法构建 LLM 应用:
python
# LangChain LCEL 管道:优雅但本质上是线性的
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
chain = ChatPromptTemplate.from_template("翻译为英文:{text}") | ChatOpenAI() | StrOutputParser()
result = chain.invoke({"text": "你好世界"})
这种模式在简单场景下非常优雅。但当应用需求增长时,Chain 的线性本质开始暴露关键局限:
python
# 问题1:如何实现"思考-行动"循环?
# Agent 需要反复调用工具直到得出最终答案
# Chain 没有原生的循环机制
# 问题2:如何在多个步骤间共享和修改状态?
# Chain 的每个步骤只接收上一步的输出
# 无法方便地维护跨步骤的累积状态
# 问题3:如何在执行中途暂停等待人工审批?
# Chain 是一次性执行完毕的
# 没有内置的中断和恢复机制
# 问题4:出错后如何从上次的检查点恢复?
# Chain 不保存中间状态
# 失败意味着从头开始
这些问题的本质可以归结为一句话:Chain 是无状态的有向无环图(DAG),而真实世界的 AI 工作流需要有状态的有向图(可以有环)。 这不是 Chain 设计上的缺陷,而是不同抽象层次的定位差异。Chain 针对的是"单次线性处理"场景,它在这个场景下做得非常好;而 LangGraph 针对的是"多步交互式工作流"场景,需要一套全新的抽象来表达更复杂的控制流和状态管理需求。
1.1.2 Agent 催生的范式转换
转折点出现在 ReAct(Reasoning + Acting)模式的流行。一个典型的 ReAct Agent 需要:
- 接收用户输入
- LLM 思考是否需要调用工具
- 如果需要,调用工具并获取结果
- 将工具结果反馈给 LLM
- LLM 判断是否需要继续调用工具
- 重复步骤 2-5 直到得出最终答案
这个模式的核心特征是循环 ------LLM 和工具之间形成一个反复调用的闭环。在 LangChain 的早期实现中,这种循环是通过 Python 的 while 循环在 Agent 执行器内部实现的:
python
# LangChain 早期 AgentExecutor 的简化逻辑
class AgentExecutor:
def invoke(self, input):
intermediate_steps = []
while True:
action = self.agent.plan(input, intermediate_steps)
if isinstance(action, AgentFinish):
return action.return_values
observation = self.tools[action.tool].run(action.tool_input)
intermediate_steps.append((action, observation))
这种命令式的循环实现存在深层问题:循环逻辑硬编码在执行器中,无法灵活定制;中间状态存在内存列表中,无法持久化;出错时只能从头开始;无法在循环中插入人工审批步骤。
LangGraph 的诞生,就是将这些命令式的控制流声明化------用图的拓扑结构来表达循环、分支和并行,用 Channel 机制来管理有状态的数据流。
这个转变的意义远不止是代码风格的变化。声明式的图定义带来了三个根本性的优势:第一,图的拓扑是可序列化、可可视化、可验证的------你可以在运行前就发现拓扑错误,而命令式的 while 循环只有在运行时才能暴露逻辑问题。第二,执行引擎可以对图拓扑做全局优化------比如自动并行化没有依赖关系的节点,而命令式代码中的并行化需要开发者手动管理。第三,图的状态管理可以被框架接管------检查点、恢复、时间旅行等高级特性只需要框架层面的支持,不需要开发者在业务代码中处理。
1.1.3 从"执行管道"到"状态机"
LangGraph 引入的范式转换可以这样理解:
| 特性 | LangChain Chain | LangGraph |
|---|---|---|
| 拓扑结构 | 有向无环图(DAG) | 有向图(支持环) |
| 状态管理 | 步骤间传递输出 | 共享可变状态 + Channel |
| 控制流 | 线性 / 简单分支 | 循环 / 条件分支 / 并行 |
| 持久化 | 无内置支持 | Checkpoint 机制 |
| 中断恢复 | 不支持 | interrupt / Command |
| 人机协作 | 手动实现 | 内置 Human-in-the-loop |
| 流式输出 | Runnable 流式 | 多模式流式(值、更新、消息等多种模式) |
1.2 有状态工作流的四个核心需求
为什么不能只用普通的 Python 代码实现这些功能?为什么需要一个专门的框架?答案在于:当你将这四个需求组合在一起时,实现的复杂度会呈指数增长。一个成熟的 AI 工作流需要同时处理循环控制、状态持久化、并发安全和可观测性------手动实现这些的交叉逻辑,很快就会变成难以维护的"胶水代码地狱"。
1.2.1 需求一:循环与条件分支
现实世界的 AI 工作流几乎都包含循环。除了前面提到的 ReAct 模式,还有:
- 自我修正:代码生成后执行测试,失败则修正后重试
- 多轮对话:持续与用户交互直到确认意图
- 迭代优化:反复改进输出直到满足质量标准
- 多步骤规划:先分解任务,逐一执行子任务,最后汇总结果
- 检索增强生成(RAG):检索文档,判断相关性,不满意则扩展查询重试
循环的本质需求是:基于运行时状态,动态决定下一步执行什么。这在 DAG 模型中无法表达------DAG 的拓扑是静态的,不能根据中间结果改变执行路径。
LangGraph 通过 add_conditional_edges 实现条件分支,通过边的拓扑自然地表达循环:
python
from langgraph.graph import StateGraph, START, END
builder = StateGraph(AgentState)
builder.add_node("llm", call_llm)
builder.add_node("tools", call_tools)
builder.add_edge(START, "llm")
builder.add_conditional_edges("llm", should_continue, {
"continue": "tools",
"end": END,
})
builder.add_edge("tools", "llm") # 这条边形成了循环
这段代码定义了一个标准的 ReAct 循环。should_continue 函数根据 LLM 的输出(是否包含工具调用)来决定路由方向。如果 LLM 决定调用工具,流程进入 tools 节点;工具执行完毕后,通过 tools -> llm 的边回到 LLM 继续思考。这个循环会一直持续,直到 LLM 返回最终答案(路由到 END)。
LangGraph 通过递归深度限制(recursion_limit,默认 25)来防止无限循环。当步骤数超过限制时,会抛出 GraphRecursionError。这是一个实用的安全阀------在生产环境中,失控的循环可能导致大量 API 调用和成本失控。
1.2.2 需求二:持久化状态与检查点
在实际的生产环境中,AI 工作流往往不是一次性运行完毕的。它可能需要等待人工审批、等待外部系统的回调、或者在运行过程中因为各种原因被中断。如果没有持久化机制,所有这些场景都意味着从头开始------之前完成的所有计算和中间结果全部丢失。这不仅浪费计算资源,更可能导致用户体验的严重下降。
在长时间运行的工作流中,持久化状态至关重要。考虑一个需要人工审批的贷款申请流程:
在"人工审批"节点,工作流需要中断执行,等待外部输入。这可能需要数小时甚至数天。如果没有持久化机制,进程的任何重启都会丢失所有状态。LangGraph 的 Checkpoint 机制将每一步的完整状态保存到外部存储中,使得:
- 进程重启后可以从上次的检查点恢复
- 支持"时间旅行"------回退到任意历史状态
- 支持分支------从同一个检查点出发探索不同路径
1.2.3 需求三:并发安全的状态更新
当多个节点并行执行并同时更新共享状态时,需要一种安全的合并机制。这正是 LangGraph 的 Reducer 模式解决的问题:
python
from typing import Annotated
from typing_extensions import TypedDict
def merge_lists(existing: list, new: list) -> list:
return existing + new
class State(TypedDict):
messages: Annotated[list, merge_lists] # 多个节点可以同时追加消息
count: int # 默认只允许一个节点写入
当多个并行节点同时向 messages 写入数据时,Reducer 函数确保所有写入都被正确合并,而不会相互覆盖。这背后的 Channel 机制是 LangGraph 架构的核心组件之一,我们将在第 4 章详细分析。
考虑一个实际的并行检索场景:
python
from typing import Annotated
import operator
class SearchState(TypedDict):
query: str
results: Annotated[list, operator.add] # Reducer: 列表拼接
sources: Annotated[set, operator.or_] # Reducer: 集合合并
def search_web(state: SearchState) -> dict:
return {"results": web_results, "sources": {"web"}}
def search_docs(state: SearchState) -> dict:
return {"results": doc_results, "sources": {"docs"}}
# search_web 和 search_docs 并行执行
# results 通过 operator.add 合并为一个列表
# sources 通过 operator.or_ 合并为 {"web", "docs"}
如果没有 Reducer 机制,你需要手动管理锁、队列或其他并发原语来安全地合并结果。LangGraph 将这个问题内化到了 Channel 层------开发者只需声明合并策略,框架自动处理并发写入。
1.2.4 需求四:可观测性与调试
复杂的 AI 工作流在生产环境中需要强大的可观测性:哪个节点正在执行?状态在每一步如何变化?错误发生在哪个环节?LangGraph 的多模式流式输出(stream_mode)提供了从粗粒度到细粒度的完整观测能力:
python
# 粗粒度:只看每步执行后的完整状态
for chunk in graph.stream(input, stream_mode="values"):
print(chunk)
# 中粒度:看每个节点的更新
for chunk in graph.stream(input, stream_mode="updates"):
print(chunk)
# 细粒度:实时看 LLM 的 token 输出
for chunk in graph.stream(input, stream_mode="messages"):
print(chunk)
1.3 Pregel:来自 Google 的灵感
LangGraph 运行时引擎的核心类名为 Pregel,这个名字直接来源于 Google 在 2010 年发表的论文 "Pregel: A System for Large-Scale Graph Processing"。理解 Pregel 模型对于理解 LangGraph 的运行机制至关重要。
1.3.1 原始 Pregel 模型
Google Pregel 用于处理大规模图计算(如 PageRank、最短路径),其核心思想是 BSP(Bulk Synchronous Parallel,批量同步并行) 模型:
BSP 模型的执行分为多个"超级步骤"(super-step),每个超级步骤内:
- 计算阶段:所有活跃节点并行执行各自的计算
- 通信阶段:节点通过消息传递交换数据
- 同步屏障:等待所有节点完成,然后才进入下一个超级步骤
关键约束:在一个超级步骤内,节点只能看到上一个超级步骤结束时的数据。这个约束看似限制,实则是保证并行安全的关键。
1.3.2 LangGraph 对 Pregel 的改造
LangGraph 的 Pregel 类借鉴了 BSP 模型的核心思想,但做了针对 AI 工作流的重要改造:
| 概念 | Google Pregel | LangGraph Pregel |
|---|---|---|
| 节点 | 图中的顶点 | 工作流中的处理函数(PregelNode) |
| 消息传递 | 节点间直接发消息 | 通过 Channel 间接通信 |
| 超级步骤 | 迭代计算轮次 | 工作流的一个执行步骤(step) |
| 终止条件 | 所有节点投票停止 | 没有活跃的触发 Channel |
| 容错 | 检查点 + 重新计算 | Checkpoint + 中断恢复 |
LangGraph Pregel 的每个 step 同样遵循三个阶段:
python
# Pregel 类 docstring 中描述的三阶段执行模型(简化)
# 源码位置:langgraph/pregel/main.py
# Phase 1: Plan(计划)
# 确定哪些 actor 需要在本步执行
# 第一步:选择订阅了输入 channel 的 actor
# 后续步骤:选择订阅了上一步更新过的 channel 的 actor
# Phase 2: Execution(执行)
# 并行执行所有选中的 actor
# 在此阶段,channel 的更新对其他 actor 不可见
# Phase 3: Update(更新)
# 将 actor 的写入应用到 channel
# 此时所有写入才对下一步可见
这段设计直接映射了 BSP 模型的核心思想------写入延迟可见。当节点 A 和节点 B 并行执行时,A 的写入不会影响 B 看到的状态,反之亦然。所有写入在步骤结束时统一应用,由 Channel 的 Reducer 负责合并。
1.3.3 为什么选择 Pregel 模型
LangGraph 团队选择 Pregel/BSP 模型而非其他并发模型(如 Actor 模型、CSP 模型),背后有深思熟虑的设计权衡:
确定性:BSP 的同步屏障保证了在相同输入下,执行顺序是确定的。这对于 AI 工作流的调试和复现至关重要。
简洁性:开发者无需关心锁、信号量等并发原语。Channel 的 Reducer 机制自然地处理了并发写入的合并。
检查点友好:每个超级步骤结束时,系统处于一个一致的状态,天然适合做检查点。如果在步骤中间出错,只需回退到上一个步骤的检查点。
可组合性:子图可以作为一个节点嵌入父图,每个子图独立地运行自己的 BSP 循环。这使得复杂工作流可以模块化构建。
1.4 与其他框架的对比
1.4.1 LangGraph vs LangChain
LangGraph 不是 LangChain 的竞争者,而是其在工作流编排维度的延伸:
- LangChain 的核心是 Runnable 接口和 LCEL 管道,擅长将 LLM 调用、Prompt 模板、输出解析器等组件串联成线性管道
- LangGraph 建立在 LangChain 的消息协议和 Runnable 接口之上,但引入了图拓扑、共享状态和检查点机制,擅长编排包含循环和分支的复杂工作流
在 LangGraph 的源码中,可以看到它对 LangChain 的依赖关系:
python
# langgraph/pregel/main.py 的 import 部分
from langchain_core.runnables import RunnableSequence
from langchain_core.runnables.config import RunnableConfig
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
LangGraph 复用了 LangChain 的配置传播机制(RunnableConfig)、回调系统(Callbacks)和追踪基础设施(LangSmith 集成),但在状态管理和执行调度层面完全重新设计。
1.4.2 LangGraph vs CrewAI
CrewAI 聚焦于"多 Agent 角色扮演"场景------定义具有不同角色、目标和工具的 Agent,让它们协作完成任务。它的核心抽象是 Crew(团队)、Agent(角色)和 Task(任务):
python
# CrewAI 的典型用法
crew = Crew(
agents=[researcher, writer, editor],
tasks=[research_task, writing_task, editing_task],
process=Process.sequential # 或 Process.hierarchical
)
result = crew.kickoff()
关键差异:
- 抽象层级:CrewAI 是更高层的抽象,开发者用"角色"和"任务"思考;LangGraph 是更底层的基础设施,开发者用"状态"和"图拓扑"思考
- 控制力:LangGraph 提供对执行流程的精确控制------哪些节点并行、何时中断、状态如何合并都可以精确定义;CrewAI 更多是黑盒式的委托执行
- 持久化:LangGraph 有内置的 Checkpoint 机制,支持中断恢复和时间旅行;CrewAI 需要额外实现
- 适用场景:CrewAI 适合快速原型和"多角色对话"场景;LangGraph 适合需要精确控制流程的生产级应用
1.4.3 LangGraph vs AutoGen
Microsoft 的 AutoGen 采用了不同的架构哲学------基于消息传递的多 Agent 对话:
python
# AutoGen 的典型用法
assistant = AssistantAgent("assistant", llm_config=llm_config)
user_proxy = UserProxyAgent("user_proxy", code_execution_config={"work_dir": "coding"})
user_proxy.initiate_chat(assistant, message="写一个排序算法")
关键差异:
- 通信模型:AutoGen 基于 Agent 间的直接消息传递(类 Actor 模型);LangGraph 基于共享状态和 Channel(类 BSP 模型)
- 执行模型:AutoGen 的对话流是动态的,由 Agent 的回复驱动;LangGraph 的执行流是预定义的图拓扑,由 Channel 触发驱动
- 确定性:LangGraph 的 BSP 模型保证确定性执行顺序;AutoGen 的消息传递模型中执行顺序取决于 Agent 的实际回复内容
- 可观测性:LangGraph 在每个 step 结束时有明确的状态快照,天然支持调试和回溯;AutoGen 的状态分散在各个 Agent 中
1.4.4 定位总结
CrewAI] --- M[中层抽象
AutoGen] M --- L[低层基础设施
LangGraph] end subgraph 控制力 direction TB LC[低控制力
CrewAI] --- MC[中等控制力
AutoGen] MC --- HC[高控制力
LangGraph] end
| 框架 | 核心范式 | 状态模型 | 持久化 | 适用场景 |
|---|---|---|---|---|
| LangChain | 线性管道 | 无状态传递 | 无内置 | 简单 Chain / RAG |
| LangGraph | 图 + BSP | 共享状态 + Channel | Checkpoint | 复杂工作流 / 生产 Agent |
| CrewAI | 角色扮演 | Agent 内部状态 | 需自行实现 | 多角色协作原型 |
| AutoGen | 消息对话 | 分布式 Agent 状态 | 需自行实现 | 多 Agent 对话 |
1.4.5 框架选择的决策树
选择 AI 编排框架是一个需要根据具体场景来判断的问题。没有"最好的"框架,只有"最适合的"框架。以下决策树可以帮助你做出合理的选择。
在选择框架时,以下场景强烈指向 LangGraph:
- 需要精确控制工作流程:你需要定义确切的节点执行顺序、分支条件和循环逻辑
- 需要持久化和恢复:工作流可能运行数小时甚至数天,需要在进程重启后继续
- 需要人机协作:流程中有人工审批、确认或输入环节
- 需要生产级可靠性:需要重试策略、错误处理、可观测性等工程化特性
- 需要流式输出:需要实时向用户展示 Agent 的思考过程和中间状态
以下场景可能不需要 LangGraph:
- 简单的检索增强生成管道:如果你的应用只是"检索文档然后回答问题"这样的线性流程,LangChain 的管道已经足够优雅
- 快速多角色协作原型:如果你需要快速搭建多个角色互相对话的原型,CrewAI 可能更快上手
- 不需要持久化的一次性对话:如果你的应用不需要保存历史状态,简单的函数调用组合就够了
1.5 为什么要读 LangGraph 源码
1.5.1 文档与现实的鸿沟
LangGraph 的官方文档写得不错,但文档的本质决定了它只能教你"怎么用",而无法告诉你"为什么这样用"。当你遇到以下问题时,文档往往无能为力:
- 为什么我的并行节点写入同一个状态键会报错? 因为默认的
LastValueChannel 不允许多写入,你需要用Annotated加 Reducer - 为什么子图的状态和父图是隔离的? 因为每个子图运行独立的 Pregel 循环,有自己的 Channel 集合
- 为什么
interrupt()只能在 checkpointer 存在时工作? 因为中断后的恢复依赖检查点来重建执行上下文 Command(goto="node_name")和add_edge有什么本质区别? 前者写入内部的branch:to:node_nameChannel,后者在编译时静态绑定
这些问题的答案都藏在源码中。
1.5.2 理解设计决策背后的权衡
更重要的是,读源码能让你理解 LangGraph 团队做出的设计权衡。例如:
为什么 Channel 的更新在步骤结束时才可见? 这是 BSP 模型的核心约束。牺牲了即时性,但换来了并发安全和确定性。在 _algo.py 的 apply_writes 函数中,可以看到所有写入是如何在步骤边界统一应用的。
为什么 StateGraph 的编译过程如此复杂? 因为它需要将开发者友好的高层 API(add_node、add_edge)转换为 Pregel 理解的底层原语(PregelNode、Channel、ChannelWrite)。state.py 的 compile() 方法和 CompiledStateGraph.attach_node() 方法完成了这个关键的转换。
为什么 Checkpoint 要保存 Channel 版本号? 这是为了支持增量更新和高效的触发判断。在 _checkpoint.py 的 create_checkpoint 函数中,可以看到 channel_versions 和 versions_seen 如何协同工作来追踪状态变化。
1.5.3 本书的路线图
本书将按照由浅入深的顺序,带领读者逐层剖析 LangGraph 的设计与实现:
每一章都遵循"源码引导"的原则:先理解问题场景,然后深入源码看 LangGraph 如何解决,最后提炼设计决策和模式。
1.5.4 源码阅读的方法论
阅读 LangGraph 源码有一个推荐的路径------不要试图从头到尾线性阅读,而应该按照数据流来追踪。所谓数据流,就是用户的输入数据从进入图到产生输出的全过程。沿着数据流追踪,你会自然地接触到框架的每一个关键组件。
具体的推荐阅读顺序如下:
- 从
StateGraph.__init__开始 :理解状态模式如何被解析为 Channel。这一步会引导你进入_get_channels和_get_channel函数,理解类型注解到 Channel 实例的映射规则。 - 跟踪
compile()方法 :理解 StateGraph 如何被转换为 Pregel。这一步涉及attach_node、attach_edge、attach_branch等关键方法,揭示了高层声明到底层原语的转换过程。 - 跟踪
invoke()方法 :理解 BSP 循环的完整执行过程。这一步会带你进入PregelLoop、prepare_next_tasks、apply_writes等核心模块,理解超级步骤的执行语义。 - 深入 Channel 实现 :理解不同 Channel 类型的行为差异。特别关注
update方法在不同 Channel 中的语义------LastValue 的单写入约束、BinaryOperatorAggregate 的 Reducer 逻辑、EphemeralValue 的自动清除行为。 - 研究 Checkpoint 机制 :理解状态如何被持久化和恢复。从
create_checkpoint到channels_from_checkpoint,看 Channel 状态如何被序列化和反序列化。
每一步都从"用户可见的行为"出发,向下追踪到"实现细节"。本书就是按照这个路径组织的。在阅读源码的过程中,善用 IDE 的跳转功能和全文搜索,很多看似复杂的调用链在追踪几层之后都会归结到简洁的核心逻辑。
1.6 快速上手:第一个 LangGraph 应用
在结束本章前,让我们用一个最小但完整的例子来感受 LangGraph 的核心工作方式:
python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. 定义状态模式
class State(TypedDict):
messages: Annotated[list[str], lambda old, new: old + new]
step_count: int
# 2. 定义节点函数
def greet(state: State) -> dict:
return {
"messages": [f"你好!这是第 {state['step_count']} 步"],
"step_count": state["step_count"] + 1,
}
def check(state: State) -> dict:
return {
"messages": [f"检查完毕,共 {len(state['messages'])} 条消息"],
"step_count": state["step_count"] + 1,
}
# 3. 构建图
builder = StateGraph(State)
builder.add_node("greet", greet)
builder.add_node("check", check)
builder.add_edge(START, "greet")
builder.add_edge("greet", "check")
builder.add_edge("check", END)
# 4. 编译并执行
graph = builder.compile()
result = graph.invoke({"messages": [], "step_count": 1})
print(result)
# {'messages': ['你好!这是第 1 步', '检查完毕,共 1 条消息'], 'step_count': 3}
在这个简单的例子背后,LangGraph 完成了以下工作:
StateGraph(State)解析TypedDict的类型注解,为messages创建BinaryOperatorAggregateChannel(因为有 Reducer 注解),为step_count创建LastValueChannelcompile()将 StateGraph 转换为CompiledStateGraph(即Pregel子类),为每个节点创建PregelNode,为每条边创建触发 Channelinvoke()启动 Pregel 的 BSP 循环:- Step 1:将输入写入 Channel,触发
greet节点 - Step 2:
greet的输出写入 Channel,触发check节点 - Step 3:
check的输出写入 Channel,没有新的触发,循环结束
- Step 1:将输入写入 Channel,触发
- 从输出 Channel 读取最终状态并返回
这个过程涉及的源码文件跨越了 graph/state.py、pregel/main.py、pregel/_algo.py、pregel/_loop.py、channels/ 目录等多个模块。接下来的各章将逐一展开这些模块的实现细节。
1.7 LangGraph 源码的技术栈
在深入源码之前,有必要了解 LangGraph 使用的核心技术栈,以便读者做好准备:
1.7.1 Python 类型系统
LangGraph 大量使用了 Python 的高级类型特性:
- TypedDict:定义状态模式的主要方式
- Annotated:为状态字段附加 Reducer 函数或 Channel 类型信息
- Generic :
StateGraph[StateT, ContextT, InputT, OutputT]提供类型安全 - Protocol:定义节点函数的多种合法签名
- Literal :条件边的返回类型注解,如
Literal["tools", "__end__"]
理解这些类型特性对于阅读源码至关重要。特别是 Annotated 类型------它是 LangGraph 的 Reducer 机制与 Python 类型系统集成的桥梁。
1.7.2 LangChain 基础
LangGraph 依赖 langchain-core 的以下组件:
- Runnable 接口 :LangGraph 的所有可执行组件(节点、图)都实现
Runnable接口 - RunnableConfig:配置传播机制,携带回调、标签、元数据等运行时上下文
- Message 协议 :
BaseMessage及其子类定义了 LLM 对话的消息格式 - Callback 系统:追踪和监控的基础设施
不需要深入理解 LangChain 的全部细节,但了解 Runnable 接口(invoke/stream/batch)的基本概念会大大降低阅读 LangGraph 源码的门槛。
1.7.3 并发模型
LangGraph 同时支持同步和异步两种执行模式,这对于适应不同的应用场景至关重要。在 Web 服务中,异步模式可以更好地利用服务器资源,支持大量并发请求;而在脚本和测试环境中,同步模式更加直观和易于调试。
- 同步模式 :使用
concurrent.futures.ThreadPoolExecutor并行执行同一步骤内的多个节点。线程池的大小可以配置,默认会根据 CPU 核心数自动调整。 - 异步模式 :使用
asyncio协程并行执行节点。对于包含大量 I/O 操作(如 LLM API 调用、数据库查询)的工作流,异步模式可以显著提升吞吐量。 - BSP 同步屏障 :无论使用哪种并发模式,步骤间的同步语义都是一致的。通过
PregelLoop的 step 边界实现同步屏障,确保一个步骤内的所有节点都完成后才进入下一个步骤。
源码中很多类都有 Sync 和 Async 两个版本(如 SyncPregelLoop / AsyncPregelLoop),它们的核心逻辑相同,只是在 I/O 调用方式上有差异。阅读源码时,建议先理解同步版本的逻辑,然后类比到异步版本------后者的控制流结构完全相同,只是将同步调用替换为了对应的异步调用。
1.8 LangGraph 的版本演进
LangGraph 自发布以来经历了多次重要的架构演进,了解这些演进有助于理解当前设计的由来。
早期版本(0.x 系列) :最初的 LangGraph 将 MessageGraph 作为主要入口,状态被简单地表示为一个消息列表。这种设计适合聊天应用但缺乏通用性。边的触发使用 start:{node} 格式的 Channel,条件边的实现相对简单。
1.0 版本的成熟 :LangGraph 1.0 带来了多项关键改进。StateGraph 成为推荐的图构建入口,支持任意 TypedDict、Pydantic 模型或 dataclass 作为状态模式。Command 类型的引入允许节点通过返回值控制流程方向,减少了对 add_conditional_edges 的依赖。MessageGraph 被标记为弃用,推荐使用 StateGraph(MessagesState) 替代。
1.1 版本的深化 :当前的 1.1.6 版本进一步增强了框架的能力。Runtime 和 context_schema 的引入提供了类型安全的上下文传递机制。defer 参数允许节点延迟到运行即将结束时才执行。边的触发 Channel 统一为 branch:to:{node} 格式,简化了内部逻辑。Overwrite 类型允许跳过 Reducer 直接替换状态值。
这些演进背后有一个清晰的趋势:LangGraph 在持续降低开发者的使用门槛(更友好的 API),同时提升框架的表达力(更丰富的控制流机制)和工程化能力(更完善的持久化、可观测性和错误处理)。
1.9 小结
本章从工程演进的视角出发,阐明了 LangGraph 诞生的必然性与独特定位。核心要点回顾:
-
从 Chain 到 Graph 的演进并非偶然,而是 AI 应用从简单管道走向复杂工作流的必然需求。循环、条件分支、持久化状态------这些在 Chain 模型中难以优雅表达的模式,在 Graph 模型中成为一等公民。当 Agent 需要反复推理、工具需要被循环调用、流程需要在人工审批处暂停时,线性管道模型就显得力不从心了。
-
Pregel/BSP 模型是 LangGraph 运行时的理论基础。通过"超级步骤加同步屏障"的执行模型,LangGraph 在保证并发安全的同时,提供了确定性的执行语义。这种模型的优势在于简化了并发编程的心智负担------开发者不需要考虑锁和竞态条件,只需要声明状态合并策略即可。
-
在框架竞争格局中,LangGraph 定位为低层基础设施------它不预设你的 Agent 应该如何交互,而是提供图拓扑、状态管理和持久化等通用基础能力,让你按需组合。相比 CrewAI 的高层角色扮演抽象和 AutoGen 的消息驱动模型,LangGraph 提供了更精确的控制力和更完善的工程化特性。
-
读源码的价值不在于记住每一行代码,而在于理解设计决策背后的权衡。当你知道"为什么"时,"怎么用"就变成了自然而然的推论。本书将引导读者按照数据流的方向逐步深入,从图定义到编译转换,从 Channel 机制到 BSP 循环,从检查点持久化到中断恢复,形成对 LangGraph 完整而深入的理解。
下一章,我们将从宏观视角俯瞰 LangGraph 的整体架构------包结构、四层架构模型,以及 graph.invoke() 的完整旅程。