LangChain 设计与实现

第15章 工具调用与 Agent 模式

作者 杨艺韬 · 10,762 字 · 发布于 · 更新于

开篇引言

上一章我们剖析了 Agent 的底层架构 -- 数据模型、基类接口和 AgentExecutor 执行循环。那是 Agent 系统的"引擎"。本章我们将目光转向"车身":LangChain 提供的各种具体 Agent 实现。

langchain-classic 中的 create_*_agent 构建函数遵循一个统一的模式:接收 LLM、工具列表和提示模板,返回一个 Runnable。这个 Runnable 可以直接传给 AgentExecutor。不同的构建函数体现了不同的 Agent 范式:有的利用模型原生的工具调用能力(Tool Calling Agent),有的通过文本提示实现推理-行动循环(ReAct Agent),有的使用 JSON 格式指定工具参数(Structured Chat Agent),还有的用 XML 标签来组织交互(XML Agent)。

这些 Agent 的内部结构惊人地相似 -- 都是由四个阶段组成的 LCEL 管道。理解了一个,就理解了全部。本章将逐一拆解每种 Agent 的实现,分析它们的设计取舍,并在最后进行全面对比。

本章要点

  • create_tool_calling_agent 如何利用模型原生 tool_calls 能力
  • create_react_agent 实现的经典 ReAct 推理-行动范式
  • create_openai_tools_agent 与 Tool Calling Agent 的关系和区别
  • create_structured_chat_agent 和 create_xml_agent 的文本解析方案
  • ToolsAgentOutputParser 如何解析 AIMessage 中的 tool_calls
  • 各种 Agent 格式化中间步骤(format_scratchpad)的不同策略
  • Agent 类型全面对比与选型指南

15.1 Agent 管道的统一结构

在深入具体实现之前,先看一个关键洞察:langchain-classic 里本章讲的这些 create_*_agent 构建函数(agent_toolkits 下直接返回 AgentExecutor 的 create_vectorstore_agent 等不在此列)返回的都是一个四阶段 LCEL 管道(1.x 的 create_agent 另起炉灶,见 §15.10)。

flowchart LR
    A["RunnablePassthrough.assign<br/>格式化 agent_scratchpad"] --> B["Prompt<br/>填充变量生成提示"]
    B --> C["LLM / LLM.bind_tools<br/>调用大语言模型"]
    C --> D["OutputParser<br/>解析输出为<br/>AgentAction/AgentFinish"]
    style A fill:#e3f2fd
    style B fill:#f3e5f5
    style C fill:#e8f5e9
    style D fill:#fff3e0

四个阶段分别负责:

  1. 格式化:将 intermediate_steps(之前的执行历史)转换为 LLM 可理解的格式
  2. 提示:将格式化后的历史与用户输入组合成完整的提示
  3. 推理:调用 LLM 生成响应
  4. 解析:将 LLM 响应解析为 AgentAction 或 AgentFinish

不同 Agent 类型的差异仅在于:每个阶段的具体实现不同。格式化方式、提示模板、LLM 绑定方式、输出解析器,这四个维度的组合定义了不同的 Agent 范式。

15.2 Tool Calling Agent:模型原生工具调用

create_tool_calling_agent(langchain-core-1.2.26/libs/langchain/langchain_classic/agents/tool_calling_agent/base.py:18)是 classic 这套 create_*_agent 函数里推荐的首选——注意它和本章其余 Agent 构件一样住在 langchain-classic 包里,导入路径是 langchain_classic.agents;新项目的官方入口则是 langchain 1.x 的 create_agent(见 §15.10)。它利用现代 LLM(如 GPT-4、Claude、Gemini)原生的工具调用能力,不依赖文本解析。

15.2.1 构建函数

# langchain_classic/agents/tool_calling_agent/base.py(省略 docstring,注释为本专栏所加)

def create_tool_calling_agent(
    llm: BaseLanguageModel,
    tools: Sequence[BaseTool],
    prompt: ChatPromptTemplate,
    *,
    message_formatter: MessageFormatter = format_to_tool_messages,
) -> Runnable:
    # 验证提示模板包含 agent_scratchpad
    missing_vars = {"agent_scratchpad"}.difference(
        prompt.input_variables + list(prompt.partial_variables),
    )
    if missing_vars:
        raise ValueError(f"Prompt missing required variables: {missing_vars}")

    # 验证 LLM 支持工具绑定
    if not hasattr(llm, "bind_tools"):
        raise ValueError(
            "This function requires a bind_tools() method "
            "be implemented on the LLM."
        )

    # 将工具 schema 绑定到 LLM
    llm_with_tools = llm.bind_tools(tools)

    # 构建四阶段管道
    return (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: message_formatter(
                x["intermediate_steps"]
            ),
        )
        | prompt
        | llm_with_tools
        | ToolsAgentOutputParser()
    )

这个函数做了三件事:验证输入、绑定工具、组装管道(管道拼装见 langchain-core-1.2.26/libs/langchain/langchain_classic/agents/tool_calling_agent/base.py:112)。去掉 docstring,整个函数体只有二十来行代码,却完成了一个功能完备的 Agent 的构建。这种简洁性来源于 LCEL 的组合能力和 Runnable 协议的统一接口。

其中 llm.bind_tools(tools) 是关键 -- 它将工具的 JSON Schema 描述附加到 LLM 的每次调用中,让模型知道可以使用哪些工具。绑定后的 llm_with_tools 在类型上仍然是一个 Runnable,可以自然地参与管道组合。

验证逻辑也值得注意。函数首先检查提示模板是否包含 agent_scratchpad 变量。这个变量名不是随意选择的 -- 它是所有 Agent 类型共享的约定,用于插入格式化后的中间步骤。然后用 hasattr(llm, "bind_tools") 检查 LLM 有没有 bind_tools 方法,没有就抛出上面那条 ValueError。这道检查只拦得住非聊天模型(BaseLLM 子类没有这个方法):BaseChatModel 自带一个默认 bind_tools,函数体直接 raise NotImplementedError(libs/core/langchain_core/language_models/chat_models.py:1585),所以没覆盖它的聊天模型能通过 hasattr,随后在 llm.bind_tools(tools) 这一行抛 NotImplementedError(实跑 FakeListChatModel 即如此)。两种情况都发生在构建阶段而非运行时,这种"提前失败"避免了到执行 Agent 时才发现兼容性问题。

15.2.2 format_to_tool_messages -- 格式化中间步骤

Tool Calling Agent 使用消息格式来表示中间步骤。format_to_tool_messages(langchain-core-1.2.26/libs/langchain/langchain_classic/agents/format_scratchpad/tools.py:51)将 (AgentAction, observation) 元组转换为 ToolMessage 对象:

