五、React Agent最简实践 ——《动手学Agent应用开发》学习心得

五、React Agent最简实践 ——《动手学Agent应用开发》学习心得

==================================================================================

最近参加了Datawhale开源组织举办的组队学习。本篇的学习内容是基础原理-选修:三、Agent原理与最简实践
【教程地址】
https://gitlink.org.cn/datawhalechina/build_good_agents

==================================================================================

参考资料:

==================================================================================

Serper注册无反应_serper注册不了-CSDN博客

通义千问3-30B-A3B-Instruct-2507 · 模型库 (modelscope.cn)

==================================================================================

实现最终效果:

5e07562ef71e5b55196b1b70152efdb1 4b49bf2bcc57083b5f5b324f48e2ea31 498fb48afa8a6b568205a65abeac8fb2

1、从零实现最简 React Agent

1.1、注意事项

  • 开发环境:自己的电脑配置比较低,使用魔搭社区的免费实例比较方便,就直接用了,不过研究了大半天怎么使用

  • 注册魔搭或硅基流动,获得api_key(推荐魔搭,因为我用的魔搭的线上环境比较配套)

  • 注册https://serper.dev/dashboard,获得api_key调用googel搜索API

  • 使用 Qwen3-30B-A3B-Instruct-2507 的作为 Agent 模型

  • 输出结果给大模型时,通常返回字符串,其他格式的结果大模型不好处理

1.2、实现步骤

1.2.1、 构造大模型

使用 Qwen3-30B-A3B-Instruct-2507 的作为我们的 Agent 模型。Qwen3-30B-A3B-Instruct-2507 模型的工具调用能力非常强大,能够处理复杂的任务。

注意:初学实现Agent最好使用Instruct模型,不要使用思考模型,因为思考模型调用工具不太准确

llm.py完整代码:

from typing import Dict, List, Tuple
from openai import OpenAI

class BaseModel:
    def __init__(self, api_key: str = '') -> None:
        self.api_key = api_key

    def chat(self, prompt: str, history: List[Dict[str, str]], system_prompt: str = "") -> Tuple[str, List[Dict[str, str]]]:
        """
        基础聊天接口
        
        Args:
            prompt: 用户输入
            history: 对话历史
            system_prompt: 系统提示
            
        Returns:
            (模型响应, 更新后的对话历史)
        """
        pass

    
class Siliconflow(BaseModel):
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.client = OpenAI(api_key=self.api_key, base_url="https://api.siliconflow.cn/v1")

    def chat(self, prompt: str, history: List[Dict[str, str]] = [], system_prompt: str = "") -> Tuple[str, List[Dict[str, str]]]:
        """
        与 Siliconflow API 进行聊天
        
        Args:
            prompt: 用户输入
            history: 对话历史
            system_prompt: 系统提示
            
        Returns:
            (模型响应, 更新后的对话历史)
        """
        # 构建消息列表
        messages = [
            {"role": "system", "content": system_prompt or "You are a helpful assistant."}
        ]
        
        # 添加历史消息
        if history:
            messages.extend(history)
        
        # 添加当前用户消息
        messages.append({"role": "user", "content": prompt})

        # 调用 API
        response = self.client.chat.completions.create(
            model="Qwen/Qwen3-30B-A3B-Instruct-2507",
            messages=messages,
            temperature=0.6,
            max_tokens=2000,
        )

        model_response = response.choices[0].message.content
        
        # 更新对话历史
        updated_history = messages.copy()
        updated_history.append({"role": "assistant", "content": model_response})

        return model_response, updated_history

if __name__ == "__main__":
    llm = Siliconflow(api_key="your api key")
    prompt = "你好"
    response, history = llm.chat(prompt)
    print("Response:", response)
    print("History:", history)

1.2.2、 构造工具

添加工具的描述信息,是为了在构造system_prompt的时候,让模型能够知道可以调用哪些工具,以及工具的描述信息和参数。

  • 首先要在 tools 中添加工具的描述信息
  • 然后在 tools 中添加工具的具体实现

使用Google搜索功能的话需要去serper官网申请一下token: https://serper.dev/dashboard, 然后在tools.py文件中填写你的key,这个key每人可以免费申请一个,且有2500次的免费调用额度,足够做实验用

