本教程基于 LangGraph 开发实战教程结合官方文档与实践经验,带你从零搭建 LangGraph 的核心认知体系。
为什么要学 LangGraph?
1.1 从 LangChain 到 LangGraph:一次思维的跃迁
在接触 LangGraph 之前,大多数开发者已经熟悉了 LangChain。LangChain 通过 LCEL(LangChain Expression Language)提供了一种极其简洁的方式来串联 LLM 调用:
python
# LangChain 的经典写法:线性流水线
chain = prompt_template | model | output_parser
result = chain.invoke({"topic": "人工智能"})
这种写法优雅、直观,但它隐含了一个前提:流程是固定的、单向的、无环的。数据从起点流向终点,不会回头,也不会在中途分叉后再汇合。
然而,现实世界的 AI 应用远比这复杂:
- 一个智能客服可能需要先判断用户意图,然后根据意图走不同的流程分支;
- 一个代码生成 Agent 可能需要"写代码 → 跑测试 → 发现错误 → 修改代码 → 再跑测试"这样的循环;
- 一个金融审批系统可能需要在关键节点暂停,等待人工审核员的确认。
这些场景需要的不是一个线性管道 ,而是一个有状态、可循环、可中断的执行引擎------这正是 LangGraph 诞生的原因。
一句话总结 :LangChain 让你快速跑起来 ,LangGraph 让你跑得复杂、跑得可控、跑得持久。两者不是替代关系,而是互补关系------你用 LangChain 的组件(模型、工具、检索器)作为积木,用 LangGraph 作为把这些积木粘合在一起的"蓝图与施工队"。
1.3 什么时候该用 LangGraph?
如果你遇到了以下任何一个问题,就该考虑从 LangChain 升级到 LangGraph:
- 你的 Agent 需要"反思":写完代码后自己检查,发现 bug 后重新修改。
- 你的流程需要"分叉":根据用户的输入,走完全不同的处理路径。
- 你的任务需要"暂停等人":在某个节点停下来,等领导审批通过后再继续。
- 你的系统需要"断点续跑":服务器重启后,Agent 能从上次中断的地方继续,而不是从头再来。
- 你有多个 Agent 需要协作:一个负责规划,一个负责执行,一个负责质检。
第二部分:LangGraph 三大核心要素
LangGraph 的所有能力都建立在三个核心概念之上:State(状态) 、Node(节点) 、Edge(边)。理解这三者,你就掌握了 LangGraph 的 80%。
2.1 State(状态):图流转的"公共黑板"
2.1.1 什么是 State?
State 是 LangGraph 的灵魂。它是一份全局共享的数据结构 ,图中的每一个节点都可以读取它、修改它。你可以把 State 想象成一块挂在墙上的公共黑板:
- 所有人(节点)都能看到黑板上写了什么;
- 每个人都可以在上面添加新内容;
- 后一个人能看到前一个人写下的所有内容。
2.1.2 如何定义 State?
在 LangGraph 中,State 通常用 Python 的 TypedDict 来定义:
python
from typing import TypedDict, List, Annotated
from langgraph.graph.message import add_messages
import operator
# 定义一个 State
class AgentState(TypedDict):
# 消息列表:使用 add_messages Reducer,实现追加而非覆盖
messages: Annotated[List, add_messages]
# 步骤计数器:使用 operator.add,每次累加
step: Annotated[int, operator.add]
# 用户意图:普通字段,会被直接覆盖
intent: str
# 是否已完成:布尔标志
is_complete: bool
Annotated是什么? Annotated是 Python 标准库 typing模块里的一个工具,它的作用是给类型提示附加额外的元信息。 对于 Python 本身来说,Annotated几乎不做任何事情------它只是给类型标注"贴了个标签"。但 LangGraph 会读取这个标签,从中提取出第二个参数(add_messages),把它当作这个字段的更新策略
python
from typing import Annotated
# 语法:Annotated[实际类型, 额外信息1, 额外信息2, ...]
x: Annotated[int, "这是一个年龄字段"]
2.1.3 Reducer(归约器)
Reducer(归约器)是一个函数,它定义了"当多个节点都想修改同一个 State 字段时,如何把这些修改合并成一个最终结果"。
或者说:Reducer 决定了 State 中某个字段的更新策略------是覆盖?是追加?是累加?还是其他自定义规则?
这是 LangGraph 新手最容易踩坑的地方。默认情况下,节点返回的状态会覆盖旧状态 。但在很多场景中,我们需要的是追加 或累加,而不是覆盖。
比如对话消息列表:如果每个节点都返回 {"messages": [new_msg]},默认行为会让旧消息消失。这时候就需要 Reducer 来救命。
python
# 不使用 Reducer:覆盖行为
state["messages"] = ["你好"] # 第一次设置
state["messages"] = ["世界"] # 第二次设置 → 覆盖了"你好"
# 使用 add_messages Reducer:追加行为
state["messages"] = ["你好"] # 第一次设置
state["messages"] = ["世界"] # 第二次设置 → 结果是 ["你好", "世界"]
LangGraph 内置了几种常用的 Reducer:
add_messages:消息列表追加(最常用)operator.add:数值累加或列表拼接operator.set:集合合并
你也可以自定义 Reducer 函数,实现任何你想要的合并逻辑。
2.2 Node(节点):图里的"执行工人"
2.2.1 什么是 Node?
Node 就是图中的一个个执行单元 。每个节点都是一个普通的 Python 函数,它接收当前的 State,执行一些业务逻辑,然后返回一个状态更新字典。
python
# 一个最简单的节点函数
def my_node(state: AgentState) -> dict:
"""
参数 state:当前的全局状态(只读+可写)
返回值 dict:要更新的状态字段
"""
# 读取当前状态
current_step = state.get("step", 0)
last_message = state["messages"][-1] if state["messages"] else ""
# 执行业务逻辑(比如调用 LLM)
# response = llm.invoke(last_message)
# 返回要更新的状态
return {
"messages": ["这是节点处理后的回复"],
"step": 1, # 由于 Reducer 是 operator.add,实际效果是 step += 1
}
2.2.2 节点的设计原则
- 无副作用:理想的节点应该只通过返回值修改 State,不要直接操作文件、数据库或全局变量。这样 LangGraph 才能正确地进行状态回溯和断点恢复。
- 单一职责:一个节点只做一件事。比如"调用 LLM"是一个节点,"格式化输出"是另一个节点。
- 可观测:节点的输入(State)和输出(State 更新)都应该清晰可辨。
2.2.3 将节点添加到图中
python
from langgraph.graph import StateGraph
# 创建图
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("llm_call", my_node) # 节点名称 + 节点函数
workflow.add_node("tool_execute", tool_node) # 可以有多个节点
workflow.add_node("format_output", format_node)
2.3 Edge(边):控制流的"神经脉络"
如果说 Node 是图中的"名词",那么 Edge 就是图中的"动词"------它决定了节点之间的连接关系和执行顺序。
2.3.1 普通边(Normal Edge)
普通边定义的是确定性的先后顺序:A 执行完之后,一定执行 B。
python
# 从 START 到 NodeA(入口)
workflow.add_edge(START, "llm_call")
# 从 NodeA 到 NodeB(顺序执行)
workflow.add_edge("llm_call", "tool_execute")
# 从 NodeB 到 END(出口)
workflow.add_edge("tool_execute", END)
这段代码的含义是:START → llm_call → tool_execute → END,一个标准的线性流程。
2.3.2 条件边(Conditional Edge)
条件边是 LangGraph 真正的杀手锏。它允许你根据当前 State 的内容,动态决定下一步去哪一个节点。
python
# 定义路由函数:根据状态决定下一步
def router_function(state: AgentState) -> str:
"""返回值是目标节点的名称"""
last_message = state["messages"][-1].content if state["messages"] else ""
# 判断逻辑
if "需要查资料" in last_message:
return "search_tool" # 去搜索节点
elif "需要计算" in last_message:
return "calculator_tool" # 去计算节点
elif "已完成" in last_message:
return END # 结束
else:
return "llm_call" # 继续对话(循环回 LLM 节点)
# 添加条件边
workflow.add_conditional_edges(
"llm_call", # 源节点
router_function, # 路由函数
{ # 路径映射(可选,用于校验)
"search_tool": "search_tool",
"calculator_tool": "calculator_tool",
END: END,
"llm_call": "llm_call",
}
)
条件边的威力 :它让图具备了动态决策能力。同样的输入,可能走完全不同的路径;同一个节点,可以被反复访问(形成循环)。
2.3.3 START 与 END
LangGraph 预定义了两个特殊节点:
START:图的入口点。所有从外部invoke()传入的数据,都会首先到达 START 节点。END:图的终止点。当执行流到达 END 时,图执行完毕,返回最终 State。
注意:你必须至少有一条路径从 START 出发,也必须至少有一条路径到达 END,否则编译会报错。
下面展示了从零构建一张可运行图的完整流程。
python
from typing import TypedDict, List, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
import operator
# ===== Step 1: 定义 State =====
class AgentState(TypedDict):
messages: Annotated[List, add_messages] # 消息历史(追加)
reflection_count: Annotated[int, operator.add] # 反思次数(累加)
max_reflections: int # 最大反思次数(覆盖)
# ===== Step 2: 定义 Node 函数 =====
llm = ChatOpenAI(model="gpt-4o-mini")
def call_model(state: AgentState) -> dict:
"""调用 LLM 生成回复"""
response = llm.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: AgentState) -> str:
"""条件路由:判断是否需要继续反思"""
# 如果反思次数已达上限,结束
if state["reflection_count"] >= state.get("max_reflections", 3):
return END
# 如果最后一条消息来自 AI,检查是否需要反思
last_message = state["messages"][-1]
if isinstance(last_message, AIMessage):
# 假设如果 AI 回复中包含"让我再想想",则需要反思
if "让我再想想" in last_message.content:
return "reflect"
return END
def reflect_node(state: AgentState) -> dict:
"""反思节点:对之前的回答进行自我审查"""
# 构造反思提示
reflection_prompt = f"""
请对你之前的回答进行反思,找出可能的不足或遗漏。
如果需要改进,请给出更好的版本。
原始回答:{state['messages'][-1].content}
"""
response = llm.invoke([HumanMessage(content=reflection_prompt)])
return {"messages": [response], "reflection_count": 1}
# ===== Step 3: 构建图 =====
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("call_model", call_model)
workflow.add_node("reflect", reflect_node)
# 添加边
workflow.add_edge(START, "call_model") # 入口 → 调用模型
workflow.add_conditional_edges( # 模型调用后,根据条件路由
"call_model",
should_continue,
{"reflect": "reflect", END: END}
)
workflow.add_edge("reflect", "call_model") # 反思后回到模型调用(形成循环)
# ===== Step 4: 编译图 =====
app = workflow.compile()
# ===== Step 5: 运行图 =====
initial_state = {
"messages": [HumanMessage(content="帮我写一首关于秋天的诗,要有意境")],
"reflection_count": 0,
"max_reflections": 2,
}
result = app.invoke(initial_state)
print(result["messages"][-1].content)