1. 项目概述:当大语言模型学会“三思而后行”

最近在复现和深入研究一个让我眼前一亮的框架,叫做ReAct。这个名字很有意思,是“Reasoning”(推理)和“Acting”(行动)的合成词。简单来说,它教会了大语言模型(LLM)一种新的工作方式:不再是接到问题就一股脑地生成最终答案,而是像人类一样,先“想一想”(推理),再“动动手”(行动),并且在行动中获取新信息后,继续“想一想”,如此循环,直到解决问题。

这听起来是不是很像我们解决复杂问题的过程?比如,当朋友问你“帮我查一下最近上映的科幻电影里,哪一部在豆瓣上的评分最高?”你不会立刻报出一个电影名。你可能会先想:“我需要一个电影资讯网站或App。”然后你打开豆瓣,搜索“科幻电影”,浏览列表,发现信息太多,于是你进一步推理:“应该按上映时间筛选最近一个月的。”接着你执行筛选,查看结果,对每一部电影,你又会想:“我需要点进去看它的评分和评价人数。”最终,经过几轮“想”和“做”,你才能给出一个可靠的答案。ReAct要做的,就是让LLM具备这种“思行合一”的能力。

传统的LLM应用,无论是简单的问答还是思维链(Chain-of-Thought),本质上都是一种“纯推理”。模型基于已有的、冻结的训练知识进行内部计算并输出。这带来了两个核心问题:一是知识可能过时或不全(比如问今天的天气),二是缺乏与外部世界交互的能力(比如操作数据库、调用API)。而另一些研究让LLM学习调用工具(Tools),但往往又陷入了“盲目行动”的陷阱,模型可能不假思索地连续执行多个工具调用,缺乏对行动必要性和顺序的规划。

ReAct的巧妙之处在于,它通过一种结构化的提示(Prompt)工程,将推理和行动以交错(interleaved)的方式结合起来。模型在生成每一步时,不仅要决定做什么(行动),还要解释为什么这么做(推理)。这种设计极大地提升了任务执行的可靠性、可解释性和效率。它特别适合需要多步信息检索、决策或工具调用的复杂任务,比如知识密集型问答、交互式决策,甚至是操控软件或机器人。

接下来,我将拆解这个框架的设计精髓、实现细节,并分享在复现和应用过程中积累的一手经验和避坑指南。

2. 核心架构与设计哲学拆解

要理解ReAct,我们不能只把它看作一个简单的“提示词模板”,而应该深入其设计哲学。它的核心目标是解决LLM在开放域任务中“知识静态”和“行动盲目”的短板。

2.1 传统范式的局限:推理与行动的割裂

在ReAct之前,主要有两种范式:

  1. 纯推理(Reasoning-only) :以思维链(CoT)为代表。模型通过“让我们一步步思考…”等提示,生成一系列推理步骤,最终得出答案。它的优势是透明化了模型的“思考”过程,在数学、逻辑推理上表现卓越。但致命伤是,它的所有“知识”都来源于训练数据。对于动态信息(股票价格)、非公开信息(你的个人日历)或需要精确验证的事实(某篇论文的具体图表),纯推理无能为力,甚至会产生“一本正经地胡说八道”(幻觉)。
  2. 纯行动(Acting-only) :让LLM学习调用搜索引擎、计算器、数据库等工具。典型的框架如Toolformer或早期的API调用模型。模型输出直接是工具调用指令,如 Search(‘Quantum computing latest breakthrough 2024’) 。这种方式虽然能获取新信息,但行动往往缺乏规划。模型可能陷入无效搜索的循环,或者因为缺少对全局任务的思考而调用错误的工具。

ReAct的论文作者通过实验发现,纯推理容易产生事实幻觉,纯行动则容易陷入行动序列的错误或低效。而将两者以特定方式结合,则能相互纠正、相互增强。

2.2 ReAct的交错协同模式

ReAct提出了一种 Thought -> Act -> Observation 的循环单元。

  • Thought(思考) :模型分析当前状况(包括初始问题、历史步骤和观察结果),规划下一步该做什么,并解释原因。这是 推理 部分。
  • Act(行动) :模型根据思考,执行一个具体的动作。通常是调用一个外部工具,并给出精确的参数。这是 行动 部分。
  • Observation(观察) :环境(或工具)执行动作后返回结果。这个结果作为新信息,输入给下一个循环的“思考”步骤。

