LangGraph设计与实现-第16章-预构建 Agent 组件

7 阅读11分钟

《LangGraph 设计与实现》完整目录

第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 验证;InjectedStateInjectedStore 让工具可以直接访问图状态和持久化存储。

本章将从这些组件的源码出发,分析它们如何将底层能力组合成开发者友好的高层 API,同时保持完整的可扩展性。

:::tip 本章要点

  1. create_react_agent 工厂函数——从参数到编译图的完整构建流程
  2. ToolNode 实现——并行执行、错误处理、状态注入、Command 支持
  3. tools_condition 路由——标准的 Agent 循环条件判断
  4. ValidationNode——工具调用的 Pydantic schema 验证
  5. InjectedStateInjectedStoreToolRuntime——工具级别的依赖注入 :::

16.2 create_react_agent 工厂函数

16.2.1 签名概览

create_react_agent 定义在 langgraph/prebuilt/chat_agent_executor.py 中,是构建 ReAct Agent 的一站式入口:

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 构建流程

flowchart TB
    subgraph 参数解析
        Model[model 参数] --> |str| InitModel["init_chat_model()"]
        Model --> |Runnable| BindTools["bind_tools(tools)"]
        Model --> |Callable| DynModel[动态模型选择]
        Tools[tools 参数] --> |list| CreateTN[创建 ToolNode]
        Tools --> |ToolNode| UseTN[直接使用]
    end

    subgraph 图构建
        CreateTN --> AddNodes
        UseTN --> AddNodes
        AddNodes[添加节点] --> Agent["'agent' 节点<br/>prompt + LLM"]
        AddNodes --> ToolsN["'tools' 节点<br/>ToolNode"]
        AddNodes --> |可选| PreHook["'pre_model_hook' 节点"]
        AddNodes --> |可选| PostHook["'post_model_hook' 节点"]
        Agent --> AddEdges[添加边]
        AddEdges --> CondEdge["条件边<br/>agent -> tools / END"]
        AddEdges --> BackEdge["tools -> agent"]
    end

    subgraph 编译
        AddEdges --> Compile["compile(<br/>checkpointer, store,<br/>interrupt_before/after)"]
        Compile --> CSG[CompiledStateGraph]
    end

16.2.3 模型处理

create_react_agent 支持三种模型传入方式:

# 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 参数控制工具节点的执行策略:

