ReAct Agent 为什么要调用两次模型?—— 用中间件可视化 Agent 执行过程

通过 LangGraph 的 Middleware 机制,我们可以清楚地看到 Agent 内部的每一步执行。本文用一个完整示例展示 ReAct Agent 的"推理-行动-观察"循环。


核心问题

Agent 调用工具时,模型会被调用两次。为什么?

因为 LLM 本身不能执行工具,它只能:

  1. 第一次调用:分析问题,决定"我需要调用某个工具"
  2. 工具执行后,第二次调用:看到工具结果,综合输出最终答案

完整代码

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 自动完成:

  1. 从模型的 tool_calls 中取出工具名和参数
  2. 执行 explain_concept("recursion")
  3. 得到结果:"递归是函数调用自身的编程技术。"
  4. 包装成 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: 最终回答

关键理解

  1. 模型不执行工具 — 它只发出"我想调用 X 工具"的请求
  2. LangGraph 负责执行 — 拿到请求后实际运行工具函数
  3. 结果要传回模型 — 模型需要看到工具结果才能生成最终答案
  4. 每次需要新信息就多一轮 — 如果模型需要调用 3 个工具,可能会有 4 次模型调用

这就是 ReAct(Reasoning + Acting)模式的核心:推理和行动交替进行,直到模型认为信息足够,直接输出最终答案。


中间件的价值

没有中间件,你只能看到最终结果,不知道中间发生了什么。加了 RequestLoggerMiddleware 后,Agent 的每一步决策都清晰可见,非常适合:

  • 调试 Agent 为什么没调用工具
  • 确认工具结果是否正确传回
  • 监控生产环境中 Agent 的行为
posted @ 2026-05-19 17:03  江鸟Dev  阅读(22)  评论(0)    收藏  举报