# langchain_classic/agents/format_scratchpad/tools.py(省略 docstring,注释为本专栏所加)

def format_to_tool_messages(
    intermediate_steps: Sequence[tuple[AgentAction, str]],
) -> list[BaseMessage]:
    messages = []
    for agent_action, observation in intermediate_steps:
        if isinstance(agent_action, ToolAgentAction):
            new_messages = [
                *list(agent_action.message_log),  # 原始 AI 消息
                _create_tool_message(agent_action, observation),
            ]
            messages.extend(
                [new for new in new_messages if new not in messages]
            )
        else:
            messages.append(AIMessage(content=agent_action.log))
    return messages

核心逻辑是:对于每个 ToolAgentAction(包含 tool_call_id 的动作),同时包含原始的 AI 消息(包含 tool_calls)和对应的 ToolMessage 响应。去重逻辑 if new not in messages 确保当一次 AI 调用产生多个工具调用时,AI 消息只出现一次。

这个格式化函数的设计处理了一个微妙但重要的问题:消息的顺序和去重。考虑一个并行工具调用的场景 -- 模型在一条 AI 消息中同时调用了搜索工具和计算工具。执行后会产生两个 intermediate_steps,但它们共享同一条原始 AI 消息。格式化时需要确保 AI 消息只出现一次,后面紧跟两条 ToolMessage。如果 AI 消息被重复包含,模型可能会误解上下文。去重逻辑正是为了解决这个问题。

对于非 ToolAgentAction 类型的动作(来自旧式 Agent 的文本输出),格式化函数回退到简单地创建一个 AIMessage,其内容为动作的日志文本。这种兼容性处理确保了新旧两种 Agent 类型可以共存于同一个系统中。

_create_tool_message(langchain-core-1.2.26/libs/langchain/langchain_classic/agents/format_scratchpad/tools.py:18)则负责将观察结果包装为 ToolMessage:

# 节选:省略 docstring
def _create_tool_message(
    agent_action: ToolAgentAction,
    observation: Any,
) -> ToolMessage:
    if not isinstance(observation, str):
        try:
            content = json.dumps(observation, ensure_ascii=False)
        except TypeError:
            content = str(observation)
        except Exception:
            _logger.exception("Unexpected error converting observation to string.")
            content = str(observation)
    else:
        content = observation
    return ToolMessage(
        tool_call_id=agent_action.tool_call_id,
        content=content,
        additional_kwargs={"name": agent_action.tool},
    )

15.2.3 ToolsAgentOutputParser -- 解析工具调用

ToolsAgentOutputParser(langchain-core-1.2.26/libs/langchain/langchain_classic/agents/output_parsers/tools.py:87,继承自 MultiActionAgentOutputParser)是 Tool Calling Agent 的"大脑翻译器",真正干活的是 parse_ai_message_to_tool_action(同文件 :24),它将 AI 消息解析为 Agent 动作:

# langchain_classic/agents/output_parsers/tools.py(节选:省略 docstring 与 JSON 解析失败分支,注释为本专栏所加)

class ToolAgentAction(AgentActionMessageLog):
    """Tool agent action."""  # 相比父类多出 tool_call_id 字段
    tool_call_id: str | None

def parse_ai_message_to_tool_action(
    message: BaseMessage,
) -> list[AgentAction] | AgentFinish:
    if not isinstance(message, AIMessage):
        raise TypeError(f"Expected an AI message got {type(message)}")

    actions: list = []
    if message.tool_calls:
        tool_calls = message.tool_calls
    else:
        if not message.additional_kwargs.get("tool_calls"):
            # 没有工具调用 -> 视为最终答案
            return AgentFinish(
                return_values={"output": message.content},
                log=str(message.content),
            )
        # 回退:从 additional_kwargs 解析
        tool_calls = []
        for tool_call in message.additional_kwargs["tool_calls"]:
            function = tool_call["function"]
            args = json.loads(function["arguments"] or "{}")
            tool_calls.append(ToolCall(
                type="tool_call",
                name=function["name"],
                args=args,
                id=tool_call["id"],
            ))

    for tool_call in tool_calls:
        function_name = tool_call["name"]
        _tool_input = tool_call["args"]
        # 处理 __arg1 兼容旧式工具(源码 output_parsers/tools.py:71)
        tool_input = _tool_input.get("__arg1", _tool_input)

        content_msg = f"responded: {message.content}\n" if message.content else "\n"
        log = f"\nInvoking: `{function_name}` with `{tool_input}`\n{content_msg}\n"
        actions.append(ToolAgentAction(
            tool=function_name,
            tool_input=tool_input,
            log=log,
            message_log=[message],
            tool_call_id=tool_call["id"],
        ))
    return actions

解析逻辑有三个分支:

  1. message.tool_calls 存在:优先使用结构化的 tool_calls
  2. additional_kwargs 中有 tool_calls:回退到旧格式(arguments 不是合法 JSON 时抛 OutputParserException,上面节选省略了这个分支)
  3. 都没有:视为最终答案,返回 AgentFinish
flowchart TD
    A["AIMessage 从 LLM 返回"] --> B{message.tool_calls<br/>非空?}
    B -->|是| C["使用结构化 tool_calls"]
    B -->|否| D{"additional_kwargs<br/>有 tool_calls?"}
    D -->|是| E["从 JSON 解析 tool_calls"]
    D -->|否| F["返回 AgentFinish<br/>(message.content 作为最终答案)"]
    C --> G["遍历 tool_calls"]
    E --> G
    G --> H["创建 ToolAgentAction 列表<br/>(包含 tool_call_id)"]
    H --> I["返回 list[AgentAction]"]

15.2.4 使用示例

