ReAct Agent 为什么要调用两次模型?—— 用中间件可视化 Agent 执行过程
通过 LangGraph 的 Middleware 机制,我们可以清楚地看到 Agent 内部的每一步执行。本文用一个完整示例展示 ReAct Agent 的"推理-行动-观察"循环。
核心问题
Agent 调用工具时,模型会被调用两次。为什么?
因为 LLM 本身不能执行工具,它只能:
- 第一次调用:分析问题,决定"我需要调用某个工具"
- 工具执行后,第二次调用:看到工具结果,综合输出最终答案
完整代码
1. 定义工具
from langchain_core.tools import tool
@tool
def explain_concept(concept: str) -> str:
"""解释一个编程概念。"""
explanations = {
"async": "异步编程允许代码在不阻塞的情况下运行。",
"recursion": "递归是函数调用自身的编程技术。"
}
return explanations.get(concept.lower(), "未找到该概念。")
2. 定义日志中间件
from langchain.agents.middleware import AgentMiddleware, AgentState, ModelRequest, ModelResponse
from typing import Any, Callable
class RequestLoggerMiddleware(AgentMiddleware):
"""在 Agent 循环的每个关键节点打印日志。"""
def before_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
message_count = len(state.get("messages", []))
print(f"[模型前] Processing {message_count} messages")
return None
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
print(f" [模型请求]")
print(f" Model: {request.model}")
print(f" Tools available: {len(request.tools) if request.tools else 0}")
return handler(request) # 实际调用模型
def after_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
last_message = state["messages"][-1]
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
print(f" [模型后] Model requested {len(last_message.tool_calls)} tool call(s)")
else:
print(f" [模型后] Model provided final response")
return None
3. 创建 Agent 并运行
from langchain.agents import create_agent
from langchain.messages import HumanMessage
agent_with_logger = create_agent(
model=model,
tools=[explain_concept],
middleware=[RequestLoggerMiddleware()]
)
result = agent_with_logger.invoke({
"messages": [HumanMessage(content="解释递归")]
})
print("\n最终回答:")
print(result["messages"][-1].content)
输出解读
[模型前] Processing 1 messages
[模型请求]
Model: mimo-v2.5-pro
Tools available: 1
[模型后] Model requested 1 tool call(s)
[模型前] Processing 3 messages
[模型请求]
Model: mimo-v2.5-pro
Tools available: 1
[模型后] Model provided final response
逐步解析
第一轮:模型决定调用工具
[模型前] Processing 1 messages
此时消息列表只有 1 条:用户的问题 "解释递归"。
[模型请求]
Model: mimo-v2.5-pro
Tools available: 1
中间件的 wrap_model_call 打印了模型信息和可用工具数量。
[模型后] Model requested 1 tool call(s)
模型看到问题后,决定调用 explain_concept("recursion") 工具来获取信息。
注意:此时模型没有直接回答,而是说"我需要调用工具"。
中间过程:LangGraph 执行工具
LangGraph 自动完成:
- 从模型的
tool_calls中取出工具名和参数 - 执行
explain_concept("recursion") - 得到结果:
"递归是函数调用自身的编程技术。" - 包装成
ToolMessage追加到消息列表
此时消息列表变成 3 条:
| # | 类型 | 内容 |
|---|---|---|
| 1 | HumanMessage | "解释递归" |
| 2 | AIMessage | tool_calls: explain_concept("recursion") |
| 3 | ToolMessage | "递归是函数调用自身的编程技术。" |
第二轮:模型综合输出最终答案
[模型前] Processing 3 messages
现在有 3 条消息了(包含工具结果)。
[模型请求]
Model: mimo-v2.5-pro
Tools available: 1
再次调用同一个模型。
[模型后] Model provided final response
模型看到工具返回的结果,综合上下文,生成最终的自然语言回答给用户。
流程图
sequenceDiagram
participant User as 用户
participant Agent as LangGraph Agent
participant LLM as 模型 (LLM)
participant Tool as 工具: explain_concept
User->>Agent: "解释递归"
Note over Agent: messages = [HumanMessage]<br/>共 1 条消息
rect rgb(255, 243, 224)
Note over Agent,LLM: 第 1 次调用模型
Agent->>LLM: 发送 1 条消息 + 工具列表
LLM-->>Agent: 返回 tool_calls: explain_concept("recursion")
Note over Agent: 模型没有直接回答<br/>而是请求调用工具
end
rect rgb(232, 245, 233)
Note over Agent,Tool: 执行工具
Agent->>Tool: explain_concept("recursion")
Tool-->>Agent: "递归是函数调用自身的编程技术。"
Note over Agent: messages 追加 AIMessage + ToolMessage<br/>共 3 条消息
end
rect rgb(255, 243, 224)
Note over Agent,LLM: 第 2 次调用模型
Agent->>LLM: 发送 3 条消息(含工具结果)
LLM-->>Agent: 返回最终自然语言回答
Note over Agent: 模型看到工具结果<br/>综合输出最终答案
end
Agent->>User: 最终回答
关键理解
- 模型不执行工具 — 它只发出"我想调用 X 工具"的请求
- LangGraph 负责执行 — 拿到请求后实际运行工具函数
- 结果要传回模型 — 模型需要看到工具结果才能生成最终答案
- 每次需要新信息就多一轮 — 如果模型需要调用 3 个工具,可能会有 4 次模型调用
这就是 ReAct(Reasoning + Acting)模式的核心:推理和行动交替进行,直到模型认为信息足够,直接输出最终答案。
中间件的价值
没有中间件,你只能看到最终结果,不知道中间发生了什么。加了 RequestLoggerMiddleware 后,Agent 的每一步决策都清晰可见,非常适合:
- 调试 Agent 为什么没调用工具
- 确认工具结果是否正确传回
- 监控生产环境中 Agent 的行为

浙公网安备 33010602011771号