深入理解 ReAct 循环:从 LLM 决策到工具执行的完整闭环

在做 AI Agent 开发时,很多教程只讲到 LLM 输出 tool_call 就戛然而止,真正关键的「如何执行工具并回传结果」却被一笔带过 。本文将补齐这块拼图,重点讲清楚 call_id 在多轮工具调用中的匹配机制。


一、ReAct 的本质是什么?

ReAct(Reasoning + Acting)不是某种特定算法,而是一种让大模型具备「动手能力的交互范式」

其核心思想很简单:

  1. Thought(思考):LLM 分析用户意图,判断自己能否直接回答
  2. Action(行动):如果需要外部数据,就生成结构化的工具调用请求
  3. Observation(观察):工具执行完成后,把结果喂回给 LLM
  4. Answer(回答):LLM 基于观察结果给出最终答案

这个过程会循环往复,直到 LLM 认为不需要再调用工具为止。


二、完整的 ReAct 交互流程

复制代码
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   用户输入   │────▶│   LLM 决策   │────▶│ tool_call?  │
└─────────────┘     └─────────────┘     └──────┬──────┘
                                                │
                    ┌───────────────────────────┘
                    ▼ 否
            ┌─────────────┐
            │  直接回答用户  │
            └─────────────┘
                    ▼ 是
            ┌─────────────┐
            │  解析工具调用  │
            │  call_id +    │
            │  func + args  │
            └──────┬──────┘
                   ▼
            ┌─────────────┐
            │  执行工具函数  │
            └──────┬──────┘
                   ▼
            ┌─────────────┐
            │  封装执行结果  │
            │  带上 call_id │
            └──────┬──────┘
                   ▼
            ┌─────────────┐
            │  回传给 LLM   │
            └──────┬──────┘
                   ▼
            ┌─────────────┐
            │  LLM 再次决策 │
            │  还有 tool_call?│
            └──────┬──────┘
                   └─────────────────────┐
                                         │
                    ◀────────────────────┘

关键点 :LLM 和工具之间不是"一问一答"就结束了,而是一个可能多轮的循环 。每轮循环中,LLM 可能发出多个 tool_call


三、代码实现:补齐那块缺失的拼图

以下是一个完整可运行的 ReAct Agent 实现,重点标注了 tool execution 环节:

python 复制代码
import json
from typing import List, Dict, Any, Callable

class Tool:
    """工具基类"""
    def __init__(self, name: str, description: str, func: Callable):
        self.name = name
        self.description = description
        self.func = func
    
    def execute(self, **kwargs) -> Any:
        return self.func(**kwargs)


class ReActAgent:
    def __init__(self, llm, tools: List[Tool], system_prompt: str):
        """
        初始化 ReAct Agent
        
        Args:
            llm: 大模型接口,需实现 chat(messages) 方法
            tools: 可用工具列表
            system_prompt: 系统级提示词,定义 Agent 角色和工具使用规范
        """
        self.llm = llm
        self.tools = {tool.name: tool for tool in tools}  # 工具注册表
        self.system_prompt = system_prompt
        self.messages = [{"role": "system", "content": system_prompt}]
    
    def run(self, user_input: str) -> str:
        """
        执行 ReAct 循环
        
        Args:
            user_input: 用户输入
            
        Returns:
            最终回答
        """
        # ========== 第1步:接收用户输入 ==========
        self.messages.append({"role": "user", "content": user_input})
        
        # ========== 第2步:LLM 决策(是否需要调用工具)==========
        response = self.llm.chat(self.messages)
        self.messages.append(response)
        
        # 提取 tool_calls(不同模型格式可能略有差异)
        tool_calls = response.get("tool_calls", [])
        
        # ========== 第3步:ReAct 循环 ==========
        while tool_calls:
            # 遍历本轮的所有工具调用
            for tool_call in tool_calls:
                # --- 解析 tool_call ---
                call_id = tool_call["id"]                              # 唯一标识
                func_name = tool_call["function"]["name"]              # 函数名
                arguments = json.loads(tool_call["function"]["arguments"])  # 参数
                
                print(f"[Tool Call] ID={call_id}, Function={func_name}, Args={arguments}")
                
                # ========== 第4步:执行工具(最容易被遗漏的部分!)==========
                if func_name in self.tools:
                    tool = self.tools[func_name]
                    try:
                        result = tool.execute(**arguments)  # 实际执行工具函数
                        result_content = json.dumps(result, ensure_ascii=False, default=str)
                    except Exception as e:
                        result_content = json.dumps({"error": str(e)}, ensure_ascii=False)
                else:
                    result_content = json.dumps(
                        {"error": f"工具 '{func_name}' 未找到"}, 
                        ensure_ascii=False
                    )
                
                # ========== 第5步:回传结果(必须带上 call_id!)==========
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": call_id,      # ← 核心!让 LLM 知道这是哪个调用的结果
                    "content": result_content
                })
            
            # ========== 第6步:LLM 再次决策(开始新一轮循环)==========
            response = self.llm.chat(self.messages)
            self.messages.append(response)
            tool_calls = response.get("tool_calls", [])
        
        # ========== 第7步:没有 tool_calls,返回最终答案 ==========
        return response.get("content", "")

四、为什么 call_id 是必须的?

这是很多初学者容易忽略的点。让我用一个具体场景说明:

场景:LLM 同时调用多个工具

用户问:"北京今天天气怎么样?上证指数多少点?"

LLM 分析后,一次输出两个 tool_call

