返回 AI大模型从0到1——理论与实操

C3-04 零框架理解 Agent:ReAct 手撕


🎯 本章目标:不依赖任何框架,用不到 150 行 Python 代码,从零实现一个能思考、能行动、能观察的 Agent。理解 Agent 的"灵魂"——Thought → Action → Observation 循环。

ReAct 是什么?

ReAct = Reasoning(推理) + Acting(行动)

这是 2022 年 Google 提出的 Agent 范式。核心思想:让 LLM 在思考和行动之间循环,直到完成任务。

flowchart LR Q[用户问题] --> T1[Thought 思考] T1 --> A[Action 调用工具] A --> O[Observation 观察结果] O --> T2{任务完成?} T2 -->|否| T1 T2 -->|是| F[Final Answer 最终答案] style T1 fill:#FFE4B5 style A fill:#87CEEB style O fill:#90EE90 style F fill:#FFB6C1

关键角色:

  • Thought:LLM 推理"我下一步该做什么"
  • Action:执行工具调用(如计算、搜索)
  • Observation:获取工具返回结果
  • Final Answer:推理出最终答案,结束循环

准备工作:定义工具

Agent 的"手脚"就是工具。每个工具都是一个普通 Python 函数,需要注册到工具字典中供 LLM 调用。

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def calculator(expression: str) -> str:
    """计算数学表达式"""
    try:
        allowed = set("0123456789+-*/().% ")
        if all(c in allowed for c in expression):
            return str(eval(expression))
        return "不支持的表达式"
    except Exception as e:
        return f"计算错误: {e}"


def search(query: str) -> str:
    """模拟搜索引擎"""
    knowledge = {
        "python": "Python 是一门解释型高级编程语言,1991 年由 Guido 创建",
        "transformer": "Transformer 是 2017 年 Google 提出的注意力机制架构",
        "rag": "RAG(检索增强生成)是 2020 年 Facebook 提出的技术",
        "agent": "AI Agent 是能自主感知、规划、行动的智能体系统",
    }
    for key, value in knowledge.items():
        if key in query.lower():
            return value
    return f"未找到 '{query}' 的相关信息"


def get_current_time() -> str:
    """获取当前时间"""
    from datetime import datetime
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


# 工具注册表
TOOLS = {
    "calculator": {"func": calculator, "desc": "计算数学表达式,参数: expression"},
    "search": {"func": search, "desc": "搜索信息,参数: query"},
    "get_time": {"func": get_current_time, "desc": "获取当前时间,无参数"},
}

💡 类比:工具就像 Agent 的"瑞士军刀"。LLM 不会算数,但有了 calculator 工具,它就"会"了。工具的本质是 把外部能力封装成函数

ReAct Prompt 模板

Prompt 是 Agent 的"灵魂",它告诉 LLM:按什么格式思考、按什么格式行动、什么时候停止

Plain Text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
你是一个智能AI助手,可以使用以下工具来解决问题:

{tools_desc}

请严格按照以下格式思考和行动:

Thought: 我需要思考如何回答这个问题
Action: 工具名(参数)
Observation: [工具返回结果,由系统填入]
... (可以重复 Thought/Action/Observation 多次)
Thought: 我现在知道答案了
Final Answer: 最终回答

重要规则:
1. 每次只调用一个工具
2. Action 格式必须是: 工具名(参数)
3. 如果不需要工具,直接给出 Final Answer
4. 最多执行 5 轮工具调用

开始!

问题: {question}

💡 Prompt 设计的三个关键点:

  1. 明确格式:Thought/Action/Observation/Final Answer 四种前缀必须对齐
  1. 停止词:调用 LLM 时设置 stop=["Observation:"],让 LLM 在需要观察结果时暂停
  1. 工具描述:{tools_desc} 必须清晰准确,LLM 靠它决定调哪个工具

完整可运行代码

这是整个课程最核心的代码。请逐行阅读并亲手运行。

Plain Text
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import re
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

REACT_PROMPT = """你是一个智能AI助手,可以使用以下工具来解决问题:

{tools_desc}

请严格按照以下格式思考和行动:

Thought: 我需要思考如何回答这个问题
Action: 工具名(参数)
Observation: [工具返回结果,由系统填入]
... (可以重复 Thought/Action/Observation 多次)
Thought: 我现在知道答案了
Final Answer: 最终回答

重要规则:
1. 每次只调用一个工具
2. Action 格式必须是: 工具名(参数)
3. 如果不需要工具,直接给出 Final Answer
4. 最多执行 5 轮工具调用

开始!

问题: {question}
"""


