从零构建ReAct智能体:让AI学会边想边做

前言

大语言模型已经能写诗、编程、做数学题,但它有一个根本性的短板:无法与外部世界交互。你问它"今天深圳天气怎么样",它只能诚恳地告诉你"我的知识截止到某某日期"------不是它不想回答,是它根本没有"看天气"这个动作能力。

这就是智能体(Agent)要解决的核心问题:让 LLM 不仅能 ,还能

业界现有的智能体框架(LangChain、LlamaIndex 等)已经将这套机制封装得非常完善,但它们的高度抽象也让很多人停留在"调包侠"阶段,对背后的运行机制一知半解。本文的目标,是带你从零构建一个 ReAct 智能体------从设计理念到代码实现,从提示词模板到工具管理,一步步拆解清楚。

读完你会发现,ReAct 的核心理念出奇地简洁,而那份让你真正理解和掌控它的"源代码级自信",只有亲手写过一遍才能获得。


一、智能体的"最后一公里":从思考到行动

1.1 两种极端,各缺一半

在 ReAct 提出之前,让 LLM 解决复杂问题的主流思路大致可以分为两派。

第一派:纯思考型。 以思维链(Chain-of-Thought, CoT)为代表------引导模型在输出最终答案前,先"自言自语"地把推理过程写出来。这种做法显著提升了模型的推理能力,但它有一个致命缺陷:模型完全活在自己的世界里。它无法查阅外部信息,所有推理都基于训练时记住的知识。当遇到它不知道的事实(比如实时股价、今天的新闻),模型就会"幻觉"一个答案------它并非故意说谎,而是它唯一能做的事就是根据概率生成文本。

第二派:纯行动型。 模型直接输出要执行的动作(调用搜索 API、执行计算、操作数据库),然后拿结果说话。这种做法让模型能够触及外部世界,但缺少了一个关键环节------规划。它不知道什么时候该搜索、搜什么关键词、搜到结果之后该如何修正方向。就像一个只会执行命令但没有判断力的助手。

1.2 ReAct 的洞见:思考与行动是硬币的两面

ReAct(Reasoning + Acting)的命名已经揭示了它的核心主张:思考指导行动,行动反过来修正思考。二者不是先后关系,而是不断交替、相互强化的关系。

当你面对一个陌生问题时------比如"帮我查一下英伟达最新的 GPU 型号,并和上一代做对比"------你不会一次性地想清楚所有步骤再行动。你更可能的做法是:先搜"英伟达最新 GPU",看看返回了什么;如果信息不够,再根据结果调整搜索词;搜到足够信息后,整理成对比结果。在每一步之间,你的思考都在被外部反馈持续修正

ReAct 就是把这种人类解决问题的自然模式,通过提示工程"移植"到了 LLM 身上。


二、ReAct 的核心循环:Think → Act → Observe

2.1 三段式节奏

ReAct 将智能体的每一步决策拆解为三个标准化的阶段:

阶段 英文 含义 示例
思考 Thought 智能体的"内心独白",分析当前状态、拆解子任务、反思上一步结果 "用户想了解英伟达最新GPU,我需要先搜索产品发布信息"
行动 Action 智能体决定执行的具体操作,通常是调用一个外部工具 Search['英伟达最新GPU型号 2025']
观察 Observation 执行 Action 后从外部环境获得的反馈 "NVIDIA GeForce RTX 5090 于 2025 年 1 月发布..."

这三个阶段形成一个闭环:Thought 决定 Action,Action 产生 Observation,Observation 又成为下一个 Thought 的输入。历史记录不断累积,智能体的"视野"也随之扩大,直到它在某个 Thought 中判断"我已经有足够信息回答用户了",然后输出最终结果。

2.2 为什么这个循环如此有效?

关键在于外部反馈的注入。一个纯 CoT 模型可能推理了十步,但这十步都基于同一个可能错误的前提------因为没有外部验证。而 ReAct 每执行一步 Action,就会得到一个来自真实世界的 Observation。这个 Observation 可以:

  • 纠正错误前提:如果智能体假设了一个错误的搜索关键词,Observation 的失败会促使它换一种方式提问
  • 提供新线索:搜索结果可能揭示用户问题中隐含的歧义,智能体可以据此进一步追问或细化搜索
  • 积累事实证据:每一步获取的真实信息逐步替换模型内部的"猜测",最终答案建立在可追溯的事实基础上

这种机制使得 ReAct 特别适用于三类场景:

  1. 需要外部知识的任务:查询实时信息(天气、新闻、股价)、搜索专业领域知识
  2. 需要精确计算的任务:将数学问题交给计算器工具,避免 LLM 的推理计算误差
  3. 需要与 API 交互的任务:操作数据库、调用业务系统的 API 完成特定功能

