完全不用 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

你可以基于这个代码,轻松添加更多功能:

  1. 添加更多工具:比如搜索工具、文件读写工具、数据库工具,只需要在 AVAILABLE_TOOLS 字典里添加新函数
  2. 优化 Prompt:根据你的需求,调整 REACT_SYSTEM_PROMPT,比如添加更多示例、更严格的规则
  3. 增加记忆功能:用向量数据库存储历史任务经验,实现跨会话的记忆复用
  4. 增加反思机制:执行失败后,让 LLM 自动反思错误原因,修正计划后重新执行
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