from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant"),
    ("placeholder", "{chat_history}"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

model = ChatOpenAI(model="gpt-4")
tools = [my_search_tool, my_calculator_tool]  # 你自己定义的工具(如 @tool 装饰的函数)

agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({"input": "3 + 5 等于多少?"})

用一个会返回 tool_calls 的假聊天模型替换 ChatOpenAI、让它先在一条消息里同时调用 add 和 mul、再给出最终答案,实跑可以看到:AgentExecutor 把这个管道包装成 RunnableMultiActionAgent;两个中间步骤都是 ToolAgentAction,log 为 "\nInvoking: `add` with `{'a': 3, 'b': 5}`\n\n\n";format_to_tool_messages 把它们还原成 [AIMessage, ToolMessage, ToolMessage]——共享的那条 AI 消息只出现一次。

15.3 ReAct Agent:经典推理-行动范式

create_react_agent 实现了经典的 ReAct(Reasoning + Acting)范式。与 Tool Calling Agent 不同,它不依赖模型的原生工具调用能力,而是通过文本提示和停止词来实现推理-行动循环。

同名警告:langgraph.prebuilt 里还有一个 create_react_agent,与本节这个同名不同物。 本节这个在 libs/langchain/langchain_classic/agents/react/agent.py:16,签名 (llm, tools, prompt, output_parser=None, tools_renderer=..., *, stop_sequence=True) -> Runnable——纯文本模拟 ReAct,配 ReActSingleInputOutputParser 一次只解析出一个 action,不支持并行工具调用,还得外套 AgentExecutor(libs/langchain/langchain_classic/agents/agent.py:1012)才能跑。LangGraph 那个在 libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:278,签名 (model, tools, *, prompt=None, ...) -> CompiledStateGraph——走模型原生 tool-calling + ToolNode,一条消息里的多个 tool call 并行执行;并且它自 LangGraph v1.0 起已标 @deprecated(警告类别 LangGraphDeprecatedSinceV10,libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py:274-277),指向 langchain.agents.create_agent。本节这个 classic 版没有挂 @deprecated,但 docstring 同样写明它「older and not well-suited for production applications」、推荐改用 create_agent。import 路径写错时导入本身不会报错;两者签名不同(classic 版 prompt 是必填的第三个位置参数,LangGraph 版 prompt 只能按关键字传),多数写法会在调用时抛 TypeError,但 create_react_agent(model, tools, prompt=p) 这种写法两边都能通过,拿到的却是能力完全不同的 Agent。

15.3.1 构建函数

# langchain_classic/agents/react/agent.py(省略 docstring,注释为本专栏所加)

def create_react_agent(
    llm: BaseLanguageModel,
    tools: Sequence[BaseTool],
    prompt: BasePromptTemplate,
    output_parser: AgentOutputParser | None = None,
    tools_renderer: ToolsRenderer = render_text_description,
    *,
    stop_sequence: bool | list[str] = True,
) -> Runnable:
    # 验证必需变量
    missing_vars = {"tools", "tool_names", "agent_scratchpad"}.difference(
        prompt.input_variables + list(prompt.partial_variables),
    )
    if missing_vars:
        raise ValueError(f"Prompt missing required variables: {missing_vars}")

    # 将工具描述填入提示模板
    prompt = prompt.partial(
        tools=tools_renderer(list(tools)),
        tool_names=", ".join([t.name for t in tools]),
    )

    # 配置停止词
    if stop_sequence:
        stop = ["\nObservation"] if stop_sequence is True else stop_sequence
        llm_with_stop = llm.bind(stop=stop)
    else:
        llm_with_stop = llm

    output_parser = output_parser or ReActSingleInputOutputParser()

    return (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: format_log_to_str(
                x["intermediate_steps"]
            ),
        )
        | prompt
        | llm_with_stop
        | output_parser
    )

15.3.2 与 Tool Calling Agent 的关键差异

ReAct Agent 在每个阶段都与 Tool Calling Agent 不同:

格式化:使用 format_log_to_str 将中间步骤转换为文本字符串,而非消息对象:

# langchain_classic/agents/format_scratchpad/log.py

def format_log_to_str(
    intermediate_steps: list[tuple[AgentAction, str]],
    observation_prefix: str = "Observation: ",
    llm_prefix: str = "Thought: ",
) -> str:
    """将 Agent 步骤格式化为文本(源码 docstring 为英文,此处意译)"""
    thoughts = ""
    for action, observation in intermediate_steps:
        thoughts += action.log
        thoughts += f"\n{observation_prefix}{observation}\n{llm_prefix}"
    return thoughts

提示:需要三个变量 -- tools(工具描述)、tool_names(工具名列表)、agent_scratchpad(历史步骤文本)。典型的 ReAct 提示格式如下(简化自 create_react_agent docstring 里的示例模板):

Answer the following questions. You have access to these tools:
{tools}

Use the following format:
Question: the input question
Thought: think about what to do
Action: the action, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (repeat N times)
Thought: I now know the final answer
Final Answer: the final answer

Question: {input}
Thought:{agent_scratchpad}

推理:使用 llm.bind(stop=["\nObservation"]) 添加停止词。当 LLM 生成到 "Observation" 时自动停止,控制权交回执行器去实际调用工具。

解析:使用 ReActSingleInputOutputParser,通过正则表达式从文本中提取 Action 和 Action Input。

15.3.3 停止词的关键作用

停止词 "\nObservation" 是 ReAct 模式的精髓。LLM 生成类似这样的文本:

Thought: I need to search for the weather
Action: search
Action Input: weather in Beijing

到此停止。执行器解析出 Action 和 Action Input,执行工具,将结果作为 Observation 拼接回去:

Thought: I need to search for the weather
Action: search
Action Input: weather in Beijing
Observation: Beijing is currently 25°C and sunny
Thought:

然后再次调用 LLM,它看到了完整的上下文,继续推理。

sequenceDiagram
    participant AE as AgentExecutor
    participant LLM as LLM (with stop)
    participant Parser as ReActSingleInputOutputParser
    participant Tool as 工具
    AE->>LLM: prompt + "Thought:"
    LLM-->>AE: "I need to search...\nAction: search\nAction Input: weather"
    Note right of LLM: 在 "\nObservation" 处停止
    AE->>Parser: parse("I need to search...")
    Parser-->>AE: AgentAction(tool="search", tool_input="weather")
    AE->>Tool: run("weather")
    Tool-->>AE: "25°C and sunny"
    AE->>LLM: prompt + 之前文本 + "\nObservation: 25°C and sunny\nThought:"
    LLM-->>AE: "I now know the final answer\nFinal Answer: 北京25度晴天"
    AE->>Parser: parse("I now know...")
    Parser-->>AE: AgentFinish(return_values={"output": "北京25度晴天"})

15.4 OpenAI Tools Agent:过渡方案

create_openai_tools_agent 是一个针对 OpenAI 工具格式的 Agent,使用 convert_to_openai_tool 手动将工具转换为 OpenAI 格式:

# langchain_classic/agents/openai_tools/base.py(省略 docstring)

