我挖掘了一个巨牛的 人工智能 学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站
如果你刚开始学习 LangGraph,大概率会遇到这样一种感觉:
State、Node、Edge、START、END、compile......每个单词单独看都不难,但真正让自己从零写一个完整 Graph 时,却很容易不知道该从哪里开始。
尤其是第一次看到这种代码:
graph = StateGraph(State)
graph.add_node(...)
graph.add_edge(...)
app = graph.compile()
app.invoke(...)
很多人都会有一种非常熟悉的感觉:
每一行代码好像都认识,但它们为什么要这样组合起来,我并没有真正理解。
我刚开始学习 LangGraph 时也遇到了类似的问题。
比如:
- 为什么一定要先定义 State?
- Node 节点本质上到底是什么?
- Edge 和条件 Edge 有什么区别?
- 为什么 Node 里面一般都要
return一个字典? - 为什么不能直接修改
state? compile()明明是"编译",为什么和 C++ / Java 的编译完全不一样?
所以这篇文章不准备单纯带你"复制代码跑通案例"。
我想通过一个非常直观的 「快递配送系统」,把 LangGraph 最核心的一套开发流程完整串起来。
我们最终要实现这样一条业务流程:
START → 揽收站 → 分拣中心 → 普通配送 / 加急配送 → 派送站 → END
其中:
普通包裹走陆运;
加急包裹走空运;
整个配送过程中,包裹状态、配送历史和运输里程都会保存在全局 State 中不断更新。
通过这个案例,你会完整理解 LangGraph 最基本的开发流程:
定义 State → 定义 Node → 注册节点 → 添加 Edge → 条件分支 → compile → invoke
如果把 LangGraph 最核心的思想压缩成一句话,其实就是:
State 负责保存数据,Node 负责处理数据,Edge 负责决定下一步去哪里。
下面,我们从最基础的 Graph API 开始。
一、先搞懂 LangGraph 的标准开发流程
构建一个 LangGraph 图,实际上存在一套非常固定的开发流程。
整体可以概括为 5 个步骤:
- 定义全局
State - 创建
StateGraph - 定义并注册
Node - 添加
Edge - 调用
compile()编译并执行 Graph
也可以进一步记成一条非常简单的路线:
State → Graph → Node → Edge → Compile → Invoke
其中:
State:保存整个工作流共享的数据Node:执行具体业务逻辑Edge:控制节点之间如何跳转compile():把前面定义好的图组装成可以执行的工作流invoke():传入初始状态,真正运行整张图
理解这条主线以后,再看 LangGraph 的代码会轻松很多。
二、一个容易误解的地方:LangGraph 的 compile 到底在"编译"什么?
第一次看到:
delivery_system = delivery.compile()
如果之前学过 Java 或 C++,很容易下意识把这个 compile() 理解成传统意义上的"代码编译"。
但实际上,两者完全不是一回事。
LangGraph 中的 compile() 并不会:
- 把 Python 代码翻译成机器码
- 生成
.exe - 生成 JVM 字节码
- 做传统编译器意义上的代码链接
它真正做的事情更接近于:
把我们前面定义好的 State、Node、Edge 和条件路由组装成一个真正可以运行的工作流对象。
也就是说:
Java / C++ 的编译主要解决的是:
代码怎么变成计算机能够执行的程序。
而 LangGraph 的 compile 主要解决的是:
你定义的这张图,怎么变成一个真正可以执行的工作流。
理解这一点之后,后面的代码就不会那么容易混淆了。
三、完整实战:用 LangGraph 模拟一个快递配送系统
接下来开始正式写代码。
我们设计一个简单的快递配送系统。
每一个包裹都有:
- 包裹编号
- 始发地
- 目的地
- 配送优先级
- 当前配送状态
- 历史流转记录
- 累计运输距离
整个流程如下:
包裹进入系统
↓
揽收站
↓
分拣中心
↓
根据 priority 判断:
普通 → 标准陆运
或者:
加急 → 空运加急
↓
派送站
↓
签收完成
3.1 第一步:定义 State,保存整张图共享的数据
LangGraph 中的 State 可以理解为:
整个工作流所有节点共享的一份数据。
后续不管包裹经过揽收站、分拣中心还是派送站,大家看到的都是同一个 State 中的数据。
因此,我们首先定义:
python
from typing import TypedDict, Annotated
from operator import add
class PackageState(TypedDict):
package_id: str
origin: str
destination: str
status: str
history: Annotated[list[str], add]
total_distance: Annotated[int, add]
priority: str
这里有一个非常重要的知识点:
State 中不同字段的更新方式并不完全一样。
例如:
status: str
默认采用覆盖更新。
旧值:
"待揽收"
节点返回:
"已揽收"
最终就会直接变成:
"已揽收"
而:
history: Annotated[list[str], add]
则代表使用 operator.add 作为 reducer。
因此:
["北京揽收"]
再返回:
["进入上海分拣中心"]
不会覆盖前面的数据,而会变成:
[
"北京揽收",
"进入上海分拣中心"
]
这也是 LangGraph State 中非常重要的一个设计:
不同字段,可以定义不同的状态合并规则。
3.2 第二步:创建 StateGraph
State 定义完成之后,创建整张图:
from langgraph.graph import StateGraph, START, END
delivery = StateGraph(PackageState)
这里的:
PackageState
相当于告诉 LangGraph:
接下来这张 Graph 中流转的数据,都按照
PackageState这个结构管理。
需要注意:
此时我们只是创建了一个图构建器。
里面:
没有节点;
没有边;
没有业务流程。
因此现在还不能直接执行。
3.3 第三步:定义 Node 节点
LangGraph 中的 Node,本质上就是一个 Python 函数。
例如揽收站:
def receive_package(state: PackageState):
origin = state["origin"]
return {
"status": "已揽收",
"history": [f"在{origin}揽收"]
}
它做的事情非常简单:
读取当前 State 中的:
origin
然后告诉 LangGraph:
这一次节点执行完以后,需要更新两个字段:
status
history
后面的分拣、运输、派送节点,本质上也都是同样的逻辑。
四、我学习时踩到的第一个坑:Node 为什么一定要 return?
这个问题我第一次写 LangGraph 时也比较疑惑。
例如:
def receive_package(state: PackageState):
origin = state["origin"]
return {
"status": "已揽收",
"history": [f"在{origin}揽收"]
}
为什么一定要:
return
能不能直接:
state["status"] = "已揽收"
然后不返回?
理解这个问题,其实就理解了 LangGraph State 更新机制很重要的一部分。
Node 更推荐采用这样的思维方式:
读取旧 State → 执行业务逻辑 → 返回本次需要更新的数据。
例如:
return {
"status": "已揽收"
}
相当于告诉 LangGraph:
我这个节点执行完成之后,请把
status更新成"已揽收"。
然后 LangGraph 会再根据这个字段对应的 reducer,完成真正的状态合并。
因此,写 Node 时不需要把完整 State 全部重新返回。
比如当前 State 有:
package_id
origin
destination
status
history
total_distance
priority
揽收节点只修改:
status
history
那么只需要:
return {
"status": "已揽收",
"history": ["完成揽收"]
}
其他字段仍然会继续保留。
如果这个节点只是打印日志,不修改任何 State,可以:
def log_node(state: PackageState):
print(state["package_id"])
return {}
所以我们可以把 Node 的写法总结成一句话:
函数体负责"计算",return 负责告诉 LangGraph"哪些 State 需要更新"。
五、Node 写好了,为什么还要 add_node?
例如我们已经定义:
def receive_package(state):
...
但这个时候,它还只是一个普通 Python 函数。
LangGraph 并不知道:
它是不是 Graph 的节点;
节点叫什么;
其他节点应该怎么连接它。
因此还需要:
delivery.add_node("揽收站", receive_package)
这里实际上完成了一次绑定:
节点名称:揽收站
↓
执行函数:receive_package
后面 Edge 使用的:
"揽收站"
实际上就是这个节点的唯一标识。
六、Edge:决定数据下一步去哪里
节点定义完以后,还需要告诉 LangGraph:
每个节点执行完成之后,下一步应该去哪个节点?
这就是 Edge。
例如:
delivery.add_edge(START, "揽收站")
delivery.add_edge("揽收站", "分拣中心")
表示:
START
↓
揽收站
↓
分拣中心
这种路线永远不会变化,因此属于:
固定边。
但是实际业务中,并不是所有流程都是固定的。
例如:
普通包裹:
分拣中心
↓
标准配送
加急包裹:
分拣中心
↓
加急配送
这时候就需要:
add_conditional_edges()
根据当前 State 动态决定下一跳。
这也是 LangGraph 相比普通顺序函数调用非常重要的能力之一:
Graph 的执行路径,可以由当前状态动态决定。
七、最后一步:compile + invoke
所有 State、Node、Edge 都设计好以后:
delivery_system = delivery.compile()
到这里,前面的"图设计"才真正变成一个:
可以运行的 LangGraph 工作流。
之后:
result = delivery_system.invoke(package)
就会从:
START
开始运行。
然后依次经过:
揽收
↓
分拣
↓
路线判断
↓
运输
↓
派送
↓
END
整个过程中,同一个 State 会不断被各个节点读取和更新。
写在最后
如果第一次学习 LangGraph 时感觉概念特别多,其实不用急着记各种 API。
先把最核心的一条主线记住:
State → Node → Edge → Compile → Invoke
其中:
State:数据是什么
Node:数据怎么处理
Edge:下一步去哪
Compile:把整张图组装起来
Invoke:真正开始执行
如果再压缩成一句话:
LangGraph = 用 State 保存数据,用 Node 处理数据,用 Edge 控制流程。
这就是整个 LangGraph 最基础的骨架。
后面的:
- 条件路由
- 循环
- Agent
- Tool Calling
- Checkpoint
- Memory
- 多 Agent
本质上都是在这套骨架上不断增加能力。
所以刚开始学习 LangGraph 时,我并不建议一上来就直接研究复杂 Agent。
先自己真正写明白一次:
START
↓
Node
↓
条件分支
↙ ↘
Node Node
↘ ↙
END
再去看复杂案例,会容易很多。
如果你也正在学习:
Python / LangChain / LangGraph / AI Agent / 大模型应用开发
可以收藏这篇文章。
以后忘记 LangGraph 的基本开发流程时,只需要回来记住这一条:
State → Node → Edge → Compile → Invoke。
