从原理到代码,拆解ReAct 智能体设计模式

0 阅读9分钟

(一)引言

在人工智能领域,智能体(Agent)被定义为任何能够通过传感器感知其所处环境,并自主地通过执行器采取行动以达成特定目标的实体。 这句话可以总结出智能体定义的四个要素:

  • 环境:智能体所处的世界
  • 传感器:智能体感知环境状态的工具
  • 执行器:智能体改变环境状态的工具
  • 行动:智能体对环境施加的影响

除此之外,真正赋予智能体“智能”属性的,是其自主性。智能体并不是执行提前写死的指令的程序,也不是被动响应外界环境的设备,而是能够基于感知,在其内部进行自主决策,从而作出行为以达成任务目标的机器。

image.png

大语言模型为智能体提供了强大的认知核心,逐渐涌现出了大模型驱动的LLM智能体这一新的范式。一个现代的智能体,其核心能力在于能将LLM的推理能力和外部世界联通。所谓的联通是一个完整的闭环——自主理解用户意图 -> 拆解复杂任务 -> 调用工具 -> 执行操作 -> 达成目标。然而,大模型的“幻觉”也会影响到智能体调用工具的环节,存在工具错误使用的挑战;同时由于大模型本身的“黑箱”特性,错误无法定位,缺乏透明推理过程。这些问题构成了智能体的能力边界。

为了更好的面对这些问题,2022年论文《ReAct: Synergizing Reasoning and Acting in Language Models》中提出ReAct范式,核心思想是模仿人类解决问题的模式,将推理(Reasoning)和行动(Acting)结合起来,通过构建"Thought-Action-Observation"的闭环机制,首次实现了语言模型推理能力与外部环境交互能力的深度协同。

(二)ReAct的工作流程

ReAct.jpg 在ReAct中,智能体的工作流程遵循一个连续循环,包括以下任务:

  • Thought: 模型基于当前收到的任务描述、已有的上下文和历史信息进行推理分析,拆解任务并规划下一步。
  • Action:模型根据推理结果选择一个适当的外部工具或操作并执行。
  • Observation:智能体获取执行动作后的反馈结果,将其纳入上下文。 智能体不断重复这个Thought -> Action -> Observation的循环,将新的观察结果追加到历史记录中,形成了一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。

我们可以将这个过程用公式展示出来:在每个时间步 tt,智能体的策略是 ππ,此时策略根据初始问题 qq 和之前所有步骤的Action和Observation轨迹((A1,O1),(A2,O2),...,(At1,Ot1))((A_1,O_1),(A_2,O_2),...,(A_{t-1},O_{t-1})) 来生成当前的思考 TtT_t 和行动 AtA_t ,公式如下:

(Tt,At)=π(q,((A1,O1),(A2,O2),...,(At1,Ot1))) (T_t,A_t) = π(q,((A_1,O_1),(A_2,O_2),...,(A_{t-1},O_{t-1})))

之后环境中的工具TOOLs会执行行动 AtA_t ,并返回一个新的观察结果 OtO_t

Ot=T(At)O_t = T(A_t)

(三)ReAct的工程实现

我们将从需求开始分析,通过环境准备和构建代码再到最后的测试从零手撕一个ReAct范式的智能体。

1. 需求分析

构建一个能够突破大语言模型(LLM)知识截止时间限制的智能助手,使其能够回答关于时事新闻、实时数据(如股价、天气)以及特定事实核查的问题。
解决痛点:

  • 幻觉问题:纯 LLM 容易编造不存在的事实。
  • 知识滞后:LLM训练数据是静态的,无法获得最新的动态信息
  • 黑盒决策:用户需要知道智能体的决策过程,而不仅仅是结果

2. 环境准备

以Ubuntu22.04系统,Python3.10版本为例进行展示。
首先我们新建一个项目文件夹 ReActAgent_demo 来存放我们的所有文件。
ReActAgent_demo/
├── .env
├── tools.py
├── llm_client.py
├── ReAct.py
└── README.md

(1)安装库

# openai库用来兼容Openai接口的大模型
# python-dotenv库用于管理环境变量,存储我们的API秘钥
pip install openai python-dotenv