三、基础设施:给智能体装上"大脑"

在开始写 ReAct 逻辑之前,我们需要一个能与 LLM 交互的客户端。这里不是简单地调用 OpenAI API------我们需要一个统一的抽象层,让后续的 ReActAgent 不需要关心底层是哪个模型、哪个服务商、流式还是非流式。

3.1 环境配置

项目使用 Python 3.10+,核心依赖只有两个:

复制代码
pip install openai python-dotenv

API 密钥通过 .env 文件管理:

ini 复制代码
LLM_MODEL_ID=Qwen/Qwen3.5-35B-A3B
LLM_API_KEY=your-api-key
LLM_BASE_URL=https://api-inference.modelscope.cn/v1/

3.2 AgentLLM:一个极简的 LLM 客户端

ini 复制代码
class AgentLLM:
    def __init__(self, model=None, apiKey=None, baseUrl=None, timeout=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))

        self.client = OpenAI(api_key=apiKey, base_url=baseUrl, timeout=timeout)

    def think(self, messages: list[dict], temperature: float = 0) -> str:
        """调用 LLM,流式输出并返回完整回复"""
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=temperature,
            stream=True
        )
        collected = []
        for chunk in response:
            content = chunk.choices[0].delta.content or ""
            if content:
                print(content, end="", flush=True)
                collected.append(content)
        return "".join(collected)

两个设计决策值得说明:

  • 流式输出 (stream=True) :对于 ReAct 这种多步推理场景,实时看到模型的思考过程比等待几十秒后一次性出现结果更有利于调试和建立信任感。你能看到它"在想什么",而不是面对一个黑箱。
  • temperature=0 作为默认值 :在多步推理中,我们更需要确定性而非创造性。每一步的输出会被解析为结构化的指令,temperature 过高会导致格式不稳定,正则解析容易失败。

四、打造"手脚":工具的定义与管理

如果说 LLM 是智能体的大脑 ,那么工具(Tools)就是它与外部世界交互的手和脚。工具系统的设计质量,直接决定了智能体的能力上限。

4.1 工具的三要素

一个良好定义的工具应具备三个核心属性:

要素 说明 重要性
名称 (Name) 简洁唯一的标识符,供智能体在 Action 中引用,如 Search 中等
描述 (Description) 自然语言说明工具的用途和使用场景 极高
执行逻辑 (Func) 真正执行任务的函数,接收输入、返回结果

这里有一个容易忽视的关键点:Description 是整个工具系统中最关键的部分,因为 LLM 正是依赖这段自然语言描述来判断"什么时候该用哪个工具"。如果你的搜索工具描述写的是"一个搜索工具",模型可能不知道何时触发它;但如果你写的是"当你需要查询实时信息、事实或训练数据中不存在的知识时使用此工具",模型的决策准确率会显著提升。

换句话说,你写工具描述时不是在写给代码看,而是在写给 LLM 看------这是提示工程的一部分。

4.2 实现搜索工具

我们选择 SerpApi 作为搜索引擎------它通过 API 返回结构化的 Google 搜索结果,能直接提取答案摘要框、知识图谱等信息,比原始 HTML 解析更可靠。

python 复制代码
def search(query: str) -> str:
    """基于 SerpApi 的网页搜索引擎"""
    params = {
        "engine": "google",
        "q": query,
        "api_key": os.getenv("SEARCH_API_KEY"),
    }
    client = serpapi.Client(api_key=params["api_key"])
    results = client.search(params)

    # 智能解析:按优先级回退
    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:
        snippets = [
            f"[{i+1}] {r.get('title', '')}\n{r.get('snippet', '')}"
            for i, r in enumerate(results["organic_results"][:3])
        ]
        return "\n\n".join(snippets)                    # 兜底:前3条自然结果
    return f"未找到关于 '{query}' 的信息。"

这个函数的智能解析策略值得留意:它不是一个"把 Google 的原始 HTML 抛给 LLM"的敷衍方案,而是在工具层面就对信息做了质量筛选------优先返回答案框(高置信度的直接答案),其次知识图谱,最次才用自然搜索结果。这层预处理极大提升了后续 LLM 推理的效率和准确率。信息质量的上游优化,比下游提示词调优更有效。

4.3 工具管理器:注册与调度

当智能体需要同时使用搜索、计算器、数据库查询等多个工具时,我们需要一个统一的注册和调度中心:

python 复制代码
class ToolExecutor:
    def __init__(self):
        self.tools: dict[str, dict] = {}

    def registerTool(self, name: str, description: str, func: Callable):
        self.tools[name] = {"description": description, "func": func}

    def getTool(self, name: str) -> Callable:
        return self.tools.get(name, {}).get("func")

    def getAvailableTools(self) -> str:
        return "\n".join([
            f"- {name}: {info.get('description', '')}"
            for name, info in self.tools.items()
        ])