这个循环会一直持续,直到模型认为已经掌握了足够信息,可以给出最终答案(Answer)。此时,模型会输出一个 Final Answer: 开头的句子。

这种交错结构带来了几个关键优势:

  • 动态知识获取 :通过行动(如搜索),模型可以获取训练数据之外的最新、最具体的信息,从根本上缓解幻觉问题。
  • 可解释性与可控性 :每一步的“思考”都白盒化了模型的决策过程。开发者可以清晰地看到模型为什么选择这个工具、为什么使用这个查询词。如果出现问题,可以精准地在对应的“思考”或“行动”环节进行干预和调整。
  • 错误恢复能力 :如果某次行动返回的结果不理想(如搜索无结果),模型可以在接下来的“思考”中意识到这一点,并调整策略(如更换关键词、尝试其他工具),而不是一条道走到黑。

2.3 一个典型ReAct循环的实例解析

假设任务为:“特斯拉的CEO埃隆·马斯克,他最近创办的那家AI公司主要产品是什么?”

一个简化的ReAct轨迹可能如下:

  1. Thought 1 : 用户想知道一家AI公司的主要产品。我需要先确认这家公司的名字。我知道特斯拉的CEO是埃隆·马斯克,他最近创办的AI公司很可能是xAI。但我需要验证一下,并获取更详细的信息。 Act 1 : Search(Elon Musk new AI company name) Observation 1 : 搜索结果:埃隆·马斯克于2023年创立了人工智能公司xAI。

  2. Thought 2 : 好的,公司名字确认是xAI。现在我需要了解xAI公司的主要产品是什么。我应该直接搜索xAI的产品信息。 Act 2 : Search(xAI main products or offerings) Observation 2 : 搜索结果:xAI推出了首个AI助手产品Grok,并发布了开源大语言模型Grok-1。

  3. Thought 3 : 我找到了两个关键产品:Grok助手和Grok-1开源模型。用户问的是“主要产品”,可能需要一个概括性的答案。我应该综合这些信息给出最终回答。 Final Answer : 埃隆·马斯克创立的AI公司xAI的主要产品包括其AI聊天助手Grok,以及开源的大语言模型Grok-1。

可以看到,模型的“思考”步骤有效地规划了行动序列(先查公司名,再查产品),并解释了每一步的意图。整个流程清晰、合理,并且答案基于实时搜索得到的信息,准确可靠。

3. 实操构建:从零实现一个ReAct智能体

理解了原理,我们动手实现一个。这里我们不依赖LangChain等高级框架,而是用最直接的Python代码和OpenAI API来揭示其核心机制。你会看到,其本质是一场精心设计的与LLM的对话。

3.1 环境准备与工具定义

首先,我们需要定义智能体可以使用的“工具”。工具本质上是一个函数,它能够根据模型的指令执行特定操作并返回结果。

import requests
import json
from typing import Callable, Dict

# 模拟一个简单的搜索引擎工具(实际应用中可替换为SerpAPI、Google Search API等)
def search_tool(query: str) -> str:
    """
    模拟搜索工具。
    在实际项目中,这里应调用真实的搜索API。
    此处我们返回一个模拟的、结构化的文本结果。
    """
    # 这是一个简化的模拟逻辑
    knowledge_base = {
        "Elon Musk new AI company name": "埃隆·马斯克于2023年创立了人工智能公司xAI。",
        "xAI main products or offerings": "xAI推出了首个AI助手产品Grok,并发布了开源大语言模型Grok-1。",
        "latest SpaceX launch date": "SpaceX最近一次发射是在2024年4月,执行了星链卫星部署任务。",
        "Python list comprehension syntax": "列表推导式语法: [expression for item in iterable if condition]。"
    }
    # 简单模拟:如果查询完全匹配key,则返回value,否则返回一个通用提示
    return knowledge_base.get(query, f"未找到关于 '{query}' 的精确信息。请尝试更具体或不同的关键词。")