(2)配置API密钥

为了让我们的代码可读性更强,我们将调用的模型的相关信息(模型ID 模型API 模型URL)统一配置为环境变量,存储到到一个名为 .env 的文件中。python-dotenv库就可以实现把环境变量导入到本地文件中来。

# .env 文件
LLM_API_KEY="YOUR-API-KEY"
LLM_MODEL_ID="YOUR-MODEL"
LLM_BASE_URL="YOUR-URL"

一般的Agent工程会给一个 .env 的demo文件,我们在git clone完项目之后可以cp .env .env.myenv,其中 .env.myenv就是存储你自己大模型信息的环境变量文件。在使用dotenv调用时替换为你自己的文件地址即可。
这里我们以DeepSeek为例来进行展示。
首先注册一下DeepSeek API,创建自己的API Key,API有免费额度,如果超出额度需要自行购买。

image.png

接下来去左右任务栏的接口文档页面,查看模型的ID和URL

image.png 于是我们的 .env 文件如下所示:

# .env 文件
LLM_API_KEY="sk-99fdb***********0b0c"
LLM_MODEL_ID="deepseek-v4-flash"
LLM_BASE_URL="https://api.deepseek.com"

(3)封装基础LLM调用函数

为了让代码结构更加清晰,更易复用,我们需要定义一个专属的LLM客户端类。这个类可以封装主程序和LLM的交互细节,将调用LLM模块化,从而使主逻辑专注于智能体结构的构建。

# llm_client.py
import os
from openai import OpenAI
from dotenv import load_dotenv # 从.env文件中导入环境变量
from typing import List, Dict

# 导入环境变量
load_dotenv()

class HelloAgentsLLM:
	# 调用兼容OpenAI接口的服务, 使用默认的流式响应(将响应内容分为chunks逐个发送给客户端,而不是等待响应完毕一次性发送)
	def __init__(self, model: str = None, apiKey: str = None, baseUrl: str = None, timeout: int = None):
	
		# 初始化客户端,优先使用本地传入参数,否则加载环境变量参数
		self.model = model or os.getenv("LLM_MODEL_ID")
		apiKey = apiKey or os.getenv("LLM_API_KEY")
		baseUrl = baseUrl or os.getenv("LLM_BASE_URL")
		timeout = timeout or int(os.getenv("LLM_TIMEOUT", 60))
				
		if not all([self.model, apiKey, baseUrl]):
			raise ValueError("model ID and apiKey must be provided in .env file")
		self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)
		print("✅ 大模型配置成功")
		
	def think(self, messages: List[Dict[str, str]], temperature: float = 0) -> str:
		# 调用大模型进行思考,并返回思考结果
		print(f"🧠 正在调用 {self.model} 模型...")
		try:
			response = self.client.chat.completions.create(
				model=self.model,
				messages=messages,
				temperature=temperature,
				stream=True, # 开启流式响应
			)
			
			# 流式响应逻辑
			print("✅ 大语言模型响应成功:")
			collected_content = []
			for chunk in response: 
				content = chunk.choices[0].delta.content or "" # 从当前数据块提取刚产生的数据
				print(content, end="", flush=True)
				collected_content.append(content) # 将每次新产生的数据进行添加
			print() # 流式输出结束后换行
			return "".join(collected_content) # 将列表元素合成为完整字符串
		except Exception as e:
			print(f"❌ 调用LLM API时发生错误: {e}")
			return None
			
			
if __name__ == '__main__':
	try:
		llmClient = HelloAgentsLLM()
                # 向think方法传入两个role-content键值对
		exampleMessages = [
			{"role": "system", "content": "You are a helpful assitant that writes Python code."},
			{"role": "user", "content": "write a bubble sort algorithm and use Chinese comments."}
		]
		print("--- 调用LLM ---")
		responseText = llmClient.think(exampleMessages)
		if responseText:
			print("\n\n--- Complete model response ---")
			print(responseText)
	except ValueError as e:
		print(e)

在项目目录运行python llm_client.py得到以下输出

image.png

3. ReAct智能体编码实现

