摘要: 本篇紧接上文,将利用已封装的 LLM 客户端和工具箱,通过精细的 Prompt 工程与正则表达式解析,从零组装一个完整的 ReAct 智能体。我们将深入代码核心,实现“思考-行动-观察”的闭环逻辑,并探讨该范式的优缺点及调试技巧。
关键词: ReAct Agent, Prompt Engineering, Regex Parsing, Agent Loop, Debugging
本文是基于Datawhale的hello-agent开源项目做的一些笔记,内容仅供参考,原PDF以及代码可以去github仓库获取https://datawhalechina.github.io/hello-agents


在 Part 1 中,我们完成了开发环境搭建,封装了 HelloAgentsLLM 客户端,并实现了一个具备智能解析功能的 Google Search 工具及其调度器 ToolExecutor。现在,万事俱备,只欠核心逻辑。

4.2.3 ReAct 智能体的编码实现

我们将通过一个 ReActAgent 类来封装核心逻辑。为了便于理解,我们将实现过程拆分为:Prompt 设计、核心循环、输出解析、工具执行与观测整合。

1. 系统提示词设计 (System Prompt)

提示词是 ReAct 机制的基石。我们需要设计一个模板,动态插入可用工具、用户问题及历史交互记录,并强制 LLM 遵循特定的输出格式(Thought/Action)。

# ReAct 提示词模板
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。
可用工具如下:
{tools}

请严格按照以下格式进行回应:
Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
  {tool_name} [{tool_input}]: 调用一个可用工具。
  Finish [{final_answer}]: 当你认为已经获得最终答案时。
  - 当你收集到足够的信息,能够回答用户的最终问题时,你必须在 Action: 字段后使用 Finish[...] 来输出最终答案。

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

这个模板定义了交互规范:

  • 角色定义:明确其具备使用工具的能力。
  • 工具清单 ({tools}):由 ToolExecutor 动态生成。
  • 格式规约:这是最关键的部分。
  • Thought:强制模型进行“内心独白”。
  • Action:规定了 ToolName [Input] 的调用格式和 Finish [Answer] 的结束指令。

💡 深度解析 (Deep Dive)
原书使用的这种基于文本格式(Text-based)的 Prompt 在早期 Agent 开发中非常普遍。但在现代工程(如 OpenAI GPT-4 Turbo)中,我们通常会结合 Function Calling (Tool Use) API,将工具定义为 JSON Schema 传入,从而获得更稳定、更结构化的输出,减少正则解析失败的概率。

2. 核心循环的实现 (The Core Loop)

ReActAgent 的核心是一个 while 循环,它不断执行“格式化 Prompt -> 调用 LLM -> 解析 -> 执行 -> 更新历史”的流程。

import re
from typing import List, Dict, Any

# 假设已导入之前定义的类
# from llm_client import HelloAgentsLLM
# from tools import ToolExecutor

class ReActAgent:
    def __init__(self, llm_client: HelloAgentsLLM, tool_executor: ToolExecutor, max_steps: int = 5):
        self.llm_client = llm_client
        self.tool_executor = tool_executor
        self.max_steps = max_steps
        self.history = [] # 存储 (Action, Observation) 历史

    def run(self, question: str):
        """运行 ReAct 智能体来回答一个问题。"""
        self.history = [] # 每次运行时重置历史记录
        current_step = 0
        
        print(f"--- 开始处理问题: {question} ---")

        while current_step < self.max_steps:
            current_step += 1
            print(f"\n--- 第 {current_step} 步 ---")

            # 1. 格式化提示词
            tools_desc = self.tool_executor.get_available_tools()
            history_str = "\n".join(self.history)
            
            prompt = REACT_PROMPT_TEMPLATE.format(
                tools=tools_desc,
                question=question,
                history=history_str,
                tool_name="Search", tool_input="query", final_answer="answer" # 填充模板说明中的占位符
            )

            # 2. 调用 LLM 进行思考
            messages = [{"role": "user", "content": prompt}]
            response_text = self.llm_client.think(messages=messages)
            
            if not response_text:
                print("错误: LLM 未能返回有效响应。")
                break
            
            # ...(后续的解析、执行、整合步骤见下文)
            # 为了代码连贯性,我们将逻辑拆解讲解,最后组合
            
            # 3. 解析 LLM 输出
            thought, action = self._parse_output(response_text)
            
            if thought:
                print(f"思考: {thought}")
            
            if not action:
                print("警告: 未能解析出有效的 Action,流程终止。")
                break
                
            # 4. 执行 Action
            if action.startswith("Finish"):
                # 提取最终答案
                final_answer_match = re.match(r"Finish\[(.*)\]", action)
                final_answer = final_answer_match.group(1) if final_answer_match else action
                print(f"\n--- 最终答案 ---\n{final_answer}")
                return final_answer
            
            # 解析工具调用
            tool_name, tool_input = self._parse_action(action)
            if not tool_name or not tool_input:
                print(f"错误: 无法解析动作 '{action}'")
                continue # 或 break,视策略而定

            print(f"行动: {tool_name} [{tool_input}]")
            
            # 5. 调用工具获取观察结果
            tool_function = self.tool_executor.get_tool(tool_name)
            if not tool_function:
                observation = f"错误: 未找到名为 '{tool_name}' 的工具。"
            else:
                observation = tool_function(tool_input)
            
            print(f"观察: {observation}")
            
            # 6. 将本轮 Action 和 Observation 添加到历史
            self.history.append(f"Action: {action}")
            self.history.append(f"Observation: {observation}")
        
        print("已达到最大步数,流程终止。")
        return None

