Day11:ReAct Agent 优化:实现多工具智能选择与稳健调用

摘要:本文聚焦于 ReAct Agent 的优化,旨在提升其根据用户问题自主选择合适工具的能力。以通义千问大模型为基础,通过扩展功能实现多工具选择,并增添工具调用失败时的重试逻辑。详细介绍优化思路、代码实现以及测试过程,帮助读者深入理解并掌握这一优化后的智能交互模式。

一、引言

在之前对 ReAct Agent 的学习基础上,进一步优化其功能,使其能够在多种工具间智能选择,以更好地满足用户多样化的需求,是提升模型实用性的关键一步。今天我们将围绕如何优化 ReAct Agent,使其能够根据用户问题准确选择合适工具,并处理工具调用失败的重试逻辑展开学习。

二、核心任务:优化 ReAct Agent 实现多工具选择

(一)优化思路

  1. 工具描述与映射:为每个工具(如天气、计算器、搜索等)创建详细的描述信息,包括工具的功能、参数以及适用场景。通过映射关系将工具描述与实际的工具函数关联起来,方便模型根据推理结果选择对应的工具。

  2. 推理与工具选择:模型在接收到用户问题后,首先进行深入推理,分析问题的意图和所需信息,依据工具描述判断哪个工具最适合解决该问题。例如,对于 “今天天气适合跑步吗” 这样的问题,模型能够识别出需要先获取天气信息,从而选择天气工具。

(二)代码实现

import time

import requests
from pydantic import BaseModel, field_validator
import json
from typing import Optional, Dict, Any

# ===================== 工具参数模型定义(严格校验)=====================
# 天气查询工具参数
class WeatherQueryParams(BaseModel):
    city: str
    api_key: str

    @field_validator('city')
    def city_not_empty(cls, v):
        if not v.strip():
            raise ValueError('城市名称不能为空')
        return v

    @field_validator('api_key')
    def api_key_not_empty(cls, v):
        if not v.strip():
            raise ValueError('api_key不能为空')
        return v

# 计算器工具参数
class CalculatorParams(BaseModel):
    num1: float
    num2: float
    operation: str

    @field_validator('operation')
    def valid_operation(cls, v):
        valid_ops = ['+', '-', '*', '/']
        if v not in valid_ops:
            raise ValueError('仅支持+、-、*、/四种运算')
        return v

# 搜索工具参数
class SearchParams(BaseModel):
    query: str
    api_key: str

    @field_validator('query')
    def query_not_empty(cls, v):
        if not v.strip():
            raise ValueError('搜索关键词不能为空')
        return v

    @field_validator('api_key')
    def api_key_not_empty(cls, v):
        if not v.strip():
            raise ValueError('api_key不能为空')
        return v

# ===================== 真实工具实现(本地/远程API)=====================
# 1. 天气查询(调用真实WeatherAPI)
def query_weather(params: WeatherQueryParams) -> Optional[Dict[str, Any]]:
    base_url = "http://api.weatherapi.com/v1/current.json"
    params_dict = {
        "key": params.api_key,
        "q": params.city,
        "lang": "zh-CN"  # 返回中文结果
    }
    try:
        # 真实GET请求,超时控制
        response = requests.get(base_url, params=params_dict, timeout=10)
        response.raise_for_status()  # 触发HTTP错误(4xx/5xx)
        return response.json()
    except requests.RequestException as e:
        print(f"【天气工具调用失败】: {str(e)}")
        return None

# 2. 计算器(本地工具,无API依赖)
def calculate(params: CalculatorParams) -> Optional[float]:
    try:
        if params.operation == '+':
            return params.num1 + params.num2
        elif params.operation == '-':
            return params.num1 - params.num2
        elif params.operation == '*':
            return params.num1 * params.num2
        elif params.operation == '/':
            if params.num2 == 0:
                print("【计算工具调用失败】: 除数不能为零")
                return None
            return round(params.num1 / params.num2, 4)  # 保留4位小数
    except Exception as e:
        print(f"【计算工具调用失败】: {str(e)}")
        return None

# 3. 通用搜索(调用真实搜索API,可替换为百度/必应搜索API)
def search(params: SearchParams) -> Optional[Dict[str, Any]]:
    base_url = "https://api.example.com/search"  # 替换为真实搜索API地址(如百度百科/必应)
    params_dict = {
        "key": params.api_key,
        "q": params.query,
        "page_size": 10  # 限制返回结果数量
    }
    try:
        response = requests.get(base_url, params=params_dict, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"【搜索工具调用失败】: {str(e)}")
        return None

# 工具映射表:大模型返回的函数名 -> 本地工具函数+参数模型
TOOL_MAPPING = {
    "query_weather": {"func": query_weather, "model": WeatherQueryParams},
    "calculate": {"func": calculate, "model": CalculatorParams},
    "search": {"func": search, "model": SearchParams}
}