智能体通过调用大模型确实可以回答我们的一些问题,但是当我们的需求是获取最新实时信息,特别是时间敏感性任务时,大模型的知识库无法同步到最新信息,因此容易产生幻觉
为解决这个问题,我们还需要赋予智能体调用工具的能力。如果说LLM是智能体的大脑,那么工具(Tools)就是智能体的手和脚,负责执行具体的任务。
类比我们人体结构,举个例子来进行说明:春天来临时,我们眼前有一片湖水,我们的大脑(LLM)根据常识认为:“春天来了天气变暖了湖水没有那么冰了”,但是事实是我们用手(Tools)去触摸一下湖水发现仍然很冰,我们这时大脑才能获取最新信息:“现在的湖水还是很冰,可能今天回暖比往年要慢”,因此如果我们不用手去感受一下(调用工具),那么我们大脑就会产生湖水不冰的幻觉,这显然这与事实相悖。
因此为了让ReAct范式能够解决我们前面提到的问题,需要赋予其调用外部工具的能力。

(1)工具定义与配置

为了能够让智能体获取网页搜索的实时结果,我们选择SerpApi作为智能体的工具。
SerpApi是一个实时API,可以访问Google搜索结果。它为客户处理代理、解决验证码问题,并解析所有丰富的结构化数据。这个API让用户可以轻松地获取Google搜索结果,而无需直接与Google搜索进行交互。
首先,需要安装SerpApi的库: pip install google-search-results 然后需要去SerpApi官网注册一个账户,并获取自己的APIKey:

image.png 然后将其添加到我们之前创建的 .env 文件中:

# .env 文件
LLM_API_KEY="YOUR-API-KEY"
LLM_MODEL_ID="YOUR-MODEL"
LLM_BASE_URL="YOUR-URL"
# 新增SerpApi的APIKey
SERPAPI_API_KEY="YOUR_SERPAPI_API_KEY"

(2)实现工具的核心逻辑

一个良好定义的工具应该包含以下三个核心要素:

  • 名称(Name):一个简洁、唯一的标识符。
  • 描述(Description):一段清晰的自然语言描述用来说明这个工具的用途,作为何时被LLM调用的触发条件。
  • 执行逻辑(Execution Logic):真正如何执行任务的函数或方法。

我们要实现一个基于SerpApi的网页搜索引擎工具,它可以智能地解析搜索结果,并返回给LLM。
我们首先来编写工具的执行逻辑:

# 搜索引擎工具的实现逻辑
from serpapi import Client
def search(query: str) -> str:
	print(f"🔍 正在执行 [Serpapi] 网页搜索: {query}")
	try: 
                # 从导入环境变量
		api_key = os.getenv("SERPAPI_API_KEY")
		if not api_key:
			return "❌ 错误:SERPAPI_API_KEY 未在 .env 文件中配置."
		params = {
			"q": query,
			"gl": "cn", # 国家代码
			"hl": "zh-cn", # 语言
		}
		client = Client(api_key=api_key) # 实例化客户端对象
		results = client.search(params) # 调用客户端的搜索方法,执行实际的搜索任务

		
		# 智能解析搜索内容
		# 返回内容遵循优先级answer_box_list/answer_box(直接答案) > knowledge_graph(百科介绍) > organic_results(网页摘要)
		# 解析具体的答案文本框列表
		if "answer_box_list" in results:
			return "\n".join(results["answer_box_list"])
                # 解析具体的答案文本框中的答案
		if "answer_box" in results and "answer" in results["answer_box"]:
			return results["answer_box"]["answer"]
                # 解析百科介绍
		if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
			return results["knowledge_graph"]["description"]
                # 解析网页摘要
		if "organic_results" in results and results["organic_results"]:
			# 如果没有直接答案或者百科介绍,返回前三个结果的摘要
			snippets = [
				f"[{i+1}] {res.get('title', '')}\n{res.get('snippet', '')}" # res: current search object(dictionary)
				for i, res in enumerate(results["organic_results"][:3]) # select the first three snippets
			]
			return "\n\n".join(snippets)
			
		return f"ℹ️ 对不起, 没有找到关于 '{query}' 的信息。"
	
	except Exception as e:
		return f"💥 搜索时发生错误:{e}"