tools.py完整代码:

import json
import requests
from typing import Dict, List, Any

class ReactTools:
    """
    React Agent 工具类
    
    为 ReAct Agent 提供标准化的工具接口
    """
    
    def __init__(self) -> None:
        self.toolConfig = self._build_tool_config()
    
    def _build_tool_config(self) -> List[Dict[str, Any]]:
        """构建工具配置信息"""
        return [
            {
                'name_for_human': '谷歌搜索',
                'name_for_model': 'google_search',
                'description_for_model': '谷歌搜索是一个通用搜索引擎,可用于访问互联网、查询百科知识、了解时事新闻等。',
                'parameters': [
                    {
                        'name': 'search_query',
                        'description': '搜索关键词或短语',
                        'required': True,
                        'schema': {'type': 'string'},
                    }
                ],
            }
        ]

    def google_search(self, search_query: str) -> str:
        """执行谷歌搜索

        可在 https://serper.dev/dashboard 申请 api key

        Args:
            search_query: 搜索关键词
            
        Returns:
            格式化的搜索结果字符串
        """
        url = "https://google.serper.dev/search"

        payload = json.dumps({"q": search_query})
        headers = {
            'X-API-KEY': 'your serper api key',
            'Content-Type': 'application/json'
        }

        try:
            response = requests.request("POST", url, headers=headers, data=payload).json()
            organic_results = response.get('organic', [])
            
            # 格式化搜索结果
            formatted_results = []
            for idx, result in enumerate(organic_results[:5], 1):
                title = result.get('title', '无标题')
                snippet = result.get('snippet', '无描述')
                link = result.get('link', '')
                formatted_results.append(f"{idx}. **{title}**\n   {snippet}\n   链接: {link}")
            
            return "\n\n".join(formatted_results) if formatted_results else "未找到相关结果"
            
        except Exception as e:
            return f"搜索时出现错误: {str(e)}"
    
    def get_available_tools(self) -> List[str]:
        """获取可用工具名称列表"""
        return [tool['name_for_model'] for tool in self.toolConfig]
    
    def get_tool_description(self, tool_name: str) -> str:
        """获取工具描述"""
        for tool in self.toolConfig:
            if tool['name_for_model'] == tool_name:
                return tool['description_for_model']
        return "未知工具"

if __name__ == "__main__":
    tools = ReactTools()
    result = tools.google_search("美国最近一次阅兵原因 2025")
    print(result)
1.2.3、 构造 React Agent
1.2.3.1、 核心组件设计

为什么要这样设计?

React Agent 的设计采用了经典的分层架构模式:

  • 接口层:对外暴露简单的 run() 方法
  • 协调层:管理思考-行动-观察的循环
  • 解析层:从模型输出中提取结构化信息
  • 执行层:调用具体工具完成任务

这种设计让代码结构清晰,易于维护和扩展。

1.2.3.2、 系统提示词构建

为什么系统提示如此重要?

系统提示是 React Agent 的"大脑",它直接决定了 Agent 的行为模式。一个好的系统提示应该包含:

  1. 时间信息:让 Agent 知道当前时间,避免过时信息
  2. 工具清单:明确告诉 Agent 有哪些工具可用
  3. 行为模式:详细的 ReAct 流程指导
  4. 输出格式:规范化的思考-行动-观察格式

构建思路

我们使用 f-string 动态生成系统提示,这样可以:

  • 自动包含当前时间
  • 动态加载可用工具列表
  • 保持提示的时效性和准确性

系统提示模板解析

prompt = f"""现在时间是 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}。
你是一位智能助手,可以使用以下工具来回答问题:

{tool_descriptions}

请遵循以下 ReAct 模式:

思考:分析问题和需要使用的工具
行动:选择工具 [google_search] 中的一个
行动输入:提供工具的参数
观察:工具返回的结果

你可以重复以上循环,直到获得足够的信息来回答问题。

最终答案:基于所有信息给出最终答案

开始!"""

设计要点

  • 中文提示:更符合国内用户习惯
  • 具体工具名:明确告诉模型可用工具
  • 循环指导:说明可以多次使用工具
  • 最终答案:明确结束条件
