LangChain 设计与实现
第15章 工具调用与 Agent 模式
第15章 工具调用与 Agent 模式
开篇引言
上一章我们剖析了 Agent 的底层架构 -- 数据模型、基类接口和 AgentExecutor 执行循环。那是 Agent 系统的"引擎"。本章我们将目光转向"车身":LangChain 提供的各种具体 Agent 实现。
LangChain 中的 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 Agent 构建函数返回的都是一个四阶段 LCEL 管道。
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 是目前推荐的主要 Agent 构建方式。它利用现代 LLM(如 GPT-4、Claude、Gemini)原生的工具调用能力,不依赖文本解析。
15.2.1 构建函数
# langchain_classic/agents/tool_calling_agent/base.py
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()
)
这个函数做了三件事:验证输入、绑定工具、组装管道。整个函数体不到二十行代码,却完成了一个功能完备的 Agent 的构建。这种简洁性来源于 LCEL 的组合能力和 Runnable 协议的统一接口。
其中 llm.bind_tools(tools) 是关键 -- 它将工具的 JSON Schema 描述附加到 LLM 的每次调用中,让模型知道可以使用哪些工具。绑定后的 llm_with_tools 在类型上仍然是一个 Runnable,可以自然地参与管道组合。
验证逻辑也值得注意。函数首先检查提示模板是否包含 agent_scratchpad 变量。这个变量名不是随意选择的 -- 它是所有 Agent 类型共享的约定,用于插入格式化后的中间步骤。然后检查 LLM 是否实现了 bind_tools 方法,如果没有则给出明确的错误消息。这种"提前失败"的策略避免了在运行时才发现兼容性问题,大大改善了开发体验。
15.2.2 format_to_tool_messages -- 格式化中间步骤
Tool Calling Agent 使用消息格式来表示中间步骤。format_to_tool_messages 将 (AgentAction, observation) 元组转换为 ToolMessage 对象:
# langchain_classic/agents/format_scratchpad/tools.py
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 则负责将观察结果包装为 ToolMessage:
def _create_tool_message(agent_action: ToolAgentAction, observation: Any):
if not isinstance(observation, str):
try:
content = json.dumps(observation, ensure_ascii=False)
except TypeError:
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 是 Tool Calling Agent 的"大脑翻译器",它将 AI 消息解析为 Agent 动作:
# langchain_classic/agents/output_parsers/tools.py
class ToolAgentAction(AgentActionMessageLog):
"""扩展了 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 兼容旧式工具
tool_input = _tool_input.get("__arg1", _tool_input)
log = f"\nInvoking: `{function_name}` with `{tool_input}`\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:回退到旧格式
- 都没有:视为最终答案,返回 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]
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = agent_executor.invoke({"input": "3 + 5 等于多少?"})
15.3 ReAct Agent:经典推理-行动范式
create_react_agent 实现了经典的 ReAct(Reasoning + Acting)范式。与 Tool Calling Agent 不同,它不依赖模型的原生工具调用能力,而是通过文本提示和停止词来实现推理-行动循环。
15.3.1 构建函数
# langchain_classic/agents/react/agent.py
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 步骤格式化为文本"""
log = ""
for action, observation in intermediate_steps:
log += action.log
log += f"\n{observation_prefix}{observation}\n{llm_prefix}"
return log
提示:需要三个变量 -- tools(工具描述)、tool_names(工具名列表)、agent_scratchpad(历史步骤文本)。典型的 ReAct 提示格式如下:
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 ReActOutputParser
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
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格式化中间步骤 - 使用
OpenAIToolsAgentOutputParser解析输出
这个 Agent 本质上是 Tool Calling Agent 的前身,在 bind_tools 抽象出现之前的 OpenAI 专用实现。它直接使用 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 是唯一可行的选择,因为它不假设任何特定提供商的工具格式。现在推荐始终使用 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
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 格式的模型(如 Claude 早期版本)设计的 Agent:
# langchain_classic/agents/xml/base.py
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["ReActOutputParser"]
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 | ReActOutputParser | JSONAgentOutputParser | XMLAgentOutputParser |
| 停止词 | 无需 | \nObservation |
\nObservation |
</tool_input> |
| 工具输入类型 | dict (结构化) | str (纯文本) | dict (JSON) | str (纯文本) |
| 并行工具调用 | 支持 | 不支持 | 不支持 | 不支持 |
| 模型要求 | 支持 bind_tools | 任意 LLM | 任意 Chat Model | 任意 LLM |
| 推荐场景 | 首选方案 | 教学/兼容 | 多参数工具 | XML 友好模型 |
选型决策树
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 输出格式的假设、提示模板的结构要求、以及错误恢复的策略。将它们统一会导致大量条件分支和配置参数,反而降低可理解性。
为什么都返回 Runnable 而非 Agent 子类?
所有 create_xxx_agent 函数都返回 Runnable 而非 BaseSingleActionAgent 的某个子类。这意味着它们的输出可以参与 LCEL 组合,可以被进一步 pipe、map、fallback。AgentExecutor 通过 validate_runnable_agent 自动将 Runnable 包装为 RunnableAgent,实现了无缝衔接。
停止词的必要性与局限性
文本格式的 Agent(ReAct、Structured Chat、XML)都依赖停止词来控制生成。这是一种"外部约束"机制 -- 模型本身并不知道何时应该停止,是停止词在物理上切断了生成流。
Tool Calling Agent 则完全不需要停止词,因为模型原生地知道何时应该调用工具、何时应该给出最终答案。这种"内在理解"与"外部约束"的差异,正是 Tool Calling Agent 被推荐为首选方案的根本原因。
OutputParser 的容错设计
每种 Agent 的 OutputParser 都包含了大量的容错逻辑。以 ToolsAgentOutputParser 为例,它先尝试 message.tool_calls,失败后回退到 additional_kwargs["tool_calls"],再失败则视为最终答案。这种渐进式回退策略确保了 Agent 在各种模型行为下都能正常工作。
15.11 深入理解工具绑定机制
工具绑定是 Tool Calling Agent 的核心机制,值得用一个完整的小节来深入讨论。当我们调用 llm.bind_tools(tools) 时,发生了一系列精密的转换过程。
首先,每个 BaseTool 的 args_schema(一个 Pydantic 模型)被转换为 JSON Schema 格式。这个转换过程需要处理各种 Pydantic 特性:字段描述变成 Schema 的 description,字段类型映射为 JSON 类型(int 变成 integer,str 变成 string 等),Optional 类型生成 nullable 标记,嵌套模型递归展开为嵌套的 Schema 结构。
然后,JSON Schema 被包装成提供商特定的格式。对于 OpenAI,它被包装为 {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}} 结构。对于 Anthropic,格式略有不同。bind_tools 方法的价值就在于屏蔽了这些格式差异 -- 开发者只需要提供标准的 BaseTool,绑定方法负责转换为正确的格式。
最后,bind_tools 实际上调用了 super().bind(tools=formatted_tools),返回一个 RunnableBinding。这个 RunnableBinding 并不修改原始的 LLM 实例,而是创建了一个新的 Runnable,在每次调用时自动将工具描述附加到请求参数中。这种不可变的设计确保了同一个 LLM 实例可以被多个不同的 bind_tools 调用复用,互不影响。
一个常见的误解是认为 bind_tools 会"教会"模型使用工具。实际上,它只是告诉 API 这次请求可以返回工具调用格式的响应。模型是否真的会调用工具、调用哪个工具,完全取决于模型自身的推理能力和提示内容。工具描述的质量 -- 特别是名称的直观性和描述的准确性 -- 对模型的工具选择行为有决定性影响。
tool_choice 参数的作用
bind_tools 还支持一个重要的可选参数 tool_choice,它可以控制模型的工具调用行为。设为 "auto" 时(默认),模型自行决定是否调用工具;设为 "required" 时,模型必须调用至少一个工具;设为特定工具名时,模型必须调用该工具。在需要强制 Agent 执行特定操作的场景下(如强制使用搜索工具检查最新信息),tool_choice 非常有用。
但需要注意,tool_choice="required" 在 Agent 循环中可能导致无限循环 -- 如果模型被强制调用工具但已经得到了答案,它就无法通过返回 AgentFinish 来终止循环。因此,这个参数在 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.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
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)
# 解析你的自定义格式
actions = self._parse_actions(text)
return actions
def create_my_custom_agent(llm, tools, prompt):
llm_with_tools = llm.bind_tools(tools)
return (
RunnablePassthrough.assign(
agent_scratchpad=lambda x: my_format_function(
x["intermediate_steps"]
),
)
| prompt
| llm_with_tools
| MyCustomOutputParser()
)
何时需要自定义 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 层保证,而非依赖模型的文本生成行为。
从信息流的角度看,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 介绍 4 种 create_*_agent 函数——把它们在 langchain_classic/agents/ 里实测——
| 路径 | 行 | 角色 |
|---|---|---|
agents/agent.py |
1792 | 本目录最大——AgentExecutor 主循环(§15.1 讨论的统一管道在这里)+ BaseSingleActionAgent / BaseMultiActionAgent 抽象 |
agents/openai_assistant/base.py |
831 | OpenAI Assistants API 适配器(不是 §15.4 的 OpenAI Tools Agent、是另一套) |
agents/agent_iterator.py |
432 | Agent 流式迭代器 |
agents/openai_functions_agent/base.py |
382 | §15.4 OpenAI Functions Agent(已废弃但仍在) |
agents/openai_functions_multi_agent/base.py |
337 | 多函数变体 |
agents/structured_chat/base.py |
317 | §15.5 Structured Chat Agent |
agents/xml/base.py |
236 | §15.6 XML Agent |
| 其余(react / chat / conversational / tool_calling_agent / mrkl / self_ask / format_scratchpad / output_parsers / agent_toolkits 等 ~16 个子目录) | 余下 ~6160 | — |
| agents/ 合计 | 10486 | — |
两条值得记住的物理事实——
agent.py单文件 1792 行 = 整个 agents/ 17%——AgentExecutor主循环 + 2 个 BaseAgent ABC + 工具调用编排 + 错误处理 + 流式输出全部塞在一文件——是 LangChain 第 7 处见到的"单文件大方法集"风格(前 6 处:runnables/base.py 6261 / callbacks/manager.py 2697 / output_parsers/openai_tools.py 384 / tools/base.py 1157 / tracers/event_stream.py 1100 / vllm loader.py 1542 跨项目)- 整个 agents/ 在
langchain_classic而不是langchain_core——印证 §1.9.1 测得的"legacy 代码量比新核心还重":旧 Agent 系统因为 LangGraph 取代而被打入langchain_classiclegacy 包——10486 行变成"保留兼容但不演进"的代码——这是 LangChain 1.x 重构后生态最大的工程债
4 种 create_*_agent 函数的实际位置——
| 函数 | 文件 |
|---|---|
create_tool_calling_agent |
agents/tool_calling_agent/base.py |
create_react_agent |
agents/react/agent.py |
create_structured_chat_agent |
agents/structured_chat/base.py |
create_xml_agent |
agents/xml/base.py |
create_openai_tools_agent(§15.4 提到) |
agents/openai_tools/base.py |
create_openai_functions_agent(§15.4 提到) |
agents/openai_functions_agent/base.py |
6 个 create_*_agent 函数分布在 6 个 base.py 文件——印证 §15.10 "接口统一、实现多样" 哲学:每种 Agent 占一个独立子目录;agents/__init__.py re-export 形成统一公共 API。
串联 ch07 §7.12.1 测得的 langchain_classic/memory/ 2157 行——agents 10486 + memory 2157 = ~12643 行 legacy 子系统——是 §1.9.1 揭示的"langchain (70682) > langchain-core (64139)"差距的具体来源;新版本用户应优先看 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 如何实现对象的持久化与动态配置。
延伸阅读:Agent 模式的光谱与选择
LangChain 提供了多种 Agent 模式——OpenAI Tools Agent、ReAct Agent、Plan-and-Execute Agent、Conversational Agent、Self-Ask-With-Search Agent——每一种都有自己的适用场景和历史背景。选哪个、取决于你的任务特征。任务是"单一步骤调一次工具"(比如查天气)——选 Tool-Calling 最简单;任务是"多步推理 + 工具调用"(比如研究一个问题)——ReAct 或 Plan-and-Execute 更合适;任务是"长对话 + 偶尔用工具"——Conversational Agent 保留对话历史;任务是"搜索增强问答"——Self-Ask-With-Search 专门优化了这个模式。
这种"为不同任务提供专门 Agent 模式"的思路、看似丰富实则有隐患——它让"Agent 模式选型"变成了一个新的学习成本。新手面对这么多模式、容易困惑"该用哪个"。这也是为什么 LangGraph(见《LangGraph 源码》第 3 章)选择了更底层的抽象(StateGraph)——让用户自己定义 Agent 模式、而不是从框架的预定义模式里选。这是"高层预设模式"和"底层构建块"两种设计哲学的对立——各有优劣、没有绝对答案。对读者的建议——先用 LangChain 的预设模式快速上手、等业务复杂到预设模式不够用时再迁移到 LangGraph 的自定义图。
延伸阅读:ReAct 范式的认知革命
ReAct(Reasoning + Acting)是 Agent 领域最有影响力的论文之一(Yao et al., 2022、Princeton 和 Google Research 合作)。它的核心思想是——让 LLM "交替进行推理和行动":先 Think(思考下一步该做什么)、再 Act(执行一个工具)、再 Observe(观察结果)、再 Think(根据结果决定下一步)——这个循环直到问题解决。这种范式接近人类的解题过程——不是一次性做完所有推理再行动、而是边想边做、不断调整。
ReAct 的革命性在于——它把"LLM 的推理能力"和"外部世界的信息获取"有机结合、打破了"模型 vs 工具"的二元对立。之前的方法要么全靠 LLM 内部知识(会幻觉)、要么只用工具查询(缺乏推理)。ReAct 让两者协同——LLM 用推理指导"下一步查什么"、外部工具给 LLM 提供"最新、准确的信息"——结果比任何单独的方法都强。这个范式现在几乎是所有 Agent 系统的标配——Tool-Calling Agent 本质上就是 ReAct 的简化版(不显式暴露 Think 步骤、让 function calling 隐式完成)。理解 ReAct、就理解了现代 Agent 的思维内核。
延伸阅读:三要素协同的乘法效应
本章提到 "工具集质量 + 提示模板 + 错误恢复" 三个要素决定 Agent 表现——这三者的关系是"乘法"而非"加法"。任何一个维度为零、整体就为零——再好的工具如果描述不清、Agent 也不会用;再好的提示如果没配合合适的工具、Agent 做不了事;再强的 Agent 如果没有错误恢复、一次 LLM 失误就会让整个流程崩溃。这种"乘法效应"意味着——你不能只优化其中一个维度、必须三个都达标。
很多 Agent 项目失败的原因、就是只在某一个维度做到了极致——工具设计精良但 prompt 太差、prompt 写得漂亮但工具接口混乱、两者都好但没有错误处理——最终都表现平平、Agent 让人感觉"能用但不好用"。好的 Agent 开发者、要像"交响乐指挥"——不是单独让某个乐手演奏极致、而是让所有乐手协调配合、整体呈现和谐。这种"系统性思维"、是 Agent 时代工程师和 LLM 应用工程师的核心竞争力。如果你在读完本书后只记住一个教训、就让它是——"Agent 是一个系统、不是一个模型"。
延伸阅读:工具描述的艺术
工具的 "description" 字段、是 Agent 能正确使用工具的关键。很多开发者把 description 写得像 docstring ——机械、冗长、技术细节多——这对 LLM 来说是噪音。好的 description 应该像 "给新来的同事写的简短说明"——用自然语言、重点突出"什么时候用"、"输入输出大概什么样"、"有哪些常见陷阱"。
有几个具体技巧——第一、"以场景开头"("当用户询问股票价格时使用" 而不是 "查询股票价格的 API")、第二、"给正反例子"("用于天气查询、不要用于空气质量查询")、第三、"明确边界"("只支持中国城市、国际城市请使用另一个工具")、第四、"用 LLM 的语言"(避免代码缩写、用完整英文 / 中文)、第五、"参数示例用真实值"({"city": "Beijing"} 比 {"city": "<string>"} 更有效)。这些技巧看起来琐碎,但能显著降低模型误选工具的概率;具体效果必须用你的工具集、模型和评测集验证。《OpenClaw 源码》第 8 章、《MCP 协议源码》第 5 章都讨论过"工具描述的工程实践"、值得交叉阅读**。
延伸阅读:max_iterations 的选择哲学
AgentExecutor 的 max_iterations 参数默认是 15——这个值看似随意、实则反映了 LangChain 团队的经验判断。15 次迭代够完成大多数 Agent 任务、又不至于让"失控的 Agent"烧太多 token。但对于特定场景、这个值需要调整——简单任务(单次调工具)设 3-5 足够、复杂研究任务(多步推理)可能需要 50+。
这种"默认值的选择"是框架设计的重要细节。默认值要覆盖 80% 常见场景、让新手不用配置就能用起来;但也要允许高级用户覆盖、满足 20% 的特殊场景。这是 "零配置可用 + 深度可配置" 的体现——和 Vite(见《Vite 源码》第 3 章的配置系统)、React(默认行为 + prop override)、Kubernetes(默认 resource limits + override)都是同一种思路。对你的启示是——设计你自己的库时、花时间精心选择默认值——这比添加新功能更能提升用户体验、这是开源库设计里最有温度的工程决策之一。
延伸阅读:多 Agent 协作的前夜
读完本章、你掌握了"单 Agent"的各种模式——这是 Agent 开发的基础。但真实世界的复杂问题、往往超出单 Agent 的能力边界——这时需要"多 Agent 协作"。多 Agent 协作有几种常见模式——"主从模式"(一个主 Agent 分派任务给多个从 Agent)、"对等模式"(多个 Agent 平等协商)、"流水线模式"(多个 Agent 依次处理同一输入)、"辩论模式"(多个 Agent 对同一问题给出不同意见、由另一个 Agent 综合)。
LangChain 本身对多 Agent 协作的支持有限——它的 Agent 抽象更适合"单 Agent 调工具"、对于多 Agent 的复杂协同缺乏原生能力。要做"多 Agent 协作"、建议迁到 LangGraph(见《LangGraph 源码》第 11 章子图)或 OpenClaw(见《OpenClaw 源码》第 18 章多 Agent 编排)——这两个框架对多 Agent 有更完整的支持。多 Agent 是 Agent 技术的下一个前沿、也是 2026 年最激动人心的技术方向之一——本书帮你打好单 Agent 基础、为这个前沿做好准备。
延伸阅读:Agent 模式与领域模型的匹配
不同的业务领域、适合不同的 Agent 模式。客服领域——Conversational Agent 最合适、因为客服天然是"长对话 + 工具辅助"。研究领域——Plan-and-Execute 更合适、因为研究任务需要先规划步骤再逐步执行。数据分析领域——ReAct 最合适、因为分析过程是"思考一步 → 查一下数据 → 思考下一步"。电商导购领域——Tool-Calling 够用、因为大部分交互是"查询商品 + 下单"的单次调用。
这种"领域匹配"的经验、需要通过实战积累。不是每个场景都需要最强的 Agent 模式——用重模式解决轻问题、只会让系统变得笨重。"Just right"(恰好合适)才是优雅的工程选择。这让我想起一个项目经验——一个客服团队最初用 ReAct Agent、每次响应要 3-5 秒;后来降级为 Tool-Calling、响应缩短到 1 秒、用户满意度反而上升——"快速响应"比"深度推理"对客服场景更重要。选择 Agent 模式、不是看"最先进的"、而是看"最匹配的"——这个道理朴素得近乎老生常谈、却是最多工程团队忽视的真理。