def create_openai_tools_agent(
    llm: BaseLanguageModel,
    tools: Sequence[BaseTool],
    prompt: ChatPromptTemplate,
    strict: bool | None = None,
) -> Runnable:
    missing_vars = {"agent_scratchpad"}.difference(
        prompt.input_variables + list(prompt.partial_variables),
    )
    if missing_vars:
        raise ValueError(f"Prompt missing required variables: {missing_vars}")

    llm_with_tools = llm.bind(
        tools=[convert_to_openai_tool(tool, strict=strict) for tool in tools],
    )

    return (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: format_to_openai_tool_messages(
                x["intermediate_steps"],
            ),
        )
        | prompt
        | llm_with_tools
        | OpenAIToolsAgentOutputParser()
    )

与 Tool Calling Agent 相比,表面上三个阶段都换了名字,实际只有第一项是真差异:

  • 使用 llm.bind(tools=...) 手动传入 OpenAI 格式的工具描述,而非 llm.bind_tools(tools)
  • 使用 format_to_openai_tool_messages 格式化中间步骤——它只是 format_to_tool_messages 的别名(libs/langchain/langchain_classic/agents/format_scratchpad/openai_tools.py 里 import ... as format_to_openai_tool_messages)
  • 使用 OpenAIToolsAgentOutputParser 解析输出——它内部调用同一个 parse_ai_message_to_tool_action,再把结果重新包成 OpenAIToolAgentAction,而后者就是 ToolAgentAction 的别名(libs/langchain/langchain_classic/agents/output_parsers/openai_tools.py)

这个 Agent 本质上是 Tool Calling Agent 的前身,在 bind_tools 抽象出现之前的 OpenAI 专用实现(git 历史:openai_tools/base.py 首见于 2023-12-28 的 #15278;BaseChatModel.bind_tools 接口与 tool_calling_agent/base.py 分别在 2024-04-11 的 #20360 和 2024-04-10 的 #20074 加入)。它直接使用 convert_to_openai_tool 函数将工具转换为 OpenAI 格式,然后通过 llm.bind(tools=...) 传递给模型。这种方式绕过了 bind_tools 抽象层,直接与 OpenAI 的 API 格式耦合。

两者的核心区别在于抽象层次。create_openai_tools_agent 假定底层模型理解 OpenAI 的工具格式,因此直接传入 OpenAI 格式的工具描述。而 create_tool_calling_agent 通过 bind_tools 让每个模型实现自行决定如何转换工具描述,从而支持 OpenAI、Anthropic、Google 等所有实现了 bind_tools 的提供商。

在实际应用中,如果你确定只使用 OpenAI 的模型,两者的行为几乎完全相同。但如果你需要在不同模型之间切换(比如使用 configurable_alternatives 在 OpenAI 和 Anthropic 之间动态选择),应选 create_tool_calling_agent,因为它不假设任何特定提供商的工具格式。注意,这种按请求的动态切换只在直接调用 agent runnable 时生效:经 AgentExecutor 执行时,configurable 传不到内部的 agent runnable,模型始终是默认值(原因与实跑见第 18 章 §18.9「配置系统与 Agent 执行的联动」);此时应在构建 AgentExecutor 之前选好模型。在 classic 这两者之间,推荐始终使用 create_tool_calling_agent。

15.5 Structured Chat Agent:JSON 格式

create_structured_chat_agent 通过 JSON blob 来指定工具调用,适用于不支持原生工具调用但支持 JSON 输出的 LLM:

# langchain_classic/agents/structured_chat/base.py(省略 docstring)

def create_structured_chat_agent(
    llm: BaseLanguageModel,
    tools: Sequence[BaseTool],
    prompt: ChatPromptTemplate,
    tools_renderer: ToolsRenderer = render_text_description_and_args,
    *,
    stop_sequence: bool | list[str] = True,
) -> Runnable:
    missing_vars = {"tools", "tool_names", "agent_scratchpad"}.difference(
        prompt.input_variables + list(prompt.partial_variables),
    )
    if missing_vars:
        raise ValueError(f"Prompt missing required variables: {missing_vars}")

    prompt = prompt.partial(
        tools=tools_renderer(list(tools)),
        tool_names=", ".join([t.name for t in tools]),
    )

    if stop_sequence:
        stop = ["\nObservation"] if stop_sequence is True else stop_sequence
        llm_with_stop = llm.bind(stop=stop)
    else:
        llm_with_stop = llm

    return (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: format_log_to_str(
                x["intermediate_steps"]
            ),
        )
        | prompt
        | llm_with_stop
        | JSONAgentOutputParser()
    )

15.5.1 与 ReAct 的区别

Structured Chat Agent 的结构与 ReAct 几乎相同,但有两个关键差异:

  1. 工具渲染器:使用 render_text_description_and_args,不仅渲染工具名称和描述,还渲染参数的 JSON Schema。这让 LLM 知道每个工具接受什么结构的输入。

  2. 输出解析器:使用 JSONAgentOutputParser,从 LLM 输出中提取 JSON blob:

{
    "action": "search",
    "action_input": {"query": "weather in Beijing"}
}

或者终止时:

{
    "action": "Final Answer",
    "action_input": "北京今天 25 度"
}

这种 JSON 格式天然支持结构化的工具输入(嵌套的字典参数),而 ReAct 的 Action Input 只能传递字符串。这正是"Structured"名称的由来。当工具需要接收多个命名参数时(如搜索工具需要同时指定查询词、结果数量和语言),结构化聊天 Agent 可以通过 JSON 的键值对清晰地表达每个参数,而 ReAct Agent 则只能把所有参数塞进一个字符串中,依赖工具自己去解析。

不过,结构化聊天 Agent 对模型的 JSON 生成能力有较高要求。如果模型生成的 JSON 格式不正确(缺少引号、括号不匹配等),输出解析器就会失败。这类格式错误在实践中并不少见。因此,如果你的目标模型支持原生工具调用,Tool Calling Agent 几乎总是更好的选择。

15.6 XML Agent:标签化交互

create_xml_agent 用 XML 标签组织工具调用,面向擅长 XML 格式的模型(源码 docstring 的示例用的是 ChatAnthropic):

# langchain_classic/agents/xml/base.py(省略 docstring)

def create_xml_agent(
    llm: BaseLanguageModel,
    tools: Sequence[BaseTool],
    prompt: BasePromptTemplate,
    tools_renderer: ToolsRenderer = render_text_description,
    *,
    stop_sequence: bool | list[str] = True,
) -> Runnable:
    missing_vars = {"tools", "agent_scratchpad"}.difference(
        prompt.input_variables + list(prompt.partial_variables),
    )
    if missing_vars:
        raise ValueError(f"Prompt missing required variables: {missing_vars}")

    prompt = prompt.partial(tools=tools_renderer(list(tools)))

    if stop_sequence:
        stop = ["</tool_input>"] if stop_sequence is True else stop_sequence
        llm_with_stop = llm.bind(stop=stop)
    else:
        llm_with_stop = llm

    return (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: format_xml(x["intermediate_steps"]),
        )
        | prompt
        | llm_with_stop
        | XMLAgentOutputParser()
    )

