LangChain 设计与实现
第15章 工具调用与 Agent 模式
开篇引言
上一章我们剖析了 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
四个阶段分别负责:
- 格式化:将
intermediate_steps(之前的执行历史)转换为 LLM 可理解的格式 - 提示:将格式化后的历史与用户输入组合成完整的提示
- 推理:调用 LLM 生成响应
- 解析:将 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
解析逻辑有三个分支:
- message.tool_calls 存在:优先使用结构化的 tool_calls
- additional_kwargs 中有 tool_calls:回退到旧格式(
arguments不是合法 JSON 时抛OutputParserException,上面节选省略了这个分支) - 都没有:视为最终答案,返回 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 几乎相同,但有两个关键差异:
-
工具渲染器:使用
render_text_description_and_args,不仅渲染工具名称和描述,还渲染参数的 JSON Schema。这让 LLM 知道每个工具接受什么结构的输入。 -
输出解析器:使用
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 的场景通常包括:
- 特殊的推理格式:如思维链(Chain-of-Thought)需要特定的输出格式
- 多阶段推理:如先规划再执行的两阶段 Agent
- 领域特定的工具调用约定:如使用特定的 DSL 来表示工具调用
- 非标准的 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 | — |
两条值得记住的物理事实——
agent.py单文件 1792 行 = 整个 agents/ 17%——AgentExecutor主循环 + 2 个 BaseAgent ABC + 工具调用编排 + 错误处理 + 流式输出全部塞在一文件——是本专栏反复见到的"单文件大方法集"风格之一(同类:corerunnables/base.py6261 /libs/core/langchain_core/callbacks/manager.py2697 /libs/core/langchain_core/tools/base.py1586 /libs/core/langchain_core/prompts/chat.py1491 /tracers/event_stream.py1100;完整清单见 §3.12.1)- 整个 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 如何实现对象的持久化与动态配置。