上述代码通过实例化Client完成了调用serapi的search方法,通过if的层级选择规定智能解析的优先级逻辑。这种智能解析为LLM提供了质量更高的信息输入。

(3)构建通用的工具执行器

我们在(2)中只是定义了一个网页搜索引擎工具,但是在执行复杂的任务时候我们需要使用多种工具(计算工具、查询数据库工具等),我们需要一个通用的工具执行器来注册和调度这些工具。为此,我们创建一个ToolExecutor类。

from typing import Dict, Any
# 工具执行器,负责注册和调度工具
class ToolExecutor:
	def __init__(self):
		# Dict键值对中[str] 是工具的名称, Dict[str, Any] 包含工具描述和工具函数
		self.tools: Dict[str, Dict[str, Any]] = {}
                
	def registerTool(self, name: str, description: str, func: callable):
		# 在工具箱中注册一个新工具
		if name in self.tools:
			print(f"⚠️ 警告: 工具 '{name}' 已存在, 将被覆盖。")
		self.tools[name] = {"description": description, "func": func} # 工具描述决定了调用工具的触发条件
		print(f"✅ 工具: '{name}' 已注册。")
		
	def getTool(self, name: str) -> callable:
		# 根据名称获取一个工具的执行函数
		return self.tools.get(name, {}).get("func")
		
	def getAvailableTools(self) -> str:
		# 获取工具箱中所有可用工具的格式化描述字符串, 作为输入给LLM的字符串
		return "\n".join([
			f"- {name}: {info['description']}"
			for name, info in self.tools.items()
			])

(4)测试工具运行

我们将search工具注册到ToolExecutor中,编写tools.py文件,模拟一次调用,以验证整个工具调用和执行流程是否正常工作。

# tools.py 文件用来验证整个工具调用和执行流程

import os
from serpapi import Client
from dotenv import load_dotenv
from typing import Dict, Any

# 加入导入环境变量的异常处理机制
try:
    load_dotenv()
except FileNotFoundError:
    print("警告:未找到 .env 文件,将使用系统环境变量。")
except Exception as e:
    print(f"警告:加载 .env 文件时出错: {e}")

def search(query: str) -> str:
	print(f"🔍 正在执行 [Serpapi] 网页搜索: {query}")
	try: 
                # 从导入环境变量
		api_key = os.getenv("SERPAPI_API_KEY")
		if not api_key:
			return "❌ 错误:SERPAPI_API_KEY 未在 .env 文件中配置."
		params = {
			"q": query,
			"gl": "cn", # 国家代码
			"hl": "zh-cn", # 语言
		}
		client = Client(api_key=api_key) # 实例化客户端对象
		results = client.search(params) # 调用客户端的搜索方法,执行实际的搜索任务

		
		# 智能解析搜索内容
		# 返回内容遵循优先级answer_box_list/answer_box(直接答案) > knowledge_graph(百科介绍) > organic_results(网页摘要)
		# 解析具体的答案文本框列表
		if "answer_box_list" in results:
			return "\n".join(results["answer_box_list"])
                # 解析具体的答案文本框中的答案
		if "answer_box" in results and "answer" in results["answer_box"]:
			return results["answer_box"]["answer"]
                # 解析百科介绍
		if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
			return results["knowledge_graph"]["description"]
                # 解析网页摘要
		if "organic_results" in results and results["organic_results"]:
			# 如果没有直接答案或者百科介绍,返回前三个结果的摘要
			snippets = [
				f"[{i+1}] {res.get('title', '')}\n{res.get('snippet', '')}" # res: current search object(dictionary)
				for i, res in enumerate(results["organic_results"][:3]) # select the first three snippets
			]
			return "\n\n".join(snippets)
			
		return f"ℹ️ 对不起, 没有找到关于 '{query}' 的信息。"
	
	except Exception as e:
		return f"💥 搜索时发生错误:{e}"
			

