手写一个最小 ReAct Agent:彻底搞懂 AI Agent 是怎么"想"和"做"的

适合人群:已跑通基础 Agent Demo、想深入理解原理的开发者


📌 引言:框架用得很熟,但你知道 Agent 内部在干嘛吗?

上篇文章中,我们用 Function Calling 十几行代码跑通了第一个 Agent。很多同学会问:“它到底是怎么决定先调哪个工具、什么时候停下来的?”

这篇文章我们不依赖任何 Agent 框架,用最原始的提示词 + 循环解析,手写一个最小可用的 ReAct Agent。写完之后你会彻底明白:所谓"智能体",本质就是一个"想 → 做 → 看结果 → 再想"的循环。


一、ReAct 是什么?

ReAct = Re(asoning 推理)+ Act(ing 行动),由 Shunyu Yao 等人在论文《ReAct: Synergizing Reasoning and Acting in Language Models》(Yao et al.) 中提出。

核心思想一句话:让模型在"思考"和"行动"之间交替进行,用行动结果支撑思考,用思考指导下一步行动。

  • 纯推理(Chain-of-Thought):模型自己一路想下去 → 遇到不知道的事实会一本正经地编
  • 纯行动(Act-only):模型不停调工具 → 没有方向,瞎试瞎撞
  • ReAct:思考指引行动,行动验证思考 → 又聪明又靠谱。

二、ReAct 的工作循环

用户问题 Question
      ↓
Thought    模型思考:我现在需要什么信息?
      ↓
Action     决定调用哪个工具(search / calculator …)
Action Input  给出工具入参
      ↓
Observation  拿到工具返回的结果
      ↓
(重复"思考 → 行动 → 观察",直到信息足够)
      ↓
Final Answer  输出最终答案

关键:每一次"行动"的输出都会成为下一轮"思考"的输入,模型在真实反馈中不断逼近答案——这就是 Agent "自主纠错"能力的来源。


三、手写实现:不依赖任何框架

下面我们用最原始的方式实现 ReAct:把格式规范写进 System Prompt,模型每轮输出 Thought / Action / Action Input,我们解析后执行工具、把结果回填进对话,循环直到模型输出 Final Answer

3.1 准备工作

pip install openai

并设置环境变量 OPENAI_API_KEY(Windows:set OPENAI_API_KEY=你的Key)。

3.2 完整代码

import json
import re
from openai import OpenAI

client = OpenAI()  # 自动读取 OPENAI_API_KEY

# ---------- 工具 1:模拟搜索引擎 ----------
def search(query: str) -> str:
    """模拟搜索(真实项目可换成 Tavily / SerpAPI 等)"""
    db = {
        "2024年奥运会在哪里举办": "2024 年奥运会在法国巴黎举办。",
        "太阳系最大的行星": "太阳系最大的行星是木星。",
    }
    return db.get(query, f"未找到与「{query}」相关的资料")

# ---------- 工具 2:计算器 ----------
def calculator(expr: str) -> str:
    """计算数学表达式(演示用,生产环境请勿直接使用 eval)"""
    return str(eval(expr))

TOOLS = {"search": search, "calculator": calculator}

# ---------- 系统提示词:规定 ReAct 输出格式 ----------
SYSTEM_PROMPT = """你是一个能自主思考并使用工具的 AI 助手。请严格按以下格式输出:

Thought: 你现在的思考
Action: 工具名(可选:search / calculator)
Action Input: JSON 格式的工具入参

重复"思考 -> 行动 -> 观察"的过程,直到信息足够,最后输出:
Final Answer: 最终答案

除了上述格式,不要输出任何多余内容。"""

# ---------- 解析模型的输出 ----------
def parse_response(text: str):
    # 1) 优先判断是否为最终答案
    if "Final Answer:" in text:
        return ("final", text.split("Final Answer:", 1)[1].strip())
    # 2) 提取 Action 与 Action Input
    action = re.search(r"Action:\s*(\w+)", text)
    action_input = re.search(r"Action Input:\s*(\{.*?\})", text, re.S)
    if action and action_input:
        try:
            args = json.loads(action_input.group(1))   # 解析 JSON 入参
        except json.JSONDecodeError:
            args = {}
        return ("action", action.group(1), args)
    # 3) 格式异常时兜底,当作最终回答返回
    return ("final", text)

# ---------- ReAct 主循环 ----------
def run_react(question: str, max_steps: int = 8) -> str:
    """循环:思考 -> 行动 -> 观察,直到模型输出 Final Answer"""
    history = f"Question: {question}\n"

    for step in range(max_steps):
        # 把"问题 + 到目前为止的思考/观察记录"一起发给模型
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": history},
            ],
        )
        output = resp.choices[0].message.content
        print(f"--- 第 {step + 1} 轮模型输出 ---\n{output}\n")

        kind, *rest = parse_response(output)
        if kind == "final":
            return rest[0]  # 任务完成

        # 执行工具调用,并把"观察结果"追加到历史
        tool_name, args = rest
        if tool_name not in TOOLS:
            history += f"Observation: 未知工具 {tool_name}\n"
            continue
        result = TOOLS[tool_name](**args)
        history += f"Observation: {result}\n"

    return "达到最大步数,任务未完成。"

if __name__ == "__main__":
    answer = run_react("2024年奥运会在哪里举办?顺便帮我算一下 365 * 24 等于多少")
    print(f"✅ 最终答案:{answer}")