class ReActAgent:
    """手撕 ReAct Agent - 零框架实现"""

    def __init__(self, tools: dict, model: str = None, max_steps: int = 5):
        self.tools = tools
        self.model = model or os.getenv("MODEL_NAME", "gpt-4o-mini")
        self.max_steps = max_steps
        self.client = OpenAI(
            api_key=os.getenv("OPENAI_API_KEY"),
            base_url=os.getenv("OPENAI_BASE_URL"),
        )

    def _build_tools_desc(self) -> str:
        lines = [f"- {name}: {info['desc']}" for name, info in self.tools.items()]
        return "\n".join(lines)

    def _parse_action(self, text: str):
        """从 LLM 输出中解析 Action(正则匹配)"""
        match = re.search(r"Action:\s*(\w+)\((.*?)\)", text)
        if match:
            return match.group(1), match.group(2).strip().strip('"').strip("'")
        return None, None

    def _check_final_answer(self, text: str):
        match = re.search(r"Final Answer:\s*(.+)", text, re.DOTALL)
        return match.group(1).strip() if match else None

    def _call_llm(self, prompt: str) -> str:
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            stop=["Observation:"],  # 关键:让 LLM 在需要观察结果时暂停
        )
        return response.choices[0].message.content

    def run(self, question: str) -> str:
        prompt = REACT_PROMPT.format(
            tools_desc=self._build_tools_desc(),
            question=question,
        )
        conversation = prompt

        for step in range(self.max_steps):
            llm_output = self._call_llm(conversation)
            print(f"\n--- 步骤 {step + 1} ---")
            print(llm_output.strip())

            final = self._check_final_answer(llm_output)
            if final:
                return final

            tool_name, tool_arg = self._parse_action(llm_output)
            if tool_name and tool_name in self.tools:
                result = (
                    self.tools[tool_name]["func"](tool_arg)
                    if tool_arg
                    else self.tools[tool_name]["func"]()
                )
                print(f"Observation: {result}")
                conversation += llm_output + f"\nObservation: {result}\n"
            else:
                conversation += (
                    llm_output + "\nObservation: 工具调用失败,请按格式重新输出\n"
                )

        return "达到最大步骤数,未能得出答案"


if __name__ == "__main__":
    from agent_course_demo import TOOLS  # 实际使用时直接 import 上一节的工具
    agent = ReActAgent(tools=TOOLS)

    questions = [
        "计算 (15 + 27) * 3 的结果",
        "什么是 AI Agent?",
        "北京有多少人口?用计算器算出如果每人每天消耗 2 升水,北京一天需要多少升水?",
    ]

    for q in questions:
        try:
            agent.run(q)
        except Exception as e:
            print(f"错误: {e}")
            break

运行结果

运行 python 01_react_agent.py 后,你会看到类似这样的输出:

Plain Text
1
2
3
4
5
6
7
--- 步骤 1 ---
Thought: 用户要求计算 (15+27)*3 的结果,这是个数学表达式。
Action: calculator((15 + 27) * 3)
Observation: 126
--- 步骤 2 ---
Thought: 我已经计算出结果 126,可以给出最终答案了。
Final Answer: (15 + 27) * 3 的结果是 126

看到了吗?这就是 Agent 在"思考"和"行动"之间的循环。LLM 不是一步给出答案,而是 先思考 → 再行动 → 观察结果 → 再思考。这种循环让 LLM 能处理需要多步推理的复杂任务。

关键设计要点

设计点 为什么这样做
正则解析 Action LLM 输出不一定是标准 JSON,正则容错性更强
stop=["Observation:"] 让 LLM 在需要观察结果时停下来,避免它"自说自话"生成 Observation
字符串拼接对话 ReAct 需要完整历史,LSTM 风格的 messages 列表反而更复杂
最大步数限制 防止死循环(LLM 没完没了地调工具),默认 5 步
错误回填 工具调用失败时,把错误作为 Observation 喂回,让 LLM 自我纠正

手撕版的局限性

手撕 ReAct Agent 帮我们理解原理,但生产环境远远不够:

  • ❌ 字符串拼接易错,Prompt 一改就崩
  • ❌ 无状态管理,长任务容易"失忆"
  • ❌ 难以处理复杂工作流(分支、循环、并行)
  • ❌ 无持久化,程序重启就丢失上下文
  • ❌ 工具调用格式不标准,不同模型行为不一致

👉 这就是为什么需要 Function Calling 和 LangGraph等开发框架