文章目录
-
- [1. LangGraph 解决什么问题](#1. LangGraph 解决什么问题)
- [2. 核心心智模型:State、Node、Edge](#2. 核心心智模型:State、Node、Edge)
- [3. 第一个可运行的状态图](#3. 第一个可运行的状态图)
-
- [3.1 安装](#3.1 安装)
- [3.2 定义状态、节点和边](#3.2 定义状态、节点和边)
- [3.3 可视化图结构](#3.3 可视化图结构)
- [4. State Schema 与 Reducer:状态设计的关键](#4. State Schema 与 Reducer:状态设计的关键)
-
- [4.1 三种常见 Schema](#4.1 三种常见 Schema)
- [4.2 默认覆盖与自定义合并](#4.2 默认覆盖与自定义合并)
- [4.3 需要覆盖 reducer 时使用 `Overwrite`](#4.3 需要覆盖 reducer 时使用
Overwrite)
- [5. 条件分支、并行与动态路由](#5. 条件分支、并行与动态路由)
-
- [5.1 条件边](#5.1 条件边)
- [5.2 用 `Send` 做 map-reduce](#5.2 用
Send做 map-reduce) - [5.3 用 `Command` 同时更新状态并跳转](#5.3 用
Command同时更新状态并跳转)
- [6. Graph API 还是 Functional API](#6. Graph API 还是 Functional API)
- [7. 持久化:让 Agent 可以暂停、恢复和记忆](#7. 持久化:让 Agent 可以暂停、恢复和记忆)
-
- [7.1 Checkpointer 与 Store 的区别](#7.1 Checkpointer 与 Store 的区别)
- [7.2 短期记忆与长期记忆](#7.2 短期记忆与长期记忆)
- [8. Human-in-the-loop:在关键动作前暂停](#8. Human-in-the-loop:在关键动作前暂停)
- [9. 流式输出与可观测性](#9. 流式输出与可观测性)
- 结语
LangGraph 是 LangChain 生态中的低层编排框架与运行时。它不替你规定提示词或 Agent 架构,而是提供状态、节点、边、持久化、流式输出和人工介入等基础能力,让复杂工作流可以被显式描述、检查和恢复。
1. LangGraph 解决什么问题
一个简单的 LLM 调用可以写成"输入提示词,得到输出"。但真实应用通常还包含路由、工具调用、重试、并行、审核和跨轮次记忆。例如,一个研究助手可能需要:
- 判断问题属于哪个主题;
- 并行搜索多个来源;
- 汇总并检查证据;
- 在发送报告前请求人工确认;
- 进程重启后从上次成功的位置继续。
如果这些逻辑散落在一个巨大的 while 循环中,状态、异常和恢复会很难维护。LangGraph 把流程拆成可组合的节点和边,并在每个步骤之间保存状态快照。
官方对生态的定位可以概括为:LangChain 提供模型、工具和高层 Agent 抽象;LangGraph 提供底层编排运行时;LangSmith 负责追踪、评估和部署。刚开始构建普通工具调用 Agent 时,可以先使用 LangChain 的 create_agent;当需要精细控制流程、确定性步骤与模型步骤混合、持久化或人工介入时,再使用 LangGraph。
2. 核心心智模型:State、Node、Edge
LangGraph 图由三个基本要素组成:
- State(状态):图运行过程中的共享数据结构,保存输入、上下文和中间结果。
- Node(节点):一个可调用对象,读取当前状态并返回局部更新,通常是普通 Python 函数。
- Edge(边):定义节点之间的流转,可以是固定边,也可以根据状态进行条件路由。
最小拓扑如下:
text
START --> node_1 --> node_2 --> END
LangGraph 的运行时以 superstep(超步)推进:先根据边决定本轮要运行的节点,再执行节点,最后统一合并节点产生的更新。并行节点读取的是同一个本轮状态快照,它们的结果会在提交阶段通过对应的 reducer 合并。
3. 第一个可运行的状态图
3.1 安装
bash
pip install -U langgraph
3.2 定义状态、节点和边
TypedDict 轻量、清晰,也最贴近"节点返回部分状态更新"的编程方式。没有特殊校验需求时,推荐优先使用它。
python
from operator import add
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, START, StateGraph
class OverallState(TypedDict):
# 每次节点返回的日志都会追加到列表,而不是覆盖旧值
logs: Annotated[list[str], add]
path: str
def node_1(state: OverallState):
return {
"logs": ["node_1 完成"],
"path": state["path"] + " -> node_1",
}
def node_2(state: OverallState):
return {
"logs": ["node_2 完成"],
"path": state["path"] + " -> node_2",
}
builder = StateGraph(OverallState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
result = graph.invoke({"logs": [], "path": "START"})
print(result)
结果类似:
python
{
"logs": ["node_1 完成", "node_2 完成"],
"path": "START -> node_1 -> node_2",
}
构建图始终分成三步:调用 StateGraph 定义图、调用 compile() 编译、调用 invoke() 或 stream() 执行。START 和 END 是官方推荐的入口和终点写法。
3.3 可视化图结构
编译后的图可以导出 Mermaid:
python
print(graph.get_graph().draw_mermaid())
在 Jupyter 中可直接展示 PNG:
python
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
可视化对调试条件分支、循环和并行路径尤其有帮助。生成 PNG 时使用的是 Mermaid 渲染服务,网络受限时优先导出 Mermaid 源码或使用本地渲染方案。
4. State Schema 与 Reducer:状态设计的关键
4.1 三种常见 Schema
LangGraph 支持 TypedDict、dataclass 和 Pydantic 模型:
| 方式 | 特点 | 适用情况 |
|---|---|---|
TypedDict |
轻量,仅提供类型提示 | 大多数工作流的默认选择 |
dataclass |
以属性方式访问字段 | 希望使用对象语义的项目 |
| Pydantic | 运行时校验更强 | 输入边界需要严格验证 |
大型应用还可以拆分输入、输出和私有状态:
python
class InputState(TypedDict):
question: str
class OverallState(InputState):
draft: str
internal_notes: str
class OutputState(TypedDict):
answer: str
这样可以限制外部输入和最终输出的字段,同时允许中间节点使用私有字段。状态 Schema 描述的是"哪些字段可以进入图和节点",节点返回的通常只是局部更新,不需要重建完整状态。
4.2 默认覆盖与自定义合并
没有 reducer 的字段采用覆盖策略:新值直接替换旧值。需要追加或聚合时,用 Annotated[类型, reducer] 声明合并函数。
python
from operator import add
from typing import Annotated
class State(TypedDict):
answer: str # 默认:覆盖
logs: Annotated[list[str], add] # 列表:追加合并
Reducer 是一个二元函数:(left, right) -> merged。left 是当前值,right 是节点返回的新值。并行节点同时更新同一字段时,reducer 决定如何汇总结果,因此它必须满足类型兼容,并尽量保持确定性。
对消息历史,应使用官方的 add_messages,而不是简单的列表拼接:它会根据消息 ID 追加新消息,并用相同 ID 的新消息替换旧消息。
python
from langgraph.graph import MessagesState
class ChatState(MessagesState):
user_id: str
MessagesState 已经预置了 messages 字段和消息 reducer,适合聊天机器人、工具调用和 Agent 工作流。
4.3 需要覆盖 reducer 时使用 Overwrite
追加日志、消息历史很常见,但有时需要把聚合字段整体替换,例如加载一份人工编辑后的完整文档。此时可以使用 LangGraph 提供的 Overwrite 语义,明确表达"绕过 reducer,直接写入新值",避免依赖隐含的覆盖顺序。
5. 条件分支、并行与动态路由
5.1 条件边
固定边适合线性流程;条件边根据状态选择下一个节点:
python
from typing import Literal
from typing_extensions import TypedDict
class RouteState(TypedDict):
question: str
def route(state: RouteState) -> Literal["answer", "fallback"]:
return "answer" if state["question"] else "fallback"
builder.add_conditional_edges(
"classify",
route,
{"answer": "answer", "fallback": "fallback"},
)
路由函数只负责决定去哪里;业务结果仍由节点通过状态更新返回。这样能让控制流和数据处理各自清晰。
5.2 用 Send 做 map-reduce
当待处理项目数量运行时才知道,或者需要为每个项目创建独立输入,可以从路由函数返回多个 Send:
python
from typing_extensions import TypedDict
from langgraph.types import Send
class ResearchState(TypedDict):
topics: list[str]
def fan_out(state: ResearchState):
return [
Send("research_one", {"topic": topic})
for topic in state["topics"]
]
builder.add_conditional_edges("start_research", fan_out)
多个 research_one 会并行执行,结果再通过 reducer 汇总。聚合字段应提前定义可结合的 reducer,例如 Annotated[list[str], add]。不要依赖并行完成的先后顺序来决定业务结果。
5.3 用 Command 同时更新状态并跳转
当一个节点既要写状态又要动态决定下一节点,可以返回 Command:
python
from typing import Literal
from langgraph.types import Command
from typing_extensions import TypedDict
class ApprovalState(TypedDict):
approved: bool
status: str
def decide(state: ApprovalState) -> Command[Literal["publish", "revise"]]:
if state["approved"]:
return Command(update={"status": "ready"}, goto="publish")
return Command(update={"status": "needs_revision"}, goto="revise")
如果只是路由,优先使用条件边;只有在"状态更新和路由必须绑定"时才使用 Command,这样图的拓扑更容易阅读。
6. Graph API 还是 Functional API
两套 API 共享同一个运行时,都支持检查点、恢复和人工介入,但表达方式不同:
| 对比项 | Graph API | Functional API |
|---|---|---|
| 核心抽象 | StateGraph、节点、边 |
@entrypoint、@task |
| 控制流 | 显式声明图结构 | 使用普通 if、循环和函数调用 |
| 可视化 | 强 | 弱 |
| 更适合 | 复杂分支、并行、团队协作 | 线性流程、已有过程式代码、快速原型 |
Functional API 的最小形态如下:
python
from langgraph.func import entrypoint, task
@task
def load_data(topic: str) -> str:
return f"data for {topic}"
@entrypoint()
def workflow(topic: str) -> str:
data = load_data(topic).result()
return data.upper()
需要注意:为了支持检查点和恢复,entrypoint 的输入输出以及 task 的输出应当是可序列化数据;外部 API 调用、写文件、发邮件等副作用应封装在 task 中,并尽量设计成幂等操作。
7. 持久化:让 Agent 可以暂停、恢复和记忆
7.1 Checkpointer 与 Store 的区别
官方将持久化分成两层:
| 组件 | 保存内容 | 常见用途 |
|---|---|---|
| Checkpointer | 某个 thread 的图状态快照 | 短期记忆、故障恢复、人工介入、时间旅行 |
| Store | 应用自定义的长期键值数据 | 用户偏好、跨会话资料、业务知识 |
启用 checkpointer 后,调用时必须提供稳定的 thread_id:
python
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-42"}}
graph.invoke(input_state, config)
InMemorySaver 适合本地开发和测试,进程重启后数据会丢失。生产环境应根据部署方式选择 SQLite、PostgreSQL 等持久化实现,并规划 checkpoint 的清理策略,避免长对话无限增长。
7.2 短期记忆与长期记忆
同一个 thread_id 表示同一条会话线程,适合保存当前任务的工作上下文;跨线程共享的用户偏好、账号资料等,应放到 Store,而不是把所有数据塞进图状态。状态越大,序列化、传输和模型上下文成本越高。
8. Human-in-the-loop:在关键动作前暂停
interrupt() 可以暂停图执行,并把一个 JSON 可序列化的 payload 暴露给调用方。用户做出决定后,通过同一个 thread_id 使用 Command(resume=...) 继续:
python
from typing import Literal
from typing_extensions import TypedDict
from langgraph.types import Command, interrupt
class ReviewState(TypedDict):
recipient: str
content: str
def approval(state: ReviewState) -> Command[Literal["send", "cancel"]]:
approved = interrupt({
"question": "是否发送邮件?",
"recipient": state["recipient"],
"preview": state["content"],
})
return Command(goto="send" if approved else "cancel")
# 第一次调用:运行到 interrupt 后暂停
result = graph.invoke(input_state, config)
# 人工确认后恢复
result = graph.invoke(Command(resume=True), config)
人工介入必须配合 checkpointer。节点恢复时会从节点边界重新执行,因此发送邮件、扣款等副作用必须封装为幂等任务,避免恢复导致重复执行。循环等待输入时,不要在同一个节点里用 while True 反复调用 interrupt();让每次恢复对应一次清晰的节点执行,更容易测试和追踪。
9. 流式输出与可观测性
invoke() 适合一次性拿到最终结果;长流程或聊天 UI 通常使用 stream():
python
for chunk in graph.stream(
{"messages": [{"role": "user", "content": "你好"}]},
stream_mode="updates",
):
print(chunk)
常用模式包括:
updates:每个节点产生的状态更新;values:每个步骤后的完整状态;messages:模型消息的增量 token;custom:节点主动写出的自定义进度信息。
生产环境建议接入 LangSmith 追踪节点输入输出、状态变化和耗时。先让每个节点职责单一、输出结构稳定,再通过追踪定位慢调用、错误路由和不必要的模型调用。
结语
LangGraph 的核心不是某个"万能 Agent 模板",而是一套可靠的状态化编排模型:状态承载上下文,节点执行局部工作,边控制流转,reducer 决定并发更新如何合并,checkpointer 让执行可以恢复,interrupt 让人可以在关键点介入。
当应用从一次性问答走向长流程、多步骤和可审计的 Agent 时,这些原语比继续堆叠提示词更重要。先把状态和控制流设计清楚,再把模型放进需要推理的节点,通常能得到更容易调试、测试和上线的系统。