# ===================== 真实通义千问API调用(带函数调用声明)=====================
def call_qwen_api(
    qwen_api_key: str,
    user_prompt: str,
    tools: list  # 声明可用工具列表,让大模型感知
) -> Optional[Dict[str, Any]]:
    """
    调用真实通义千问兼容OpenAI格式的API(compatible-mode)
    自动携带工具声明,大模型会返回function_call或直接回答
    """
    url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {qwen_api_key}"
    }
    # 通义千问函数调用核心参数:声明工具列表+强制函数调用判断
    request_data = {
        "model": "qwen-plus",  # 可替换为qwen-turbo/qwen-max
        "messages": [{"role": "user", "content": user_prompt}],
        "tools": tools,  # 告诉大模型有哪些工具可用
        "tool_choice": "auto",  # 自动判断是否调用工具:auto/required/none
        "stream": False,
        "temperature": 0.1  # 低温度保证推理稳定性
    }
    try:
        response = requests.post(url, headers=headers, json=request_data, timeout=15)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"【通义千问API调用失败】: {str(e)}")
        return None

# ===================== 定义通义千问可感知的工具描述(标准化JSON)=====================
# 严格遵循OpenAI函数调用格式,大模型会解析该描述判断工具使用方式
QWEN_TOOLS_DESC = [
    {
        "type": "function",
        "function": {
            "name": "query_weather",
            "description": "查询指定城市的实时天气,包括温度、天气状况等,用于回答天气相关问题",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "要查询的城市名称,如北京、上海"},
                    "api_key": {"type": "string", "description": "天气API的有效密钥"}
                },
                "required": ["city", "api_key"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "执行简单的数学运算,仅支持加、减、乘、除四种操作",
            "parameters": {
                "type": "object",
                "properties": {
                    "num1": {"type": "number", "description": "第一个运算数"},
                    "num2": {"type": "number", "description": "第二个运算数"},
                    "operation": {"type": "string", "description": "运算操作,仅支持+、-、*、/"}
                },
                "required": ["num1", "num2", "operation"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search",
            "description": "搜索互联网公开信息,用于回答人工智能、科技发展、时事等非常识性问题",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词,如人工智能的发展历程"},
                    "api_key": {"type": "string", "description": "搜索API的有效密钥"}
                },
                "required": ["query", "api_key"]
            }
        }
    }
]

# ===================== 核心ReAct流程实现(推理-行动-反馈)=====================
def react_flow(
    user_input: str,
    qwen_api_key: str,
    max_retries: int = 3
) -> str:
    """
    真实ReAct流程主函数
    步骤:1.调用大模型推理→2.判断是否调用工具→3.工具参数校验+执行→4.返回结果/二次推理
    """
    # 步骤1:调用真实通义千问API,携带工具声明
    llm_response = call_qwen_api(qwen_api_key, user_input, QWEN_TOOLS_DESC)
    if not llm_response or "choices" not in llm_response:
        return "【ReAct流程失败】通义千问API无有效返回"

    llm_choice = llm_response["choices"][0]["message"]
    # 步骤2:判断大模型是否要求调用工具
    if "tool_calls" in llm_choice:  # 通义千问返回工具调用指令(兼容OpenAI tool_calls格式)
        for tool_call in llm_choice["tool_calls"]:
            func_name = tool_call["function"]["name"]
            func_args = json.loads(tool_call["function"]["arguments"])  # 解析参数JSON

            # 校验工具是否在本地映射表中
            if func_name not in TOOL_MAPPING:
                return f"【ReAct流程失败】不支持的工具:{func_name}"

            tool_func = TOOL_MAPPING[func_name]["func"]
            tool_model = TOOL_MAPPING[func_name]["model"]
            tool_result = None

            # 步骤3:工具调用+失败重试(参数校验+执行)
            for attempt in range(max_retries):
                try:
                    # 严格校验参数格式
                    params = tool_model(**func_args)
                    # 执行真实工具函数
                    tool_result = tool_func(params)
                    if tool_result is not None:
                        break
                    else:
                        print(f"【工具重试】{func_name} 调用失败,重试 {attempt+1}/{max_retries}")
                except Exception as e:
                    print(f"【参数校验失败】{func_name}{str(e)},重试 {attempt+1}/{max_retries}")
                    tool_result = None

            if tool_result is None:
                return f"【ReAct流程失败】{func_name} 工具调用多次失败,请检查参数/API密钥"

            # 步骤4:工具结果处理(个性化返回,贴合用户问题)
            if func_name == "query_weather":
                try:
                    city = func_args["city"]
                    condition = tool_result["current"]["condition"]["text"]
                    temp_c = tool_result["current"]["temp_c"]
                    wind = tool_result["current"]["wind_kph"]
                    # 针对「是否适合出行」类问题的个性化反馈
                    if "适合" in user_input or "跑步" in user_input or "出行" in user_input:
                        if "雨" in condition or "雪" in condition or temp_c < 10 or temp_c > 32:
                            return f"{city}实时天气:{condition},温度{temp_c}℃,风速{wind}km/h,当前天气不太适合出行/跑步~"
                        else:
                            return f"{city}实时天气:{condition},温度{temp_c}℃,风速{wind}km/h,当前天气很适合出行/跑步~"
                    else:
                        return f"{city}实时天气:{condition},温度{temp_c}℃,风速{wind}km/h,体感温度{tool_result['current']['feelslike_c']}℃"
                except KeyError as e:
                    return f"【天气结果解析失败】缺失字段:{str(e)},原始返回:{tool_result}"
            elif func_name == "calculate":
                return f"计算结果:{func_args['num1']}{func_args['operation']}{func_args['num2']} = {tool_result}"
            elif func_name == "search":
                return f"【搜索结果】关键词:{func_args['query']}\n{json.dumps(tool_result, ensure_ascii=False, indent=2)}"
    else:
        # 大模型无需调用工具,直接返回自然语言回答
        return llm_choice.get("content", "【ReAct流程完成】无有效回答内容")