# 工具执行器,负责注册和调度工具
class ToolExecutor:
	def __init__(self):
		# Dict键值对中[str] 是工具的名称, Dict[str, Any] 包含工具描述和工具函数
		self.tools: Dict[str, Dict[str, Any]] = {}
                
	def registerTool(self, name: str, description: str, func: callable):
		# 在工具箱中注册一个新工具
		if name in self.tools:
			print(f"⚠️ 警告: 工具 '{name}' 已存在, 将被覆盖。")
		self.tools[name] = {"description": description, "func": func} # 工具描述决定了调用工具的触发条件
		print(f"✅ 工具: '{name}' 已注册。")
		
	def getTool(self, name: str) -> callable:
		# 根据名称获取一个工具的执行函数
		return self.tools.get(name, {}).get("func")
		
	def getAvailableTools(self) -> str:
		# 获取工具箱中所有可用工具的格式化描述字符串, 作为输入给LLM的字符串
		return "\n".join([
			f"- {name}: {info['description']}"
			for name, info in self.tools.items()
			])
			
# 编写测试程序入口
if __name__ == '__main__':
	# 1. 初始化工具执行器
	print("🚀 正在初始化工具执行器...")
	toolExecutor = ToolExecutor()
	print("✅ 工具执行器初始化成功。")
	# 2. 注册搜索工具
	search_description = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具"
	toolExecutor.registerTool("Search",search_description, search) # 工具命名为Search
	
	# 3. 打印出可用工具
	print("\n🧰 --- 可用的工具 ---")
	print(toolExecutor.getAvailableTools())
	
	# 4. Invocation of the agent
	print("\n-- 执行 Action: Search['英伟达最新的GPU型号是什么?'] ---")
	tool_name = "Search"
	tool_input = "英伟达最新的GPU型号是什么?"
	# 根据名称获取工具的执行函数,这是工具主要的执行逻辑
	tool_function = toolExecutor.getTool(tool_name)
	if tool_function:
		# 理解外部世界的信息, 从而执行下一步推理操作
		observation = tool_function(tool_input)
		print("\n👁️ --- Observation ---")
		print(observation)
	else:
		print(f"错误: 未找到名为 '{tool_name}' 的工具")

在终端运行 python tools.py,查看到以下输出:

image.png 同时打开SerpApi的网站可以查看到调用记录:

image.png 但是细心的朋友会发现返回结果似乎也不是最新的内容,这里是因为我们的问题和提示词相对来说比较简单,进而影响了返回的结果。考虑到我们的主线任务是测试工具的调用流程是否可以跑通,因此可以暂且忽略此问题。

(5)ReAct智能体实现

我们将上述提到的所有独立的组件、LLM客户端和工具执行器组装起来,构建一个完整的ReAct智能体。我们通过一个 ReActAgent类来封装智能体的核心逻辑。下面主要讲解ReActAgent类的实现逻辑。

(i)系统提示词设计

提示词是整个ReAct机制的基石,它为大语言模型提供了行动的操作指令。我们需要精心设计一个Prompt模板,它将动态地插入可用工具、用户问题以及中间步骤的交互历史。

# ReAct 提示词模板
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手,需要确保回答的实时性。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在`Action:`字段后使用 `Finish[最终答案]` 来输出最终答案。