XML Agent 使用 XML 标签来表示工具调用和最终答案:

<tool>search</tool><tool_input>weather in Beijing</tool_input>
<observation>25°C and sunny</observation>

<final_answer>北京今天 25 度,晴天</final_answer>

停止词是 "</tool_input>",确保模型在生成完工具输入后停止,等待实际的观察结果。格式化函数 format_xml 将中间步骤转换为 XML 格式的字符串。

15.7 Agent 类型全面对比

flowchart TB
    subgraph "Tool Calling Agent"
        direction LR
        TC1["format_to_tool_messages<br/>(消息格式)"] --> TC2["ChatPromptTemplate"]
        TC2 --> TC3["llm.bind_tools(tools)"]
        TC3 --> TC4["ToolsAgentOutputParser"]
    end
    subgraph "ReAct Agent"
        direction LR
        RE1["format_log_to_str<br/>(文本格式)"] --> RE2["PromptTemplate"]
        RE2 --> RE3["llm.bind(stop)"]
        RE3 --> RE4["ReActSingleInputOutputParser"]
    end
    subgraph "Structured Chat Agent"
        direction LR
        SC1["format_log_to_str<br/>(文本格式)"] --> SC2["ChatPromptTemplate"]
        SC2 --> SC3["llm.bind(stop)"]
        SC3 --> SC4["JSONAgentOutputParser"]
    end
    subgraph "XML Agent"
        direction LR
        XM1["format_xml<br/>(XML 格式)"] --> XM2["PromptTemplate"]
        XM2 --> XM3["llm.bind(stop)"]
        XM3 --> XM4["XMLAgentOutputParser"]
    end

四维对比表

维度 Tool Calling ReAct Structured Chat XML
格式化 消息 (ToolMessage) 文本 (str) 文本 (str) XML (str)
LLM 绑定 bind_tools bind(stop=) bind(stop=) bind(stop=)
输出解析 ToolsAgentOutputParser ReActSingleInputOutputParser JSONAgentOutputParser XMLAgentOutputParser
停止词 无需 \nObservation \nObservation </tool_input>
工具输入类型 dict (结构化) str (纯文本) dict (JSON) str (纯文本)
一次返回多个工具调用 支持 不支持 不支持 不支持
模型要求 支持 bind_tools 任意 LLM 任意 Chat Model 任意 LLM
推荐场景 首选方案 教学/兼容 多参数工具 XML 友好模型

「一次返回多个工具调用」指模型一轮能给出几个 action;真正怎么执行由 AgentExecutor 决定:同步路径的 _iter_next_step 逐个执行(libs/langchain/langchain_classic/agents/agent.py:1370),只有异步路径的 _aiter_next_step 用 asyncio.gather 并发(同文件第 1511 行)。

选型决策树

flowchart TD
    A[选择 Agent 类型] --> B{模型支持<br/>bind_tools?}
    B -->|是| C["create_tool_calling_agent<br/>(推荐)"]
    B -->|否| D{需要结构化<br/>工具输入?}
    D -->|是| E["create_structured_chat_agent<br/>(JSON 格式)"]
    D -->|否| F{模型擅长<br/>XML 格式?}
    F -->|是| G["create_xml_agent"]
    F -->|否| H["create_react_agent<br/>(纯文本格式)"]

15.8 ToolAgentAction 与普通 AgentAction 的差异

Tool Calling Agent 引入了 ToolAgentAction,它比普通的 AgentAction 多了一个关键字段 tool_call_id:

class ToolAgentAction(AgentActionMessageLog):
    tool_call_id: str | None

这个 ID 将 AI 消息中的 tool_call 与后续的 ToolMessage 关联起来。在 OpenAI 等 API 中,每个 tool_call 都有唯一的 ID,对应的 ToolMessage 必须携带相同的 ID 才能被正确匹配。

这种关联在并行工具调用场景下尤为重要。当一条 AI 消息包含三个 tool_calls 时,后续需要三条 ToolMessage 来一一对应。没有 tool_call_id,系统无法区分哪个结果属于哪个调用。

15.9 format_scratchpad 策略对比

不同 Agent 格式化中间步骤的方式体现了不同的设计哲学:

消息格式 (Tool Calling)

# 输出: [AIMessage(tool_calls=[...]), ToolMessage(...)]
messages = format_to_tool_messages(intermediate_steps)

优势:保持消息的结构化信息,支持并行工具调用,与 Chat API 原生对齐。

文本格式 (ReAct / Structured Chat)

# 输出: "Thought: ...\nAction: ...\nObservation: ...\nThought: "
text = format_log_to_str(intermediate_steps)

优势:简单直观,兼容所有 LLM(包括纯文本模型),可读性强。

XML 格式

# 输出: "<tool>search</tool><tool_input>query</tool_input><observation>result</observation>"
xml = format_xml(intermediate_steps)

优势:结构清晰,与擅长 XML 的模型配合良好。

15.10 设计决策分析

为什么不是一个统一的 create_agent?

LangChain 提供了多个 create_xxx_agent 函数而非一个统一的函数,这是一个刻意的设计选择。不同 Agent 模式的差异不仅仅在参数上,更在于对 LLM 输出格式的假设、提示模板的结构要求、以及错误恢复的策略。将它们统一会导致大量条件分支和配置参数,反而降低可理解性。

有意思的是,1.0 之后的新 langchain 包(pin 快照 1.2.15)恰恰提供了一个统一的 create_agent(libs/langchain_v1/langchain/agents/factory.py:684)。它能统一,是因为只保留了原生工具调用这一种范式,文本解析的几种变体全部留在 classic:签名是 (model, tools=None, *, system_prompt=None, middleware=(), response_format=None, state_schema=None, context_schema=None, checkpointer=None, store=None, ...),返回 LangGraph 的 CompiledStateGraph 而不是交给 AgentExecutor 的 Runnable;图里有 model 与 tools 两个节点,后者是 LangGraph 的 ToolNode,一轮里的多个 tool call 由 Send 分发成独立任务。定制点也换了一套:不再替换格式化函数和输出解析器,而是通过 middleware 挂钩子,结构化输出通过 response_format(ToolStrategy / ProviderStrategy / AutoStrategy,见 agents/structured_output.py)。用 §15.2.4 那种假模型实跑,create_agent(model, [add, mul], system_prompt=...) 编出的图节点为 ['__start__', 'model', 'tools', '__end__'],返回的 messages 依次是 HumanMessage, AIMessage, ToolMessage, ToolMessage, AIMessage。

