刚接触 LangGraph 的人,多半会被它那套"图"的术语拦住:节点、边、状态、超级步、条件路由……名词一个接一个。其实把这些词拆开看,它的核心想法很朴素——把一个 agent 的工作流画成一张图,节点负责干活,边负责告诉流程下一步去哪。这篇文章把 addNode、addEdge、条件边、多 Agent Supervisor 模式这几个关键概念串起来讲清楚,看完你应该能动手搭出自己的第一张图。
先把三个词记住:State、Nodes、Edges
LangGraph 对自己的定位讲得很直白:它把 agent 工作流建模成图,而你用三样东西来定义 agent 的行为——State、Nodes、Edges。官方那句被反复引用的话说得最精炼:
nodes do the work, edges tell what to do next.
翻译过来就是:节点做事,边决定下一步做什么。State 则是贯穿全图的共享上下文,所有节点都对着它读写。官方对 State 的定义是:它由图的 schema 以及一组 reducer 函数组成,reducer 决定了状态更新如何被合并进去。这意味着状态不是被某个节点独占的局部变量,而是整张图累积共享的"黑板"。
底层执行机制上,LangGraph 用的是消息传递(message passing),受 Google 的 Pregel 系统启发。执行按离散的 super-step 推进。这里有个容易记混的点,值得一开始就说清楚:
同一个 super-step 内的节点是并行执行的;顺序执行的节点则分属不同的 super-step。
把 super-step 想成"一轮迭代"就好理解了——同一轮里能并行跑的节点归在一起,跨轮的节点自然得排队。
addNode 和 addEdge 各管什么
理解了三件套,再看两个最常用的 API。addNode 注册一个可执行节点,addEdge 定义节点之间的流转关系。一个回答"图里有什么人",一个回答"这些人按什么顺序接力"。
| 维度 | addNode |
addEdge |
|---|---|---|
| 作用 | 注册一个可执行节点(agent / 函数) | 定义节点之间的流转关系 |
| 类比 | 招聘员工,告诉公司"有哪些人" | 制定流程,告诉员工"下一步找谁" |
| 参数 | addNode("名称", 函数) |
addEdge("来源节点", "目标节点") |
| 调用时是否执行 | 否,只是注册 | 否,只是定义结构 |
| 链式调用 | 支持,返回 builder 自身 | 支持,返回 builder 自身 |
官方给的最小示例长这样:
import { StateGraph, START, END } from "@langchain/langgraph";
const graph = new StateGraph(StateAnnotation)
.addNode("agent", agentNode) // 注册节点:定义"有什么"
.addNode("tools", toolsNode) // 注册节点:定义"有什么"
.addEdge(START, "agent") // 定义边:从哪开始
.addConditionalEdges("agent", routeAgent) // 条件边:动态路由
.addEdge("tools", "agent") // tools 做完回 agent
.compile(); // 编译图(必须步骤)
末尾那句 .compile() 不是装饰,官方明确说过:图必须编译之后才能用。编译这一步会做一些基础的结构检查(比如有没有孤儿节点),也是你挂载 checkpointers、breakpoints 这类运行时参数的地方。
执行顺序的真相:配置阶段 vs 运行阶段
很多人第一次写 LangGraph 会踩一个坑:以为 addNode、addEdge 按链式调用的书写顺序执行。其实不是。这两个调用都只在做"注册"和"定义结构"的事,真正跑节点逻辑发生在 .invoke(state) 或 .stream(state) 的时候。三阶段的差别如下:
| 阶段 | 代码 | 实际行为 |
|---|---|---|
| 配置阶段 | .addNode() / .addEdge() |
只是注册 / 定义,完全不执行 |
| 编译阶段 | .compile() |
验证图结构合法性,不执行节点逻辑 |
| 运行阶段 | .invoke(state) / .stream(state) |
真正按边的定义顺序执行节点函数 |
为了说明"执行顺序只由边决定",可以对比下面两种写法。第一种按直觉顺序写,第二种把边和节点的注册顺序打乱,但执行结果完全一样:
// 写法1:按注册顺序
builder.addNode("A", fnA)
.addNode("B", fnB)
.addEdge(START, "A")
.addEdge("A", "B");
// 写法2:打乱顺序,执行结果不变
builder.addEdge("A", "B") // 先定义边
.addNode("B", fnB) // 后注册节点
.addEdge(START, "A")
.addNode("A", fnA);
两种写法的执行路径都是 START → A → B → END,因为边才是顺序的唯一裁判。记住这点,后面看别人写的图就不会被链式调用的排版带偏。
边的三种类型与一个易踩的坑
LangGraph 把边分成几种关键类型:Normal Edge、Conditional Edge,以及 Entry Point 和 Conditional Entry Point 这类入口边。日常用得最多的是前两种。
**Normal Edge(普通边)**用于固定流转——A 做完一定去 B,没有任何条件分支:
graph.addEdge("node_a", "node_b"); // A 做完总是去 B
**Conditional Edge(条件边)**用于需要动态决策的场景。它接受一个节点名和一个路由函数:节点执行完之后,调用路由函数,由它返回下一个该去哪。路由函数同样接收当前 state,返回值默认就被当作目标节点名(或节点名列表):
// routing_function 接收当前 state,返回下一个节点名
graph.addConditionalEdges("node_a", routingFunction);
// 可选:提供路径映射字典,把返回值显式映射到节点
graph.addConditionalEdges("node_a", routingFunction, {
true: "node_b",
false: "node_c"
});
这里有一条容易被忽略的约束,官方专门提醒过:对同一个节点,要么用普通边做静态路由,要么用条件边或 Command 做动态路由,不要混用。原因是两条路径都可能被执行,会让图的行为变得很难推理。简单说,一个节点要么"铁定去某处",要么"看情况决定",别让它同时具备两种身份。
需要快速判断该用哪种边时,可以套下面这几条:
固定流程(A 做完一定做 B) → addEdge("A", "B")
需要决策(A 做完可能去 B/C/D) → addConditionalEdges("A", router, {...})
入口点 → addEdge(START, "A")
结束点 → addEdge("A", END)
多 Agent 怎么搭:Supervisor 模式
单 agent 能做的事有限,真正复杂的任务往往要多个 agent 协同。LangGraph 官方和社区实践中常见三种多 Agent 拓扑:
| 拓扑 | 结构 | 适用场景 | 缺点 |
|---|---|---|---|
| Network | 每个 agent 可以调用其他任意 agent | 真正的点对点协作;较少见 | 组合路由决策复杂,难评估和调试 |
| Supervisor | 一个 Supervisor 路由到 N 个 Worker,Worker 完成后回 Supervisor | 约 90% 的真实团队:一个调度器带多个专家 | Supervisor 思考负担过重时会成瓶颈 |
| Hierarchical | Supervisor 上面还有 Supervisor | 大团队里有子团队(比如"研究部"有自己的内部调度) | 成本翻倍,约 8 个专家以上才值得 |
对大多数场景来说,Supervisor 模式是首选:结构清晰、容易调试,一个调度中心配几个分工明确的专家 agent,刚好对应现实中"一个项目经理带几个工种"的协作方式。
它的架构大致是这样:
┌─────────┐
│ START │
└────┬────┘
▼
┌─────────────────┐
│ Supervisor │ ← 调度中心:分析状态,决定下一步
│ (循环执行) │
└────────┬────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│Researcher│ │ Coder │ │ Writer │
│(信息检索)│ │(代码生成)│ │(文案撰写)│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└────────────┴────────────┘
│
▼
┌─────────────────┐
│ Supervisor │ ← 检查完成度,继续调度或结束
└────────┬────────┘
▼
┌─────────┐
│ END │
└─────────┘
核心循环很紧凑:Supervisor 执行 → 决定路由 → 子 agent 执行 → 回到 Supervisor。每一轮子 agent 把结果写回 state,Supervisor 再根据最新状态判断是该继续派活还是收尾。
路由决策怎么写
Supervisor 的"决定下一步"通常交给一个 LLM 来做。下面这段代码展示了典型的写法:Supervisor 节点拼好 prompt 调模型,拿到决策后做一道白名单校验,再写回 state.next;另一个独立的 router 函数只负责读 state.next 并返回目标节点名。
// Supervisor 节点:LLM 动态决策
async function supervisorNode(state) {
const prompt = `根据当前任务状态,决定下一步:
任务: ${state.task}
已完成: ${JSON.stringify(state.results)}
可选: researcher / coder / writer / finish
请返回一个名称。`;
const response = await model.invoke(prompt);
const decision = response.content.trim().toLowerCase();
// 安全校验:防止 LLM 输出异常
const validAgents = ["researcher", "coder", "writer", "finish"];
const next = validAgents.includes(decision) ? decision : "finish";
return { next }; // 这个 next 会被 router 函数读取
}
// 路由函数:读取 state.next,返回目标节点名
function router(state) {
return state.next; // 返回 "researcher" / "coder" / "writer" / "finish"
}
那道白名单校验不是多余的。LLM 偶尔会返回多余的文字、大小写不一致甚至幻觉出一个不存在的节点名,直接拿去路由轻则报错、重则跑飞。用 validAgents 兜个底,遇到异常输出就回落到 finish,是最省心的做法。
把上面这些拼成一张完整的图:
const builder = new StateGraph(StateAnnotation)
// 注册所有 Agent 节点
.addNode("supervisor", supervisorNode)
.addNode("researcher", researcherNode)
.addNode("coder", coderNode)
.addNode("writer", writerNode)
// 定义入口
.addEdge(START, "supervisor")
// Supervisor 的条件路由(动态决策)
.addConditionalEdges("supervisor", router, {
researcher: "researcher",
coder: "coder",
writer: "writer",
finish: END,
})
// 子 Agent 完成后返回 Supervisor(关键!)
.addEdge("researcher", "supervisor")
.addEdge("coder", "supervisor")
.addEdge("writer", "supervisor");
注意最后三行普通边——它们是 Supervisor 模式能循环起来的关键。子 agent 做完不直接结束,而是回到 supervisor,由调度中心决定是再派一个专家还是收工。少了这几行,图跑一轮就停了。
跟着一次任务走一遍执行过程
光看代码还不够直观,拿一个具体任务跟着图走一遍就清楚了。假设用户给的任务是"写一个爬虫并生成文档",整张图的执行轨迹是这样的:
用户任务: "写一个爬虫并生成文档"
Step 1: START → supervisor
Supervisor 分析: 需要技术研究 → 返回 { next: "researcher" }
Step 2: supervisor → researcher (条件路由)
Researcher 执行: 研究爬虫技术 → 返回研究结果
researcher → supervisor (普通边,自动返回)
Step 3: supervisor (再次执行)
Supervisor 分析: 研究完成,需要代码 → 返回 { next: "coder" }
Step 4: supervisor → coder (条件路由)
Coder 执行: 编写爬虫代码 → 返回代码
coder → supervisor (普通边,自动返回)
Step 5: supervisor (再次执行)
Supervisor 分析: 代码完成,需要文档 → 返回 { next: "writer" }
Step 6: supervisor → writer (条件路由)
Writer 执行: 撰写使用文档 → 返回文档
writer → supervisor (普通边,自动返回)
Step 7: supervisor (再次执行)
Supervisor 分析: 全部完成 → 返回 { next: "finish" }
Step 8: supervisor → END (条件路由)
流程结束
留意 Step 3、5、7:每次子 agent 回来,Supervisor 都会重新执行一次。这正是 Supervisor 模式"循环"的体现——它不是一次性派完所有活,而是每完成一步就回来重新评估。也正因为这样,state 的累积共享才显得重要:researcher 写进 state 的研究结果,coder 和 writer 后续都能读到,不必自己重复研究一遍。
几条值得记牢的要点
把前面的内容收一收,有几条结论值得你动手时贴在显示器边上:
addNode定义"有什么",addEdge定义"怎么走",addConditionalEdges是"智能地怎么走"——前两个只注册不执行,顺序由边说了算。- 图必须
.compile()才能用,这是硬性步骤,跳过会直接报错。 - 同一个节点别把普通边和条件边混用,两条路径都可能执行,调试起来会很痛。
- Supervisor 模式的循环靠两件事撑起来:子 agent 用普通边回到 supervisor,supervisor 每次都被重新执行。
- state 是全图共享的累积上下文,靠 reducer 合并更新,别把它当某个节点的局部变量用。
- LLM 做路由决策时,记得加白名单校验兜底,别把模型输出直接当节点名。
把这些记住,搭一张能跑的多 Agent 图其实没那么难。难的部分 LangGraph 已经替你抽象成了边和节点,你要做的,主要是想清楚"有哪些活、按什么顺序干、什么时候算完"——这恰好也是你平时安排一件复杂事情时会想的问题。