现在,请开始解决以下问题:
Question: {question}
History: {history}
"""

以上模板内容体现了智能体与LLM之间交互的规范:

  • 角色定义:设置LLM的角色。
  • 工具清单({tools}):告诉LLM有哪些可用的工具。
  • 格式规范(Thought/Action):强制设定LLM的输出格式,使我们能通过代码解析其意图。
  • 动态上下文({questions}/{history}):将用户的原始问题和不断添加的交互历史注入到prompt中,让LLM基于完整的上下文进行决策。
(ii)ReActAgent核心循环设计

ReActAgent的本质是一个迭代循环,它通过不断地执行“格式化提示词 -> 调用LLM思考 -> 解析LLM输出为Thought和Action -> 执行Action -> 生成Observation ”循环来优化回答,最终达到最大迭代次数停止。

class ReActAgent:
	def __init__(self, llm_client: HelloAgentsLLM, tool_executor: ToolExecutor, max_steps: int = 5):
		self.llm_client = llm_client
		self.tool_executor = tool_executor
		self.max_steps = max_steps
		self.history = [] # 保存每一步的Thought 和 Action
		
	def run(self, question: str):
		# run方法是智能体入口
		self.history = []
		current_step = 0
		# ReAct智能体的主循环逻辑,设置max_steps防止智能体陷入无限循环浪费资源
		while current_step < self.max_steps:
			current_step += 1
			print(f"--- 第 {current_step} 步 ---")
			
			# 1. 格式化提示词
			tools_desc = self.tool_executor.getAvailableTools() # 获取可用工具的描述
			history_str = "\n".join(self.history)
			# 提示词 = 工具描述 + 问题 + 历史 
			prompt = REACT_PROMPT_TEMPLATE.format(
				tools=tools_desc,
				question=question,
				history=history_str
			)
			
			# 2. 调用大模型思考
			messages = [{"role": "user", "content": prompt}] # 构建消息格式
			response_text = self.llm_client.think(messages=messages)
			
			if not response_text:
				print("❌️ 错误: LLM未能返回有效响应"); break
				
			# 3. 解析LLM输出为Thought和Action
			
			thought, action = self._parse_output(response_text)
			if thought:
				print(f"Thought: {thought}")
			
			if not action:
				print("警告: 未能解析出有效的Action,流程终止。"); break
			
			# 4. 执行Action
			if action.startswith("Finish"):
				# 如果是 Finish 指令, 提取最终答案并结束
				final_answer = re.match(r"Finish\[(.*)\]", action).group(1)
				print(f"🎉 最终答案: {final_answer}")
				return final_answer
			
			tool_name, tool_input = self._parse_action(action)
			if not tool_name or not tool_input:
				# 处理无效的Action格式
				self.history.append("Observation: 无效的Action 格式, 请检查。"); continue
				
			print(f"🎬 Action: {tool_name}[{tool_input}]")
                        
			# 5. 生成Observation
			tool_function = self.tool_executor.getTool(tool_name)
			if not tool_function:
				observation = f"❌️ 错误: 未找到名为 '{tool_name}' 的工具。"
			else:
				observation = tool_function(tool_input) # 调用真实工具
				
			print(f"👀 Observation: {observation}")
			# 把这一轮的 Action 和 Observation 加入到历史中,智能体在下一轮生成提示词时,就能看到上一步Action和Observation的结果
			self.history.append(f"Action: {action}")
			self.history.append(f"Observation: {observation}")
			
		print("已经达到最大步数, 流程终止。")
		return None
(iii)解析函数设计

将search工具的返回结果解析为Thought-Action-Observation至关重要,直接决定了ReAct范式能否按照我们的提示词运行。

# 解析函数设计(放到ReActAgent类里面)
import re # 使用正则表达式进行解析
def _parse_output(self, text: str):
        # 分析 LLM 输出并提取 Thought 和 Action
    
        # Thought: --- 匹配字面上的“Thought:”字符串
        # \s* --- 匹配“:”之后可能出现的零个或者多个空白字符
        # (.*?) --- 非贪婪捕获组。提取“思考”的具体内容,直到遇到后续的终止条件为止
        # (?=\nAction:|$) --- 正向先行断言,表示匹配的内容后面必须紧跟着“\nAction”或者字符串结尾($)
        # DOTALL --- 此标志允许(.) 匹配包含换行符在内的所有字符
    
        thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
        action_match = re.search(r"Action:\s*(.*?)$$", text, re.DOTALL)
        thought = thought_match.group(1).strip() if thought_match else None
        action = action_match.group(1).strip() if action_match else None
        return thought, action
                
def _parse_action(self, action_text: str):
	     # 进一步分析 Action 字符串并获取(工具名称, 参数字符串)
	     match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
	     if match:
		     return match.group(1), match.group(2)
	     return None, None
                
def _parse_action_input(self, action_text: str):
	     # 只分析 Action 字符串并获取(参数字符串),不关心工具名称
	     match = re.match(r"\w+\[(.*)\]", action_text, re.DOTALL)
	     return match.group(1) if match else ""

4. 测试效果

我们将以上 ReActAgent 类放入 ReAct.py 文件中,并在 ReAct.py 文件中调用 llm_client.pyHelloAgentsLLM 方法以及 tools.pyToolExecutor search 方法

import re
from llm_client import HelloAgentsLLM
from tools import ToolExecutor, search
from dotenv import load_dotenv

# 加入导入环境变量的异常处理机制
try:
    load_dotenv()
except FileNotFoundError:
    print("警告:未找到 .env 文件,将使用系统环境变量。")
except Exception as e:
    print(f"警告:加载 .env 文件时出错: {e}")

# 设计ReAct范式智能体的Prompt
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手,需要确保回答的实时性。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在`Action:`字段后使用 `Finish[最终答案]` 来输出最终答案。


现在,请开始解决以下问题:
Question: {question}
History: {history}
"""