为什么都返回 Runnable 而非 Agent 子类?

本章这些 create_xxx_agent 函数都返回 Runnable 而非 BaseSingleActionAgent 的某个子类。这意味着它们的输出可以参与 LCEL 组合,可以被进一步 pipe、map、fallback。AgentExecutor 通过 validate_runnable_agent 校验器自动把 Runnable 包装成 RunnableAgent(单动作)或 RunnableMultiActionAgent(多动作,ToolsAgentOutputParser 走的就是这条路),实现了无缝衔接。

停止词的必要性与局限性

文本格式的 Agent(ReAct、Structured Chat、XML)都依赖停止词来控制生成。这是一种"外部约束"机制 -- 模型本身并不知道何时应该停止,是停止词在物理上切断了生成流。

Tool Calling Agent 则完全不需要停止词,因为模型原生地知道何时应该调用工具、何时应该给出最终答案。这种"内在理解"与"外部约束"的差异,正是 Tool Calling Agent 被推荐为首选方案的根本原因。

OutputParser 的容错设计

每种 Agent 的 OutputParser 都包含了大量的容错逻辑。以 ToolsAgentOutputParser 为例,它先尝试 message.tool_calls,失败后回退到 additional_kwargs["tool_calls"],再失败则视为最终答案。这种渐进式回退策略让 Agent 能兼容新旧两种工具调用格式;但它不处理 invalid_tool_calls(见 §15.13),参数 JSON 不合法的调用会被当成最终答案。

15.11 深入理解工具绑定机制

工具绑定是 Tool Calling Agent 的核心机制,值得用一个完整的小节来深入讨论。当我们调用 llm.bind_tools(tools) 时,发生了一系列精密的转换过程。

首先,每个 BaseTool 的 args_schema(一个 Pydantic 模型)被转换为 JSON Schema 格式。这个转换过程需要处理各种 Pydantic 特性:字段描述变成 Schema 的 description,字段类型映射为 JSON 类型(int 变成 integer,str 变成 string 等),Optional 类型生成 anyOf: [{...}, {"type": "null"}](实跑 convert_to_openai_tool,Pydantic v2 不产出 nullable 标记),嵌套模型递归展开为嵌套的 Schema 结构。

然后,JSON Schema 被包装成提供商特定的格式。对于 OpenAI,它被包装为 {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}} 结构。对于 Anthropic,格式略有不同。bind_tools 方法的价值就在于屏蔽了这些格式差异 -- 开发者只需要提供标准的 BaseTool,绑定方法负责转换为正确的格式。

最后,bind_tools 由各提供商的聊天模型自己实现(BaseChatModel 的默认实现只抛 NotImplementedError);以 ChatOpenAI 为例,它末尾调用 super().bind(tools=formatted_tools, **kwargs)(libs/partners/openai/langchain_openai/chat_models/base.py),返回一个 RunnableBinding。这个 RunnableBinding 并不修改原始的 LLM 实例,而是创建了一个新的 Runnable,在每次调用时自动将工具描述附加到请求参数中。这种不可变的设计确保了同一个 LLM 实例可以被多个不同的 bind_tools 调用复用,互不影响。

一个常见的误解是认为 bind_tools 会"教会"模型使用工具。实际上,它只是告诉 API 这次请求可以返回工具调用格式的响应。模型是否真的会调用工具、调用哪个工具,完全取决于模型自身的推理能力和提示内容。工具描述的质量 -- 特别是名称的直观性和描述的准确性 -- 对模型的工具选择行为有决定性影响。

tool_choice 参数的作用

bind_tools 还支持一个重要的可选参数 tool_choice,它可以控制模型的工具调用行为。不传(ChatOpenAI.bind_tools 的默认值是 None,不下发该字段)或设为 "auto" 时,模型自行决定是否调用工具;设为 "required" 时,模型必须调用至少一个工具(ChatOpenAI 还把 "any" 和 True 都转成 "required");设为特定工具名时,模型必须调用该工具。在需要强制 Agent 执行特定操作的场景下(如强制使用搜索工具检查最新信息),tool_choice 非常有用。

但需要注意,tool_choice="required" 会让 Agent 循环停不下来 -- 如果模型被强制调用工具但已经得到了答案,它就无法通过返回 AgentFinish 来终止循环,只能一直跑到 AgentExecutor 的 max_iterations(默认 15)被强制停止,输出 "Agent stopped due to max iterations.";若把 max_iterations 设为 None,则真的是无限循环。因此,这个参数在 Agent 场景中应该谨慎使用。

15.12 自定义 Agent 的构建策略

理解了四种标准 Agent 的内部结构后,你完全可以构建自己的 Agent 类型。关键是遵循统一的四阶段管道模式。

构建自定义 Agent 的步骤

第一步是确定格式化策略。你的中间步骤如何传递给 LLM?是作为消息列表、文本字符串、还是其他格式?这取决于你的 LLM 接口和提示设计。

第二步是设计提示模板。提示模板需要包含工具描述和 agent_scratchpad 占位符。agent_scratchpad 是格式化后的中间步骤,它让 LLM 看到之前的行动和观察结果。

第三步是选择 LLM 调用方式。如果你的模型支持原生工具调用,优先使用 bind_tools;否则使用 bind(stop=...) 配合停止词。

第四步是实现输出解析器。解析器需要继承 AgentOutputParser 或 MultiActionAgentOutputParser,实现 parse 方法,将 LLM 输出转换为 AgentAction 或 AgentFinish。

下面是一个完整的自定义 Agent 示例,它使用自定义的标记格式:

from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.runnables import RunnablePassthrough
from langchain_classic.agents.agent import MultiActionAgentOutputParser

class MyCustomOutputParser(MultiActionAgentOutputParser):
    def parse(self, text: str) -> list[AgentAction] | AgentFinish:
        if "DONE:" in text:
            answer = text.split("DONE:")[1].strip()
            return AgentFinish(return_values={"output": answer}, log=text)
        # 解析你的自定义格式(_parse_actions 需自行实现)
        actions = self._parse_actions(text)
        return actions

def create_my_custom_agent(llm, tools, prompt):
    # 自定义文本格式:工具说明写进 prompt,靠停止词截断,而不是 bind_tools
    llm_with_stop = llm.bind(stop=["\nObservation"])
    return (
        RunnablePassthrough.assign(
            agent_scratchpad=lambda x: my_format_function(  # 自行实现,如 format_log_to_str
                x["intermediate_steps"]
            ),
        )
        | prompt
        | llm_with_stop
        | MyCustomOutputParser()
    )