getAvailableTools() 是连接工具层和 LLM 的桥梁------它的输出会被直接嵌入提示词模板的 {tools} 占位符中,成为 LLM 决定调用哪个工具的唯一信息来源。


五、组装 ReActAgent:提示词、循环与解析

有了大脑(AgentLLM)和手脚(ToolExecutor),现在进入最关键的部分:将二者组合成一个完整的 ReAct 智能体。

5.1 提示词:整个机制的"宪法"

提示词是 ReAct 的灵魂。它不只告诉 LLM "你是谁",还定义了一套强制性的交互协议------LLM 必须按照约定格式输出,否则后续的代码解析会失败。

ini 复制代码
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{tool_name}[{tool_input}]`: 调用一个可用工具。
- `Finish[最终答案]`: 当你认为已经获得最终答案时。

现在,请开始解决以下问题:
Question: {question}
History: {history}
"""

这个模板看似简单,但每一部分都在承担结构性职能:

  • 角色定义:"有能力调用外部工具"------这句话激活了 LLM 的"工具使用模式"
  • 工具清单 {tools} :让 LLM 知道它有哪些可用的能力,这是它做决策的信息基础
  • 格式规约 Thought/Action:这是最关键的设计------它强制 LLM 的输出具有可解析的结构。每一个 Thought 和 Action 都是确定的字符串模式,让代码能精确提取
  • 动态上下文 {question}/{history} :每一轮历史都被完整保留并注入,形成不断增长的推理链

5.2 核心循环:永不停歇的 Think-Act-Observe

ReActAgent 的 run 方法是整个智能体的心脏:

ini 复制代码
class ReActAgent:
    def __init__(self, llm_client, tool_executor, max_steps=5):
        self.llm_client = llm_client
        self.tool_executor = tool_executor
        self.max_steps = max_steps
        self.history = []

    def run(self, question: str):
        self.history = []
        current_step = 0

        while current_step < self.max_steps:
            current_step += 1

            # 1. 格式化提示词 ------ 把完整上下文喂给 LLM
            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. 调用 LLM 思考
            response = self.llm_client.think([{"role": "user", "content": prompt}])

            # 3. 解析输出
            thought, action = self._parse_output(response)

            # 4. 判断终止条件
            if action.startswith("Finish"):
                return self._extract_final_answer(action)

            # 5. 执行工具调用
            tool_name, tool_input = self._parse_action(action)
            tool_func = self.tool_executor.getTool(tool_name)
            observation = tool_func(tool_input)

            # 6. 记录历史,进入下一轮
            self.history.append(f"Action: {action}")
            self.history.append(f"Observation: {observation}")

        return None  # 超出最大步数

这里有一个关键的架构选择值得展开:每一步都重新格式化完整提示词

为什么不维持一个对话 session 而是每次从头构建?因为 ReAct 的每一步都需要 LLM 看到"截至目前为止发生的所有事"------工具列表、用户原始问题、每一轮 Thought-Action-Observation 的历史。构建完整 Prompt 是确保 LLM 拥有全部上下文的最可靠方式。

max_steps=5 则是防止无限循环的安全阀------在实际使用中,你可以根据任务复杂度调整这个值。

5.3 输出解析:正则的艺术

LLM 返回的文本是自然语言,而我们的代码需要从中提取结构化的指令。这里用正则表达式来完成:

python 复制代码
def _parse_output(self, text: str):
    """提取 Thought 和 Action"""
    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

_parse_output 负责拆分 LLM 输出的两个核心部分,_parse_action 负责从 Search['英伟达最新 GPU'] 这样的字符串中提取出工具名 Search 和输入参数 英伟达最新 GPU。正则虽然看起来笨拙,但在这种强格式约束的场景下反而是最可靠的选择------比任何"智能解析"都更可预测。


六、客观评价:ReAct 的优势与暗面

任何技术方案都是取舍的结果。在亲手实现过 ReAct 之后,你会对它的优点和局限有更切身的体会。

6.1 三大优势

高可解释性 。这是 ReAct 最大的杀手锏。通过 Thought 链,你可以逐字逐句地看到智能体的"心路历程"------它为什么选择这个工具、下一步打算做什么、上一步的结果如何影响它的判断。对于调试、审计和建立用户信任,这种透明度无可替代。

动态规划与纠错。ReAct 是"走一步看一步"的,而非一次性生成完整计划。如果上一步搜索结果不理想,智能体可以在下一步修正搜索词;如果发现走错了方向,它可以根据 Observation 调头。这种灵活性让它对真实世界的噪音和不确定性有更好的适应力。

工具协同。LLM 负责"运筹帷幄"(规划和推理),工具负责"解决具体问题"(搜索、计算、API 调用)。二者的能力边界清晰互补,突破了单一 LLM 在知识时效性、计算准确性方面的固有局限。

6.2 四点局限

对 LLM 能力的强依赖。ReAct 是"强模型依赖"的------如果底层 LLM 的逻辑推理能力、指令遵循能力或格式化输出能力不足,整个流程随时可能中断。在能力较弱的小模型上,Thought 可能逻辑不通,Action 格式可能不规则。

执行效率问题。完成一个任务通常需要多次 LLM 调用,每次调用都伴随着网络延迟和计算成本。对于需要很多步骤的复杂任务,这种串行的"思考-行动"循环可能导致较高的总耗时和费用。

提示词的脆弱性。整个机制的稳定运行建立在一个精心设计的提示词模板之上。模板中任何微小变动都可能影响 LLM 的行为。而且并非所有模型都能持续稳定地遵循预设格式------你在 GPT-4 上调好的 Prompt,换到另一个模型上可能完全不工作。

可能陷入局部最优。步进式决策意味着智能体缺乏全局的、长远的规划。它可能因为眼前的 Observation 而选择一个看似正确但长远来看并非最优的路径,甚至在某些情况下陷入"原地打转"的循环------不断搜索、不满意、换个词再搜、再不满意......

6.3 调试之道

当你构建的 ReAct 智能体出现问题时,以下是四个最有效的排查手段:

  1. 打印完整的格式化提示词。在此之前,你不知道 LLM 到底"看到了什么"。把所有历史记录渲染进 Prompt 后整体检查,是追溯决策源头的最直接方式。
  2. 分析 LLM 的原始输出 。当正则解析失败时,先把 LLM 返回的原始文本打出来。问题可能是 LLM 没有遵循格式(比如写成了 Action: 后面跟了多行),也可能是你的正则写得太死。
  3. 在提示词中加入 Few-shot 示例。如果模型频繁输出不规则格式,在 Prompt 中加一两个完整的 "Thought-Action-Observation" 成功案例。示例是最好的格式老师。
  4. 调整模型或参数。换一个指令遵循能力更强的模型,或者将 temperature 设为 0 以保证输出确定性。temperature 的微小差异在多步推理场景下会逐轮放大。

总结与延伸

ReAct 的价值不在于它有多"先进"------事实上它是最基础的智能体范式之一------而在于它用极其简洁的机制解决了 LLM 从"思考"到"行动"的跃迁问题。三个概念(Thought、Action、Observation)、一个循环、一个提示词模板,就构成了一个能与外部世界交互的智能体。

但 ReAct 也开启了更多问题,这些问题指向了后续更高级的范式:

  • ReAct 的"走一步看一步"在复杂任务中效率太低怎么办?Plan-and-Solve:先制定完整计划,再按计划执行------"三思而后行"
  • ReAct 缺乏自我纠错怎么办?Reflection:让智能体对自己的输出进行自我批判和迭代优化
  • 一个工具不够用怎么办? → 工具组合与自动选择:根据任务动态编排多个工具的调用序列
  • 单次对话有状态限制怎么办? → 记忆模块:跨对话保持上下文,让智能体"记住"之前的交互

从使用者到创造者的转变,始于对最基础范式的深刻理解。当你亲手写过 ReActAgent 的那几百行代码之后,再去学习 LangChain 或 LlamaIndex,你不会觉得那些抽象层不可思议------你会在心里默默点头:"原来如此,底层就是这么工作的。"

而这,正是从零构建的意义。

相关推荐
葫芦和十三9 小时前
图解 MongoDB 23|两地三中心:跨可用区部署怎么扛机房故障
后端·mongodb·agent
Hyyy12 小时前
SSE和WebSocket 是什么,AI 场景下如何选择
llm
冬奇Lab12 小时前
Workflow 系列(04):Multi-Agent 协调——编排器边界、并发控制与上下文隔离
人工智能·工作流引擎
冬奇Lab12 小时前
每日一个开源项目(第147篇):HyperGraphRAG - 用超图表示 N 元关系,RAG 的第三代范式
人工智能·开源·graphql
甲维斯12 小时前
Github + 阿里云oss实现类似codex的自动更新!
人工智能
阿里云大数据AI技术14 小时前
光轮智能 × 阿里云:共建 Physical AI 云上数据、评测与持续学习基础设施
人工智能·机器学习
机器之心14 小时前
实锤了:Claude Code偷查用户,时区、中国AI实验室全是关键词
人工智能·openai