(一)引言
在人工智能领域,智能体(Agent)被定义为任何能够通过传感器感知其所处环境,并自主地通过执行器采取行动以达成特定目标的实体。 这句话可以总结出智能体定义的四个要素:
- 环境:智能体所处的世界
- 传感器:智能体感知环境状态的工具
- 执行器:智能体改变环境状态的工具
- 行动:智能体对环境施加的影响
除此之外,真正赋予智能体“智能”属性的,是其自主性。智能体并不是执行提前写死的指令的程序,也不是被动响应外界环境的设备,而是能够基于感知,在其内部进行自主决策,从而作出行为以达成任务目标的机器。
大语言模型为智能体提供了强大的认知核心,逐渐涌现出了大模型驱动的LLM智能体这一新的范式。一个现代的智能体,其核心能力在于能将LLM的推理能力和外部世界联通。所谓的联通是一个完整的闭环——自主理解用户意图 -> 拆解复杂任务 -> 调用工具 -> 执行操作 -> 达成目标。然而,大模型的“幻觉”也会影响到智能体调用工具的环节,存在工具错误使用的挑战;同时由于大模型本身的“黑箱”特性,错误无法定位,缺乏透明推理过程。这些问题构成了智能体的能力边界。
为了更好的面对这些问题,2022年论文《ReAct: Synergizing Reasoning and Acting in Language Models》中提出ReAct范式,核心思想是模仿人类解决问题的模式,将推理(Reasoning)和行动(Acting)结合起来,通过构建"Thought-Action-Observation"的闭环机制,首次实现了语言模型推理能力与外部环境交互能力的深度协同。
(二)ReAct的工作流程
在ReAct中,智能体的工作流程遵循一个连续循环,包括以下任务:
- Thought: 模型基于当前收到的任务描述、已有的上下文和历史信息进行推理分析,拆解任务并规划下一步。
- Action:模型根据推理结果选择一个适当的外部工具或操作并执行。
- Observation:智能体获取执行动作后的反馈结果,将其纳入上下文。 智能体不断重复这个Thought -> Action -> Observation的循环,将新的观察结果追加到历史记录中,形成了一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。
我们可以将这个过程用公式展示出来:在每个时间步 ,智能体的策略是 ,此时策略根据初始问题 和之前所有步骤的Action和Observation轨迹 来生成当前的思考 和行动 ,公式如下:
之后环境中的工具TOOLs会执行行动 ,并返回一个新的观察结果
(三)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有免费额度,如果超出额度需要自行购买。
接下来去左右任务栏的接口文档页面,查看模型的ID和URL
于是我们的 .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得到以下输出
3. ReAct智能体编码实现
智能体通过调用大模型确实可以回答我们的一些问题,但是当我们的需求是获取最新实时信息,特别是时间敏感性任务时,大模型的知识库无法同步到最新信息,因此容易产生幻觉。
为解决这个问题,我们还需要赋予智能体调用工具的能力。如果说LLM是智能体的大脑,那么工具(Tools)就是智能体的手和脚,负责执行具体的任务。
类比我们人体结构,举个例子来进行说明:春天来临时,我们眼前有一片湖水,我们的大脑(LLM)根据常识认为:“春天来了天气变暖了湖水没有那么冰了”,但是事实是我们用手(Tools)去触摸一下湖水发现仍然很冰,我们这时大脑才能获取最新信息:“现在的湖水还是很冰,可能今天回暖比往年要慢”,因此如果我们不用手去感受一下(调用工具),那么我们大脑就会产生湖水不冰的幻觉,这显然这与事实相悖。
因此为了让ReAct范式能够解决我们前面提到的问题,需要赋予其调用外部工具的能力。
(1)工具定义与配置
为了能够让智能体获取网页搜索的实时结果,我们选择SerpApi作为智能体的工具。
SerpApi是一个实时API,可以访问Google搜索结果。它为客户处理代理、解决验证码问题,并解析所有丰富的结构化数据。这个API让用户可以轻松地获取Google搜索结果,而无需直接与Google搜索进行交互。
首先,需要安装SerpApi的库:
pip install google-search-results
然后需要去SerpApi官网注册一个账户,并获取自己的APIKey:
然后将其添加到我们之前创建的 .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,查看到以下输出:
同时打开SerpApi的网站可以查看到调用记录:
但是细心的朋友会发现返回结果似乎也不是最新的内容,这里是因为我们的问题和提示词相对来说比较简单,进而影响了返回的结果。考虑到我们的主线任务是测试工具的调用流程是否可以跑通,因此可以暂且忽略此问题。
(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.py 的 HelloAgentsLLM 方法以及 tools.py 的 ToolExecutor 和 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 观察到以下输出:
如上图所示,智能体清晰地展示了它的逻辑链条,首先意识到自己的知识不足,需要使用搜索工具;然后调用搜索工具,根据搜索结果进行推理和总结,最终得出结果。
5. ReAct的特点与局限性
(1)ReAct的特点
- 可解释性:通过
T-A-O链,我们可以清楚地看到智能体的每一步“心路历程”——它为什么会做出当前的举动以及它下一步要打算做什么。这对于理解、信任和调试智能体的行为。 - 动态演化性:ReAct走一步看一步,根据每一步从外界获得的
Observation来动态调整后续的Thought和Action。如果上一步的搜索结果不理想,可以在下一步中修正搜索词,重新尝试。 - 工具协同能力: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返回的原始的文本打印出来,从而去回溯问题的出处。