原生 Python 实现 ReAct Agent(计算器版)
·
完全不用 LangChain 等第三方框架,只用原生 Python + 豆包 OpenAI 兼容接口,实现一个能自主调用计算器、解决数学题的 ReAct Agent,代码结构清晰、注释详细,你可以直接运行、修改、扩展。
一、核心设计思路
1. ReAct 三大核心要素(严格遵循论文定义)
表格
| 要素 | 实现方式 |
|---|---|
| Thought(思考) | 让 LLM 分析当前状态,规划下一步是否需要计算 |
| Action(行动) | 调用 calculator 工具,传入数学表达式 |
| Observation(观察) | 计算器返回的计算结果,注入下一轮推理 |
2. 计算器工具安全设计
用 Python 内置的 eval 函数,但严格限制输入,只允许数字、运算符、括号,防止代码注入,保证安全。
二、完整可运行代码
import os
import re
from dotenv import load_dotenv
from openai import OpenAI
# ====================== 1. 初始化配置 ======================
# 加载环境变量(.env文件里放你的豆包API Key)
load_dotenv()
# 初始化豆包客户端(完全兼容OpenAI接口)
client = OpenAI(
api_key=os.getenv("DOUBAO_API_KEY"),
base_url="https://ark.cn-beijing.volces.com/api/v3"
)
# ====================== 2. 定义工具:安全计算器 ======================
def safe_calculator(expression: str) -> str:
"""
安全的计算器工具,只允许数学运算,防止代码注入
:param expression: 数学表达式,比如 "35*24+18"
:return: 计算结果
"""
# 安全检查:只允许数字、运算符、括号、空格
if not re.fullmatch(r'^[\d+\-*/().\s]+$', expression):
return "错误:表达式包含非法字符,只允许数字、运算符、括号"
try:
# 计算表达式,限制全局命名空间,防止代码注入
result = eval(expression, {"__builtins__": None}, {})
return f"计算结果:{result}"
except ZeroDivisionError:
return "错误:除数不能为0"
except SyntaxError:
return "错误:表达式语法错误"
except Exception as e:
return f"错误:{str(e)}"
# 工具字典:LLM 可以调用的所有工具
AVAILABLE_TOOLS = {
"calculator": safe_calculator
}
# ====================== 3. ReAct 核心 Prompt 模板 ======================
REACT_SYSTEM_PROMPT = """
你是一个严格遵循 ReAct 框架的智能数学助手,你的任务是解决用户的数学问题。
## 核心规则(必须严格遵守,不能改变格式)
1. 每一轮你只能输出 **Thought** 或 **Action** 中的一种,不能同时输出
2. **Thought(思考)**:
- 分析当前问题,判断是否需要调用计算器
- 如果需要计算,明确说明要计算什么表达式
- 如果不需要计算,直接给出答案
3. **Action(行动)**:
- 只有当你需要计算时才输出 Action
- 格式必须严格为:Action: calculator("数学表达式")
- 数学表达式里不要有中文,只保留数字、运算符、括号
4. **Observation(观察)**:
- 这是计算器返回的结果,你不需要生成
- 你需要根据 Observation 的结果继续思考
5. **Final Answer(最终答案)**:
- 当你获取了足够的信息,能完整回答用户的问题时,输出:Final Answer: 你的完整答案
- 最终答案要清晰、完整,包含计算过程和结果
## 可用工具
- calculator(expression):安全计算器,输入数学表达式,返回计算结果
## 示例
用户问题:35乘以24加上18等于多少?
你的执行过程:
Thought:用户的问题需要计算 35*24+18,我需要调用计算器工具。
Action: calculator("35*24+18")
Observation:计算结果:858
Thought:我已经得到了计算结果,现在可以给出最终答案了。
Final Answer: 35乘以24加上18的计算过程是:35×24=840,840+18=858,最终结果是858。
现在开始解决用户的问题:
"""
# ====================== 4. ReAct 核心执行循环 ======================
def react_math_agent(question: str, max_rounds: int = 5) -> str:
"""
ReAct 数学 Agent 主函数
:param question: 用户的数学问题
:param max_rounds: 最大执行轮数,防止无限循环
:return: 最终答案
"""
# 初始化上下文记忆
messages = [
{"role": "system", "content": REACT_SYSTEM_PROMPT},
{"role": "user", "content": question}
]
print(f"🧑 用户问题:{question}\n")
print("🤖 ReAct Agent 开始执行...\n")
# 循环执行 ReAct 流程
for round_num in range(1, max_rounds + 1):
print(f"===== 第 {round_num} 轮执行 =====")
# 1. 调用 LLM 生成 Thought/Action
response = client.chat.completions.create(
model="doubao-lite-32k", # 推荐用 lite 模型,速度快、效果好
messages=messages,
temperature=0.3, # 数学题用低温度,保证严谨
max_tokens=500
)
llm_output = response.choices[0].message.content.strip()
print(f"🤖 LLM 输出:\n{llm_output}\n")
# 2. 把 LLM 输出加入上下文记忆
messages.append({"role": "assistant", "content": llm_output})
# 3. 判断是否已经输出最终答案
if "Final Answer:" in llm_output:
final_answer = llm_output.split("Final Answer:")[-1].strip()
print("===== 🎉 执行完成,最终答案 =====")
return final_answer
# 4. 解析 Action,调用工具
if "Action:" in llm_output:
# 提取工具名和参数
try:
action_part = llm_output.split("Action:")[-1].strip()
# 匹配 calculator("表达式") 格式
tool_match = re.match(r'(\w+)\("([^"]+)"\)', action_part)
if not tool_match:
raise ValueError("Action 格式错误,正确格式是:Action: calculator(\"表达式\")")
tool_name = tool_match.group(1)
tool_param = tool_match.group(2)
# 检查工具是否可用
if tool_name not in AVAILABLE_TOOLS:
raise ValueError(f"没有找到工具:{tool_name},可用工具:{list(AVAILABLE_TOOLS.keys())}")
# 调用工具,获取 Observation
print(f"🔧 调用工具:{tool_name},参数:{tool_param}")
observation = AVAILABLE_TOOLS[tool_name](tool_param)
observation_str = f"Observation: {observation}"
print(f"📊 工具返回:{observation_str}\n")
# 把 Observation 加入上下文记忆
messages.append({"role": "user", "content": observation_str})
except Exception as e:
error_msg = f"Observation: 执行错误:{str(e)}"
print(f"❌ {error_msg}\n")
messages.append({"role": "user", "content": error_msg})
# 超过最大轮数,结束执行
return "⚠️ 执行轮数已达上限,未能完成任务,请尝试简化问题。"
# ====================== 5. 测试运行 ======================
if __name__ == "__main__":
# 测试问题1:简单混合运算
test_question1 = "35乘以24加上18等于多少?"
# 测试问题2:简单应用题
test_question2 = "一个长方形的长是12.5厘米,宽是8.4厘米,它的面积是多少平方厘米?"
# 测试问题3:多步计算
test_question3 = "小明有100元钱,买了3本笔记本,每本12.8元,又买了2支笔,每支8.5元,他还剩多少钱?"
# 运行测试
result = react_math_agent(test_question3)
print(result)
执行结果示例:
🧑 用户问题:小明有100元钱,买了3本笔记本,每本12.8元,又买了2支笔,每支8.5元,他还剩多少钱?
🤖 ReAct Agent 开始执行...
======第1轮执行======
🤖LLM输出:
Thought:首先,需要计算小明买笔记本和笔总共花费了多少钱。笔记本的总费用是3*12.8元,笔的总费用是2*8.5元。然后从100元中减去这个总数得到剩余的钱数。我将先计算总支出。
Action: calculator("3*12.8+2*8.5")
🔧调用工具:calculator, 参数: 3*12.8+2*8.5
📊 工具返回:observation=计算结果:55.400000000000006
======第2轮执行======
🤖LLM输出:
Thought:根据计算器的结果,小明购买笔记本和笔总共花费了55.4元。接下来,我将从他原有的100元中减去这个数,以确定他还剩下多少钱。
Action: calculator("100-55.4")
🔧调用工具:calculator, 参数: 100-55.4
📊 工具返回:observation=计算结果:44.6
======第3轮执行======
🤖LLM输出:
Thought:根据计算器的结果,小明在购买了笔记本和笔之后还剩下44.6元。现在我可以给出最终答案了。
Final Answer: 小明有100元钱,买了3本笔记本(每本12.8元)和2支笔(每支8.5元)。笔记本的总费用是\(3 \times 12.8 = 38.4\)元,笔的总费用是\(2 \times 8.5 = 17\)元,总共花费\(38.4 + 17 = 55.4\)元。因此,他还剩下\(100 - 55.4 = 44.6\)元。所以,小明最后还剩44.6元。
🎉 执行完成,最终答案
小明有100元钱,买了3本笔记本(每本12.8元)和2支笔(每支8.5元)。笔记本的总费用是\(3 \times 12.8 = 38.4\)元,笔的总费用是\(2 \times 8.5 = 17\)元,总共花费\(38.4 + 17 = 55.4\)元。因此,他还剩下\(100 - 55.4 = 44.6\)元。所以,小明最后还剩44.6元。
三、代码结构详解
1. 核心模块说明
表格
| 模块 | 作用 |
|---|---|
safe_calculator |
安全的计算器工具,用正则限制输入,防止代码注入 |
REACT_SYSTEM_PROMPT |
ReAct 核心 Prompt,严格定义了执行格式、规则、示例 |
react_math_agent |
ReAct 主循环,负责调用 LLM、解析输出、调用工具、管理上下文 |
2. 关键安全设计
- 正则限制输入:只允许
[\d+\-*/().\s],禁止其他字符 - 限制
eval命名空间:eval(expression, {"__builtins__": None}, {}),防止访问内置函数 - 异常处理:捕获除零、语法错误等常见问题,返回友好提示
四、运行效果演示
以测试问题 3 为例:
小明有 100 元钱,买了 3 本笔记本,每本 12.8 元,又买了 2 支笔,每支 8.5 元,他还剩多少钱?
执行过程:
五、如何扩展这个 Agent
你可以基于这个代码,轻松添加更多功能:
- 添加更多工具:比如搜索工具、文件读写工具、数据库工具,只需要在
AVAILABLE_TOOLS字典里添加新函数 - 优化 Prompt:根据你的需求,调整
REACT_SYSTEM_PROMPT,比如添加更多示例、更严格的规则 - 增加记忆功能:用向量数据库存储历史任务经验,实现跨会话的记忆复用
- 增加反思机制:执行失败后,让 LLM 自动反思错误原因,修正计划后重新执行
更多推荐


所有评论(0)