# 定义ReActAgent主循环逻辑
class ReActAgent:
	def __init__(self, llm_client: HelloAgentsLLM, tool_executor: ToolExecutor, max_steps: int = 5):
		self.llm_client = llm_client
		self.tool_executor = tool_executor
		self.max_steps = max_steps
		self.history = [] # 保存每一步的Thought 和 Action
		
	def run(self, question: str):
		# run方法是智能体入口
		self.history = []
		current_step = 0
		# ReAct智能体的主循环逻辑,设置max_steps防止智能体陷入无限循环浪费资源
		while current_step < self.max_steps:
			current_step += 1
			print(f"--- 第 {current_step} 步 ---")
			
			# 1. 格式化提示词
			tools_desc = self.tool_executor.getAvailableTools() # 获取可用工具的描述
			history_str = "\n".join(self.history)
			# 提示词 = 工具描述 + 问题 + 历史 
			prompt = REACT_PROMPT_TEMPLATE.format(
				tools=tools_desc,
				question=question,
				history=history_str
			)
			
			# 2. 调用大模型思考
			messages = [{"role": "user", "content": prompt}] # 构建消息格式
			response_text = self.llm_client.think(messages=messages)
			
			if not response_text:
				print("❌️ 错误: LLM未能返回有效响应"); break
				
			# 3. 解析LLM输出为Thought和Action
			
			thought, action = self._parse_output(response_text)
			if thought:
				print(f"Thought: {thought}")
			
			if not action:
				print("警告: 未能解析出有效的Action,流程终止。"); break
			
			# 4. 执行Action
			if action.startswith("Finish"):
				# 如果是 Finish 指令, 提取最终答案并结束
				final_answer = re.match(r"Finish\[(.*)\]", action).group(1)
				print(f"🎉 最终答案: {final_answer}")
				return final_answer
			
			tool_name, tool_input = self._parse_action(action)
			if not tool_name or not tool_input:
				# 处理无效的Action格式
				self.history.append("Observation: 无效的Action 格式, 请检查。"); continue
				
			print(f"🎬 Action: {tool_name}[{tool_input}]")
                        
			# 5. 生成Observation
			tool_function = self.tool_executor.getTool(tool_name)
			if not tool_function:
				observation = f"❌️ 错误: 未找到名为 '{tool_name}' 的工具。"
			else:
				observation = tool_function(tool_input) # 调用真实工具
				
			print(f"👀 Observation: {observation}")
			# 把这一轮的 Action 和 Observation 加入到历史中,智能体在下一轮生成提示词时,就能看到上一步Action和Observation的结果
			self.history.append(f"Action: {action}")
			self.history.append(f"Observation: {observation}")
			
		print("已经达到最大步数, 流程终止。")
		return None
			
	def _parse_output(self, text: str):
		# 分析 LLM 输出并提取 Thought 和 Action
    
           # Thought: --- 匹配字面上的“Thought:”字符串
           # \s* --- 匹配“:”之后可能出现的零个或者多个空白字符
           # (.*?) --- 非贪婪捕获组。提取“思考”的具体内容,直到遇到后续的终止条件为止
           # (?=\nAction:|$) --- 正向先行断言,表示匹配的内容后面必须紧跟着“\nAction”或者字符串结尾($)
           # DOTALL --- 此标志允许(.) 匹配包含换行符在内的所有字符
    
           thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
           action_match = re.search(r"Action:\s*(.*?)$$", text, re.DOTALL)
           thought = thought_match.group(1).strip() if thought_match else None
           action = action_match.group(1).strip() if action_match else None
           return thought, action
		
	def _parse_action(self, action_text: str):
		# 进一步分析 Action 字符串并获取(工具名称, 参数字符串)
	        match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
	        if match:
		       return match.group(1), match.group(2)
	        return None, None
		
	def _parse_action_input(self, action_text: str):
		# 只分析 Action 字符串并获取(参数字符串),不关心工具名称
	        match = re.match(r"\w+\[(.*)\]", action_text, re.DOTALL)
	        return match.group(1) if match else ""


