【Agent从入门到实践】33 集成多工具,实现Agent的工具选择与执行

文章目录

目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步。想要系统学习AI知识的朋友可以看看我的教程http://blog.csdn.net/jiangjunshow,教程通俗易懂,风趣幽默,从深度学习基础原理到各领域实战应用都有讲解。

前言

各位小伙伴,上一篇咱们把代码运行、网页检索、文件操作 三大核心工具都封装好了,每个工具都能单独干活。但真正的Agent不是"工具列表",而是会自己选工具、按顺序调用、处理结果、直到解决问题的智能体。

这篇就带大家把这些工具集成到一个完整Agent里,实现:

  • 自动判断"要不要用工具、用哪个工具"
  • 多工具链式调用(比如:查资料 → 写代码 → 存文件)
  • 上下文记忆 + 错误处理 + 循环决策
  • 可直接运行的Python代码,复制粘贴就能用

全程口语化,不讲虚的,直接上干货!


一、先搞懂:多工具Agent的核心架构

咱们的Agent就像一个项目经理

  1. 用户提需求 → 2. 理解意图 → 3. 查工具库 → 4. 选工具/组合工具 → 5. 执行工具 → 6. 看结果 → 7. 判断是否完成 → 8. 没完成就继续,完成就回答

架构拆成 4 块,清晰又好维护:

  • LLM 核心:大模型(GPT/通义千问等),负责"思考+选工具"
  • 工具库:上一篇封装的所有工具(code_runner、web_search、file_* 等)
  • 执行器:负责"调用工具、捕获结果、处理异常"
  • 对话管理器:负责"存上下文、多轮记忆、控制循环"

二、项目结构(清晰版,直接照着建)

复制代码
agent_tool_integrated/
├── .env                # 密钥配置
├── main.py             # 启动入口
├── agent.py            # Agent核心逻辑(选择+执行+循环)
├── tools.py            # 所有工具(上一篇的完整版)
└── agent_workspace/     # 文件操作安全目录(自动生成)

三、第一步:工具库完整版(tools.py

把上一篇的工具整合在一起,加上工具描述+映射,方便Agent调用。

python 复制代码
# tools.py
import sys
import io
import traceback
import time
import os
import json
import re
import math
import random
import datetime
from contextlib import redirect_stdout, redirect_stderr
import ast
import requests
from bs4 import BeautifulSoup
from pathlib import Path
import shutil
from dotenv import load_dotenv

load_dotenv()

# ====================== 1. 代码运行工具(安全沙箱) ======================
SAFE_MODULES = {
    "math", "random", "datetime", "json", "re", "collections",
    "pandas", "numpy", "matplotlib.pyplot", "seaborn"
}

def code_runner(code: str, timeout: int = 5) -> str:
    try:
        tree = ast.parse(code)
        for node in ast.walk(tree):
            if isinstance(node, (ast.Exec, ast.Eval)):
                return "❌ 禁止执行:exec/eval 存在安全风险"
            if isinstance(node, ast.Import):
                for alias in node.names:
                    if alias.name not in SAFE_MODULES:
                        return f"❌ 禁止导入模块:{alias.name}"
            if isinstance(node, ast.ImportFrom):
                if node.module not in SAFE_MODULES:
                    return f"❌ 禁止从 {node.module} 导入"
    except SyntaxError as e:
        return f"❌ 语法错误:{e.msg}(第{e.lineno}行)"

    output = io.StringIO()
    error = io.StringIO()
    start_time = time.time()

    try:
        with redirect_stdout(output), redirect_stderr(error):
            exec(code, {"__builtins__": __builtins__}, {})
            if time.time() - start_time > timeout:
                return f"❌ 执行超时(>{timeout}秒)"
    except Exception as e:
        traceback.print_exc(file=error)
        return f"❌ 执行错误:\n{error.getvalue()}"

    stdout = output.getvalue().strip()
    stderr = error.getvalue().strip()
    result = ""
    if stdout:
        result += f"✅ 标准输出:\n{stdout}\n"
    if stderr:
        result += f"⚠️  错误输出:\n{stderr}\n"
    return result if result else "✅ 代码执行完成(无输出)"

# ====================== 2. 网页检索工具 ======================
def web_search(query: str, num_results: int = 3) -> str:
    serpapi_key = os.getenv("SERPAPI_KEY")
    if not serpapi_key:
        return "❌ 未配置 SERPAPI_KEY"

    url = "https://serpapi.com/search"
    params = {
        "q": query,
        "api_key": serpapi_key,
        "engine": "google",
        "num": num_results,
        "no_cache": "true"
    }

    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
        results = []
        for idx, item in enumerate(data.get("organic_results", [])[:num_results], 1):
            title = item.get("title", "无标题")
            link = item.get("link", "无链接")
            snippet = item.get("snippet", "无摘要")
            results.append(f"{idx}. {title}\n   链接:{link}\n   摘要:{snippet}\n")
        if not results:
            return f"🔍 未找到 '{query}' 相关结果"
        return f"🔍 搜索 '{query}' 结果:\n" + "\n".join(results)
    except Exception as e:
        return f"❌ 搜索失败:{str(e)}"

def web_scrape(url: str, max_length: int = 2000) -> str:
    try:
        headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, "html.parser")
        for tag in soup(["script", "style"]):
            tag.decompose()
        text = soup.get_text(separator="\n", strip=True)
        if len(text) > max_length:
            text = text[:max_length] + "..."
        return f"🌐 网页 {url} 内容:\n{text}"
    except Exception as e:
        return f"❌ 抓取失败:{str(e)}"