# ===================== 真实场景测试(需替换为自己的有效API_KEY)=====================
if __name__ == "__main__":
    time.sleep(1)
    # 替换为自己的真实密钥(必填!)
    QWEN_API_KEY = "QWEN_API_KEY"  # 阿里云dashscope获取
    WEATHER_API_KEY = "你的WeatherAPI_KEY"  # weatherapi.com获取
    SEARCH_API_KEY = "你的搜索API_KEY"  # 替换为真实搜索API密钥

    # 测试用例(真实用户问题,通义千问会自动推理是否调用工具)
    test_cases = [
        f"今天上海适合跑步吗?需要调用天气工具,api_key填{WEATHER_API_KEY}",
        "计算100.5除以2.5的结果",
        f"搜索2025年人工智能行业发展报告,api_key填{SEARCH_API_KEY}",
        "介绍一下ReAct范式的核心原理"  # 无需调用工具,大模型直接回答
    ]

    # 执行ReAct流程测试
    for idx, question in enumerate(test_cases, 1):
        print(f"\n===== 测试用例{idx}{question} =====")
        result = react_flow(question, QWEN_API_KEY)
        print(f"结果:{result}")

代码说明:

  • 工具函数定义:分别定义了天气查询(query_weather)、计算器(calculate)和搜索(search)三个工具函数,并使用 pydantic 进行参数校验。

  • 通义千问 API 调用函数call_qwen_api 函数按照通义千问 API 要求构建请求并处理响应,实际使用时需替换为真实 API 密钥。

  • ReAct 流程函数 **react_flow**:接收用户输入、API 密钥以及最大重试次数。通过调用通义千问 API 获取响应,根据响应中的 function_call 信息选择并调用相应工具函数。若工具调用失败,按照重试逻辑进行重试,最多重试 max_retries 次。根据工具调用结果生成回答。

  • 测试部分:设置了三个不同的用户问题,分别测试天气、计算器和搜索工具的调用,展示了优化后的 ReAct Agent 对不同问题的工具选择和处理能力。

三、补充任务:添加工具与测试

(一)添加多个工具

如上述代码中添加了计算器和搜索工具,丰富了 Agent 可使用的功能。

(二)测试 Agent 对不同问题的工具选择能力

通过在 if name == “main”: 部分设置不同的用户问题,模拟实际场景下 Agent 对各类问题的处理。观察

输出结果,验证 Agent 是否能正确选择工具并给出合理回答。

(三)处理工具调用失败的重试逻辑

在 react_flow 函数中,针对工具调用可能出现的失败情况,设置了重试逻辑。当工具调用返回 None 时,

进行重试,并在重试次数用尽后返回提示信息。

四、总结

通过今天的学习,我们成功优化了 ReAct Agent,使其具备多工具选择能力,并完善了工具调用失败时的

重试逻辑。这一优化显著提升了 Agent 的智能性和稳定性,能够更好地应对复杂多样的用户问题。

在实际应用中,可以根据具体需求进一步扩展工具种类和优化推理逻辑。记得将优化后的多工具选择

ReAct Agent 代码整理保存,方便后续参考和改进。若在学习过程中遇到问题,仔细检查工具函数的定

义、API 调用以及重试逻辑的设置。

希望大家通过掌握这些优化技巧,在基于大语言模型的应用开发中实现更强大、可靠的功能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