想象你刚入职一家新公司,要请三天假。HR 告诉你:得先在 OA 系统里发起一张请假单——这张单子要依次经过直属经理审批、HR 审核、财务登记补贴。每一道关卡都有一个“岗位”处理这张单子:经理签“同意/驳回”,HR 更新休假天数,财务备注补贴处理。这张单子从头到尾始终是同一张,只是内容被一步一步填满;路线也可能因为“同意”或“驳回”而分叉。 这就是 LangGraph 在做的事——它管理的是 AI 应用内部的“审批单流转”。要构建一个有状态的 Agent 工作流,核心就两个构件:节点(Node)负责做事(处理请求、调用模型、更新信息),边(Edge)负责决定下一步去哪儿。而整个过程中被传递、被更新的“请假单”,就是状态(State)。本文会带你从零理解:节点如何定义处理逻辑、边如何控制流转方向、条件分支如何让流程“看情况走”、状态如何在节点间被传递合并,以及如何设计状态的数据结构 Schema。学完你就能用 LangGraph 搭出“客服助手”“多步检索”这样的有状态 Agent 工作流。
对 AI 从业者来说,LangGraph 的节点与边机制是构建生产级 Agent 的骨架:它把业务逻辑、状态管理和流程控制解耦,让 AI 应用的行为可预测、可回放、可维护。
可视化演示
交互展示LangGraph中节点与边的有状态流转机制:状态传递、条件分支和组合模式。
本节配有交互动效(进入视口后自动加载,约 3KB)。
状态流转:节点间传递 节点接收State并返回更新字典 边控制节点间执行顺序和方向 Node(State) → State Update 状态在节点间有序传递和更新
节点定义
把公司审批流想象成一条流水线:直属经理、HR、财务各有一个工位,每个工位只负责在请假单上完成自己那一步——经理签意见、HR 核天数、财务登记补贴。在 LangGraph 里,这些工位就是节点(Node):它是图里最小的执行单元,负责接收上一站送来的状态(State),做一段自己的处理,再把更新后的状态交出去。
技术上,节点可以理解为一个“输入一个输出”的函数。在 Python 中,最简单的节点就是一个普通函数:参数 state 是当前这张“请假单”,返回值是你想改的字段增量。LangGraph 会自动把返回值合并回全局状态,不需要你手写 state["approval"] = ...:
节点把一张单子从一个工位交给下一个工位,每个节点只负责自己最熟悉的格子,改完就传下去,这正是 LangGraph 让复杂 Agent 逻辑保持清晰的基本功。
- 当前状态 S节点开始处理前,全局“请假单/状态字典”长什么样。
- 节点函数 N(·)针对“这个工位要干什么”而封装的业务逻辑——判断、改写、追加消息、调用模型等。
- 更新结果 S'节点处理后返回的字段增量,LangGraph 将其合并进全局状态,形成新状态 S'。
def manager_node(state): return {"approval": "approved"}
边与流转
在审批流程里,经理签完字,假条会被送到 HR;HR 复核完,又送到财务。这些“下一站送到哪”的指令,就是 LangGraph 里的边(Edge)。边定义了节点之间固定的流转方向,它本身不处理数据,只告诉执行引擎:完成当前节点后,下一个要去哪个节点。
这段代码按顺序连接了入口、经理节点、HR 节点和出口。如果业务流程是“永远固定走同一条路”,那么普通边就足够用了。
如果请假流程永远“经理→HR→财务”固定不变,确实不需要任何判断。但现实中审批往往出现“同意走 A 流程、驳回走 B 流程”的情况——这就到条件分支了。
- 起点 START图执行的唯一入口,所有状态都从这里进入。
- 普通边 e_i从一个节点指向下一个节点的有向线,代表“无条件、固定走这里”。
- 终点 END所有流程的出口,执行到这里即完成一次完整调用。
from langgraph.graph import StateGraph, START, END g = StateGraph(State)g.add_node("manager", manager_node)g.add_edge(START, "manager")g.add_edge("manager", "hr")g.add_edge("hr", END)
条件分支
职场里,经理在假条上签“同意”,单子去 HR;签“驳回”,单子退回员工修改。决定下一步走哪条路的不是固定边,而是条件分支(Conditional Edge):一个带判断的函数,看完当前状态后返回一个“路线 key”,LangGraph 根据 key 走对应分支。
使用 add_conditional_edges 时,条件函数返回的是字符串 key,映射表再把 key 翻译成具体节点:
条件分支是 Agent 工作流的灵魂:让流程“带着脑子”走,而不是机械地顺序执行。那枚小小的章,是审批单能自己拐弯的原因。
- 条件函数 Next(S)以当前状态 S 为输入、输出“下一站 key”的纯函数,类似审批人盖章。
- 分支决策条件
state["approval"] == "approved"这类业务判断,决定取哪个分支。 - 路由映射表分支 key 与目标节点的字典,LangGraph 通过它找到实际要跳转的节点。
def route_after_manager(state): if state["approval"] == "approved": return "hr" return "rewrite"
g.add_conditional_edges("manager", route_after_manager, {"hr": "hr", "rewrite": "rewrite"})
状态传递
请假单始终是同一张,但从经理到 HR 再到财务,单子上不断多出新格子:经理签了意见,HR 补了天数,财务备注了补贴。这个现象在 LangGraph 里叫状态传递与更新(State Passing)。同一次运行中,状态是全局共享的一份数据对象(如一个 dict),节点把它放进函数参数,返回的东西则通过合并策略累加回去。
LangGraph 默认采用增量更新:每个节点不必返回完整状态,只返回你要变更的字段。引擎会把返回值与旧状态合并,生成新状态后再传给下一个节点。
这也正是“同一张请假单被不断填充”的严谨表述:没有人重新打印单子,只是在原单子上继续书写;如果两个节点都改了同一个字段,就看那个字段的“合并规则”来决定谁覆盖谁。
- 旧状态 S_t进入节点前已有的全局状态,可能包含历史消息、步骤计数、用户输入等。
- 增量 ΔS_t节点返回值里需要新增或修改的字段集合,只描述变化本身。
- 合并操作符 ⊕LangGraph 根据字段的 reducer 决定如何合并——标量覆盖、列表追加,还是自定义策略。
状态Schema
如果公司不规定请假单格式,有的员工写“三天”,有的写“3 days“,HR 根本没法统计。状态 Schema(结构定义) 要做的就是给”这张单子长什么样”立规矩——哪些字段、什么类型、如何合并。
在 LangGraph 中,最常用 TypedDict 定义状态 schema:
没有 schema,LangGraph 无法知道字段怎么合并;有了 schema,节点可以放心只返回一个小增量——正如每张请假单上每个格子都有明确的主人。
- 字段集合状态由若干字段组成,例如
messages是 list,step是 int。 - Annotated 元数据
Annotated[T, reducer]附加合并信息,告诉引擎“新值到来时如何合并”。 - Reducer 合并策略决定“两个值相遇时谁说了算”,如
add_messages会把新消息追加到旧列表后。
from typing import Annotated, TypedDictfrom langgraph.graph.message import add_messages class State(TypedDict): messages: Annotated[list, add_messages] # 消息自动追加 step: int # 流程计数,普通覆盖 user_id: str # 用户标识
工作流实战
现在把岗位(节点)、路线(边)、盖章决策(条件分支)、单子(状态 Schema)全部安排好,只需要把它们拼装成一个完整的“公司流程系统”——这就是构建与编译图(Build & Compile)。
所有单子(状态)会在一次调用中自动走完全程——这就是 LangGraph 构建有状态 Agent 工作流的基本套路:先定义状态,再加节点,再连边,最后编译运行。
- 构造 StateGraph(S)传入状态 Schema,初始化一个图对象。
- 节点集合 {N_i}通过
add_node将函数打包成图里的节点。 - 边集合 {E_j}通过
add_edge与add_conditional_edges定义边和条件路由。 - 编译执行
compile()返回可直接调用的应用对象,invoke()正式运行整个流程。
from langgraph.graph import StateGraph, START, END g = StateGraph(State)g.add_node("manager", manager_node)g.add_node("hr", hr_node)g.add_node("finance", finance_node)g.add_edge(START, "manager")g.add_conditional_edges("manager", route_after_manager, {"hr": "hr", "rewrite": "rewrite"})g.add_edge("hr", "finance")g.add_edge("finance", END) app = g.compile()result = app.invoke({"messages": [], "step": 0, "user_id": "007"})
AI 应用场景
智能客服分流
阶段:Agent 编排与条件路由
解决的问题:传统客服对话靠 if-else 硬编码分支,新增一个类目就要改代码;用 LangGraph 的条件分支和状态传递,路由逻辑变成一张可维护的映射表,用户消息和决策记录都沉淀在状态里,可回溯。
具体案例:用户说“我要退款”,客服 Agent 先经过“意图识别节点”,条件边根据识别结果把请求路由到“退款政策节点”或“人工客服节点”;每一步都把模型输出追加到 messages 列表留痕,最终由“回复生成节点”输出答案。
多步 RAG 检索
阶段:检索增强生成(有状态工作流)
解决的问题:单次检索往往质量不稳定,需要“检索→评分→重写→再检索”的循环;用条件分支控制是否重写,用状态里的 retry_count 防止死循环。
具体案例:用户提问进入“文档检索节点”,拿到候选文档后经“相关性评分节点”打分;若分数低于阈值,条件边把流程送回“问题重写节点”再进检索,直到分数达标或重试次数耗尽,最后交给“生成回答节点”。
代码生成修复 Agent
阶段:Agent 工具调用循环
解决的问题:大模型一次生成的代码常有语法错误,人工回填太慢;用 LangGraph 编排“生成→编译→修复”循环,让修复节点自动返回并重试。
具体案例:“代码生成节点”产出源码写入 state.code,“编译检查节点”执行检查;条件边根据编译结果决定走“输出节点”还是“修复节点”,max_retries 字段控制终止条件,避免无限循环。
Python 演示
from typing import Annotated, TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messages class State(TypedDict): messages: Annotated[list, add_messages] step: int def inquiry_node(state: State) -> dict: return {"messages": [{"role": "user", "content": "我想查快递"}], "step": state["step"] + 1} def solution_node(state: State) -> dict: return {"messages": [{"role": "assistant", "content": "请提供运单号"}], "step": state["step"] + 1} def route_by_step(state: State) -> str: return "solution" if state["step"] >= 1 else "end" g = StateGraph(State)g.add_node("inquiry", inquiry_node)g.add_node("solution", solution_node)g.add_edge(START, "inquiry")g.add_conditional_edges("inquiry", route_by_step, {"solution": "solution", "end": END})g.add_edge("solution", END) app = g.compile() result = app.invoke({"messages": [], "step": 0})print(result["step"])print([m.content for m in result["messages"]])
class State(TypedDict):定义状态 Schema,声明messages与step两个字段messages: Annotated[list, add_messages]给消息字段配置追加型 reducer,新消息自动拼接def inquiry_node(state: State) -> dict:节点函数,接收当前状态并返回增量字段g.add_node("inquiry", inquiry_node)把函数注册成图里的节点g.add_conditional_edges("inquiry", route_by_step, ...)给节点添加条件边,按路由函数动态选择下一站g.compile()编译状态图,返回可执行的应用对象app.invoke({"messages": [], "step": 0})传入初始状态运行工作流
常见误区
误区我以为每个节点拿到的是一份独立拷贝,改完不影响其他节点,所以节点里随便改 `state` 都没关系。
正解LangGraph 中单次 invoke 流程里,所有节点共享同一份全局状态,节点返回的增量会被合并回去,下一个节点能看到上一个节点的改动。它不是“每站复印一份”,而是“同一张假条轮流填写”。
为何常见:受普通函数“参数按值传递”的直觉影响,很容易把 Python 的默认行为套到图执行引擎上。
误区我以为条件边只能返回“是/否”两个方向,顶多加一个默认值,分支再多就做不了。
正解条件函数可以返回任意多个 key,路由映射表是普通字典,可以配置 3 个、5 个甚至更多分支;同一 key 也可以映射到不同节点,分支本质是“从当前节点到下一节点的动态编号”。
为何常见:生活里“同意/驳回”的二值直觉太强,容易忽略路由表其实是个通用映射。
误区我以为必须把 `state` 里的每个字段都重新写一遍再返回,否则其他字段就会被清空。
正解LangGraph 的合并机制决定,节点只需返回要修改的字段增量,其余字段自动沿用旧值;如果返回了 Schema 之外的字段,可能被忽略或校验失败。
为何常见:写普通 Python 函数时习惯把所有变量一起 return,天然担心“不发完整数据就会被覆盖”。
误区我以为 `messages` 这类列表字段每次直接赋值就好,新节点写入时不会影响历史数据。
正解列表字段如果不配 add_messages 之类的 reducer,新节点返回的列表会整组覆盖旧列表,导致历史消息丢失;配了 reducer 后才会自动追加。
为何常见:把“谁后写谁占格”的直觉带到图引擎,忽略了在 LangGraph 里 reducer 才是合并规则的最终裁判。
面试题
Q1. 在 LangGraph 中,一个节点(Node)最常见的定义方式是?
- A.异步生成器对象
- B.接收状态返回更新字典← 正确答案
- C.一种只读配置文件
- D.数据库的连接语句
答案:B。节点本质是一个普通函数,它接收当前状态(State),返回要更新的字段增量(dict),LangGraph 会自动把返回值合并进全局状态。生成器、配置文件、数据库语句都不是节点的标准形态。
Q2. 边(Edge)在 LangGraph 中的核心作用是?
- A.缓存中间计算结果
- B.决定节点之间的流转顺序← 正确答案
- C.存储所有用户聊天记录
- D.对模型进行参数微调
答案:B。边定义的是“完成当前节点后下一步去哪”,它决定流程的流转顺序,本身不处理业务数据。缓存、存储、微调是其他组件或外部系统的职责。
Q3. 关于 START 与 END 节点,下列说法正确的是?
- A.必须显式添加多个 START
- B.前者是入口,后者是出口← 正确答案
- C.二者都不能连接任何边
- D.它们是两个特殊的数据节点
答案:B。START 是图执行的唯一入口,END 是流程出口;两者都必须存在,但各自只能有一个。它们不是普通的数据处理节点,而是图结构的边界标记。
Q4. 实现条件分支,需要用到的方法是?
- A.add_conditional_edges← 正确答案
- B.add_message_history
- C.update_state_schema
- D.compile_and_execute
答案:A。add_conditional_edges 专门用于给节点添加条件边,它接收一个路由函数和分支映射表。其余方法分别与消息历史、Schema 更新、编译执行相关,都不负责条件路由。
Q5. 状态在节点间传递时,LangGraph 默认采用的更新方式是?
- A.全量复制一份新状态
- B.节点返回增量,自动合并← 正确答案
- C.每次清空状态重新输入
- D.由用户手动锁字段
答案:B。LangGraph 默认增量更新:节点只需返回要修改的字段,引擎将其与旧状态合并成新状态。不需要全量复制,也不会清空状态,更不需要用户手动加锁。
Q6. 以下哪些属于 LangGraph 中定义状态 Schema 的常用方式?
- A.使用 TypedDict 声明字段类型← 正确答案
- B.使用 Annotated 指定合并策略← 正确答案
- C.使用 JSX 描述界面结构
- D.使用 CSV 文件保存记录
答案:AB。TypedDict 用来声明状态字段及类型,Annotated 用来给字段附加 reducer 合并策略。JSX 是前端语法,CSV 是数据存储格式,都与状态 Schema 无关。
Q7. 关于条件边,正确的说法有哪些?
- A.路由函数返回字符串 key← 正确答案
- B.key 映射到具体节点← 正确答案
- C.分支数量最多不能超过三个
- D.每个节点必须有条件边
答案:AB。条件函数输出字符串 key,路由映射表把 key 翻译成目标节点,因此分支数量没有硬性上限。普通节点完全可以只用普通边,不强制使用条件边。
Q8. 若节点的返回 dict 包含状态 Schema 中未定义的字段,最可能出现什么结果?
- A.自动创建一个新分支
- B.字段被 Schema 校验拦截← 正确答案
- C.直接修改全局配置文件
- D.静默把字段转成字符串
答案:B。LangGraph 依赖 Schema 进行状态合并,未定义的字段不会被自动加入状态,通常会被忽略或触发类型校验错误。它不会创建分支、修改配置文件,也不会被静默改成字符串。
Q9. 同一状态里若两个节点先后给 list 字段赋值且未配 reducer,最终结果是?
- A.后一个节点整组覆盖前一个← 正确答案
- B.两个列表自动拼接
- C.LangGraph 随机选取一个
- D.触发异常并停止运行
答案:A。没有 reducer 的列表字段采用“覆盖”语义,后一个节点返回的列表会整体替换旧值。两个列表自动拼接需要 add_messages 之类的 reducer 才能实现。
Q10. 关于 add_messages reducer,以下说法最准确的是?
- A.它会删除重复的历史消息
- B.它把新消息追加到现有列表← 正确答案
- C.它只允许保存一条最新消息
- D.它只能处理纯文本消息
答案:B。add_messages 的核心行为是把新消息追加到现有消息列表末尾,并完成消息格式标准化。它不会去重,不限制列表只留一条,也不只接受纯文本——结构化消息同样支持。