3.3 运行效果(示意)

--- 第 1 轮模型输出 ---
Thought: 我需要先查询奥运会的举办地点,再计算乘法。
Action: search
Action Input: {"query": "2024年奥运会在哪里举办"}

--- 第 2 轮模型输出 ---
Thought: 已查到举办地是巴黎。现在计算 365 * 24。
Action: calculator
Action Input: {"expr": "365 * 24"}

--- 第 3 轮模型输出 ---
Thought: 两个问题都解决了,可以回答。
Final Answer: 2024 年奥运会在法国巴黎举办;365 * 24 = 8760。

✅ 最终答案:2024 年奥运会在法国巴黎举办;365 * 24 = 8760。

注意:我们没有写任何"先查后算"的流程,顺序完全是模型自己规划的——这就是自主性。


四、ReAct 的进阶变体:三大主流规划范式对比

ReAct 是地基,但工程上还会遇到三种更"聪明"的变体,它们解决的是 ReAct 的不同短板:

范式核心思想优点短板适用场景
ReAct边想边做,思考-行动交替灵活、自适应强步骤不可控、token 消耗多信息收集、开放探索类任务
Plan-and-Execute先出完整计划,再按序执行步骤稳定、可预测、省 token计划一旦出错,中途难调整数据流水线、批量处理
ReflexionReAct + 失败后"自我反思"再重试能吸取教训、越试越准多轮重试,成本最高代码修复、需要高准确率

怎么选?给三个判断标准:

  1. 任务能否提前预判步骤? 能 → Plan-and-Execute;不确定 → ReAct;
  2. 错误代价高不高? 高(如写代码、填单)→ Reflexion 的反思机制更值得;
  3. 预算是否敏感? 敏感 → 减少无谓的"想",优先 Plan-and-Execute。

💡 工程上常常混用:先用 Plan-and-Execute 出骨架,遇到分叉点再降级为 ReAct 模式——这正是 LangGraph 这类框架支持"图式编排"的意义。


五、这个实现的两个坑(实战中必踩)

5.1 坑一:文本解析不稳定 ✂️

手写 ReAct 靠"正则解析文本格式",而大模型输出格式偶尔会跑偏(少个冒号、多句废话)。解决办法:

  • Final Answer / Thought 的关键词做宽松匹配(我们已做兜底);
  • 升级方案:使用 Function Calling / Tool Calling(结构化输出,模型直接返回 JSON),这正是工具调用协议要解决的;
  • 更稳的方案:用 json_mode 或结构化输出(Structured Outputs)约束格式。

5.2 坑二:死循环与失控 🔄

模型可能反复调用同一工具、或一直不输出最终答案。三个防护手段缺一不可:

  1. 最大步数限制(我们已用 max_steps=8);
  2. 重复调用检测:连续 N 次相同 Action 就强制终止;
  3. 错误注入:工具报错也要作为 Observation 回传,让模型自己"反思换路"。

5.3 安全提醒 ⚠️

calculator 里的 eval() 存在代码注入风险,仅用于教学。生产环境请用 ast.literal_eval 或专门的计算库,并对工具调用做白名单 + 权限校验


六、动手实验:三个改造任务

只看不练等于白看。给你三个递增难度的改造任务,跑通一个算入门:

任务 1:加第三个工具(最简单)
仿照 search 加一个 get_time() 工具,返回当前时间,然后问 Agent"现在几点了?"(提示:用 datetime 模块获取当前时间)。

任务 2:补上重复调用检测(中等)
run_react 里记录上一步的工具名,连续 3 次相同就打断并输出"任务中止":

last_action, repeat_count = None, 0
# ...在循环内:
if tool_name == last_action:
    repeat_count += 1
    if repeat_count >= 3:
        return "检测到重复调用,任务中止。"
else:
    last_action, repeat_count = tool_name, 1

任务 3:接入真实搜索 API(进阶)
search 函数内部替换为 Tavily / SerpAPI 调用,体会"模拟工具换真实工具"只动一个函数的好处。

做完这三个任务,你对 ReAct 的理解就超过 90% 只会调框架的人。


七、从"手写"到"框架":框架帮你解决了什么?

手写一遍最大的价值是看清本质。但生产环境我们会用框架,因为它们把下面这些脏活都包了:

手写版需要自己做的框架替你做的(LangGraph / AutoGen 等)
正则解析输出格式结构化工具调用协议
手工维护 history内置状态管理(State)与记忆
自己写死循环防护内置循环检测 / 中断机制
无法可视化中间过程自带追踪、回放、调试面板
单线循环图式编排:条件分支、并行、多 Agent

💡 所以推荐学习路径:先手写一遍 → 再上框架,你才知道框架每一步在帮你省什么力。


✅ 总结

  • ReAct = 思考 ↔ 行动交替循环,用真实反馈支撑推理,是 Agent 自主性的核心范式;
  • 手写实现只需三件套:格式化的 System Prompt + 输出解析 + 循环执行工具
  • 进阶变体:Plan-and-Execute(先计划后执行)、Reflexion(反思重试),按任务特性选用;
  • 两大坑:输出格式不稳定(→ 用 Function Calling)和死循环(→ 步数上限 + 重复检测);
  • 框架的价值在于把解析、状态、防护、观测全部工程化,但理解内核才是关键。

📌 觉得有用就 点赞 + 收藏 + 关注,更多 AI Agent 干货持续输出。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