为什么需要 LangGraph
在第 2 周的学习中,我们使用 create_agent() 一行代码就构建出了能调用工具的智能助手。这种简洁性是 LangChain 高层 API 的优势,它隐藏了 ReAct 循环、工具调用解析、消息管理等复杂细节。然而当你需要构建的不仅仅是"提问→思考→回答"的线性流程,而是多步骤、有条件分支、需要人工审批、甚至要在失败后从断点恢复的复杂工作流时,这种黑盒抽象就成了桎梏。这就是 LangGraph 诞生的背景,它不是一个更高层的 Agent 封装,而是一个低层的、透明的编排框架,让你精确控制 Agent 执行的每一步。
LangGraph 的核心灵感来源于 Google 的 Pregel 系统和 Apache Beam,它把 Agent 工作流建模为一个有向图,节点负责执行具体逻辑(调用 LLM、执行工具、进行判断),边负责决定下一步走向哪里。这种设计让你可以用 Python 函数定义节点的行为,再用简单的图构建 API 把它们串联起来,最终得到一个完全可控、可观测、可持久化的状态机。正如官方文档所强调的,LangGraph 非常底层且专注于 Agent 编排,你可以脱离 LangChain 单独使用它,也可以与 LangChain 的模型和工具生态无缝集成。
在 2026 年的 LangChain 生态中,LangGraph 已经从可选扩展成长为框架的核心基础设施,官方推荐的 create_agent() 底层正是基于 LangGraph 构建的。这个信号告诉我们,有状态的、可定制的 Agent 工作流已经成为 LLM 应用的主流范式。
核心概念:图、状态、节点与边
LangGraph 的世界由四个核心概念构成,它们在官方 Graph API 文档中有详尽的阐述。
StateGraph ,这是 LangGraph 提供的主要图类,它接收一个用户自定义的状态 Schema 作为类型参数,所有节点和边的读写都围绕这个共享状态进行。状态 Schema 通常使用 Python 的 TypedDict 或 dataclass 来定义,也可以使用 Pydantic 的 BaseModel 获得递归数据校验能力(Pydantic 性能略低于 TypedDict)。状态中的每个字段都有独立的 reducer 函数,决定节点返回的更新如何与现有状态合并,默认行为是覆盖,但通过 Annotated 类型你可以指定自定义 reducer,比如使用 operator.add 将新值追加到列表而不是替换。由于消息列表是 Agent 场景中最高频的状态形式,LangGraph 提供了预置的 MessagesState,它包含一个 messages 字段,类型为 Annotated[list[AnyMessage], add_messages],其中 add_messages 是一个智能 reducer,新消息会被追加到列表,但如果消息 ID 已存在(比如人工编辑了某条消息),它会执行原地更新而非重复添加,同时还能自动将字典格式的消息反序列化为 LangChain 的 HumanMessage、AIMessage 等对象。你可以直接扩展 MessagesState 来添加额外的状态字段:
python
from langgraph.graph import MessagesState
class State(MessagesState):
documents: list[str]
llm_calls: int
这段代码展示了 MessagesState 的扩展方式,直接继承它并添加自定义字段。MessagesState 本身只定义了 messages 一个字段,但在实际 Agent 中我们往往还需要追踪其他信息,比如检索到的文档列表、LLM 调用次数等。通过继承方式添加的 documents 和 llm_calls 字段会自动获得默认的覆盖式 reducer,而 messages 字段继承了父类的 add_messages reducer 行为。这种设计让你不需要重复声明消息列表的 reducer 逻辑,只需关注业务特有的状态字段。
Node(节点) 是图中执行实际工作的 Python 函数。每个节点函数接收当前图状态作为第一个参数,还可以接收 config(包含 thread_id 等运行时配置的 RunnableConfig)和 runtime(包含上下文、流写入器、心跳维护等运行时工具),并返回一个状态更新字典,注意节点不需要返回完整的状态 Schema,只需返回需要更新的字段。节点可以调用 LLM、执行工具、运行任意 Python 代码。LangGraph 不限定节点内部做什么,它只关心节点返回的更新如何影响状态以及下一步走向哪里。你通过 add_node("node_name", node_function) 将节点注册到图中,节点名称就是后续连接边时使用的标识符。如果你不显式指定节点名称,LangGraph 会使用函数名作为默认名称。在底层,节点函数被转换为 RunnableLambda,从而自动获得批处理和异步支持。
Edge(边) 定义了节点之间的路由关系。最简单的边是普通边,通过 add_edge("node_a", "node_b") 建立一条从 A 到 B 的固定路径,每次 A 执行完毕后无条件进入 B。更强大的路由机制是条件边 ,通过 add_conditional_edges("node_a", routing_function) 实现动态路由,routing_function 接收当前状态,返回一个字符串(或字符串列表),LangGraph 根据返回值决定下一步执行哪个节点。你可以额外传入一个字典作为路由映射表,将函数返回值映射到节点名称。条件边是实现 ReAct 循环、分支决策和工具调用的核心机制。
除了普通边和条件边,LangGraph 还提供了两个特殊的虚拟节点:START 和 END。START 表示图的入口点,add_edge(START, "first_node") 指定了图启动时首先执行哪个节点。END 表示终止节点,add_edge("last_node", END) 标记某个节点执行后流程结束。这两个虚拟节点让图的起点和终点定义变得显式和可读。重要的是一个节点可以有多条出边,所有目标节点将在同一个 super-step 中并行执行。但对于每个节点,你应该选择一种路由机制,要么使用普通边实现静态路由,要么使用条件边或 Command 实现动态路由,不要混用两者,否则两条路径都可能被执行,使图行为难以预测。
此外,LangGraph 还提供了两个高级路由原语。Command 允许节点在返回状态更新的同时指定下一步路由,通过 Command(update=..., goto="target_node") 将状态更新和控制流合并在一步中完成。Send 则支持 map-reduce 模式,让你从一个节点向多个下游节点发送不同的状态片段,当节点数量在运行前不可知时尤其有用。
构建第一个 LangGraph 工作流
让我们按照最新官方文档的实践方式,从头构建一个基于 ReAct 循环的 Agent。首先确保安装了 LangGraph:
bash
pip install -U langgraph
第一步是定义工具和模型。我们沿用前两周的模式,定义几个简单的 @tool 工具,然后使用 model.bind_tools(tools) 将工具绑定到模型上,让模型在需要时自动生成 tool_call:
python
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from dotenv import load_dotenv
load_dotenv()
model = init_chat_model("Qwen/Qwen2.5-7B-Instruct", model_provider="openai")
@tool
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
@tool
def add(a: int, b: int) -> int:
"""Add a and b."""
return a + b
@tool
def divide(a: int, b: int) -> float:
"""Divide a and b."""
return a / b
tools = [add, multiply, divide]
tools_by_name = {tool.name: tool for tool in tools}
model_with_tools = model.bind_tools(tools)
这里有几个关键设计值得注意。@tool 装饰器将普通 Python 函数转换为 LangChain 工具对象,函数的 docstring 会自动成为工具的描述信息(description),模型正是通过这个描述来决定何时调用哪个工具,因此 docstring 必须准确描述工具的用途和参数含义。tools_by_name 字典将工具名映射到工具对象,这是后续在工具执行节点中按名称查找工具的桥梁,由于模型返回的 tool_call 只包含工具名称和参数而不包含工具对象本身,这个字典就充当了"工具注册表"的角色。model.bind_tools(tools) 是关键的绑定操作,它告诉模型"你有这些工具可用",此后模型在推理时会自动判断是否需要调用工具,并在需要时生成对应的 tool_call。注意 bind_tools 返回的是一个新的模型实例而非修改原模型,这是 LangChain 不可变设计模式的体现。
第二步是定义图的状态。我们使用 MessagesState 并扩展一个 llm_calls 计数器来追踪 LLM 调用次数:
python
from langchain.messages import AnyMessage
from langgraph.graph import add_messages
from typing_extensions import Annotated, TypedDict
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
llm_calls: int
这里我们手动定义了 MessagesState 而非直接使用 LangGraph 预置的版本,目的是展示 add_messages reducer 的使用方式。Annotated[list[AnyMessage], add_messages] 是 Python 类型注解的高级用法,第一个参数 list[AnyMessage] 是基础类型,第二个参数 add_messages 是 reducer 函数,LangGraph 在每次节点返回状态更新时会调用这个 reducer 来合并新旧消息。add_messages 的行为比简单的列表追加更智能,它会用消息 ID 来判断是新消息还是已有消息的更新,新消息直接追加到列表末尾,而已有消息(通过 ID 匹配)则原地替换内容。llm_calls 字段没有指定 reducer,因此默认行为是覆盖,每次节点返回 {"llm_calls": N} 时,旧值会被直接替换为新值。这也解释了一个重要规则,节点只需要返回需要更新的字段,不需要返回完整状态。
第三步是定义两个核心节点。LLM 调用节点负责向模型发送消息并获取响应,注意这里我们需要包含系统提示,而 LangGraph 节点函数内部可以直接构造消息列表并调用模型。工具执行节点检查最后一条 AI 消息中是否包含 tool_calls,如果有则逐个执行对应的工具,将结果封装为 ToolMessage 返回:
python
from langchain.messages import SystemMessage, ToolMessage
def llm_call(state: dict):
"""LLM decides whether to call a tool or not."""
return {
"messages": [
model_with_tools.invoke(
[SystemMessage(content="You are a helpful assistant tasked with performing arithmetic.")]
+ state["messages"]
)
],
"llm_calls": state.get("llm_calls", 0) + 1
}
def tool_node(state: dict):
"""Performs the tool call."""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
return {"messages": result}
llm_call 节点的核心逻辑在于消息的组装方式。[SystemMessage(...)] + state["messages"] 将系统提示放在消息列表最前面,确保模型在每次推理时都能看到系统指令,这与 OpenAI 的 messages API 格式完全一致。注意我们返回的是一个包含单条 AI 消息的列表("messages": [...]),而不是直接返回消息对象。这是因为 add_messages reducer 期望接收一个消息列表来与现有列表合并。llm_calls 的更新使用了 state.get("llm_calls", 0) + 1,在首次调用时提供默认值 0 防止 KeyError,随后每次调用递增计数。
tool_node 的实现体现了"一个 LLM 响应可能包含多个 tool_call"的设计,模型可以一次请求并行调用多个工具,因此我们用 for 循环遍历 state["messages"][-1].tool_calls 列表。state["messages"][-1] 取最后一条消息(即 llm_call 节点刚生成的 AI 消息),.tool_calls 是该消息中模型请求的工具调用列表。每个 tool_call 包含三个关键字段:name(工具名称)、args(调用参数)、id(调用唯一标识)。tools_by_name[tool_call["name"]] 通过名称查找工具对象,tool.invoke(tool_call["args"]) 执行工具,ToolMessage(content=..., tool_call_id=...) 将执行结果封装为工具消息,其中 tool_call_id 必须与原始 tool_call 的 id 对应,这样模型才能将结果与请求匹配起来。
第四步是定义条件路由函数。这是 ReAct 循环的核心决策点,检查最后一条消息是否包含工具调用,如果包含就返回 "tool_node" 继续执行工具,否则返回 END 终止循环:
python
from typing import Literal
from langgraph.graph import StateGraph, START, END
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
"""Decide if we should continue the loop or stop."""
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tool_node"
return END
这个函数虽然只有几行代码,但它承载了 ReAct 循环的核心决策逻辑。返回类型 Literal["tool_node", END] 不是普通的类型注解,它同时服务于两个目的,一是让类型检查器验证返回值是否合法,二是让 LangGraph 在编译时知道这个条件边可能路由到哪些节点,从而正确渲染图结构和检查完整性。last_message.tool_calls 是判断依据,当模型决定调用工具时,AI 消息的 tool_calls 属性会是一个非空列表(包含模型请求的工具调用),此时返回 "tool_node" 将流程导向工具执行。当模型认为不需要调用工具、直接给出了最终答案时,tool_calls 为空列表(空列表在 Python 中是 falsy 的),此时返回 END 终止图的执行。这个设计体现了 LangGraph 的一个重要理念,路由逻辑与业务逻辑分离 ,should_continue 只做判断不做执行,让图的控制流保持清晰可读。
最后一步是构建和编译图。我们将两个节点添加进去,用 START → llm_call 设置入口,用条件边 llm_call → should_continue → tool_node 或 END 建立分支,用 tool_node → llm_call 形成循环,然后调用 compile() 编译为可执行应用:
python
# Build workflow
agent_builder = StateGraph(MessagesState)
# Add nodes
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
# Add edges to connect nodes
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges(
"llm_call",
should_continue,
{"tool_node": "tool_node", END: END}
)
agent_builder.add_edge("tool_node", "llm_call")
# Compile the agent
agent = agent_builder.compile()
这段构建代码揭示了 LangGraph 的图组装的四个标准步骤。第一步 StateGraph(MessagesState) 创建一个以 MessagesState 为共享状态的空图,这是整张图的"画布",后续所有操作都在这张画布上进行。第二步通过 add_node 注册两个处理节点,分别负责 LLM 推理和工具执行,节点注册后不会立即执行,它们只是"待命"状态,等待边来触发。第三步建立边的关系,add_edge(START, "llm_call") 定义了图的入口,意味着用户输入首先到达 llm_call 节点,add_conditional_edges 则在 llm_call 节点之后插入了一个"分叉路口",should_continue 函数充当交通指挥,第三个参数是路由映射表,将函数的返回值("tool_node" 和 END)分别映射到目标节点,add_edge("tool_node", "llm_call") 形成了关键的"循环回路",工具执行完毕后一定会回到 LLM 节点重新推理,这正是 ReAct 循环的引擎。第四步 compile() 看似简单实则关键,它校验图的完整性(例如是否有节点没有被任何边连接)、绑定运行时配置、并将图编译为可执行的 CompiledStateGraph 对象。编译是必须的,未编译的图无法执行。
至此,一个完整的 ReAct Agent 就构建好了。编译这一步做了两件重要的事,一是对图结构进行基本检查(确保没有孤立节点等),二是绑定运行时参数如 checkpointer 和断点。你可以用以下命令可视化图结构:
python
from IPython.display import Image, display
display(Image(agent.get_graph(xray=True).draw_mermaid_png()))
执行 Agent 同样简单,传入初始消息,invoke() 会驱动完整的 ReAct 循环直到 LLM 不再调用工具:
python
from langchain.messages import HumanMessage
messages = [HumanMessage(content="Add 3 and 4.")]
messages = agent.invoke({"messages": messages})
for m in messages["messages"]:
m.pretty_print()
可视化代码中 agent.get_graph(xray=True).draw_mermaid_png() 生成的是 Mermaid 格式的图结构渲染------xray=True 参数会展开子图的内部结构,让整个流程一览无余。这对于调试复杂工作流非常有用,你可以直观地看到节点之间的连接关系和条件分支的走向。执行代码则展示了 LangGraph 最简洁的调用方式:agent.invoke({"messages": messages}) 传入初始消息列表,LangGraph 自动驱动完整的 ReAct 循环,LLM 收到"Add 3 and 4"后生成 add(3, 4) 的 tool_call,工具节点执行加法返回结果 7,LLM 再次收到工具结果后给出最终答案。整个过程对调用者完全透明,invoke() 返回的是所有消息累积后的最终状态。pretty_print() 是 LangChain 消息对象的便捷方法,会按角色(Human / AI / Tool)格式化输出每条消息的内容,方便在终端中查看完整的对话历史。
理解执行模型:Super-Step 与消息传递
LangGraph 的底层图算法采用消息传递模型,灵感来源于 Google 的 Pregel 系统。图执行被划分为离散的 super-step,每个 super-step 中所有被激活的节点并行执行。一个节点在收到入边上的新消息(状态更新)时变为 active 状态,执行其函数,然后通过出边将更新后的消息发送给下游节点。在每个 super-step 结束时,没有收到新消息的节点投票 halt,将自己标记为 inactive。当所有节点都处于 inactive 状态且没有消息在传输中时,图执行终止。
对于 START → llm_call → tool_node → llm_call → ... → END 这样的顺序图,每个 super-step 只包含一个节点。但如果一个节点有多个出边目标,比如条件边返回多个节点名称,这些目标节点将在同一个 super-step 中并行执行。你可以通过 recursion_limit 参数(从 v1.0.6 开始默认值为 1000 步)来控制最大执行步数,LangGraph 还提供了 RemainingSteps 托管值让你在节点内主动感知剩余步数并实现优雅降级。
流式输出:实时监控 Agent 的每一步
invoke() 适用于一次性获取最终结果,但在交互式应用或调试场景中,你需要实时观测 Agent 的每一步执行。LangGraph 提供了强大的 stream() 方法,支持多种流模式。每个流模式的输出在 v2 格式下统一为 StreamPart 字典({"type": ..., "ns": ..., "data": ...}),通过 chunk["type"] 即可区分。
最常用的三种模式是:updates 流式输出每个节点返回的状态更新;values 输出每一步后的完整状态快照;messages 以 token 级别流式输出 LLM 的生成文本,适合前端逐字展示。你还可以通过 get_stream_writer() 在节点内部发送自定义事件,配合 stream_mode="custom" 来接收------这对于报告进度、发送日志等场景非常实用:
python
from langgraph.config import get_stream_writer
def my_node(state: State):
writer = get_stream_writer()
writer({"progress": "thinking..."})
# ... do work ...
writer({"progress": "done"})
return {"result": "completed"}
# 使用多个流模式
for chunk in graph.stream(inputs, stream_mode=["updates", "custom"], version="v2"):
if chunk["type"] == "updates":
for node_name, state_update in chunk["data"].items():
print(f"Node {node_name} updated: {state_update}")
elif chunk["type"] == "custom":
print(f"Custom: {chunk['data']}")
get_stream_writer() 是 LangGraph 流式体系中最灵活的机制,它在节点内部返回一个可调用的 writer 对象,你可以随时调用 writer(some_dict) 将任意数据推送到流中。这里的 {"progress": "thinking..."} 只是示例,实际上你可以推送任何 JSON 可序列化的数据,比如中间计算结果、当前步骤的描述、甚至是前端 UI 需要渲染的组件信息。在消费端,stream_mode=["updates", "custom"] 同时订阅了两种流模式,chunk["type"] 用于区分每个 chunk 的来源,"updates" 类型的 chunk 的 data 字段是一个字典,键是节点名称、值是该节点返回的状态更新,"custom" 类型的 chunk 的 data 字段就是你通过 writer() 推送的原始数据。这种设计让业务数据(节点状态更新)和 UI 数据(进度通知、日志等)可以在同一条流中传输但互不干扰。version="v2" 启用了统一的 StreamPart 格式,无论你订阅了几种模式,每个 chunk 的结构都是 {"type": ..., "ns": ..., "data": ...},这极大简化了消费端的类型判断逻辑。
状态持久化与检查点
LangGraph 的持久化层是它区别于简单 Chain 调用的关键特性之一。当你用 compile(checkpointer=InMemorySaver()) 编译图时,LangGraph 会在每个 super-step 边界自动保存一个检查点(checkpoint)------这是图状态的完整快照。检查点按 thread(由 thread_id 标识)组织,同一 thread 内的多次调用会沿着同一条时间线累积状态。这意味着你的 Agent 天然具备"记忆"能力:下一轮对话中传入相同的 thread_id,Agent 就能访问之前的全部消息历史。
python
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = agent_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "conversation-1"}}
graph.invoke({"messages": [HumanMessage(content="北京天气怎么样?")]}, config)
# 第二轮对话自动继承之前的消息历史
graph.invoke({"messages": [HumanMessage(content="我刚才问了什么?")]}, config)
这段代码演示了 LangGraph 持久化最直接的应用,跨轮次会话记忆。InMemorySaver() 将所有检查点保存在内存中,适合开发调试但重启后会丢失。生产环境应使用 SqliteSaver 或 PostgresSaver。关键是 config 中的 thread_id,它相当于"会话标识符",LangGraph 的 checkpointer 以它为主键存储和检索检查点。当第一轮 invoke 执行完毕后,完整的消息历史(用户问题、LLM 的 tool_call、工具返回结果、LLM 最终回答)都保存在了 thread_id="conversation-1" 的检查点链中。第二轮调用使用相同的 thread_id,LangGraph 会自动从上次的检查点恢复状态,因此模型能看到"北京天气怎么样?"这条历史消息并回答"我刚才问了什么?"这类指代性问题。如果不传 thread_id 或每次使用新的 thread_id,每轮对话都会从零开始、彼此隔离。
持久化还支撑了更高级的特性,人工介入(human-in-the-loop)允许你在图的任意节点通过 interrupt() 暂停执行,等待人工审核或修改状态后再通过 Command(resume=...) 恢复。时间旅行让你可以回放到历史的任意检查点,分叉出新的执行路径。故障恢复让你在节点失败后从上一个成功的检查点重试,而不需要重新执行已完成的工作。通过 graph.get_state(config) 可以随时获取最新的状态快照,通过 graph.get_state_history(config) 可以查看整个执行时间线的完整历史。对于本地开发,InMemorySaver 和 SqliteSaver 足够满足需求,生产环境则可以使用 PostgresSaver,它支持异步操作和加密序列化。
关键 API 速览
| API | 用途 | 示例 |
|---|---|---|
StateGraph(State) |
以给定的状态 Schema 创建图 | StateGraph(MessagesState) |
add_node(name, func) |
向图注册一个处理节点 | add_node("agent", call_model) |
add_edge(from, to) |
建立无条件固定边 | add_edge("tools", "agent") |
add_conditional_edges(src, fn) |
以函数动态决定下一个节点 | add_conditional_edges("agent", should_continue) |
add_conditional_edges(src, fn, mapping) |
同上,附带路由映射表 | add_conditional_edges("agent", fn, {"yes": "node_a", "no": END}) |
compile(checkpointer=...) |
编译校验并绑定运行时参数 | graph.compile(checkpointer=InMemorySaver()) |
invoke(input, config) |
同步执行整个图 | agent.invoke({"messages": [...]}) |
stream(input, stream_mode=...) |
逐步流式输出执行过程 | agent.stream(input, stream_mode="updates") |
add_edge 与 add_conditional_edges 的核心区别在于:add_edge 建立的是静态、无条件 路由------A 节点执行完毕后始终进入 B 节点;add_conditional_edges 建立的是动态、有条件 路由------A 节点执行完毕后调用一个函数,根据函数返回值决定进入哪个节点。使用场景上,add_edge 适合固定的管道式流程(如"工具执行完后务必返回 LLM 再思考"),而 add_conditional_edges 适合有分支决策的流程(如"LLM 输出包含工具调用则去执行工具,否则结束")。两者都是从一个源节点出发定义路由------不要混用普通边和条件边指向同一个源节点。
AgentExecutor 与 LangGraph 的对比
在学习 LangGraph 之前,你已经在第 2 周接触了 create_agent() 和 AgentExecutor。两者都实现了 ReAct 循环,但在架构哲学上有本质区别。
AgentExecutor 是一个封装好的 Runnable,内部通过一个 while 循环驱动"LLM 思考 → 工具调用 → LLM 再思考"的迭代,循环逻辑固化在源码中、不可干预。它适合快速原型,但当你需要在循环中插入人工审批步骤、根据中间结果动态切换策略、或者并行调用多个工具时,AgentExecutor 的灵活性就显得捉襟见肘。从状态管理的角度看,AgentExecutor 的状态散落在 Runnable 的内部闭包中,外部几乎无法感知和修改。
LangGraph 把同样的循环展开为一个显式的图结构------每一步都是独立可寻址的节点,路由逻辑是纯粹的函数,状态管理是完全透明的。这意味着你可以随时在图中插入新的节点来增强流程,比如在工具执行后增加一个验证节点、在 LLM 调用前增加一个检索节点、或者在某个条件分支挂载一个完全不同的子图。从扩展性角度,LangGraph 天然支持子图嵌套、多 Agent 协作和手把手交接(handoff),是构建复杂多智能体系统的基础设施。
| 维度 | AgentExecutor | LangGraph |
|---|---|---|
| 架构模式 | 黑盒 while 循环 | 显式有向图,每步可寻址 |
| 状态管理 | 隐式闭包,外部不可见 | 显式 TypedDict/dataclass,可读写 |
| 流程控制 | 固定的思考-工具循环 | 自定义节点+条件边,任意拓扑 |
| 流式输出 | 基础 token 流 | 多模式流(状态/更新/token/自定义) |
| 持久化 | 不支持 | 内置检查点机制,支持断点恢复 |
| 人工介入 | 不支持 | 原生 interrupt() 暂停-审批-恢复 |
| 扩展性 | 修改源码或包装 Runnable | 插入节点/嵌套子图/多 Agent 协作 |
| 调试可见性 | 低 | 高------每一步状态可查询、可回放 |
综合来看,AgentExecutor 是 LangChain 早期为简化 Agent 创建而设计的便捷工具,适合简单、线性的 Agent 场景;LangGraph 则是为生产级、可定制、有状态的 Agent 工作流而生的编排框架。随着 LangChain 生态的演进,官方推荐使用 create_agent()(底层基于 LangGraph)作为新的标准入口------这印证了 LangGraph 已经从可选扩展成长为框架的核心基础设施。
练习任务
- 参照本文中的代码示例,绘制"用户提问 → LLM → 工具 → LLM → 回答"的 Mermaid 状态图
- 参照本文中的代码实现完整的 ReAct 循环 LangGraph 版本,至少包含 3 个不同功能的工具
- 在上述代码基础上添加
InMemorySaver持久化,验证多轮对话的记忆能力 - 撰写 AgentExecutor vs LangGraph 对比分析,重点讨论架构、状态管理和扩展性三个维度
考核点 ✅
- 状态图绘制:提交 LangGraph 状态图(Mermaid/PlantUML 或手绘拍照)
- 代码实现:提交完整可运行的 LangGraph ReAct 循环代码(含持久化)
- 对比分析:提交 AgentExecutor vs LangGraph 对比表(架构/状态管理/扩展性)
- API 掌握 :口头解释
add_conditional_edges和add_edge的区别及使用场景