# 计算器工具
def calculator_tool(expression: str) -> str:
    """一个安全的计算器工具,仅处理基本算术。"""
    try:
        # 警告:实际使用中应对表达式做严格的安全过滤,避免代码注入。
        # 这里仅作演示,使用eval有安全风险。
        allowed_chars = set('0123456789+-*/(). ')
        if all(c in allowed_chars for c in expression):
            result = eval(expression)
            return str(result)
        else:
            return "错误:表达式包含不安全字符。"
    except Exception as e:
        return f"计算错误:{e}"

# 工具字典:将工具名称映射到函数和描述
TOOLS: Dict[str, Dict] = {
    "Search": {
        "function": search_tool,
        "description": "用于搜索最新的一般性知识或事实信息。输入应为搜索查询字符串。"
    },
    "Calculator": {
        "function": calculator_tool,
        "description": "用于执行数学计算。输入应为数学表达式字符串,如 '2 + 3 * 4'。"
    }
}

注意 :这里的 search_tool 是极度简化的模拟。在生产环境中,你需要集成真实的API(如SerpAPI、Bing Search API),并处理好认证、速率限制和错误处理。 calculator_tool 使用 eval 是极不安全的,仅用于演示。真实场景应使用 ast.literal_eval 或专门的数学表达式解析库。

3.2 构建ReAct提示模板

这是ReAct的核心“魔法”。提示模板需要清晰地告诉模型游戏规则:如何思考,如何行动,如何观察。

def build_react_prompt(question: str, scratchpad: str = "") -> str:
    """
    构建ReAct格式的提示词。
    question: 用户的问题。
    scratchpad: 之前的思考-行动-观察记录(用于多轮对话)。
    """
    # 工具描述部分,告诉模型有哪些工具可用
    tools_text = "\n".join([f"{name}: {info['description']}" for name, info in TOOLS.items()])
    
    prompt = f"""请以ReAct格式回答以下问题。你可以使用以下工具:
{tools_text}

你必须严格按照以下格式回应:
Thought: [你的思考过程,分析当前情况,决定下一步做什么]
Act: [要执行的动作,必须是以下之一:{', '.join(TOOLS.keys())}。例如:Search(查询词)] 或 [Calculator(表达式)]
Observation: [工具执行的结果]

当你确信已经获得足够信息来回答问题后,请输出:
Final Answer: [你的最终答案]

开始!

问题:{question}
{scratchpad}"""
    return prompt

这个模板明确规定了输出的格式。 scratchpad 参数是关键,它用于在多次循环中累积历史记录,让模型拥有“记忆”。

3.3 实现ReAct循环执行引擎

现在,我们创建引擎来驱动整个“思考-行动-观察”循环。

import openai # 假设使用OpenAI API
import re

class ReActAgent:
    def __init__(self, model="gpt-3.5-turbo", max_steps=10):
        self.model = model
        self.max_steps = max_steps # 防止无限循环
        self.client = openai.OpenAI(api_key="your-api-key") # 请替换为你的API Key

    def parse_action(self, text: str):
        """从模型输出中解析出行动指令。"""
        # 匹配 Act: ToolName(arguments) 的模式
        pattern = r"Act:\s*(\w+)\(([^)]*)\)"
        match = re.search(pattern, text)
        if match:
            tool_name = match.group(1)
            tool_input = match.group(2).strip()
            return tool_name, tool_input
        return None, None

    def run(self, question: str):
        scratchpad = ""
        for step in range(self.max_steps):
            # 1. 构建当前轮次的完整提示
            prompt = build_react_prompt(question, scratchpad)
            
            # 2. 调用LLM,获取响应
            try:
                response = self.client.chat.completions.create(
                    model=self.model,
                    messages=[{"role": "user", "content": prompt}],
                    temperature=0, # 低温度保证输出格式稳定
                    max_tokens=500
                )
                full_response = response.choices[0].message.content.strip()
            except Exception as e:
                return f"调用模型API时出错:{e}"

            # 3. 将本次响应追加到scratchpad
            scratchpad += full_response + "\n"
            
            # 4. 检查是否已给出最终答案
            if "Final Answer:" in full_response:
                # 提取最终答案部分
                lines = full_response.split('\n')
                for line in lines:
                    if line.startswith('Final Answer:'):
                        return line.replace('Final Answer:', '').strip()
                return full_response.split('Final Answer:')[-1].strip()
            
            # 5. 解析并执行行动
            tool_name, tool_input = self.parse_action(full_response)
            if tool_name and tool_name in TOOLS:
                tool_func = TOOLS[tool_name]["function"]
                observation = tool_func(tool_input)
                # 将观察结果格式化后加入scratchpad,供下一轮思考
                scratchpad += f"Observation: {observation}\n"
            else:
                # 如果解析行动失败,可能模型格式输出错误,我们将错误作为观察
                observation = f"错误:无法解析行动指令,或工具'{tool_name}'不存在。请检查格式是否为'Act: ToolName(arguments)'。"
                scratchpad += f"Observation: {observation}\n"
                
        # 如果循环达到最大步数仍未输出最终答案
        return f"达到最大步数({self.max_steps})仍未得出最终答案。当前记录:\n{scratchpad}"

