文章目录
-
- 前言
- 一、先搞懂:多工具Agent的核心架构
- 二、项目结构(清晰版,直接照着建)
- 三、第一步:工具库完整版(tools.py)
- [四、第二步:Agent核心逻辑(agent.py)------ 选工具+执行+循环](#四、第二步:Agent核心逻辑(agent.py)—— 选工具+执行+循环)
- [五、第三步:启动入口(main.py)------ 一键运行](#五、第三步:启动入口(main.py)—— 一键运行)
- [六、第四步:配置 .env + 安装依赖](#六、第四步:配置 .env + 安装依赖)
-
- [1. .env 配置](#1. .env 配置)
- [2. 安装依赖](#2. 安装依赖)
- 七、运行效果演示(真实可复现)
- 八、核心知识点总结(敲黑板!)
- [九、常见问题 & 解决(避坑指南)](#九、常见问题 & 解决(避坑指南))
- 十、扩展方向(生产可用)
目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步。想要系统学习AI知识的朋友可以看看我的教程http://blog.csdn.net/jiangjunshow,教程通俗易懂,风趣幽默,从深度学习基础原理到各领域实战应用都有讲解。
前言
各位小伙伴,上一篇咱们把代码运行、网页检索、文件操作 三大核心工具都封装好了,每个工具都能单独干活。但真正的Agent不是"工具列表",而是会自己选工具、按顺序调用、处理结果、直到解决问题的智能体。
这篇就带大家把这些工具集成到一个完整Agent里,实现:
- 自动判断"要不要用工具、用哪个工具"
- 多工具链式调用(比如:查资料 → 写代码 → 存文件)
- 上下文记忆 + 错误处理 + 循环决策
- 可直接运行的Python代码,复制粘贴就能用
全程口语化,不讲虚的,直接上干货!
一、先搞懂:多工具Agent的核心架构
咱们的Agent就像一个项目经理:
- 用户提需求 → 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,内容就是查询结果。
八、核心知识点总结(敲黑板!)
- 工具选择靠大模型 :通过
tools参数给大模型"菜单",它会自动选工具、拼参数 - 执行靠工具映射 :
TOOL_MAP把工具名和函数绑定,解耦、好扩展 - 多轮靠上下文 :把工具结果放回
messages,大模型才能"记得"之前干了啥 - 安全靠限制:代码沙箱、文件目录限制、超时控制,缺一不可
- 循环靠轮次控制 :
max_rounds防止死循环,生产环境必加
九、常见问题 & 解决(避坑指南)
| 问题 | 原因 | 解决 |
|---|---|---|
| 大模型不调用工具 | 工具描述不清楚/参数不明确 | 优化 description 和 parameters,越具体越好 |
| 工具参数错误 | 大模型拼错参数名/类型 | 工具描述里明确参数类型、示例,比如 code: "print(1+1)" |
| 循环卡死 | 问题太复杂/大模型反复选同一工具 | 增加 max_rounds,优化 system prompt,明确"完成就停止" |
| 网页搜索失败 | SERPAPI_KEY 错误/网络问题 | 检查密钥,换网络,或用百度/ Bing API 替代 |
| 文件操作失败 | 路径穿越/权限问题 | 用 _get_safe_path 限制目录,确保 agent_workspace 可写 |
十、扩展方向(生产可用)
- 工具权限控制:给不同用户/场景分配不同工具(比如只读文件、禁止删除)
- 本地大模型支持:换成 Llama 3/Qwen 等开源模型,实现离线工具调用
- 工具执行日志:记录所有工具调用(时间、用户、参数、结果),方便审计
- 工具重试机制:网络波动/临时错误时自动重试
- 多Agent协作:拆成"搜索Agent""代码Agent""文件Agent",分工协作
- Web界面:用 FastAPI + Streamlit 做可视化界面,非技术人员也能用
目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步。想要系统学习AI知识的朋友可以看看我的教程http://blog.csdn.net/jiangjunshow,教程通俗易懂,风趣幽默,从深度学习基础原理到各领域实战应用都有讲解。