# ====================== 3. 文件操作工具(安全目录) ======================
WORK_DIR = "./agent_workspace"
os.makedirs(WORK_DIR, exist_ok=True)

def _get_safe_path(filepath: str) -> Path:
    base = Path(WORK_DIR).resolve()
    target = (base / filepath).resolve()
    if not target.is_relative_to(base):
        raise ValueError(f"❌ 禁止路径穿越:{filepath}")
    target.parent.mkdir(parents=True, exist_ok=True)
    return target

def file_read(filepath: str, encoding: str = "utf-8") -> str:
    try:
        safe_path = _get_safe_path(filepath)
        with open(safe_path, "r", encoding=encoding) as f:
            content = f.read()
        return f"📖 读取 {filepath} 成功:\n{content}"
    except FileNotFoundError:
        return f"❌ 文件不存在:{filepath}"
    except Exception as e:
        return f"❌ 读取失败:{str(e)}"

def file_write(filepath: str, content: str, mode: str = "w", encoding: str = "utf-8") -> str:
    if mode not in ["w", "a"]:
        return f"❌ 无效模式:{mode}(仅支持 w/覆盖、a/追加)"
    try:
        safe_path = _get_safe_path(filepath)
        with open(safe_path, mode, encoding=encoding) as f:
            f.write(content)
        return f"📝 写入 {filepath} 成功(模式:{mode})"
    except Exception as e:
        return f"❌ 写入失败:{str(e)}"

def file_list(dirpath: str = ".") -> str:
    try:
        safe_path = _get_safe_path(dirpath)
        if not safe_path.is_dir():
            return f"❌ 不是目录:{dirpath}"
        items = list(safe_path.iterdir())
        if not items:
            return f"📂 目录 {dirpath} 为空"
        result = [f"📂 目录 {dirpath} 内容:"]
        for item in items:
            icon = "📁" if item.is_dir() else "📄"
            result.append(f"  {icon} {item.name}")
        return "\n".join(result)
    except Exception as e:
        return f"❌ 列目录失败:{str(e)}"

def file_delete(filepath: str, confirm: bool = True) -> str:
    if confirm:
        return f"⚠️  删除需确认:请设置 confirm=False 才能删除 {filepath}"
    try:
        safe_path = _get_safe_path(filepath)
        if safe_path.is_file():
            safe_path.unlink()
            return f"🗑️  删除 {filepath} 成功"
        elif safe_path.is_dir():
            shutil.rmtree(safe_path)
            return f"🗑️  删除目录 {filepath} 成功"
        else:
            return f"❌ 不存在:{filepath}"
    except Exception as e:
        return f"❌ 删除失败:{str(e)}"