# 使用智能体
if __name__ == "__main__":
    agent = ReActAgent(model="gpt-3.5-turbo", max_steps=6)
    result = agent.run("特斯拉的CEO埃隆·马斯克,他最近创办的那家AI公司主要产品是什么?")
    print("最终答案:", result)

这个 ReActAgent 类封装了整个流程。它循环地:生成提示 -> 调用LLM -> 解析输出 -> 判断是否结束 -> 执行工具 -> 收集观察结果。 max_steps 是一个重要的安全阀,防止在任务无法完成时陷入死循环。

4. 关键实现细节与调优经验

在实际复现和应用ReAct时,有几个细节至关重要,直接决定了智能体的成功率和效率。

4.1 提示工程的艺术:少样本(Few-Shot)示例

上面给出的提示模板是零样本(Zero-Shot)的。对于复杂的任务或性能要求高的场景,在提示中加入2-3个完整的ReAct轨迹示例(Few-Shot),能显著提升模型遵循格式和推理的质量。

示例:在提示模板中加入一个例子

def build_react_prompt_with_example(question: str, scratchpad: str = "") -> str:
    tools_text = "\n".join([f"{name}: {info['description']}" for name, info in TOOLS.items()])
    
    example = """
例如,对于问题:“珠穆朗玛峰的高度乘以2是多少米?”
一个正确的ReAct轨迹如下:
Thought: 用户需要计算珠穆朗玛峰高度的两倍。首先我需要知道珠穆朗玛峰的准确高度。
Act: Search(珠穆朗玛峰高度)
Observation: 珠穆朗玛峰的岩面高(裸高)为8848.86米。
Thought: 我得到了高度8848.86米。现在需要计算这个数字乘以2。这需要一个计算器。
Act: Calculator(8848.86 * 2)
Observation: 17697.72
Thought: 我已经计算出结果是17697.72米。可以给出最终答案了。
Final Answer: 珠穆朗玛峰高度的两倍大约是17697.72米。
"""
    
    prompt = f"""请以ReAct格式回答以下问题。你可以使用以下工具:
{tools_text}

你必须严格按照以下格式回应:
Thought: [你的思考过程]
Act: [要执行的动作,如:Search(查询词)] 或 [Calculator(表达式)]
Observation: [工具执行的结果]

当你确信已经获得足够信息来回答问题后,请输出:
Final Answer: [你的最终答案]

{example}
现在开始新的任务。

问题:{question}
{scratchpad}"""
    return prompt

加入示例后,模型能更好地理解“思考”应该多详细,“行动”的格式如何,以及何时该结束。这是提升ReAct稳定性的最有效手段之一。

4.2 工具设计的核心原则