🛠️ 实战映射 (Implementation)
max_steps 是一个极其重要的安全阀。在生产环境中,Agent 很容易因为陷入死循环(Loop)而耗尽 Token 额度。LangChain 的 AgentExecutor 中也有类似的 max_iterations 参数。

3. 输出解析器的实现 (Output Parser)

LLM 返回的是非结构化文本,我们需要用正则表达式提取关键信息。

    # (这些方法是 ReActAgent 类的一部分)

    def _parse_output(self, text: str):
        """解析 LLM 的输出,提取 Thought 和 Action。"""
        # 非贪婪匹配 Thought,贪婪匹配 Action
        thought_match = re.search(r"Thought: (.*?)Action:", text, re.DOTALL) 
        # 如果没有明确的 Thought 标记,尝试获取 Action 之前的所有内容
        if not thought_match:
             thought_match = re.search(r"Thought: (.*)", text, re.DOTALL)

        action_match = re.search(r"Action: (.*)", text, re.DOTALL)
        
        thought = thought_match.group(1).strip() if thought_match else None
        # 如果 action_match 包含换行符,只取第一行,或者根据具体情况调整正则
        action = action_match.group(1).strip() if action_match else None
        
        # 简单的清理
        if thought and "Action:" in thought:
            thought = thought.split("Action:")[0].strip()
            
        return thought, action

    def _parse_action(self, action_text: str):
        """解析 Action 字符串,提取工具名称和输入。例如 Search[Query]"""
        match = re.match(r"(\w+)\[(.*)\]", action_text)
        if match:
            return match.group(1), match.group(2)
        return None, None

(注:原书中的正则较简单 r"Thought: (.*)",但在实际多行输出中可能需要 re.DOTALL 或更复杂的逻辑来防止匹配错误,此处尽量保持原书逻辑并微调以确保可运行)

4. 运行实例与分析

将所有组件组合后,我们可以运行一次真实的查询。

输入问题:“华为最新的手机是哪一款?它的主要卖点是什么?”

运行轨迹记录 (Log)

  1. 第 1 步
  • Thought: 我需要查找华为最新发布的手机型号。这些信息可能在我的知识库之外,需要使用搜索。
  • Action: Search [华为最新手机型号及主要卖点]
  • Observation: (SerpApi 返回结果) [1] HUAWEI Pura 70 系列... [2] Mate 60 Pro...
  1. 第 2 步
  • Thought: 根据搜索结果,Pura 70 是最新发布的。Mate 60 也很新。我需要总结它们的卖点。
  • Action: Finish [根据搜索结果,华为最新的旗舰是 HUAWEI Pura 70 系列... 卖点是先锋影像...]

4.2.4 ReAct 的特点、局限性与调试技巧

亲手实现 ReAct 后,我们可以总结其工程特性。

1. 主要特点

  • 高可解释性:通过 Thought 链,我们可以清晰看到 Agent 为什么选择这个工具,这对于调试至关重要 。

  • 动态规划:它是“走一步,看一步”。如果第一步搜索结果通过,它可以在第二步立即调整关键词重新搜索,而不是死板地执行预设计划 。

  • 工具协同:LLM 负责运筹帷幄(Plan/Reason),工具负责解决具体问题(Data/Calc) 。

2. 固有局限性 (Limitations)

  • 依赖模型能力:如果 LLM 逻辑推理弱,或者不遵循指令(Instruction Following),很容易在 Thought 环节产生幻觉,或者输出无法解析的 Action 格式 。

  • 执行效率:串行循环。每一步都需要一次 LLM 调用 + 网络请求。复杂任务可能导致高延迟和高 Token 消耗 。

  • 提示词脆弱性:Prompt 中的标点符号、换行符差异都可能导致 Agent 行为异常 。

  • 局部最优:由于缺乏全局规划,它可能陷入“原地打转”的死循环(Looping) 。

3. 调试技巧 (Debugging)

当你构建的 Agent 不工作时,请按以下清单检查 :

  1. 检查完整 Prompt:打印出最终发给 LLM 的字符串,检查历史记录 (History) 拼接是否正确。
  2. 分析原始输出 (Raw Output):解析失败时,查看 LLM 到底输出了什么。往往是因为 LLM 加了“Note:”或者没换行。
  3. 验证工具输入:确保 Agent 生成的 tool_input 符合函数要求(例如 JSON 格式是否正确)。
  4. Few-shot Prompting:如果模型总是格式错误,在 Prompt 中加入 1-2 个成功的 Thought -> Action -> Observation 示例。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