《LangGraph 设计与实现》完整目录
- 前言
- 第1章 为什么需要理解 LangGraph
- 第2章 架构总览
- 第3章 StateGraph 图构建 API
- 第4章 Channel 状态管理与 Reducer
- 第5章 图编译:从 StateGraph 到 CompiledStateGraph
- 第6章 Pregel 执行引擎
- 第7章 任务调度与并行执行
- 第8章 Checkpoint 持久化
- 第9章 中断与人机协作
- 第10章 Command 与高级控制流
- 第11章 子图与嵌套
- 第12章 Send 与动态并行
- 第13章 流式输出与调试
- 第14章 Runtime 与 Context
- 第15章 Store 与长期记忆
- 第16章 预构建 Agent 组件(当前)
- 第17章 多 Agent 模式实战
- 第18章 设计模式与架构决策
第16章 预构建 Agent 组件
16.1 引言
前面的章节深入剖析了 LangGraph 的底层基础设施------StateGraph、Channel、Pregel 调度、Checkpoint、Send、Runtime、Store。这些原语提供了极大的灵活性,但直接使用它们构建一个完整的 Agent 需要编写大量的样板代码:定义状态 schema、创建 ToolNode、编写条件边路由、处理错误和重试。
langgraph.prebuilt 模块正是为了解决这个问题。它在底层原语之上提供了一组经过实战验证的高层组件:create_react_agent 工厂函数可以一行代码创建完整的 ReAct Agent;ToolNode 封装了工具执行的并行化、错误处理和状态注入;tools_condition 提供了标准的条件路由;ValidationNode 支持工具调用的 schema 验证;InjectedState 和 InjectedStore 让工具可以直接访问图状态和持久化存储。
本章将从这些组件的源码出发,分析它们如何将底层能力组合成开发者友好的高层 API,同时保持完整的可扩展性。
:::tip 本章要点
create_react_agent工厂函数------从参数到编译图的完整构建流程ToolNode实现------并行执行、错误处理、状态注入、Command 支持tools_condition路由------标准的 Agent 循环条件判断ValidationNode------工具调用的 Pydantic schema 验证InjectedState、InjectedStore、ToolRuntime------工具级别的依赖注入 :::
16.2 create_react_agent 工厂函数
16.2.1 签名概览
create_react_agent 定义在 langgraph/prebuilt/chat_agent_executor.py 中,是构建 ReAct Agent 的一站式入口:
python
def create_react_agent(
model: str | LanguageModelLike | Callable,
tools: Sequence[BaseTool | Callable | dict] | ToolNode,
*,
prompt: Prompt | None = None,
response_format: StructuredResponseSchema | None = None,
pre_model_hook: RunnableLike | None = None,
post_model_hook: RunnableLike | None = None,
state_schema: StateSchemaType | None = None,
context_schema: type[Any] | None = None,
checkpointer: Checkpointer | None = None,
store: BaseStore | None = None,
interrupt_before: list[str] | None = None,
interrupt_after: list[str] | None = None,
debug: bool = False,
version: Literal["v1", "v2"] = "v2",
name: str | None = None,
) -> CompiledStateGraph:
16.2.2 构建流程
prompt + LLM"] AddNodes --> ToolsN["'tools' 节点
ToolNode"] AddNodes --> |可选| PreHook["'pre_model_hook' 节点"] AddNodes --> |可选| PostHook["'post_model_hook' 节点"] Agent --> AddEdges[添加边] AddEdges --> CondEdge["条件边
agent -> tools / END"] AddEdges --> BackEdge["tools -> agent"] end subgraph 编译 AddEdges --> Compile["compile(
checkpointer, store,
interrupt_before/after)"] Compile --> CSG[CompiledStateGraph] end
16.2.3 模型处理
create_react_agent 支持三种模型传入方式:
python
# 1. 字符串标识符(需要 langchain 包)
graph = create_react_agent("openai:gpt-4", tools)
# 2. LangChain ChatModel 实例
from langchain_openai import ChatOpenAI
graph = create_react_agent(ChatOpenAI(model="gpt-4"), tools)
# 3. 动态模型选择函数
def select_model(state, runtime: Runtime[ModelContext]):
if runtime.context.use_premium:
return ChatOpenAI(model="gpt-4").bind_tools(tools)
return ChatOpenAI(model="gpt-3.5-turbo").bind_tools(tools)
graph = create_react_agent(select_model, tools, context_schema=ModelContext)
对于静态模型,框架自动调用 bind_tools 绑定工具。如果模型已经通过 model.bind_tools() 绑定了工具,框架会检查绑定的工具是否与传入的 tools 参数匹配。
16.2.4 v1 vs v2 版本差异
version 参数控制工具节点的执行策略:
tool_calls: A, B, C] --> TN1[ToolNode] TN1 --> |并行执行 A B C| Result1[三个 ToolMessage] end subgraph "v2:Send API 分发工具调用" AI2[AIMessage
tool_calls: A, B, C] --> Send2{Send 分发} Send2 --> TN2a["ToolNode(call A)"] Send2 --> TN2b["ToolNode(call B)"] Send2 --> TN2c["ToolNode(call C)"] end
v2 版本使用 Send API 将每个 tool_call 分发为独立的 ToolNode 实例。这种设计的优势:
- 中断粒度更细:可以单独中断/恢复某个工具调用
- 超时隔离:一个工具超时不影响其他工具
- Checkpoint 更精确:每个工具调用有独立的 checkpoint 状态
16.2.5 remaining_steps 安全机制
create_react_agent 使用 RemainingSteps managed value 来防止无限循环:
python
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
remaining_steps: NotRequired[RemainingSteps]
当 remaining_steps 降至 2 以下且 LLM 仍在请求工具调用时,Agent 会返回一条友好的终止消息,而不是抛出 GraphRecursionError。
16.3 ToolNode 实现
16.3.1 核心职责
ToolNode 是工具执行的中枢,负责:
- 从最后一条 AIMessage 中提取 tool_calls
- 查找对应的工具实现
- 注入状态、Store 等依赖
- 并行执行工具
- 处理错误和返回结果
python
class ToolNode(RunnableCallable):
"""A node that runs tools requested by an AI model."""
def __init__(
self,
tools: Sequence[BaseTool | Callable],
*,
name: str = "tools",
tags: list[str] | None = None,
handle_tool_errors: bool | str | Callable | tuple = True,
):
self.tools_by_name = {tool.name: tool for tool in resolved_tools}
self.handle_tool_errors = handle_tool_errors
16.3.2 工具执行流程
16.3.3 错误处理策略
handle_tool_errors 支持多种配置:
python
# 默认:返回错误消息
ToolNode(tools, handle_tool_errors=True)
# ToolMessage(content="Error: ... Please fix your mistakes.")
# 自定义错误消息
ToolNode(tools, handle_tool_errors="Something went wrong, try again.")
# 自定义错误处理函数
def my_handler(e: ValueError) -> str:
return f"Got a value error: {e}"
ToolNode(tools, handle_tool_errors=my_handler)
# 只捕获特定异常类型
ToolNode(tools, handle_tool_errors=(ValueError, TypeError))
# 不捕获错误,直接抛出
ToolNode(tools, handle_tool_errors=False)
当 handle_tool_errors 是一个 callable 时,框架会通过 _infer_handled_types 分析其类型注解,推断它能处理哪些异常类型:
python
def _infer_handled_types(handler: Callable) -> tuple[type[Exception], ...]:
"""分析 handler 的类型注解,推断可处理的异常类型"""
sig = inspect.signature(handler)
# 检查第一个参数的类型注解
type_hints = get_type_hints(handler)
# 支持 Union[ValueError, TypeError] 等联合类型
...
16.3.4 ToolCallRequest 与拦截器
v2 版本引入了 ToolCallRequest 和 ToolCallWrapper,支持工具调用的中间件模式:
python
@dataclass
class ToolCallRequest:
tool_call: ToolCall # 工具调用信息
tool: BaseTool | None # 工具实例
state: Any # 当前图状态
runtime: ToolRuntime # 工具运行时
def override(self, **overrides) -> ToolCallRequest:
"""创建修改后的请求副本"""
return replace(self, **overrides)
拦截器模式允许在工具执行前后插入自定义逻辑:
python
ToolCallWrapper = Callable[
[ToolCallRequest, Callable[[ToolCallRequest], ToolMessage | Command]],
ToolMessage | Command,
]
# 拦截器示例:重试逻辑
def retry_wrapper(request, execute):
for attempt in range(3):
result = execute(request)
if isinstance(result, ToolMessage) and result.status != "error":
return result
return result
# 拦截器示例:参数修改
def sanitize_wrapper(request, execute):
modified_call = {**request.tool_call, "args": sanitize(request.tool_call["args"])}
return execute(request.override(tool_call=modified_call))
16.3.5 Command 返回支持
工具可以返回 Command 对象来直接控制图的执行流:
python
@tool
def transfer_to_agent(agent_name: str) -> Command:
"""将对话转移给另一个 Agent"""
return Command(goto=agent_name, update={"transferred": True})
ToolNode 会识别 Command 类型的返回值,将其直接传播到图的控制流中,而不是包装为 ToolMessage。
16.4 tools_condition 路由
16.4.1 实现
python
def tools_condition(
state: list[AnyMessage] | dict[str, Any] | BaseModel,
messages_key: str = "messages",
) -> Literal["tools", "__end__"]:
"""Conditional routing: if tool_calls present, route to 'tools'; else END."""
if isinstance(state, list):
ai_message = state[-1]
elif isinstance(state, dict):
ai_message = state[messages_key][-1]
elif messages := getattr(state, messages_key, None):
ai_message = messages[-1]
else:
raise ValueError(f"No messages found in state: {state}")
if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
return "tools"
return "__end__"
这个函数实现了 ReAct Agent 的核心循环逻辑:如果 LLM 输出包含 tool_calls,继续执行工具;否则结束。它支持三种状态格式------列表、字典和 BaseModel。
16.4.2 在图中使用
python
builder = StateGraph(AgentState)
builder.add_node("agent", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
graph = builder.compile()
16.5 ValidationNode
16.5.1 设计动机
在提取(extraction)和结构化输出场景中,我们经常需要让 LLM 生成符合特定 schema 的数据。ValidationNode 不执行工具,而是用 Pydantic 验证工具调用的参数是否合法:
python
class ValidationNode(RunnableCallable):
def __init__(
self,
schemas: Sequence[BaseTool | type[BaseModel] | Callable],
*,
format_error: Callable | None = None,
name: str = "validation",
):
self.schemas_by_name: dict[str, type[BaseModel]] = {}
for schema in schemas:
# 支持 BaseModel、BaseTool、Callable 三种输入
...
16.5.2 验证流程
16.5.3 使用示例
python
from pydantic import BaseModel, field_validator
class SelectNumber(BaseModel):
a: int
@field_validator("a")
def a_must_be_meaningful(cls, v):
if v != 37:
raise ValueError("Only 37 is allowed")
return v
builder = StateGraph(Annotated[list, add_messages])
llm = ChatAnthropic(model="claude-3-5-haiku-latest").bind_tools([SelectNumber])
builder.add_node("model", llm)
builder.add_node("validation", ValidationNode([SelectNumber]))
builder.add_edge(START, "model")
def should_validate(state):
if state[-1].tool_calls:
return "validation"
return END
builder.add_conditional_edges("model", should_validate)
builder.add_conditional_edges("validation", should_reprompt)
graph = builder.compile()
16.6 InjectedState 与 InjectedStore
16.6.1 InjectedState
InjectedState 让工具函数能够访问图的当前状态,而不需要将状态字段显式作为工具参数:
python
from langgraph.prebuilt import InjectedState
from typing import Annotated
@tool
def get_context(
question: str,
state: Annotated[dict, InjectedState]
) -> str:
"""根据对话历史回答问题"""
messages = state["messages"]
context = "\n".join(m.content for m in messages[-5:])
return f"Based on context: {context}\nAnswer: ..."
InjectedState 标记的参数不会出现在工具的 schema 中(LLM 不会尝试填充它),它由 ToolNode 在执行时自动注入。
16.6.2 InjectedStore
类似地,InjectedStore 让工具直接访问 Store:
python
@tool
def save_note(
content: str,
store: Annotated[BaseStore, InjectedStore]
) -> str:
"""保存笔记"""
store.put(("notes",), f"note_{hash(content)}", {"content": content})
return "Note saved."
16.6.3 ToolRuntime:统一的工具运行时
LangGraph 1.1.6 引入了 ToolRuntime,统一了 InjectedState、InjectedStore 和其他注入:
python
class ToolRuntime(_DirectlyInjectedToolArg, Generic[ContextT, StateT]):
"""Runtime context automatically injected into tools."""
context: ContextT # 运行时上下文(与 Runtime 共享)
store: BaseStore | None # 持久化存储(与 Runtime 共享)
stream_writer: StreamWriter # 流式写入器(与 Runtime 共享)
config: RunnableConfig # 工具特有:当前配置
state: StateT # 工具特有:图状态
tool_call_id: str # 工具特有:工具调用 ID
使用 ToolRuntime 的工具示例:
python
@tool
def smart_tool(query: str, runtime: ToolRuntime) -> str:
"""一个能访问所有运行时信息的工具"""
# 访问图状态
history = runtime.state["messages"]
# 访问 Store
cached = runtime.store.get(("cache",), query) if runtime.store else None
if cached:
return cached.value["result"]
# 访问上下文
user_id = runtime.context.user_id if runtime.context else "anon"
# 流式写入
runtime.stream_writer({"status": "processing", "user": user_id})
result = f"Result for {query} by {user_id}"
# 缓存结果
if runtime.store:
runtime.store.put(("cache",), query, {"result": result})
return result
16.7 ToolCallWithContext:v2 的内部机制
16.7.1 数据结构
在 v2 版本中,每个工具调用通过 Send API 分发到独立的 ToolNode 实例。ToolCallWithContext 是 Send 携带的有效载荷:
python
class ToolCallWithContext(TypedDict):
tool_call: ToolCall
__type: Literal["tool_call_with_context"]
state: Any
16.7.2 分发流程
Send("tools", {tool_call: B, state})] Send->>TN1: ToolCallWithContext(tool_call=A) Send->>TN2: ToolCallWithContext(tool_call=B) TN1-->>Route: ToolMessage for A TN2-->>Route: ToolMessage for B
这使得每个工具调用在 Send 的框架下获得了独立的 checkpoint、中断能力和错误隔离。
16.8 设计决策
16.8.1 为什么 create_react_agent 接受 str 类型的 model?
python
graph = create_react_agent("openai:gpt-4", tools)
这是一个便利性设计------通过 langchain.chat_models.init_chat_model 支持字符串格式的模型标识。在快速原型开发时,开发者不需要导入和实例化具体的 ChatModel 类。
16.8.2 为什么 ToolNode 支持 handle_tool_errors?
在 Agent 循环中,工具执行失败是常态而非异常。LLM 可能生成无效的工具参数,外部 API 可能暂时不可用。如果每次工具失败都中断整个图的执行,用户体验会很差。handle_tool_errors 的默认行为是将错误转化为 ToolMessage,让 LLM 有机会修正错误并重试。
16.8.3 为什么 v2 使用 Send 而非内部并行?
v1 版本的 ToolNode 在内部使用线程池并行执行工具调用。v2 改用 Send API 的原因:
- Checkpoint 粒度:每个 Send 任务有独立的 checkpoint,中断恢复更精确
- 人机交互:可以对单个工具调用设置 interrupt_before,实现细粒度的审批
- 架构一致性:复用 Pregel 的任务调度,而非在 ToolNode 中引入自己的并行机制
16.8.4 InjectedState vs ToolRuntime
InjectedState 和 InjectedStore 是较早的注入机制,使用 Annotated 类型标记。ToolRuntime 是更新的统一方案,将所有注入点合并为一个对象。推荐新代码使用 ToolRuntime:
python
# 旧方式(仍然支持)
@tool
def old_tool(x: int, state: Annotated[dict, InjectedState]) -> str: ...
# 新方式(推荐)
@tool
def new_tool(x: int, runtime: ToolRuntime) -> str:
state = runtime.state # 同样的能力
store = runtime.store
16.9 组件之间的关系
16.10 小结
本章分析了 LangGraph 预构建 Agent 组件层的设计与实现。create_react_agent 作为一站式工厂函数,将 StateGraph 构建、ToolNode 配置、条件路由等步骤封装为单一调用,同时通过丰富的参数(prompt、response_format、pre/post_model_hook 等)保持完整的可配置性。
ToolNode 是这套组件的核心执行器,它处理了工具执行中的所有复杂性------参数注入、并行执行、错误处理、Command 传播。v2 版本通过 Send API 实现了更精细的工具调用分发,使每个工具调用获得了独立的 checkpoint 和中断能力。
ToolRuntime 的引入统一了工具级别的依赖注入,将 state、store、context、config、stream_writer、tool_call_id 打包为一个类型安全的对象。结合 InjectedState 和 InjectedStore 的向后兼容,开发者可以根据偏好选择注入方式。
这些预构建组件体现了 LangGraph 的分层设计哲学:底层提供灵活的原语(StateGraph、Channel、Send),上层提供开箱即用的解决方案(create_react_agent、ToolNode),中间层通过标准化接口(BaseStore、Runtime、StreamWriter)连接两者。开发者可以直接使用上层组件快速启动,也可以在理解底层原理后进行深度定制。
下一章我们将探讨多 Agent 模式------如何使用 LangGraph 构建 Supervisor、Swarm、分层和协作等多种 Agent 架构。