工具是智能体的“手脚”,设计好坏直接影响能力边界。

  1. 功能原子化 :每个工具应只做一件事,并把它做好。避免设计“万能工具”。例如, Search 只负责搜索, Calculator 只负责计算, GetWeather 只获取天气。这降低了模型的决策难度。
  2. 描述清晰精准 :工具的描述( description )至关重要。它应该用模型能理解的语言,明确说明工具的用途、输入格式和预期的输出类型。模糊的描述会导致模型误用工具。
  3. 输入验证与错误处理 :在工具函数内部,必须对输入进行验证。例如, Calculator 工具必须严格过滤输入字符串,防止代码注入。工具还应能处理各种边界情况和错误,并返回对人类和模型都友好的错误信息(如“查询词过长,请精简”而非Python异常栈)。
  4. 结果格式化 :工具返回的观察结果(Observation)应该简洁、信息密集且格式统一。避免返回HTML、复杂的JSON或无关信息。通常纯文本摘要是最佳选择。

4.3 控制循环与停止条件

除了设置最大步数 max_steps 外,更智能的停止条件能提升体验。

  • 检测无效循环 :如果连续多轮的 Observation 都是“未找到信息”或类似的错误,可以主动终止循环,并提示用户重新表述问题。
  • 答案置信度 :有些任务可能没有明确终点。可以在最终答案前,让模型输出一个置信度分数(例如在Thought里写“我已有95%的把握”),当置信度超过阈值且连续两轮没有新行动时,触发停止。
  • 用户中断 :在交互式应用中,需要保留用户手动中断的通道。

5. 常见问题、故障排查与性能优化

在实际运行中,你肯定会遇到各种问题。以下是一些典型场景及解决方案。

5.1 模型不遵循格式(格式崩坏)

这是最常见的问题。模型可能忘记输出 Thought: ,或者把 Act: 写成了 Action: ,或者不把工具调用放在括号里。

解决方案

  1. 强化提示 :使用更严厉的措辞,如“ 必须 严格按照格式”、“格式错误将导致系统失败”。在Few-Shot示例中展示正确的格式。
  2. 后处理与纠正 :在 parse_action 函数中增加鲁棒性。使用更灵活的正则表达式,或尝试匹配多种变体(如 Action: , Act : )。如果解析失败,可以将一个格式错误信息作为 Observation 反馈给模型,让它在下一次循环中纠正。例如: Observation: 格式错误。请确保以'Act: ToolName(arguments)'的格式输出行动指令。
  3. 降低Temperature :将API调用的 temperature 参数设为0或接近0(如0.1),可以极大提高输出格式的稳定性。
  4. 更换模型 :通常,更强大的模型(如GPT-4)在遵循复杂指令方面远优于较小模型(如GPT-3.5-turbo)。如果任务关键,升级模型是立竿见影的方法。

5.2 智能体陷入循环或无效行动

智能体可能反复搜索相同的关键词,或者在“思考”和“行动”间徘徊,无法推进。

解决方案

  1. 在思考中引入历史总结 :在每一轮新的 Thought 生成前,可以隐式或显式地要求模型总结当前已知信息和已尝试的失败路径。我们的 scratchpad 机制已经提供了完整历史,但模型有时会忽略长上下文。可以在提示词中强调:“请仔细回顾之前的Observation,避免重复行动。”
  2. 工具返回更有指导性的错误信息 :当 Search 工具多次返回无结果时,可以返回如“Observation: 使用关键词‘XXX’和‘YYY’均未搜索到有效信息。建议尝试更换核心概念或使用更通用的词汇。”这样的引导性文本,帮助模型调整策略。
  3. 实现动作黑名单 :记录最近N步的行动(特别是失败的),如果模型试图发起一个完全相同的行动,工具可以返回一个特殊的Observation来阻止它。
  4. 设计更精细的工具 :如果搜索总是失败,也许是搜索工具本身能力不足。考虑增加更专业的工具,如 SearchScientificPaper(论文主题) SearchNews(事件名称, 日期范围)

5.3 处理复杂、多跳问题

对于需要多个信息片段综合推理的问题(例如,“去年获得诺贝尔物理学奖的团队,其负责人毕业于哪所大学?”),模型可能在中途丢失主线。