这里不用 bind_tools 是有原因的:parse 只拿到文本——解析器基类的 parse_result 默认把 result[0].text 交给 parse,模型原生返回的 tool_calls 不在其中。如果想走原生工具调用,应当像 ToolsAgentOutputParser 那样覆盖 parse_result,直接读 ChatGeneration.message。补上 my_format_function 后用假模型实跑(模型回 "thinking DONE: 42"),AgentExecutor 把它包装成 RunnableMultiActionAgent,输出 {'input': 'q', 'output': '42'}。

何时需要自定义 Agent

在大多数情况下,create_tool_calling_agent 已经足够。需要自定义 Agent 的场景通常包括:

  1. 特殊的推理格式:如思维链(Chain-of-Thought)需要特定的输出格式
  2. 多阶段推理:如先规划再执行的两阶段 Agent
  3. 领域特定的工具调用约定:如使用特定的 DSL 来表示工具调用
  4. 非标准的 LLM 接口:如通过 HTTP API 调用的内部模型,不支持标准的工具调用格式

无论哪种情况,遵循四阶段管道模式都能确保你的自定义 Agent 与 AgentExecutor 无缝配合,自动获得停止保护、错误处理、流式输出等能力。

15.13 ReAct 与 Tool Calling 的本质区别

虽然 ReAct Agent 和 Tool Calling Agent 在功能上看起来相似 -- 都是让模型选择工具、执行工具、观察结果 -- 但它们的工作原理有本质性的差异,理解这些差异对于在生产环境中做出正确选择至关重要。

ReAct Agent 是一种"文本模拟"方案。模型被要求按照特定的文本格式输出它的"思考"和"行动",然后通过正则表达式从文本中提取工具名和参数。这意味着模型需要同时做两件事:推理问题的答案,以及遵循格式约定。当模型的格式遵循能力不够强时(特别是较小的开源模型),它可能会产生无法解析的输出,导致解析错误。

Tool Calling Agent 则是一种"原生能力"方案。模型在训练阶段就被教会了工具调用的格式,它的输出中包含结构化的 tool_calls 字段,不需要从自由文本中提取。这种方式更加可靠,因为输出的结构由 API 层给出,而非依赖模型的文本生成行为。不过参数本身仍可能不是合法 JSON:这类调用会进入 AIMessage.invalid_tool_calls 而不是 tool_calls,而 parse_ai_message_to_tool_action 不看 invalid_tool_calls——实跑一条只含无效调用、content 为空的消息,得到的是 AgentFinish(return_values={'output': ''}),Agent 会带着空答案静默结束。

从信息流的角度看,ReAct Agent 将所有信息(思考、行动、观察)编码为一个长文本字符串,通过字符串拼接传递上下文。Tool Calling Agent 则使用结构化的消息列表,每种信息(AI 消息、工具消息)都有明确的类型和字段。结构化的表示不仅更容易被模型理解,也更容易被程序处理和追踪。

最后,Tool Calling Agent 天然支持并行工具调用 -- 模型可以在一条消息中返回多个 tool_calls。而 ReAct Agent 由于文本格式的限制,每次只能指定一个 Action,实现并行调用需要额外的工程工作。

综合来看,如果你使用的模型支持原生工具调用(目前主流的商业模型都支持),应该毫不犹豫地选择 Tool Calling Agent。ReAct Agent 的价值在于教学(它的文本格式更容易理解 Agent 的推理过程)和兼容性(适用于不支持工具调用的开源模型)。

15.14 Agent 提示模板的设计原则

Agent 的行为在很大程度上取决于提示模板的设计。尽管不同类型的 Agent 有不同的模板结构要求,但有几个通用的设计原则值得遵循。

系统消息应该明确告诉模型它是一个什么角色、有什么能力、以及应该如何行事。一个好的系统消息不仅描述角色定位,还应该包含行为指南 -- 比如"如果你不确定答案,应该使用搜索工具查证,而不是凭记忆回答"这样的指导。这类行为指南对 Agent 的可靠性有显著影响。

工具使用指南也是提示模板的重要组成部分。虽然 Tool Calling Agent 不需要在提示中描述工具调用的格式(模型原生理解),但告诉模型何时应该使用工具、何时可以直接回答,仍然很有价值。例如,"对于数学计算,始终使用计算器工具,不要心算"这样的指示可以显著减少计算错误。

关于 agent_scratchpad 的放置位置,不同的策略会影响模型的行为。将它放在消息列表的末尾(作为最新的上下文)是最常见的做法,因为语言模型倾向于更关注最近的内容。但在某些场景下,将早期的关键步骤固定在前面(通过自定义的 trim_intermediate_steps 函数),可以确保重要的上下文不会被后续步骤"淹没"。

聊天历史(chat_history)的处理也需要谨慎。如果 Agent 需要在多轮对话中工作,聊天历史应该插入在系统消息之后、当前输入之前。这样模型既能看到之前的对话上下文,又能清楚地区分"历史对话"和"当前任务"。不建议将聊天历史与 Agent 的中间步骤混在一起,因为这会让模型难以区分哪些是之前的对话回忆,哪些是当前任务的工具调用结果。

15.15 Agent 管道的可视化与调试

每个 Agent 管道作为 Runnable,都可以通过 get_graph() 方法获取其执行图,并渲染为 Mermaid 或 ASCII 图。这对于理解 Agent 的内部结构非常有帮助。

在调试 Agent 时,最有价值的信息来源是 AgentExecutor 的 return_intermediate_steps=True 设置。它返回完整的中间步骤列表,让你能够逐步追踪 Agent 的决策过程。结合 LangSmith 等追踪工具,你可以看到每次 LLM 调用的完整输入输出、每次工具调用的参数和结果、以及整个执行循环的时间分布。

对于流式场景,AgentExecutor 的 stream 方法会逐步输出 AddableDict,包含 actions、steps 和 output 等键。这使得前端应用可以实时展示 Agent 的思考和行动过程,提供更好的用户体验。

15.16 Agent 模式的演进趋势

当前 Agent 模式的发展呈现出几个明确的趋势。首先是从文本解析向模型原生工具调用的迁移。随着越来越多的 LLM 提供商支持原生工具调用能力,基于停止词和正则表达式的文本解析方案正在逐步被取代。Tool Calling Agent 之所以成为推荐方案,正是因为它利用了模型的原生理解能力,不需要模型在特定格式上进行"演戏"。