prompt = f"""现在时间是 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}。
你是一位智能助手,可以使用以下工具来回答问题:

{tool_descriptions}

请遵循以下 ReAct 模式:

思考:分析问题和需要使用的工具
行动:选择工具 [google_search] 中的一个
行动输入:提供工具的参数
观察:工具返回的结果

你可以重复以上循环,直到获得足够的信息来回答问题。

最终答案:基于所有信息给出最终答案

开始!"""
1.2.3.3、 行动解析机制

为什么解析这么复杂?

在实际应用中,大模型的输出格式往往不够规范,可能出现:

  • 中英文混合的冒号
  • JSON格式不规范
  • 参数缺失或格式错误
  • 多余的空格或换行

因此我们需要一个鲁棒的解析机制

解析思路

  1. 多层匹配:先用正则提取,再用JSON解析
  2. 容错设计:解析失败时提供降级方案
  3. 格式兼容:支持JSON字符串和纯文本参数

解析流程详解

模型输出:
思考:用户询问特朗普生日,需要搜索
行动:google_search
行动输入:{"search_query": "特朗普生日"}

解析步骤:
1. 正则提取行动 → google_search
2. 正则提取参数 → {"search_query": "特朗普生日"}
3. JSON解析 → {'search_query': '特朗普生日'}
4. 返回结构化数据 → ('google_search', {'search_query': '特朗普生日'})