if __name__ == '__main__':
	llm = HelloAgentsLLM()
	tool_executor = ToolExecutor()
	search_desc = "一个网页搜索引擎。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
	tool_executor.registerTool("Search", search_desc, search)
	agent = ReActAgent(llm_client=llm, tool_executor=tool_executor)
	question = "2026年ChatGPT的最新版本是什么?"
	agent.run(question)

在终端执行 python ReAct.py 观察到以下输出:

image.png 如上图所示,智能体清晰地展示了它的逻辑链条,首先意识到自己的知识不足,需要使用搜索工具;然后调用搜索工具,根据搜索结果进行推理和总结,最终得出结果。

5. ReAct的特点与局限性

(1)ReAct的特点

  • 可解释性:通过 T-A-O 链,我们可以清楚地看到智能体的每一步“心路历程”——它为什么会做出当前的举动以及它下一步要打算做什么。这对于理解、信任和调试智能体的行为。
  • 动态演化性:ReAct走一步看一步,根据每一步从外界获得的 Observation 来动态调整后续的 ThoughtAction。如果上一步的搜索结果不理想,可以在下一步中修正搜索词,重新尝试。
  • 工具协同能力:ReAct范式天然的将LLM的推理能力和外部工具的执行能力结合起来。LLM负责运筹帷幄,工具负责解决具体问题,二者协同工作,突破了单一LLM在知识时效性方面的局限。
  • 缓解幻觉:ReAct将LLM内部推理与外部知识检索相结合,有效降低了模型幻觉的风险。当模型缺少某方面知识时,可以通过调用搜索或数据库查询来获得真实信息,从而大幅提高答案的准确性和可信度。

(2)ReAct的局限性

尽管ReAct有以上优势特点,但是仍然存在一些固有局限性:

  • 对LLM自身能力的强依赖:ReAct流程的成功与否高度依赖于底层LLM的综合能力。如果LLM的推理能力不足或者格式化输出能力不足,就很容易在 Thought 环节产生错误的规划,进而导致后续步骤也出错。
  • 执行效率问题:每完成一个任务都需要多次调用LLM。每一次调用都伴随着网络延迟和计算成本。对于复杂任务,这意味着时间和计算成本都很高。
  • 陷入局部最优的风险:步进式的决策模式意味着 智能体缺乏一个全局的、长远的规划。它可能会因为眼前的 Observation 而选择一个看似正确但是并非全局最优的路径。
  • 长链推理误差累积:尽管ReAct可以通过观察反馈修正错误,但在过长的推理链中,小错误仍可能逐步放大并累积,导致最终结果偏离最优解。
  • 安全性与不可控因素:由于ReAct允许LLM触发外部动作,模型可能调用敏感接口或在工具输入中注入不安全内容。此外,恶意用户可能通过巧妙构造输入,引导agent执行不良操作。因此在实际部署ReAct智能体时,开发者需要在工具层面加入权限控制和沙盒机制,防止代理越权行动。

(3)ReAct的调试技巧

  • 调整提示词中的示例(Few-shot Prompting):如果模型频繁出错,可以在提示词中加入一两个完整的T-A-O成功案例,以此来引导模型更好的执行指令。
  • 分析原始输入:当解析输出失败时,需要LLM返回的原始的文本打印出来,从而去回溯问题的出处。