json 复制代码
{
  "tool_calls": [
    {
      "id": "call_001",           // 第一个调用的 ID
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\": \"北京\"}"
      }
    },
    {
      "id": "call_002",           // 第二个调用的 ID
      "function": {
        "name": "get_stock_index",
        "arguments": "{\"index\": \"上证指数\"}"
      }
    }
  ]
}

两个工具并行执行后,返回结果:

python 复制代码
# 工具执行结果
weather_result = {"temperature": "25°C", "condition": "晴"}
stock_result = {"index": "3087.53", "change": "+0.32%"}

如果没有 call_id,回传结果会变成这样

python 复制代码
# ❌ 错误示例:LLM 无法知道哪个结果对应哪个调用
messages.append({"role": "tool", "content": str(weather_result)})
messages.append({"role": "tool", "content": str(stock_result)})

LLM 看到两条 role: "tool" 的消息,但无法区分哪个是天气、哪个是股票,回答时就容易"张冠李戴"。

正确的做法

python 复制代码
# ✅ 正确示例:每个结果都带上对应的 call_id
messages.append({
    "role": "tool",
    "tool_call_id": "call_001",  # ← 对应 get_weather
    "content": str(weather_result)
})
messages.append({
    "role": "tool",
    "tool_call_id": "call_002",  # ← 对应 get_stock_index
    "content": str(stock_result)
})

这样 LLM 就能精确匹配 ,知道 call_001 的结果是天气,call_002 的结果是股票。


五、完整示例:跑一个实际的 Agent

python 复制代码
# 定义工具函数
def get_weather(city: str) -> dict:
    """模拟天气查询"""
    mock_data = {
        "北京": {"temp": "25°C", "weather": "晴"},
        "上海": {"temp": "28°C", "weather": "多云"},
    }
    return mock_data.get(city, {"error": "未知城市"})

def calculate(expression: str) -> str:
    """计算表达式"""
    try:
        return str(eval(expression))  # 生产环境不要用 eval!
    except:
        return "计算错误"

# 创建工具实例
tools = [
    Tool("get_weather", "查询指定城市的天气", get_weather),
    Tool("calculate", "计算数学表达式", calculate)
]

# 系统提示词(关键!要告诉 LLM 有哪些工具、怎么用)
system_prompt = """你是一个智能助手,可以使用以下工具:

1. get_weather(city: str) - 查询城市天气
2. calculate(expression: str) - 计算数学表达式

如果需要使用工具,请输出 tool_call。如果可以直接回答,直接回答即可。"""

# 初始化 Agent(假设 llm 已实例化)
agent = ReActAgent(llm=your_llm_client, tools=tools, system_prompt=system_prompt)

# 运行
result = agent.run("北京今天多少度?顺便算一下 15 * 23")
print(result)

六、常见坑点汇总

坑点 现象 解决方案
忘记传 tool_call_id LLM 回答混乱,把工具 A 的结果当成工具 B 的 回传时务必带上 tool_call_id
工具执行异常未捕获 整个 Agent 崩溃 用 try-except 包裹工具执行
arguments 未解析 报错说参数格式不对 json.loads() 解析
死循环 LLM 反复调用同一个工具 设置最大轮次限制,或检查 context 是否有效更新
工具未注册 提示找不到工具 确保 tools 字典中包含了所有可用工具

七、总结

ReAct 的核心是一个**"决策 → 执行 → 观察 → 再决策"**的循环。

Tool Execution 是循环中最容易被忽略但最关键的环节。它不仅仅是"跑一个函数",还涉及到:

  1. 解析 :从 tool_call 中提取 call_idfunction.namearguments
  2. 分发:根据函数名找到对应的工具并执行
  3. 回传 :把执行结果包装成 role: "tool" 的消息,必须带上 tool_call_id
  4. 匹配 :LLM 通过 call_id 将结果与请求一一对应,确保多工具调用时不混乱

理解了这个闭环,你就真正掌握了 ReAct Agent 的工作原理。


写作初衷 :做 Agent 开发时踩了不少坑,尤其是多工具并行调用时 LLM 回答混乱的问题,追溯到根源就是 call_id 没传对。希望这篇文章能帮到你。

如果对你有帮助,欢迎点赞收藏,有问题评论区见!


参考

  • ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022)
  • OpenAI Function Calling 官方文档
  • LangChain Agent 源码实现
相关推荐
2403_883261092 小时前
SQL视图数据不实时怎么办_利用SQL触发器与视图联动方案
jvm·数据库·python
zz0723202 小时前
大模型开发框架 —— SpringAI
ai·springai
z小天才b2 小时前
Django ORM、中间件与信号 — 完全指南
python·中间件·django
m0_684501982 小时前
如何利用 watchEffect 实现在线人数实时统计?Socket 与响应式结合
jvm·数据库·python
重庆若鱼文化创意2 小时前
高端包装设计公司哪家好,报价差异常藏在纸张和印刷工艺里。
人工智能·python
zhangchaoxies2 小时前
C#怎么使用全局Using C#global using全局引用怎么配置减少每个文件的using声明【语法】
jvm·数据库·python
张忠琳2 小时前
【vllm】(八)vLLM v1 Simple KV Offload — 系统级架构深度分析之二
ai·架构·vllm
m0_676544382 小时前
mysql执行预处理语句流程是怎样的_SQL执行优化解析
jvm·数据库·python
AI360labs_atyun2 小时前
GPT-5.5 和 DeepSeek V4同期发布,谁更行?
人工智能·gpt·学习·ai·agi