前言
随着大语言模型 (LLM) 的爆发,应用架构正在从简单的“提示词工程 (Prompt Engineering)”向复杂的“智能体工程 (Agent Engineering)”演变。LangChain 团队推出的 LangGraph 正是为了应对这一挑战而生。
我们将从最底层的图计算理论讲起,深入源码剖析其运行机制,探讨各种复杂的设计模式,最后落地到生产环境的部署与监控。无论你是初学者还是资深架构师,都能从中获得启发。
第一章:LangGraph 哲学与架构深度剖析
1.1 为什么是 Graph?从 Chain 到 Graph 的演变
在 LangChain 早期,核心抽象是 Chain (链)。SequentialChain 让我们可以将步骤串联起来:A -> B -> C。这种有向无环图 (DAG) 结构非常适合确定性的工作流,比如:
检索 (RAG) -> 格式化 -> 生成。
然而,真实的智能体 (Agent) 往往是非线性的:
- 循环 (Cycles): "思考 -> 行动 -> 观察 -> 思考..." 是一个无限循环,直到任务完成。
- 分支 (Branching): 根据 LLM 的输出,可能跳转到工具 A,也可能跳转到工具 B,或者直接结束。
- 状态保持 (Statefulness): 在多轮对话中,需要持续维护上下文。
LangGraph 引入了 图 (Graph) 的概念来解决这些问题。图由 节点 (Nodes) 和 边 (Edges) 组成,原生支持循环和条件跳转。
1.2 Pregel 模型详解:LangGraph 的计算引擎
LangGraph 的运行机制深受 Google Pregel 图计算模型的启发。这是一种 BSP (Bulk Synchronous Parallel) 模型。理解这一点对于掌握 LangGraph 至关重要。
运行流程图解
- Super-step (超步): 图的执行过程被划分为一系列离散的步骤,称为 Super-step。
- 并行执行: 在同一个 Super-step 中,所有 活跃 (Active) 的节点是 并行运行 的。这意味着如果节点 A 和节点 B 同时收到消息,它们会同时启动(在异步模式下)。
- 消息传递: 节点运行结束后,并不直接调用下一个函数,而是返回一个 状态更新 (State Update)。这个更新被视为一条“消息”。
- Barrier (同步点): 系统等待当前 Super-step 的所有节点运行完毕。然后应用所有的状态更新(执行 Reducer)。
- 控制流决策: 系统根据更新后的状态,评估所有的 条件边 (Conditional Edges),决定下一轮 Super-step 哪些节点将变为“活跃”。
核心源码逻辑 (伪代码):
while not should_terminate:
# 1. 找出当前活跃的节点 (根据上一轮的边)
active_nodes = get_active_nodes()
# 2. 并行执行这些节点
results = parallel_execute(active_nodes)
# 3. 应用状态更新 (Reducer)
current_state = apply_reducers(current_state, results)
# 4. 持久化检查点 (Checkpointer)
save_checkpoint(current_state)
# 5. 决定下一轮的路由
next_nodes = evaluate_edges(current_state)
1.3 核心对象生命周期
StateGraph: 这是图的定义阶段。你在这里定义 Schema、注册节点、添加边。此时图还未运行,只是一个蓝图。CompiledGraph(App): 调用graph.compile()后生成的对象。它是一个Runnable,具备了运行时的能力(如检查点管理、中断处理)。Runtime: 当你调用app.invoke()或app.stream()时,LangGraph 会实例化一个运行时环境来管理具体的执行。
第二章:状态管理 (State Management) 的艺术
2.1 Schema 定义:TypedDict vs Pydantic
状态 (State) 是 LangGraph 中最重要的概念。它定义了图的“内存”。
方案 A: TypedDict (推荐)
最轻量,性能最好。适合大多数场景。
from typing import TypedDict, Annotated, List
import operator
class AgentState(TypedDict):
# 必须使用 Annotated 来指定 Reducer,否则默认是覆盖行为
messages: Annotated[List[str], operator.add]
user_id: str # 默认覆盖
count: int # 默认覆盖
方案 B: Pydantic BaseModel
提供运行时数据验证。如果你需要确保 LLM 输出的数据格式严格符合要求,可以使用 Pydantic。但会有微小的性能开销。
from pydantic import BaseModel, Field
class AgentState(BaseModel):
messages: List[str] = Field(default_factory=list)
# Pydantic 模式下,Reducer 的定义方式略有不同,通常在 add_node 时处理
2.2 Reducer 详解与源码分析
Reducer 决定了新产生的状态如何合并到旧状态中。
operator.add 原理
# 实际上就是 list 的 extend 或 + 操作
def add(a, b):
return a + b
当节点返回 {"messages": [new_msg]} 时,LangGraph 执行 old_state["messages"] + [new_msg]。
自定义 Reducer 实战:去重消息列表
有时 LLM 可能会重复生成消息,或者我们需要保持消息窗口大小(只保留最近 10 条)。
def deduplicate_reducer(current: List[str], new: List[str]) -> List[str]:
# 合并并去重
combined = current + new
return list(set(combined))
class State(TypedDict):
tags: Annotated[List[str], deduplicate_reducer]
自定义 Reducer 实战:保留最近 N 条 (Sliding Window)
def sliding_window_reducer(current: list, new: list) -> list:
combined = current + new
return combined[-10:] # 只保留最后10条
class State(TypedDict):
messages: Annotated[List[BaseMessage], sliding_window_reducer]
2.3 状态隔离设计模式 (Input/Output/Internal Schema)
在复杂的图中,你可能不希望向外部暴露所有的内部状态(如中间推理步骤、临时变量)。
class InputState(TypedDict):
question: str
class OutputState(TypedDict):
answer: str
class InternalState(TypedDict):
question: str
answer: str
scratchpad: List[str] # 内部草稿,不对外暴露
retry_count: int
# 定义图时指定
workflow = StateGraph(input=InputState, output=OutputState, state=InternalState)
def node_a(state: InternalState):
# 可以访问所有字段
return {"scratchpad": ["step 1..."]}
这样,当用户调用 app.invoke({"question": "..."}) 时,最终返回的字典只会包含 answer 字段,scratchpad 会被过滤掉。
第三章:节点 (Nodes) 与 边 (Edges) 编程指南
3.1 Node 的多种形态
节点本质上是 Callable[[State], Update]。
1. 同步函数
def my_node(state: State):
return {"key": "value"}
2. 异步函数 (Async)
LangGraph 原生支持异步。如果你的节点涉及 IO (如 LLM 调用、数据库查询),务必使用 async def。
async def my_node(state: State):
response = await llm.ainvoke(...)
return {"messages": [response]}
3. 返回 Command 对象 (v0.2+) 这是最新的控制流方式,允许节点同时更新状态并决定下一步跳转,跳过 Edge 的计算。
from langgraph.types import Command
def router_node(state: State):
if "error" in state:
# 直接跳转到 'handle_error' 节点,并更新状态
return Command(goto="handle_error", update={"status": "failed"})
return Command(goto="next_step")
3.2 Edge 的高级用法与动态路由
add_conditional_edges 是实现 Agent 逻辑的核心。
参数详解:
source: 出发节点。path: 路由函数,接收 State,返回一个字符串 (目标节点名)。path_map(可选): 字典,将路由函数的返回值映射到实际的节点名。
实战:基于 LLM 的动态路由
def route_logic(state: State) -> str:
last_msg = state["messages"][-1]
if not last_msg.tool_calls:
return "end" # 映射到 END
return "continue" # 映射到 tools
workflow.add_conditional_edges(
"agent",
route_logic,
{
"end": END,
"continue": "tools"
}
)
3.3 Map-Reduce 模式与 Send API
当我们需要对列表中的每个元素并行执行某个操作时(例如:在这个主题列表中的每个主题下生成一篇博客),普通的 Edge 无法做到。这需要 Map-Reduce 模式。
LangGraph 提供了 Send API 来实现动态分支。
from langgraph.types import Send
def continue_to_jokes(state: State):
# Map 步骤:为每个 subject 生成一个 Send 对象
return [
Send("generate_joke", {"subject": s})
for s in state["subjects"]
]
# 注册条件边
workflow.add_conditional_edges("map_node", continue_to_jokes)
注意: 这里的 generate_joke 节点会并行运行多次,每次接收不同的 state。
第四章:人机协同 (Human-in-the-Loop) 终极指南
4.1 中断机制 (Interrupts) 的底层原理
当你调用 workflow.compile(interrupt_before=["node_a"]) 时,LangGraph 会在进入 node_a 之前抛出一个特殊的异常 GraphInterrupt。
- Runtime 捕获这个异常。
- 保存当前的 Checkpoint。
- 停止执行。
这就解释了为什么必须配置 Checkpointer 才能使用中断功能。没有 Checkpointer,程序停止后状态就丢失了,无法恢复。
4.2 Command 模式详解
在 v0.2 版本中,interrupt 函数结合 Command 对象提供了更细粒度的控制。
from langgraph.types import interrupt
def human_node(state: State):
# 程序在此处挂起
# 这里的 value 将是用户 resume 时提供的值
user_feedback = interrupt("请审批...")
return {"feedback": user_feedback}
恢复执行 (Resume):
# 获取当前暂停的线程配置
config = {"configurable": {"thread_id": "123"}}
# 这里的 value 会被传递给上面的 user_feedback 变量
app.invoke(
Command(resume="同意"),
config=config
)
4.3 实战:构建企业级审批流 Agent
场景:用户要求转账 -> Agent 生成工具调用 -> 系统暂停 -> 管理员审批 -> 系统恢复 -> 执行转账。
代码实现片段:
def human_approval(state: State):
last_msg = state["messages"][-1]
# 只有当有工具调用时才中断
if not last_msg.tool_calls:
return
# 抛出中断,把工具调用详情返回给前端
approval = interrupt({
"type": "tool_approval",
"tool_call": last_msg.tool_calls[0]
})
# 如果管理员拒绝
if approval["status"] == "rejected":
# 我们可以修改消息历史,假装工具调用报错了,让 LLM 重试
return {
"messages": [
ToolMessage(
tool_call_id=last_msg.tool_calls[0]["id"],
content=f"Error: User rejected operation. Reason: {approval['reason']}"
)
]
}
# 如果同意,什么都不做,继续执行(进入 ToolNode)
return
第五章:持久化 (Persistence) 与 记忆 (Memory)
5.1 Checkpointer 深度对比
| Checkpointer | 存储介质 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| MemorySaver | Python 内存 (Dict) | 测试、Demo、本地脚本 | 速度极快,无依赖 | 重启即丢失,无法横向扩展 |
| SqliteSaver | SQLite 文件 | 单机生产、桌面应用 | 持久化,轻量 | 并发写入性能差,单点故障 |
| PostgresSaver | PostgreSQL 数据库 | 企业级生产环境 | 高并发,可靠,支持 JSONB | 需要维护数据库实例 |
PostgresSaver 配置示例:
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool
pool = ConnectionPool("postgresql://user:pass@localhost:5432/db")
checkpointer = PostgresSaver(pool)
# 首次使用需初始化表结构
checkpointer.setup()
app = workflow.compile(checkpointer=checkpointer)
5.2 时间旅行 (Time Travel) 调试法
Checkpointer 保存了每一步的状态快照。我们可以像 git 一样回滚。
场景:Agent 陷入死循环,一直在重复同样的错误工具调用。
-
查看历史:
history = list(app.get_state_history(config)) # history[0] 是最新状态, history[1] 是上一步... -
修改状态 (Forking): 我们不仅要回滚,还要“篡改”记忆,告诉 Agent 它之前做错了,引导它走正确的路。
# 假设我们回滚到倒数第二步,并插入一条 SystemMessage 提示 config_to_fork = history[2].config app.update_state( config_to_fork, {"messages": [SystemMessage(content="注意:不要使用 Search 工具,直接回答。")]}, ) # 此时,LangGraph 会基于这个旧的 Checkpoint 创建一个新的分支
5.3 Long-term Memory (Store) 实战
Checkpointer 只管当前会话 (Session)。如果用户第二天再来,换了一个 thread_id,之前的记忆就没了。
Store 接口用于解决跨会话记忆。
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 在节点中使用 Store
def memory_node(state: State, store: InMemoryStore):
user_id = state["user_id"]
# 读取用户偏好
prefs = store.get("user_prefs", user_id)
# 写入新的记忆
store.put("user_prefs", user_id, {"theme": "dark", "language": "zh"})
第六章:多智能体 (Multi-Agent) 架构模式
6.1 Supervisor (主管模式)
核心思想: 星型拓扑。一个中心节点 (Supervisor) 连接所有 Worker 节点。
适用场景: 任务复杂,需要统筹规划。例如:写一个游戏,需要策划、程序、美术协同。
实现技巧:
- Supervisor 应该是一个专门微调过的 LLM,或者使用强大的 Prompt,明确它的职责是“路由”而非“执行”。
- 使用
structured_output强制 Supervisor 输出结构化的路由指令 ({"next_worker": "Coder", "instructions": "..."})。
6.2 Network (协作模式)
核心思想: 网状拓扑。没有领导,每个 Agent 知道它可以把任务转交给谁。
适用场景: 流程相对固定,或者 Agent 之间职责边界非常清晰。例如:客服系统。
GeneralBot遇到技术问题 -> 转交TechSupportBot。TechSupportBot遇到退款问题 -> 转交BillingBot。
实现: 使用 Command(goto="TechSupportBot") 直接跳转。
6.3 Hierarchical (分层模式)
核心思想: 树状拓扑。父图调用子图。
代码实现:
# 1. 定义子图
research_graph = StateGraph(ResearchState)
# ... 添加节点 ...
research_app = research_graph.compile()
# 2. 在父图中作为节点调用
def call_researcher(state: MainState):
# 转换状态
input_state = {"query": state["messages"][-1].content}
# 调用子图
result = research_app.invoke(input_state)
# 返回结果
return {"messages": [result["summary"]]}
main_graph.add_node("researcher", call_researcher)
这种模式的好处是封装性。父图不需要知道子图内部有多少个节点,也不需要关心子图的中间状态。
第七章:流式输出 (Streaming) 与 异步并发
7.1 三种流式模式详解
构建 Chatbot 时,流式输出是必须的。LangGraph 提供了精细的控制。
-
stream_mode="values":- 每次图中的任何节点完成并更新状态后,通过流发送完整的最新状态。
- 适合:前端需要全量刷新 UI 的场景。
-
stream_mode="updates":- 只发送增量。即某个节点返回的那个字典。
- 适合:前端进行增量渲染,节省带宽。
-
stream_mode="messages"(V2):- 自动聚合 LLM 的流式 Token。即使你在 Node 内部调用 LLM,LangGraph 也会帮你把 Token 像水流一样通过管道传出来。
- 这是最适合 Chatbot 的模式。
7.2 Token 级流式最佳实践
在 Node 内部,你应该只返回最终结果。但通过配置 .astream_events,你可以捕获中间过程。
# 客户端代码
async for event in app.astream_events(inputs, version="v2"):
kind = event["event"]
# 捕获 LLM 的 token 生成
if kind == "on_chat_model_stream":
content = event["data"]["chunk"].content
if content:
print(content, end="")
# 捕获工具调用的开始
elif kind == "on_tool_start":
print(f"\n正在调用工具: {event['name']}...")
第八章:生产环境部署与运维
8.1 部署方案
-
LangGraph Cloud (推荐):
- LangChain 官方托管服务。
- 自动将你的 Graph 变成高并发 API。
- 内置持久化、Task Queue (异步任务)、Cron Jobs (定时任务)。
- 只需要一个
langgraph.json配置文件。
-
自托管 (FastAPI + Docker):
- 你需要自己编写 API 包装器。
- 需要自己维护 Postgres 数据库作为 Checkpointer。
- 难点: 处理并发冲突(Optimistic Locking)、WebSocket 连接管理。
8.2 监控与评估 (LangSmith)
LangGraph 与 LangSmith 是天作之合。
- Trace: 在 LangSmith 后台,你可以看到完整的图执行可视化路径。
- 哪个节点耗时最长?
- 哪个节点报错了?
- Token 消耗统计。
- Evaluation:
- 可以把历史上的 Trace 转换为测试集。
- 运行回归测试,确保修改 Graph 结构后没有破坏原有逻辑。
结语
LangGraph 不仅仅是一个库,它是一种构建复杂系统的思维方式。通过将系统分解为 Node 和 Edge,我们将不可控的 LLM 关进了可控的流程笼子里。
掌握了 LangGraph,你就掌握了构建 Agentic Application 的钥匙。希望这份指南能成为你开发路上的得力助手。