1. LangGraph 核心概念
LangGraph 是基于 LangChain 的图编排框架,专门用来构建多轮 Agent、带循环、分支、人在回路的 LLM 应用,不是简单链式调用(LCEL)。
- 核心思想:把 Agent 流程抽象成节点 (Node) 和边 (Edge) ,消息 / 数据保存在状态 State中,图按照边规则流转执行。
- 与普通 LCEL 区别:LCEL 适合线性、无循环链路;LangGraph 天然支持循环、条件分支、中断暂停、状态持久化,是复杂 Agent 首选。
- 基础术语
- Node 节点:图中的执行单元,可以是 LLM 调用、工具执行、自定义业务函数、子图。输入 state,返回 state 增量更新。
- Edge 边:节点之间的流转通路,分为普通边、条件边。
- State 状态:整个图的全局数据容器,图每一步都会读取 / 更新状态。
- Reducer(归约器):定义同一个字段多次更新时如何合并 ,而不是直接覆盖。最常用
add_messages。 - Checkpointer 检查点:保存每一步状态快照,实现持久化、暂停恢复。
- Thread 线程:会话隔离 ID,一个 thread 对应一轮独立对话任务。
✅笔记补充:LangGraph 本质是状态机;每一个节点返回的不是完整 state,是增量更新字典,由 reducer 合并进全局 state。
2. 状态 State 与 Reducer
2.1 State 定义(TypedDict / Pydantic)
两种定义 state 方式:
-
TypedDict(推荐,轻量,笔记示例统一用这个):仅声明字段类型,不需要实例化,适合快速开发。 -
Pydantic BaseModel:带校验、默认值,适合生产强校验场景。
from typing import TypedDict, Annotated
class State(TypedDict):
# Annotated[类型, reducer函数]
messages: Annotated[list, add_messages]
query: str
result: str
Annotated第二个参数就是 reducer,用来控制字段合并策略。
2.2 Reducer 归约器原理
- 默认行为:不带 reducer,节点返回更新会直接覆盖原有字段
add_messages:LangGraph 内置 reducer,追加消息列表,不覆盖历史,Agent 消息字段标配- 自定义 reducer:自己写函数
func(old_val, new_val),接收旧值、新值,返回合并后的结果。
示例自定义 reducer:
def append_list(old_list:list, new_list:list) -> list:
return old_list + new_list
⚠️坑点:忘记加
add_messages,节点返回消息会直接覆盖历史对话,上下文丢失。
2.3 状态更新规则
节点 return 字典,是增量更新,不需要返回完整 State。
def my_node(state:State):
# 只需要返回要修改的字段
return {"result": "计算结果xxx"}
3. StateGraph 图基础
3.1 StateGraph 核心组件
StateGraph是图构建器,用来注册节点、添加边、最后 compile 编译成可执行 graph 实例。
builder = StateGraph(State):传入状态类型,实例化图构建器builder.add_node(name, func):注册节点,name 是节点标识字符串,func 是执行函数builder.add_edge(from_node, to_node):普通静态边,A 执行完固定流向 Bbuilder.add_conditional_edges(from_node, route_func):条件边,路由函数根据 state 动态选择下一个节点START:图入口,不是自定义节点;END:图终止标记,到达 END 图停止执行graph = builder.compile():编译,生成可调用图对象,compile 时可以传入 checkpointer
3.2 基础线性图示例(无分支无循环)
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
query:str
reply:str
def preprocess(state:State):
return {"query": state["query"].strip()}
def reply_node(state:State):
return {"reply": f"收到你的问题:{state['query']}"}
builder = StateGraph(State)
builder.add_node("preprocess", preprocess)
builder.add_node("reply_node", reply_node)
builder.add_edge(START, "preprocess")
builder.add_edge("preprocess", "reply_node")
builder.add_edge("reply_node", END)
graph = builder.compile()
res = graph.invoke({"query":" 你好LangGraph "})
print(res["reply"])
执行链路:START → preprocess → reply_node → END
3.3 invoke /ainvoke/astream_events 调用方式
graph.invoke(input, config={}):同步调用,阻塞直到图执行完成,返回最终完整 state。适合简单调试。await graph.ainvoke(input, config={}):异步调用,异步服务后端推荐。astream_events:流式事件,输出每一步节点启停、tool 调用事件,适合前端流式输出(后面 10.1 展开)
✅笔记补充:
config用来传递thread_id、中断参数,不属于 state,不会存入状态快照。 ⚠️坑点:START不能 add_node 注册;END只是终止标记,不能作为节点添加。
4. 工具调用集成
4.1 ToolNode 内置工具节点
ToolNode 是 LangGraph 内置节点,专门用来解析 AIMessage 里的工具调用参数,自动执行对应函数,并返回 ToolMessage 写入状态,不用手动写工具解析逻辑。
适用场景:LLM 输出 tool_calls → ToolNode 自动调度工具。
- 输入:状态中包含带
tool_calls的AIMessage - 输出:追加一条或多条
ToolMessage到 messages - 异常:工具执行报错时,错误信息会封装进 ToolMessage 回传给 LLM,方便 LLM 重试
4.2 tools_condition 内置路由
tools_condition 是 LangGraph 预置条件边函数,用来判断 LLM 输出是否包含工具调用:
- 如果消息存在
tool_calls→ 返回tools,路由到 ToolNode - 没有 tool_calls → 返回
END,结束当前 Agent 轮次
不用自己手写判断工具调用的路由函数,单 Agent 模板标配。
4.3 工具绑定、ToolMessage 注入、工具异常处理
- 工具绑定 :使用 LangChain 的
bind_tools把 python 函数绑定到大模型,让 LLM 知道可用工具的名称、参数 schema。 - ToolMessage 注入 :ToolNode 执行完工具,自动生成 ToolMessage,依靠
add_messagesreducer 追加进 state.messages。 - 工具异常处理
- 默认:工具抛出异常,ToolNode 捕获异常,包装成 ToolMessage 返回,交给 LLM 感知错误,LLM 可以重新调用 / 修正参数。
- 自定义:可以在工具函数内部 try-catch,自定义错误消息返回;也可以封装异常节点,走单独降级分支。
4.4 单 Agent 标准模板:LLM -> 工具路由 -> ToolNode
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain.tools import tool
# 1. 定义状态
class State(TypedDict):
messages: Annotated[list, add_messages]
# 2. 定义工具
@tool
def multiply(a:int, b:int) -> int:
"""计算两个数字相乘"""
return a * b
tools = [multiply]
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)
# 3. LLM节点
def llm_node(state:State):
resp = llm.invoke(state["messages"])
return {"messages": [resp]}
# 4. 构建图
builder = StateGraph(State)
builder.add_node("llm", llm_node)
builder.add_node("tools", ToolNode(tools))
# 边
builder.add_edge(START, "llm")
# 条件边:llm执行后交给tools_condition路由
builder.add_conditional_edges("llm", tools_condition)
builder.add_edge("tools", "llm") #工具执行完回到LLM继续判断
graph = builder.compile()
# 调用
res = graph.invoke({"messages": [("user", "3乘以7等于多少")]})
print(res["messages"])
执行流程:用户消息 → LLM 生成 tool_call → 路由到 ToolNode 执行 multiply → ToolMessage 写回 state → 回到 LLM 生成最终回答。
5. 持久化、线程与检查点
5.1 Checkpointer
为什么需要 checkpointer
默认 graph 执行是无状态的,每次 invoke 都是全新会话。Checkpointer 负责保存每一步执行后的状态快照、下一步路由信息,实现会话保存、暂停恢复、时间旅行。
三类常用 Checkpointer
MemorySaver:内存存储,仅进程有效,重启丢失;适合本地调试。SqliteSaver:本地 sqlite 文件持久化,单实例,适合小流量、demo。PostgresSaver:生产推荐,支持多进程、多服务并发,高可用。
检查点保存内容:state 快照、下一跳节点信息、中断标记。不是只存 messages,包含完整状态 + 执行指针。
5.2 Thread 与多会话
thread_id 是会话隔离标识。同一个 thread_id 代表同一个会话,不同 thread_id 状态完全隔离。
-
调用 graph.invoke 的时候,传入 config={"configurable":{"thread_id":"xxx"}}
-
同一线程:可以中断、恢复,读取历史状态
-
多用户最佳实践:每个用户 / 每个独立对话分配唯一 thread_id;生产不要复用 thread_id 跨不同任务。
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable":{"thread_id":"session_001"}}
graph.invoke({"messages":[("user","你好")]}, config=config)
5.3 时间旅行
get_state(config)获取当前最新状态快照get_state_history(config)获取该 thread 全部历史检查点列表update_state(config, state_updates):手动修改当前线程的状态快照- 回放:拿到历史检查点 id,在 config 指定
checkpoint_id,从历史快照重新执行,用于分支调试、回滚。
时间旅行底层就是读取历史 checkpoint,重置图执行指针到该快照。适合调试、人工修改历史消息。
6. 控制流:条件边、循环、并行与子图
6.1 条件边与分支
add_conditional_edges 支持多分支路由。条件函数接收 state,返回目标节点名称。
def route(state):
query = state["query"]
if "计算" in query:
return "calc_node"
elif "搜索" in query:
return "search_node"
else:
return "reply_node"
builder.add_conditional_edges("preprocess", route)
6.2 循环与递归
循环就是条件边路由回前面节点,形成闭环。必须设计终止条件,否则死循环。 LangGraph 有递归深度限制 ,防止无限循环。编译时可以配置recursion_limit,默认一般 100。
典型场景:Agent 多轮调用工具,直到满足退出条件。
6.3 并行与扇出 / 扇入
Send API:运行时动态创建多个独立任务,并行执行同一个节点。
-
Send 接收目标节点名称 + 该任务独立子 state
-
并行节点共享主图 state,依靠 reducer 汇总结果
-
扇出:一次分发多个 Send 任务;扇入:全部并行任务完成后汇总。
from langgraph.graph import Send
def dispatch(state):
tasks = []
for item in state["task_list"]:
tasks.append(Send("sub_task_node", {"item": item}))
return tasks
builder.add_conditional_edges("dispatch", dispatch)
6.4 子图
子图就是把一段完整 StateGraph 封装成一个节点,实现逻辑复用。
-
子图有自己的状态定义,也可以复用主图 state
-
主图调用子图,子图执行完成后,将更新合并入主图状态
-
优势:模块化,多个业务流程复用同一套子 Agent 逻辑
子图构建
sub_builder = StateGraph(SubState)
...子图节点和边定义
subgraph = sub_builder.compile()
主图添加子图作为节点
builder.add_node("subgraph_node", subgraph)
6.5 复杂流程图设计示例
典型多轮 Agent 流程图结构: START → 预处理节点 → LLM 节点 →【条件路由】
- 需要工具 → ToolNode → 回到 LLM(循环)
- 需要人工审批 → interrupt 暂停(HITL)
- 任务完成 → END
7. 人在回路 Human-in-the-Loop
7.1 interrupt 两种暂停方式
interrupt_before=["nodeA"]:在执行 nodeA之前暂停,执行节点前触发中断interrupt_after=["nodeB"]:nodeB 执行完成后暂停- 节点内部
interrupt():节点函数内部主动抛出中断,适合动态判断何时暂停。
中断后:图执行停止,可以读取 state、修改 state,再调用invoke恢复继续跑。
7.2 人工审批与状态修改
中断后人工可以:
- 查看当前 state,阅读 Agent 生成的计划
- 审批通过:直接继续执行
- 拒绝 / 修改:
update_state修改 state(修改消息、修改参数),恢复执行后使用修改后的状态。
7.3 外部系统对接模式
- 前端轮询:后端保存 thread_id,前端轮询查询任务状态(是否中断、是否完成)
- Webhook:图执行结束 / 触发中断回调外部服务通知
- 任务队列:LangGraph 放到后台任务队列,异步执行,适合长耗时任务。
7.4 人工兜底与错误恢复
当 LLM 自动流程无法完成任务,自动触发 interrupt 交给人工兜底;人工修正后继续。 可搭配异常分支:节点报错后,路由到人工审批节点。
8. 异常处理、重试、超时
8.1 节点异常捕获
节点函数内部使用 try/except 捕获业务异常;未捕获异常会终止图执行。 也可以捕获异常,返回状态更新写入错误字段,交给条件边路由降级分支。
8.2 节点重试策略
可以使用@retry装饰器(tenacity)包裹节点函数,对网络波动、LLM 限流自动重试。
注意:重试会重复执行节点逻辑,有副作用的操作(调用外部 API、写数据库)要考虑幂等。
8.3 异常路由:失败降级分支
节点捕获异常,将异常信息写入 state "error";条件边读取 error 字段,如果存在错误,路由到降级 / 通知节点,而直接走到 END。
8.4 超时控制
两层超时:
- 节点内:LLM 调用、工具调用单独设置 http 超时
- 图级别:外部封装 invoke 调用,增加全局超时,防止任务卡死。
9. 多 Agent 架构
9.1 多 Agent 常见模式
- Supervisor 主管模式:主管 Agent 接收总任务,拆分任务调度多个子 Agent,汇总子 Agent 结果。最常用。
- Hierarchical 层级:多层级主管,一级主管拆大任务,二级主管再细分。
- Peer-to-peer 对等网络:Agent 之间互相 handoff 交接任务,无中心主管。
9.2 Supervisor 主管 Agent 实现
主管节点负责:
- 读取全局任务状态
- LLM 判断当前该交给哪个子 Agent 执行
- 返回 Command 完成任务交接
- 收集子 Agent 输出,判断任务是否全部完成。 子 Agent 拥有独立业务逻辑;可以共享主图 state,也可以隔离状态。
9.3 Agent 通信与状态共享
- 共享图状态:所有 Agent 读写同一个 state,数据互通,适合强协作场景;缺点:容易出现消息污染。
- 独立消息传递:Agent 之间只传递消息,子 Agent 状态隔离,完成任务后返回结果合并入主状态。
- Command /handoff:handoff 是高层封装,底层基于 Command,用于 Agent 之间任务交接。
9.4 多 Agent 实战坑点
- 循环控制:防止 Agent 之间来回无限互相调用,设置最大轮次。
- 消息去重:防止重复消息不断追加,消耗 token。
- 上下文窗口管理:消息过长时做摘要、截断,避免超出模型上下文上限。
10. 调试、可视化与工程上线
10.1 流式事件 astream_events
astream_events 可以拿到全链路事件:节点开始、节点结束、tool 调用事件、断点中断事件。适合前端流式渲染、日志埋点。
async for event in graph.astream_events(inputs, config=config, version="v2"):
print(event["event"], event["name"])
10.2 图可视化
# 输出mermaid文本
print(graph.get_graph().draw_mermaid())
# 输出图片(需要额外依赖)
graph.get_graph().draw_png("graph.png")
LangGraph Studio:官方 IDE,可以加载 checkpoint,可视化每一步状态、断点调试,本地开发强烈推荐。
10.3 生产优化
- 状态大小控制:messages 不断累积,及时消息摘要、截断,防止 state 过大,序列化开销高。
- Checkpointer 选型:Sqlite 只适合单实例本地;生产环境 PostgresSaver。
- 并发注意:多进程并发读写同一个 thread 的 checkpoint 要注意锁;避免多实例同时操作同一个 thread_id。
10.4 自定义 Checkpointer(进阶可选)
实现自定义 Checkpointer 抽象基类,对接 Redis、MongoDB 等存储,实现企业自有存储持久化。需要实现 put/get/list 等检查点读写接口。
Command:节点内部直接控制跳转完整示例
Command 优先级高于边定义的路由规则。节点 return Command,可以同时做两件事:更新 state,并且直接指定下一跳节点。非常适合 Supervisor 多 Agent 任务交接。
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END, Command
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
task: str
# 子节点A
def agent_a(state: State):
print("=== Agent A 执行 ===")
return {
"messages": [("ai", "A完成初步分析")]
}
# 子节点B
def agent_b(state: State):
print("=== Agent B 执行 ===")
return {
"messages": [("ai", "B完成详细计算")]
}
# Supervisor主管节点:使用Command动态跳转
def supervisor(state: State):
task = state["task"]
if "分析" in task:
# 更新state,并且指定下一步跳 agent_a
return Command(
goto="agent_a",
update={"messages": [("ai", "主管:交给AgentA分析任务")]}
)
elif "计算" in task:
return Command(
goto="agent_b",
update={"messages": [("ai", "主管:交给AgentB计算任务")]}
)
else:
# 直接跳到END结束
return Command(goto=END, update={"messages":[("ai","任务无法处理,结束")]})
# 构建图
builder = StateGraph(State)
builder.add_node("supervisor", supervisor)
builder.add_node("agent_a", agent_a)
builder.add_node("agent_b", agent_b)
builder.add_edge(START, "supervisor")
# 不需要额外条件边!由Command控制跳转
builder.add_edge("agent_a", END)
builder.add_edge("agent_b", END)
graph = builder.compile()
# 测试1:路由agent_a
res1 = graph.invoke({"task":"业务分析需求"})
print("\n结果1消息:", res1["messages"])
# 测试2:路由agent_b
res2 = graph.invoke({"task":"数据计算需求"})
print("\n结果2消息:", res2["messages"])
Command 关键参数说明
goto:目标节点名称,可以是节点名字符串,也可以是END;支持列表,一次性多 Send 并行。update:状态更新字典,和普通节点返回的增量更新语法一致,会经过 reducer 合并进 state。- 适用场景:主管 Agent 动态分配任务、跨子图跳转、人工中断恢复后定向路由。
- 注意:
Command是节点返回值;不能在条件边函数返回 Command,条件边只能返回节点名称字符串。