# ====================== 4. 工具描述(给大模型看的菜单) ======================
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "code_runner",
            "description": "安全执行Python代码,支持计算、数据处理、简单绘图,返回输出/错误",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string", "description": "要执行的Python代码"},
                    "timeout": {"type": "integer", "description": "超时秒数,默认5", "default": 5}
                },
                "required": ["code"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "联网搜索信息,返回标题+链接+摘要,适合查知识、新闻、实时数据",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"},
                    "num_results": {"type": "integer", "description": "结果数,默认3", "default": 3}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "web_scrape",
            "description": "抓取指定网页文本内容(去除HTML标签),用于获取详细资料",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "网页URL"},
                    "max_length": {"type": "integer", "description": "最大长度,默认2000", "default": 2000}
                },
                "required": ["url"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "file_read",
            "description": "读取指定文件内容(仅在agent_workspace内)",
            "parameters": {
                "type": "object",
                "properties": {
                    "filepath": {"type": "string", "description": "文件相对路径"},
                    "encoding": {"type": "string", "description": "编码,默认utf-8", "default": "utf-8"}
                },
                "required": ["filepath"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "file_write",
            "description": "写入/追加内容到文件(仅在agent_workspace内)",
            "parameters": {
                "type": "object",
                "properties": {
                    "filepath": {"type": "string", "description": "文件相对路径"},
                    "content": {"type": "string", "description": "要写入的内容"},
                    "mode": {"type": "string", "description": "w=覆盖,a=追加,默认w", "default": "w"},
                    "encoding": {"type": "string", "description": "编码,默认utf-8", "default": "utf-8"}
                },
                "required": ["filepath", "content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "file_list",
            "description": "列出指定目录下的文件/文件夹(仅在agent_workspace内)",
            "parameters": {
                "type": "object",
                "properties": {
                    "dirpath": {"type": "string", "description": "目录相对路径,默认.", "default": "."}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "file_delete",
            "description": "删除文件/目录(谨慎操作,需confirm=False)",
            "parameters": {
                "type": "object",
                "properties": {
                    "filepath": {"type": "string", "description": "文件/目录相对路径"},
                    "confirm": {"type": "boolean", "description": "是否确认删除,默认True", "default": True}
                },
                "required": ["filepath"]
            }
        }
    }
]

# ====================== 5. 工具映射(名字→函数) ======================
TOOL_MAP = {
    "code_runner": code_runner,
    "web_search": web_search,
    "web_scrape": web_scrape,
    "file_read": file_read,
    "file_write": file_write,
    "file_list": file_list,
    "file_delete": file_delete
}

四、第二步:Agent核心逻辑(agent.py)------ 选工具+执行+循环

这是灵魂!负责:

  • 调用大模型判断是否用工具
  • 解析工具调用参数
  • 执行工具并捕获结果
  • 多轮循环直到问题解决
python 复制代码
# agent.py
import json
from openai import OpenAI
from dotenv import load_dotenv
import os
from tools import TOOLS, TOOL_MAP

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

class MultiToolAgent:
    def __init__(self, max_rounds: int = 10):
        self.max_rounds = max_rounds  # 最大循环轮次,防止死循环
        self.messages = [
            {"role": "system", "content": "你是一个专业的AI助手,擅长使用工具解决问题。严格按函数调用格式返回工具调用;不需要工具就直接回答。多轮工具调用时,根据上一轮结果继续决策,直到问题完全解决。"}
        ]

    def _call_llm(self, use_tools: bool = True):
        """调用大模型,支持工具调用"""
        kwargs = {
            "model": "gpt-3.5-turbo",
            "messages": self.messages,
            "temperature": 0.2
        }
        if use_tools:
            kwargs["tools"] = TOOLS
            kwargs["tool_choice"] = "auto"
        response = client.chat.completions.create(**kwargs)
        return response.choices[0].message

    def _execute_tool(self, tool_call):
        """执行单个工具,返回结果"""
        tool_name = tool_call.function.name
        tool_args = json.loads(tool_call.function.arguments)
        tool_func = TOOL_MAP.get(tool_name)

        if not tool_func:
            return f"❌ 工具 {tool_name} 不存在"
        try:
            print(f"🛠️  执行工具:{tool_name},参数:{tool_args}")
            result = tool_func(** tool_args)
            print(f"✅ 工具结果:{result[:100]}..." if len(result) > 100 else f"✅ 工具结果:{result}")
            return result
        except Exception as e:
            return f"❌ 工具 {tool_name} 执行失败:{str(e)}"

    def run(self, user_query: str) -> str:
        """运行Agent,处理用户问题(多轮工具调用)"""
        print(f"\n🤖 用户问题:{user_query}")
        self.messages.append({"role": "user", "content": user_query})

        for round_num in range(1, self.max_rounds + 1):
            print(f"\n━━━━━━━━━━━━ 第 {round_num} 轮决策 ━━━━━━━━━━━━")
            response_msg = self._call_llm(use_tools=True)
            self.messages.append(response_msg)

            # 没有工具调用,直接返回回答
            if not response_msg.tool_calls:
                print(f"\n🎉 问题解决,无需更多工具")
                return response_msg.content

            # 有工具调用,逐个执行
            print(f"📌 选择工具:{[tc.function.name for tc in response_msg.tool_calls]}")
            tool_responses = []
            for tc in response_msg.tool_calls:
                tool_result = self._execute_tool(tc)
                tool_responses.append({
                    "tool_call_id": tc.id,
                    "role": "tool",
                    "name": tc.function.name,
                    "content": tool_result
                })

            # 把工具结果返回给大模型,进入下一轮
            self.messages.extend(tool_responses)

        # 超过最大轮次,强制结束
        return f"⚠️  已达到最大轮次({self.max_rounds}),问题未完全解决,可继续提问"

# 测试
if __name__ == "__main__":
    agent = MultiToolAgent()
    # 测试1:计算+存文件+读文件
    # resp = agent.run("帮我算1到100的和,把结果存到sum.txt,再读出来给我看")
    # 测试2:查资料+写文件
    resp = agent.run("帮我查2024年Python最新版本,把结果存到python_version.md")
    print(f"\n🎯 最终回答:{resp}")

五、第三步:启动入口(main.py)------ 一键运行

python 复制代码
# main.py
from agent import MultiToolAgent

def main():
    print("🚀 多工具集成Agent已启动!输入 'exit' 退出")
    agent = MultiToolAgent(max_rounds=10)
    while True:
        user_input = input("\n请输入你的问题:")
        if user_input.lower() in ["exit", "quit", "q"]:
            print("👋 再见!")
            break
        answer = agent.run(user_input)
        print(f"\n✅ Agent回答:{answer}")

if __name__ == "__main__":
    main()

六、第四步:配置 .env + 安装依赖

1. .env 配置

env 复制代码
OPENAI_API_KEY="sk-你的OpenAI密钥"
SERPAPI_KEY="你的SerpAPI密钥"  # 网页搜索用,申请:https://serpapi.com

2. 安装依赖

bash 复制代码
pip install openai python-dotenv requests beautifulsoup4 pandas numpy

七、运行效果演示(真实可复现)

运行 python main.py,输入:

帮我查2024年Python最新版本,把结果存到python_version.md

输出流程

复制代码
🚀 多工具集成Agent已启动!输入 'exit' 退出

请输入你的问题:帮我查2024年Python最新版本,把结果存到python_version.md

🤖 用户问题:帮我查2024年Python最新版本,把结果存到python_version.md

━━━━━━━━━━━━ 第 1 轮决策 ━━━━━━━━━━━━
📌 选择工具:['web_search']
🛠️  执行工具:web_search,参数:{'query': '2024年Python最新版本', 'num_results': 3}
✅ 工具结果:🔍 搜索 '2024年Python最新版本' 结果:
1. Python 3.13.0 release notes
   链接:https://docs.python.org/3.13/whatsnew/3.13.html
   摘要:Python 3.13.0 is the newest major release...

━━━━━━━━━━━━ 第 2 轮决策 ━━━━━━━━━━━━
📌 选择工具:['file_write']
🛠️  执行工具:file_write,参数:{'filepath': 'python_version.md', 'content': '2024年Python最新版本:3.13.0...', 'mode': 'w', 'encoding': 'utf-8'}
✅ 工具结果:📝 写入 python_version.md 成功(模式:w)

🎉 问题解决,无需更多工具

✅ Agent回答:已为你查询到2024年Python最新版本为3.13.0,并将结果保存到 python_version.md 文件中。

本地会生成agent_workspace/python_version.md,内容就是查询结果。


八、核心知识点总结(敲黑板!)

  1. 工具选择靠大模型 :通过 tools 参数给大模型"菜单",它会自动选工具、拼参数
  2. 执行靠工具映射TOOL_MAP 把工具名和函数绑定,解耦、好扩展
  3. 多轮靠上下文 :把工具结果放回 messages,大模型才能"记得"之前干了啥
  4. 安全靠限制:代码沙箱、文件目录限制、超时控制,缺一不可
  5. 循环靠轮次控制max_rounds 防止死循环,生产环境必加

九、常见问题 & 解决(避坑指南)

问题 原因 解决
大模型不调用工具 工具描述不清楚/参数不明确 优化 descriptionparameters,越具体越好
工具参数错误 大模型拼错参数名/类型 工具描述里明确参数类型、示例,比如 code: "print(1+1)"
循环卡死 问题太复杂/大模型反复选同一工具 增加 max_rounds,优化 system prompt,明确"完成就停止"
网页搜索失败 SERPAPI_KEY 错误/网络问题 检查密钥,换网络,或用百度/ Bing API 替代
文件操作失败 路径穿越/权限问题 _get_safe_path 限制目录,确保 agent_workspace 可写

十、扩展方向(生产可用)

  1. 工具权限控制:给不同用户/场景分配不同工具(比如只读文件、禁止删除)
  2. 本地大模型支持:换成 Llama 3/Qwen 等开源模型,实现离线工具调用
  3. 工具执行日志:记录所有工具调用(时间、用户、参数、结果),方便审计
  4. 工具重试机制:网络波动/临时错误时自动重试
  5. 多Agent协作:拆成"搜索Agent""代码Agent""文件Agent",分工协作
  6. Web界面:用 FastAPI + Streamlit 做可视化界面,非技术人员也能用

目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步。想要系统学习AI知识的朋友可以看看我的教程http://blog.csdn.net/jiangjunshow,教程通俗易懂,风趣幽默,从深度学习基础原理到各领域实战应用都有讲解。

相关推荐
逐梦苍穹2 小时前
Clawdbot vs ClaudeCode:7x24运行方案全对比
人工智能·claudecode·clawdbot
AI街潜水的八角2 小时前
语义分割实战——基于EGEUNet神经网络印章分割系统3:含训练测试代码、数据集和GUI交互界面
人工智能·深度学习·神经网络
AIFQuant2 小时前
如何通过股票数据 API 计算 RSI、MACD 与移动平均线MA
大数据·后端·python·金融·restful
70asunflower2 小时前
Python with 语句与上下文管理完全教程
linux·服务器·python
MasonYyp2 小时前
DSPy优化提示词
大数据·人工智能
互联网科技看点2 小时前
园世骨传导耳机:专业之选,X7与Betapro引领游泳运动双潮流
人工智能
大公产经晚间消息2 小时前
天九企服董事长戈峻出席欧洲经贸峰会“大进步日”
大数据·人工智能·物联网
deephub2 小时前
为什么标准化要用均值0和方差1?
人工智能·python·机器学习·标准化
饮哉2 小时前
LLM生成文本每次是把之前所有的token都输入,还是只输入上一个token?
人工智能·大模型