graph LR
    subgraph "v1:单节点处理所有工具调用"
        AI1[AIMessage<br/>tool_calls: A, B, C] --> TN1[ToolNode]
        TN1 --> |并行执行 A B C| Result1[三个 ToolMessage]
    end

    subgraph "v2:Send API 分发工具调用"
        AI2[AIMessage<br/>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 实例。这种设计的优势:

  1. 中断粒度更细:可以单独中断/恢复某个工具调用
  2. 超时隔离:一个工具超时不影响其他工具
  3. Checkpoint 更精确:每个工具调用有独立的 checkpoint 状态

16.2.5 remaining_steps 安全机制

create_react_agent 使用 RemainingSteps managed value 来防止无限循环:

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 是工具执行的中枢,负责:

  1. 从最后一条 AIMessage 中提取 tool_calls
  2. 查找对应的工具实现
  3. 注入状态、Store 等依赖
  4. 并行执行工具
  5. 处理错误和返回结果
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 工具执行流程

sequenceDiagram
    participant State as 图状态
    participant TN as ToolNode
    participant Tool as 具体工具
    participant Result as 结果

    State->>TN: 最后一条 AIMessage
    TN->>TN: 提取 tool_calls

    loop 每个 tool_call
        TN->>TN: 查找工具 by name
        TN->>TN: 注入 InjectedState / InjectedStore
        TN->>Tool: invoke(args)
        alt 正常返回
            Tool-->>TN: 工具输出
            TN->>Result: ToolMessage(content=output)
        else 工具异常
            Tool-->>TN: Exception
            alt handle_tool_errors=True
                TN->>Result: ToolMessage(content=error_msg, status="error")
            else handle_tool_errors=False
                TN-->>State: 抛出异常
            end
        end
    end

    TN->>State: {"messages": [ToolMessage, ...]}

16.3.3 错误处理策略

handle_tool_errors 支持多种配置:

# 默认:返回错误消息
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 分析其类型注解,推断它能处理哪些异常类型:

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 版本引入了 ToolCallRequestToolCallWrapper,支持工具调用的中间件模式:

@dataclass
class ToolCallRequest:
    tool_call: ToolCall        # 工具调用信息
    tool: BaseTool | None      # 工具实例
    state: Any                 # 当前图状态
    runtime: ToolRuntime       # 工具运行时

    def override(self, **overrides) -> ToolCallRequest:
        """创建修改后的请求副本"""
        return replace(self, **overrides)

拦截器模式允许在工具执行前后插入自定义逻辑:

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 对象来直接控制图的执行流:

@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 实现

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 在图中使用

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()
graph LR
    Start[START] --> Agent[agent]
    Agent -->|tool_calls 存在| Tools[tools]
    Agent -->|无 tool_calls| End[END]
    Tools --> Agent

16.5 ValidationNode

16.5.1 设计动机

在提取(extraction)和结构化输出场景中,我们经常需要让 LLM 生成符合特定 schema 的数据。ValidationNode 不执行工具,而是用 Pydantic 验证工具调用的参数是否合法:

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 验证流程

sequenceDiagram
    participant LLM as 模型
    participant Val as ValidationNode
    participant State as 状态

    LLM->>Val: AIMessage with tool_calls
    loop 每个 tool_call
        Val->>Val: 查找 schema by name
        Val->>Val: schema.model_validate(args)
        alt 验证通过
            Val->>State: ToolMessage(content=validated_json)
        else 验证失败
            Val->>State: ToolMessage(content=error, is_error=True)
        end
    end
    State->>LLM: 重新生成(如果有错误)

16.5.3 使用示例

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 让工具函数能够访问图的当前状态,而不需要将状态字段显式作为工具参数:

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:

@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 和其他注入:

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
graph TB
    subgraph "Runtime(节点级)"
        R_ctx[context]
        R_store[store]
        R_sw[stream_writer]
        R_prev[previous]
        R_ei[execution_info]
        R_si[server_info]
    end

    subgraph "ToolRuntime(工具级)"
        TR_ctx[context]
        TR_store[store]
        TR_sw[stream_writer]
        TR_config[config]
        TR_state[state]
        TR_tcid[tool_call_id]
    end

    R_ctx -.->|共享| TR_ctx
    R_store -.->|共享| TR_store
    R_sw -.->|共享| TR_sw

使用 ToolRuntime 的工具示例:

@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 携带的有效载荷:

class ToolCallWithContext(TypedDict):
    tool_call: ToolCall
    __type: Literal["tool_call_with_context"]
    state: Any

16.7.2 分发流程

sequenceDiagram
    participant Agent as agent 节点
    participant Route as 条件边
    participant Send as Send API
    participant TN1 as ToolNode 实例 1
    participant TN2 as ToolNode 实例 2

    Agent->>Route: AIMessage(tool_calls=[A, B])
    Route->>Send: [Send("tools", {tool_call: A, state}),<br/>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?

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 的原因:

  1. Checkpoint 粒度:每个 Send 任务有独立的 checkpoint,中断恢复更精确
  2. 人机交互:可以对单个工具调用设置 interrupt_before,实现细粒度的审批
  3. 架构一致性:复用 Pregel 的任务调度,而非在 ToolNode 中引入自己的并行机制

16.8.4 InjectedState vs ToolRuntime

InjectedStateInjectedStore 是较早的注入机制,使用 Annotated 类型标记。ToolRuntime 是更新的统一方案,将所有注入点合并为一个对象。推荐新代码使用 ToolRuntime

# 旧方式(仍然支持)
@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 组件之间的关系

graph TB
    CRA["create_react_agent"] -->|创建| SG[StateGraph]
    CRA -->|配置| TN[ToolNode]
    CRA -->|使用| TC[tools_condition]

    SG -->|编译| CSG[CompiledStateGraph]

    TN -->|使用| TCR[ToolCallRequest]
    TN -->|使用| TCW[ToolCallWrapper]
    TN -->|注入| IS[InjectedState]
    TN -->|注入| ISt[InjectedStore]
    TN -->|注入| TR[ToolRuntime]

    TC -->|路由| TN
    TC -->|路由| END_[END]

    CSG -->|执行| Pregel[Pregel 调度]
    Pregel -->|Send API| TN

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 打包为一个类型安全的对象。结合 InjectedStateInjectedStore 的向后兼容,开发者可以根据偏好选择注入方式。

这些预构建组件体现了 LangGraph 的分层设计哲学:底层提供灵活的原语(StateGraph、Channel、Send),上层提供开箱即用的解决方案(create_react_agent、ToolNode),中间层通过标准化接口(BaseStore、Runtime、StreamWriter)连接两者。开发者可以直接使用上层组件快速启动,也可以在理解底层原理后进行深度定制。

下一章我们将探讨多 Agent 模式——如何使用 LangGraph 构建 Supervisor、Swarm、分层和协作等多种 Agent 架构。