其次是从单一循环向图状态机的演进。AgentExecutor 的 while 循环只能表达线性的思考-行动序列。实际的复杂任务可能需要条件分支("如果搜索结果不满意,换一个搜索引擎")、并行执行("同时搜索两个数据库")、人工审核节点("在执行危险操作前等待人工确认")等高级控制流。LangGraph 正是为这些场景设计的。

第三个趋势是多模态 Agent 的兴起。随着视觉语言模型的成熟,Agent 不仅能处理文本,还能理解图片、视频等多模态输入,并调用屏幕操作、代码执行等多模态工具。这对 Agent 管道中每个阶段的设计都提出了新的挑战,特别是中间步骤的格式化和观察结果的表示。

15.16.1 实测:langchain_classic/agents/ 10486 行——legacy Agent 系统的真实重量

§15.2-15.6 介绍 5 种 create_*_agent 函数——把它们在 langchain_classic/agents/ 里实测——

路径 行 角色
libs/langchain/langchain_classic/agents/agent.py 1792 本目录最大——AgentExecutor 主循环(循环驱动 §15.1 那条四阶段管道)+ BaseSingleActionAgent / BaseMultiActionAgent 抽象
libs/langchain/langchain_classic/agents/openai_assistant/base.py 831 OpenAI Assistants API 适配器(不是 §15.4 的 OpenAI Tools Agent、是另一套)
libs/langchain/langchain_classic/agents/agent_iterator.py 432 Agent 流式迭代器
libs/langchain/langchain_classic/agents/openai_functions_agent/base.py 382 更早的 OpenAI Functions Agent(create_openai_functions_agent;同文件的 OpenAIFunctionsAgent 类已标 @deprecated,工厂函数本身未标)
libs/langchain/langchain_classic/agents/openai_functions_multi_agent/base.py 337 多函数变体
libs/langchain/langchain_classic/agents/structured_chat/base.py 317 §15.5 Structured Chat Agent
libs/langchain/langchain_classic/agents/xml/base.py 236 §15.6 XML Agent
其余(react / chat / conversational / tool_calling_agent / mrkl / self_ask / format_scratchpad / output_parsers / agent_toolkits 等 12 个子目录,加 agent_types / initialize / loading / load_tools 等顶层文件) 余下 6159 —
agents/ 合计 10486 —

两条值得记住的物理事实——

  1. agent.py 单文件 1792 行 = 整个 agents/ 17%——AgentExecutor 主循环 + 2 个 BaseAgent ABC + 工具调用编排 + 错误处理 + 流式输出全部塞在一文件——是本专栏反复见到的"单文件大方法集"风格之一(同类:core runnables/base.py 6261 / libs/core/langchain_core/callbacks/manager.py 2697 / libs/core/langchain_core/tools/base.py 1586 / libs/core/langchain_core/prompts/chat.py 1491 / tracers/event_stream.py 1100;完整清单见 §3.12.1)
  2. 整个 agents/ 在 langchain_classic 而不是 langchain_core——对应 §1.9.1 测得的 langchain_classic(70649 行)反而比 langchain_core(64050 行)大:1.0 拆包时旧 Agent 系统随 Chain、Memory 一起整体迁入 langchain_classic,新的 Agent 入口改为基于 LangGraph 的 create_agent(§15.10)——这 10486 行主要是为兼容存量代码而保留

本章提到的 6 个 create_*_agent 函数的实际位置——

函数 文件
create_tool_calling_agent libs/langchain/langchain_classic/agents/tool_calling_agent/base.py
create_react_agent libs/langchain/langchain_classic/agents/react/agent.py
create_structured_chat_agent libs/langchain/langchain_classic/agents/structured_chat/base.py
create_xml_agent libs/langchain/langchain_classic/agents/xml/base.py
create_openai_tools_agent(§15.4 提到) libs/langchain/langchain_classic/agents/openai_tools/base.py
create_openai_functions_agent libs/langchain/langchain_classic/agents/openai_functions_agent/base.py

上表 6 个 create_*_agent 函数分布在 6 个独立子目录(5 个 base.py + react/agent.py)——印证 §15.10「为什么不是一个统一的 create_agent?」里的分立设计:每种 Agent 占一个独立子目录;libs/langchain/langchain_classic/agents/__init__.py re-export 形成统一公共 API。

串联 ch13 §13.11.4 测得的 langchain_classic/memory/*.py 2157 行——agents 10486 + memory 2157 = ~12643 行 legacy 子系统——是 §1.9.1 揭示的"langchain_classic (70649) > langchain_core (64050)"差距的具体来源;新版本用户应优先看 LangGraph + langchain_v1。

15.17 小结

本章全面剖析了 LangChain 提供的四种 Agent 构建方式。它们的内部结构遵循统一的四阶段 LCEL 管道模式(格式化 -> 提示 -> LLM -> 解析),差异仅在于每个阶段的具体实现。

create_tool_calling_agent 利用模型原生能力,是当前的推荐方案。create_react_agent 实现了经典的推理-行动范式,适用于教学和不支持工具调用的模型。create_structured_chat_agent 通过 JSON 格式支持结构化工具输入。create_xml_agent 则为擅长 XML 的模型提供了专用方案。

从设计角度看,本章这些 Agent 构建函数都返回 Runnable 而非 Agent 子类,这使得 Agent 管道可以参与 LCEL 组合。AgentExecutor 通过自动检测和包装机制,无缝地将 Runnable 管道接入执行循环。这种"接口统一、实现多样"的设计哲学,让 Agent 系统在保持灵活性的同时,为所有类型的 Agent 提供了统一的运行时保障 -- 停止保护、错误恢复、流式输出、异步路径下的并发执行,全部由 AgentExecutor 统一管理。

在实际项目中,Agent 模式的选择往往不是技术决策的终点,而只是起点。真正决定 Agent 表现的是三个要素的协同:工具集的设计质量(名称是否直观、描述是否准确、参数是否合理)、提示模板的引导能力(是否告诉了模型何时该用工具、何时该直接回答、如何处理不确定性)、以及错误恢复的策略(是否开启了错误处理、是否设置了合理的迭代上限、是否提供了有意义的错误提示)。这三个要素的组合质量,远比 Agent 类型的选择更加重要。

从框架设计的角度看,LangChain 通过将 Agent 实现为 Runnable 管道,实现了一个优雅的职责划分:框架负责执行循环、错误恢复和资源管理等机制性工作,开发者负责工具设计、提示工程和业务逻辑等创意性工作。这种划分让开发者可以把精力集中在真正产生差异化价值的地方,而不是反复实现相同的基础设施代码。

下一章,我们将转向另一个基础设施话题 -- 序列化与配置系统,看看 LangChain 如何实现对象的持久化与动态配置。