解决方案

  1. 在思考中明确子目标 :鼓励模型在 Thought 中将复杂问题分解为明确的子问题。例如:“要回答A,我需要先知道B和C。目前我知道D,所以下一步应该先查B。”
  2. 使用更强的上下文模型 :确保使用的LLM具有足够长的上下文窗口(如128K),以容纳整个复杂的推理轨迹。
  3. 人工设计推理链 :对于极其重要且固定的复杂任务,可以预先设计好一部分推理步骤作为Few-Shot示例,引导模型沿着预定路径前进。

5.4 性能与成本优化

ReAct需要多次调用LLM,成本可能较高。

  1. 缓存工具结果 :对相同的工具调用(如 Search(“Python tutorial”) )进行缓存,避免重复调用昂贵的外部API。
  2. 限制最大步数 :根据任务复杂度合理设置 max_steps 。简单问答可能3-5步就够了。
  3. 使用更快的模型进行“思考” :可以采用混合模型策略,用快速、便宜的小模型(如 Claude Haiku)负责常规的思考和格式控制,只在需要复杂推理或生成最终答案时调用大模型(如GPT-4)。
  4. 并行化工具调用 :在某些场景下,多个工具调用之间如果没有依赖关系,可以尝试让模型规划一批并行行动,然后同时执行,减少循环轮数。但这需要更复杂的提示设计和结果整合逻辑。

6. 超越基础:ReAct的进阶模式与应用场景

基础的ReAct已经很强大了,但我们可以在此基础上构建更强大的智能体系统。

6.1 与规划器(Planner)结合:ReAct + ToT / CoT

对于极其复杂、需要长程规划的任务,可以让一个专门的“规划器”LLM先制定一个高级计划(例如使用思维树Tree of Thoughts),然后将每个计划步骤作为一个子任务,交给ReAct智能体去执行。这样,ReAct负责可靠的“战术执行”,而规划器负责高层次的“战略部署”。

6.2 动态工具检索与调用

当工具数量非常多时(比如企业内部有上百个API),让模型记住所有工具是不现实的。可以引入一个“工具检索”步骤:首先,模型根据当前任务描述,从一个工具向量数据库中检索出最相关的几个工具,然后再进行标准的ReAct循环。这大大扩展了智能体的能力范围。

6.3 应用于垂直领域

ReAct范式在垂直领域大有可为:

  • 数据分析智能体 :工具集包括 QueryDatabase(SQL) , PlotChart(data, type) , SummaryStatistics(table) 。智能体可以接受自然语言问题(如“上季度销售额最高的三个产品是什么?用柱状图展示”),并自动完成数据查询、分析和可视化。
  • 运维与故障排查智能体 :工具集包括 CheckServerStatus(hostname) , ViewLogs(service, time_range) , RestartService(service_name) 。智能体可以根据告警信息(如“网站响应慢”),自动执行一系列诊断和修复动作。
  • 游戏与模拟环境 :ReAct是构建游戏AI的绝佳框架。 Thought 是AI的决策推理, Act 是游戏动作(如“移动”、“攻击”、“使用物品”), Observation 是游戏状态反馈。通过这种方式,可以创造出具有解释性和策略性的游戏角色。

6.4 评估与持续改进

如何知道你的ReAct智能体表现好坏?需要建立评估体系。

  1. 单元测试 :针对每个工具,编写测试用例,确保其功能正常。
  2. 集成测试 :构建一个涵盖各种类型(事实问答、计算、多跳推理)的测试问题集。
  3. 评估指标
    • 任务成功率 :智能体能否在最大步数内给出正确答案?
    • 平均步数 :完成任务需要多少步?步数越少通常效率越高。
    • 工具调用准确率 :模型选择的工具和输入参数是否合理?
    • 人工评估 :最终答案的质量、推理过程的合理性。
  4. 基于评估的迭代 :根据测试结果,反推问题所在。是提示词不清晰?工具描述不准确?还是模型能力不足?然后有针对性地优化提示、改进工具或升级模型。

构建一个稳健的ReAct智能体是一个迭代过程。它不仅仅是一个技术实现,更是一种让大语言模型与真实世界安全、可靠、透明地进行交互的系统工程思想。从最简单的搜索-计算例子开始,逐步增加工具、优化提示、处理边界情况,你会发现,一个能够“三思而后行”的AI助手正在你手中逐渐变得强大而可靠。

更多推荐