常见错误场景

  • 行动输入:特朗普生日 → 自动转为 {"search_query": "特朗普生日"}
  • 行动输入:{"search_query":"特朗普生日" → 补全JSON格式
  • 行动输入:"特朗普生日" → 去除多余引号
1.2.3.4、 React主循环

什么是 ReAct 循环?

ReAct 循环是 Agent 的"心跳",它让 Agent 能够:

  • 持续思考:基于新信息不断调整策略
  • 工具调用:在需要时主动获取外部信息
  • 结果整合:将工具结果与已有知识结合

循环的四个阶段

  1. 思考阶段(Thought):模型分析问题,决定是否需要工具
  2. 行动阶段(Action):选择合适的工具并提供参数
  3. 观察阶段(Observation):执行工具并获取结果
  4. 整合阶段(Integration):将新信息整合到上下文中

为什么需要 max_iterations?

  • 防止无限循环:避免模型陷入死循环
  • 控制成本:限制 API 调用次数
  • 用户体验:避免过长的响应时间

状态管理的重要性

每次循环都需要:

  • 保持上下文:将观察结果加入对话历史
  • 更新输入:为下一轮循环准备新的提示
  • 历史记录:确保模型知道之前做了什么

agent.py完整代码:

import json5
import re
import time

from llm import Siliconflow
from tool import ReactTools


class ReactAgent:
    def __init__(self, api_key: str = '') -> None:
        self.api_key = api_key
        self.tools = ReactTools()
        self.model = Siliconflow(api_key=self.api_key)
        self.system_prompt = self._build_system_prompt()
        
    def _build_system_prompt(self) -> str:
        """构建系统提示,使用 ReAct 模式"""
        tool_descriptions = []
        for tool in self.tools.toolConfig:
            tool_descriptions.append(
                f"{tool['name_for_model']}: {tool['description_for_model']}"
                f" 参数: {json5.dumps(tool['parameters'], ensure_ascii=False)}"
            )
        
        tool_names = [tool['name_for_model'] for tool in self.tools.toolConfig]
        
        prompt = f"""现在时间是 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}。你是一位智能助手,可以使用以下工具来回答问题:

{chr(10).join(tool_descriptions)}

请遵循以下 ReAct 模式:


思考:分析问题和需要使用的工具
行动:选择工具 [{', '.join(tool_names)}] 中的一个
行动输入:提供工具的参数
观察:工具返回的结果

你可以重复以上循环,直到获得足够的信息来回答问题。

最终答案:基于所有信息给出最终答案

开始!"""
        return prompt
    
    def _parse_action(self, text: str, verbose: bool = False) -> tuple[str, dict]:
        """从文本中解析行动和行动输入"""
        # 更灵活的正则表达式模式
        action_pattern = r"行动[::]\s*(\w+)"
        action_input_pattern = r"行动输入[::]\s*({.*?}|\{.*?\}|[^\n]*)"
        
        action_match = re.search(action_pattern, text, re.IGNORECASE)
        action_input_match = re.search(action_input_pattern, text, re.DOTALL)
        
        action = action_match.group(1).strip() if action_match else ""
        action_input_str = action_input_match.group(1).strip() if action_input_match else ""
        
        # 清理和解析JSON
        action_input_dict = {}
        if action_input_str:
            try:
                # 尝试解析为JSON对象
                action_input_str = action_input_str.strip()
                if action_input_str.startswith('{') and action_input_str.endswith('}'):
                    action_input_dict = json5.loads(action_input_str)
                else:
                    # 如果不是JSON格式,尝试解析为简单字符串参数
                    action_input_dict = {"search_query": action_input_str.strip('"\'')}
            except Exception as e:
                if verbose:
                    print(f"[ReAct Agent] 解析参数失败,使用字符串作为搜索查询: {e}")
                action_input_dict = {"search_query": action_input_str.strip('"\'')}
        
        return action, action_input_dict
    
    def _execute_action(self, action: str, action_input: dict) -> str:
        """执行指定的行动"""
        if action == "google_search":
            search_query = action_input.get("search_query", "")
            if search_query:
                results = self.tools.google_search(search_query)
                return f"观察:搜索完成,结果如下:\n{results}"
            else:
                return "观察:缺少搜索查询参数"
        
        return f"观察:未知行动 '{action}'"
    
    def _format_response(self, response_text: str) -> str:
        """格式化最终响应"""
        if "最终答案:" in response_text:
            return response_text.split("最终答案:")[-1].strip()
        return response_text
    
    def run(self, query: str, max_iterations: int = 3, verbose: bool = True) -> str:
        """运行 ReAct Agent
        
        Args:
            query: 用户查询
            max_iterations: 最大迭代次数
            verbose: 是否显示中间执行过程
        """
        conversation_history = []
        current_text = f"问题:{query}"
        
        # 绿色ANSI颜色代码
        GREEN = '\033[92m'
        RESET = '\033[0m'
        
        if verbose:
            print(f"{GREEN}[ReAct Agent] 开始处理问题: {query}{RESET}")
        
        for iteration in range(max_iterations):
            if verbose:
                print(f"{GREEN}[ReAct Agent] 第 {iteration + 1} 次思考...{RESET}")
            
            # 获取模型响应
            response, history = self.model.chat(
                current_text, 
                conversation_history, 
                self.system_prompt
            )
            
            if verbose:
                print(f"{GREEN}[ReAct Agent] 模型响应:\n{response}{RESET}")
            
            # 解析行动
            action, action_input = self._parse_action(response, verbose=verbose)
            
            if not action or action == "最终答案":
                final_answer = self._format_response(response)
                if verbose:
                    print(f"{GREEN}[ReAct Agent] 无需进一步行动,返回最终答案{RESET}")
                return final_answer
            
            if verbose:
                print(f"{GREEN}[ReAct Agent] 执行行动: {action} | 参数: {action_input}{RESET}")
            
            # 执行行动
            observation = self._execute_action(action, action_input)
            
            if verbose:
                print(f"{GREEN}[ReAct Agent] 观察结果:\n{observation}{RESET}")
            
            # 更新当前文本以继续对话
            current_text = f"{response}\n观察结果:{observation}\n"
            conversation_history = history
        
        # 达到最大迭代次数,返回当前响应
        if verbose:
            print(f"{GREEN}[ReAct Agent] 达到最大迭代次数,返回当前响应{RESET}")
        return self._format_response(response)

if __name__ == '__main__':
    agent = ReactAgent(api_key="your api key")

    response = agent.run("中国最近一次阅兵的原因有哪些?", max_iterations=3, verbose=True)
    print("最终答案:", response)

1.3、调试技巧

查看详细日志:设置 verbose=True 查看完整流程

减少迭代次数:测试时使用 max_iterations=1 快速验证

检查API响应:确保网络连接和API key有效

错误排查:观察绿色输出中的每一步执行状态

posted @ 2025-11-18 10:32  老羅  阅读(0)  评论(0)    收藏  举报