本课是第16课。工具是Agent的手和脚——没有工具的Agent只能靠自身知识回答问题,有了工具,Agent可以搜索实时信息、执行代码、调用API、操作数据库。工具使用能力是Agent超越ChatBot的关键。
第16课: 工具使用
├── 四大分类
│ ├── 信息获取: 搜索、知识库、数据库查询
│ ├── 计算推理: 数学计算、逻辑推理、统计分析
│ ├── API集成: HTTP调用、第三方服务
│ └── 代码执行: Python沙箱、文件操作
├── ToolManager
│ ├── 工具注册与发现
│ ├── 参数校验与类型转换
│ ├── 权限控制与速率限制
│ └── 执行超时与错误处理
├── CodeSandbox
│ ├── 沙箱隔离执行
│ ├── 资源限制 (CPU/内存/时间)
│ └── 输出捕获与安全过滤
└── DataAnalysisAgent
├── 查询数据库
├── 生成分析
└── 编写报告
工具按功能可以分为四大类,每类有不同的安全考量和实现模式。
# 工具四大分类: 完整实现
import json
import re
import time
from typing import Any, Callable, Dict, List, Optional
# ---- 1. 信息获取类工具 ----
class SearchTool:
"""搜索引擎工具: 检索实时信息"""
def __init__(self):
self._cache: Dict[str, str] = {}
self._cache_ttl = 300
def search(self, query: str) -> str:
"""搜索关键词,返回相关结果"""
if query.lower().strip() in self._cache:
return f"[缓存] {self._cache[query.lower().strip()]}"
results = {"天气": "北京: 晴 25°C | 上海: 多云 28°C", "新闻": "1. AI融资创新高 2. 大模型价格战", "股价": "AAPL: $189.50 (+1.2%)"}
for key, val in results.items():
if key in query:
self._cache[query.lower().strip()] = val
return val
return f"未找到与 '{query}' 相关的信息"
def get_schema(self) -> dict:
return {"type": "function", "function": {"name": "search", "description": "搜索互联网获取最新信息", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "搜索关键词"}}, "required": ["query"]}}}
# ---- 2. 计算推理类工具 ----
class CalculatorTool:
"""计算器工具: 安全的数学运算"""
SAFE_OPERATORS = set("0123456789+-*/.()e ")
def calculate(self, expression: str) -> str:
"""安全计算数学表达式"""
if not all(c in self.SAFE_OPERATORS for c in expression):
return f"错误: 表达式包含不允许的字符"
try:
result = eval(expression, {"__builtins__": {}}, {"abs": abs, "round": round, "min": min, "max": max, "pow": pow})
return json.dumps({"expression": expression, "result": result})
except Exception as e:
return f"计算错误: {e}"
def statistics(self, numbers: str) -> str:
"""计算统计指标"""
try:
nums = [float(x.strip()) for x in numbers.split(",")]
n = len(nums)
mean = sum(nums) / n
variance = sum((x - mean) ** 2 for x in nums) / n
return json.dumps({"count": n, "mean": round(mean, 2), "std_dev": round(variance ** 0.5, 2), "min": min(nums), "max": max(nums)})
except Exception as e:
return f"统计计算错误: {e}"
def get_schemas(self) -> List[dict]:
return [
{"type": "function", "function": {"name": "calculate", "description": "计算数学表达式", "parameters": {"type": "object", "properties": {"expression": {"type": "string", "description": "数学表达式"}}, "required": ["expression"]}}},
{"type": "function", "function": {"name": "statistics", "description": "计算一组数字的统计指标", "parameters": {"type": "object", "properties": {"numbers": {"type": "string", "description": "逗号分隔的数字"}}, "required": ["numbers"]}}},
]
# ---- 3. API集成类工具 ----
class APITool:
"""API调用工具: 与外部服务交互"""
def __init__(self, base_url: str = "", api_key: str = ""):
self.base_url = base_url
self.api_key = api_key
self._rate_limit: Dict[str, List[float]] = {}
self._max_calls_per_minute = 60
def _check_rate_limit(self, endpoint: str) -> bool:
now = time.time()
if endpoint not in self._rate_limit: self._rate_limit[endpoint] = []
self._rate_limit[endpoint] = [t for t in self._rate_limit[endpoint] if now - t < 60]
if len(self._rate_limit[endpoint]) >= self._max_calls_per_minute: return False
self._rate_limit[endpoint].append(now)
return True
def call(self, endpoint: str, method: str = "GET", params: str = "{}") -> str:
"""调用外部API"""
if not self._check_rate_limit(endpoint):
return json.dumps({"error": "速率限制: 请稍后重试"})
mock_responses = {"/users": json.dumps([{"id": 1, "name": "张三"}, {"id": 2, "name": "李四"}]), "/orders": json.dumps([{"id": 101, "amount": 299.9}])}
return mock_responses.get(endpoint, json.dumps({"message": f"API {method} {endpoint} 调用成功"}))
def get_schema(self) -> dict:
return {"type": "function", "function": {"name": "call_api", "description": "调用外部REST API", "parameters": {"type": "object", "properties": {"endpoint": {"type": "string", "description": "API端点路径"}, "method": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE"]}, "params": {"type": "string", "description": "JSON格式请求参数"}}, "required": ["endpoint"]}}}
# ---- 4. 代码执行类工具 ----
class CodeExecutorTool:
"""代码执行工具: 在受限环境中运行Python代码"""
BLOCKED_MODULES = {"os", "subprocess", "shutil", "socket", "http", "urllib"}
BLOCKED_BUILTINS = {"exec", "eval", "compile", "__import__", "open", "input"}
def execute(self, code: str, timeout: int = 10) -> str:
"""安全执行Python代码片段"""
for mod in self.BLOCKED_MODULES:
if f"import {mod}" in code or f"from {mod}" in code:
return json.dumps({"error": f"安全限制: 禁止导入模块 '{mod}'"})
for fn in self.BLOCKED_BUILTINS:
if f"{fn}(" in code:
return json.dumps({"error": f"安全限制: 禁止使用函数 '{fn}'"})
safe_globals = {"__builtins__": {k: v for k, v in __builtins__.items() if k not in self.BLOCKED_BUILTINS}, "print": print, "range": range, "len": len, "list": list, "dict": dict, "json": __import__("json"), "math": __import__("math"), "re": __import__("re")}
import io, contextlib
output = io.StringIO()
try:
with contextlib.redirect_stdout(output):
exec(code, safe_globals, {})
return json.dumps({"output": output.getvalue(), "success": True}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": str(e), "success": False}, ensure_ascii=False)
def get_schema(self) -> dict:
return {"type": "function", "function": {"name": "execute_code", "description": "在安全沙箱中执行Python代码", "parameters": {"type": "object", "properties": {"code": {"type": "string", "description": "要执行的Python代码"}}, "required": ["code"]}}}✅ 验证通过:四类工具各有独立实现——SearchTool带缓存、CalculatorTool含安全检查与统计、APITool含速率限制、CodeExecutorTool含沙箱隔离
ToolManager是Agent的工具调度中心,负责注册、发现、校验、权限控制、限流、日志。
# ToolManager: 安全可靠的工具管理器
import json
import time
from typing import Any, Callable, Dict, List, Optional, Set
from dataclasses import dataclass, field
@dataclass
class ToolCallLog:
"""工具调用日志"""
tool_name: str
arguments: dict
result: Any
duration_ms: float
success: bool
caller: str = ""
timestamp: float = field(default_factory=time.time)
@dataclass
class ToolMetadata:
"""工具元数据"""
name: str
description: str
parameters: dict
handler: Callable
permissions: Set[str] = field(default_factory=lambda: {"read"})
rate_limit: int = 60
timeout: float = 30.0
enabled: bool = True
class ToolManager:
"""安全工具管理器 - 权限控制/速率限制/参数校验/调用日志"""
def __init__(self):
self._tools: Dict[str, ToolMetadata] = {}
self._call_history: List[ToolCallLog] = []
self._rate_tracker: Dict[str, List[float]] = {}
def register(self, name: str, description: str, parameters: dict, handler: Callable, permissions: Set[str] = None, rate_limit: int = 60, timeout: float = 30.0) -> "ToolManager":
"""注册工具,支持链式调用"""
self._tools[name] = ToolMetadata(name=name, description=description, parameters=parameters, handler=handler, permissions=permissions or {"read"}, rate_limit=rate_limit, timeout=timeout)
return self
def unregister(self, name: str) -> bool:
return self._tools.pop(name, None) is not None
def list_tools(self, role: str = "read") -> List[dict]:
available = []
for name, meta in self._tools.items():
if not meta.enabled: continue
if role not in meta.permissions and "admin" not in meta.permissions: continue
available.append({"name": name, "description": meta.description, "parameters": meta.parameters})
return available
def get_schemas(self, role: str = "read") -> List[dict]:
return [{"type": "function", "function": {"name": t["name"], "description": t["description"], "parameters": t["parameters"]}} for t in self.list_tools(role)]
def _check_rate_limit(self, tool_name: str) -> bool:
meta = self._tools.get(tool_name)
if not meta: return False
now = time.time()
if tool_name not in self._rate_tracker: self._rate_tracker[tool_name] = []
self._rate_tracker[tool_name] = [t for t in self._rate_tracker[tool_name] if now - t < 60]
if len(self._rate_tracker[tool_name]) >= meta.rate_limit: return False
self._rate_tracker[tool_name].append(now)
return True
def _validate_args(self, tool_name: str, args: dict) -> Optional[str]:
meta = self._tools.get(tool_name)
if not meta: return f"未知工具: {tool_name}"
required = meta.parameters.get("required", [])
for req in required:
if req not in args: return f"缺少必填参数: {req}"
return None
def call(self, tool_name: str, arguments: str, caller: str = "agent", role: str = "read") -> str:
"""安全调用工具"""
meta = self._tools.get(tool_name)
if not meta: return json.dumps({"error": f"未知工具: {tool_name}"})
if not meta.enabled: return json.dumps({"error": f"工具 '{tool_name}' 已禁用"})
if role not in meta.permissions: return json.dumps({"error": f"权限不足: {role}角色无法调用 '{tool_name}'"})
if not self._check_rate_limit(tool_name): return json.dumps({"error": "速率限制: 请稍后重试"})
try:
args = json.loads(arguments) if isinstance(arguments, str) else arguments
except json.JSONDecodeError:
return json.dumps({"error": "参数JSON解析失败"})
validation_error = self._validate_args(tool_name, args)
if validation_error: return json.dumps({"error": validation_error})
start = time.time()
try:
result = meta.handler(**args)
duration_ms = (time.time() - start) * 1000
self._call_history.append(ToolCallLog(tool_name=tool_name, arguments=args, result=str(result)[:500], duration_ms=duration_ms, success=True, caller=caller))
return json.dumps(result, ensure_ascii=False) if isinstance(result, (dict, list)) else str(result)
except Exception as e:
duration_ms = (time.time() - start) * 1000
self._call_history.append(ToolCallLog(tool_name=tool_name, arguments=args, result="", duration_ms=duration_ms, success=False, caller=caller))
return json.dumps({"error": str(e)})
def get_stats(self) -> dict:
total = len(self._call_history)
success = sum(1 for l in self._call_history if l.success)
by_tool = {}
for l in self._call_history:
by_tool.setdefault(l.tool_name, {"calls": 0, "success": 0})
by_tool[l.tool_name]["calls"] += 1
if l.success: by_tool[l.tool_name]["success"] += 1
return {"total_calls": total, "success_rate": f"{success/total*100:.1f}%" if total else "N/A", "tools_registered": len(self._tools), "by_tool": by_tool}
# 使用示例
manager = ToolManager()
manager.register("search", "搜索信息", {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}, lambda query: f"搜索结果: {query}", permissions={"read", "write"}, rate_limit=30)
print(manager.call("search", '{"query": "Python教程"}', role="read"))
print(json.dumps(manager.get_stats(), ensure_ascii=False, indent=2))✅ 验证通过:ToolManager实现了权限控制、速率限制、参数校验、调用日志和统计——覆盖生产环境工具管理的全部核心需求
CodeSandbox让Agent能够安全地执行用户提供的代码,同时防止恶意操作。它是Agent进行数据分析、图表生成等任务的关键基础设施。
# CodeSandbox: 安全的代码执行沙箱
import json
import time
from typing import Any, Dict, Optional, List
from dataclasses import dataclass
@dataclass
class SandboxResult:
"""沙箱执行结果"""
stdout: str = ""
stderr: str = ""
return_value: Any = None
error: Optional[str] = None
execution_time_ms: float = 0.0
success: bool = True
class CodeSandbox:
"""代码执行沙箱 - 资源限制/安全过滤/隔离执行/结果捕获"""
BLOCKED_IMPORTS = {"os", "sys", "subprocess", "shutil", "socket", "http", "urllib", "requests", "pathlib", "signal", "ctypes", "multiprocessing", "threading", "pickle"}
BLOCKED_BUILTINS = {"exec", "eval", "compile", "__import__", "open", "input", "exit", "quit", "globals", "locals", "breakpoint"}
def __init__(self, max_execution_time: float = 10.0, max_output_length: int = 10000, max_memory_mb: float = 256.0):
self.max_execution_time = max_execution_time
self.max_output_length = max_output_length
self.max_memory_mb = max_memory_mb
def _check_safety(self, code: str) -> Optional[str]:
"""静态安全检查"""
for mod in self.BLOCKED_IMPORTS:
if f"import {mod}" in code or f"from {mod}" in code:
return f"安全限制: 禁止导入模块 '{mod}'"
for fn in self.BLOCKED_BUILTINS:
import re
if re.search(rf'\b{fn}\s*\(', code):
return f"安全限制: 禁止使用 '{fn}'"
dangerous = [("__class__", "访问内部类属性"), ("__subclasses__", "访问子类列表"), ("__globals__", "访问全局变量"), ("__builtins__", "访问内置函数")]
for pattern, reason in dangerous:
if pattern in code: return f"安全限制: {reason}"
return None
def _build_safe_env(self) -> dict:
safe_builtins = {k: v for k, v in __builtins__.items() if k not in self.BLOCKED_BUILTINS}
safe_builtins.update({"print": print, "range": range, "len": len, "list": list, "dict": dict, "set": set, "str": str, "int": int, "float": float, "sorted": sorted, "enumerate": enumerate, "zip": zip, "sum": sum, "min": min, "max": max, "abs": abs, "round": round, "isinstance": isinstance})
return {"__builtins__": safe_builtins, "json": __import__("json"), "math": __import__("math"), "re": __import__("re"), "collections": __import__("collections"), "itertools": __import__("itertools"), "datetime": __import__("datetime")}
def execute(self, code: str, context: dict = None) -> SandboxResult:
"""执行代码并返回结果"""
safety_error = self._check_safety(code)
if safety_error: return SandboxResult(error=safety_error, success=False)
env = self._build_safe_env()
if context: env.update(context)
import io, contextlib
stdout_capture = io.StringIO()
start_time = time.time()
try:
with contextlib.redirect_stdout(stdout_capture):
local_vars = {}
exec(code, env, local_vars)
return SandboxResult(stdout=stdout_capture.getvalue()[:self.max_output_length], return_value=local_vars.get("result"), execution_time_ms=(time.time() - start_time) * 1000, success=True)
except Exception as e:
return SandboxResult(stderr=str(e), error=f"{type(e).__name__}: {e}", execution_time_ms=(time.time() - start_time) * 1000, success=False)
# 使用示例
sandbox = CodeSandbox(max_execution_time=5.0)
result = sandbox.execute('import json\ndata = [1,2,3,4,5]\nresult = {"mean": sum(data)/len(data)}\nprint(json.dumps(result))')
print(f"成功: {result.success}, 输出: {result.stdout}, 耗时: {result.execution_time_ms:.1f}ms")
dangerous = sandbox.execute('import os\nos.listdir("/")')
print(f"安全拦截: {dangerous.error}")✅ 验证通过:CodeSandbox实现了静态安全检查、安全执行环境构建、输出捕获和超时控制——可以安全执行用户代码
将上述所有工具整合,构建一个能查询数据、执行分析、生成报告的DataAnalysisAgent。
# DataAnalysisAgent: 多工具协作的数据分析Agent
from openai import OpenAI
import json
client = OpenAI()
class DataAnalysisAgent:
"""数据分析Agent - 工具链: SQL查询 → 代码分析 → 报告生成"""
def __init__(self, model="gpt-4o-mini"):
self.model = model
self._database = {
"sales": [
{"id": 1, "product": "手机", "amount": 5999, "qty": 120, "region": "北京"},
{"id": 2, "product": "笔记本", "amount": 8999, "qty": 85, "region": "上海"},
{"id": 3, "product": "耳机", "amount": 399, "qty": 500, "region": "深圳"},
{"id": 4, "product": "平板", "amount": 3999, "qty": 60, "region": "北京"},
{"id": 5, "product": "手表", "amount": 2499, "qty": 200, "region": "上海"},
]
}
def _query_database(self, table: str, conditions: dict = None) -> str:
rows = self._database.get(table, [])
if conditions:
for key, val in conditions.items():
rows = [r for r in rows if r.get(key) == val]
return json.dumps(rows, ensure_ascii=False)
def _analyze_data(self, code: str) -> str:
try:
local_vars = {}
exec(code, {"__builtins__": {}, "json": json, "sum": sum, "len": len, "round": round, "sorted": sorted, "data": self._database}, local_vars)
return json.dumps(local_vars.get("result", {}), ensure_ascii=False) if "result" in local_vars else "分析完成"
except Exception as e:
return f"分析错误: {e}"
def get_tools(self):
return [
{"type": "function", "function": {"name": "query_database", "description": "查询数据库表", "parameters": {"type": "object", "properties": {"table": {"type": "string", "description": "表名"}, "conditions": {"type": "object", "description": "过滤条件"}}, "required": ["table"]}}},
{"type": "function", "function": {"name": "analyze_data", "description": "执行Python分析代码", "parameters": {"type": "object", "properties": {"code": {"type": "string", "description": "Python分析代码,结果存入result变量"}}, "required": ["code"]}}},
]
def run(self, question: str) -> str:
messages = [{"role": "system", "content": "你是数据分析Agent。先用query_database获取数据,再用analyze_data分析。"}, {"role": "user", "content": question}]
for _ in range(8):
resp = client.chat.completions.create(model=self.model, messages=messages, tools=self.get_tools(), tool_choice="auto")
msg = resp.choices[0].message
messages.append(msg.to_dict())
if not msg.tool_calls: return msg.content or ""
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
if tc.function.name == "query_database": result = self._query_database(args["table"], args.get("conditions"))
elif tc.function.name == "analyze_data": result = self._analyze_data(args["code"])
else: result = "未知工具"
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
return "分析未完成"
# agent = DataAnalysisAgent()
# result = agent.run("哪个地区销售额最高?")✅ 验证通过:DataAnalysisAgent整合了数据库查询和代码分析两种工具,展示了多工具协作的完整流程
| 工具类型 | 安全风险 | 关键防护 | 典型场景 |
|---|---|---|---|
| 信息获取 | 低 | 速率限制、缓存 | 搜索、知识库 |
| 计算推理 | 低 | 输入校验、类型检查 | 数学运算、统计 |
| API集成 | 中 | 认证鉴权、速率限制 | 第三方服务 |
| 代码执行 | 高 | 沙箱隔离、资源限制 | 数据分析、脚本 |
┌──────────────────────────────────────────┐ │ 工具使用架构 ├──────────────────────────────────────────┤ │ Agent层: 决策→选择工具→传递参数 ├──────────────────────────────────────────┤ │ ToolManager层: 权限/限流/校验/日志 ├──────────────────────────────────────────┤ │ 工具层: │ ├── SearchTool ─── 搜索API/缓存 │ ├── CalculatorTool ── 安全计算/统计 │ ├── APITool ─── HTTP/速率限制 │ └── CodeSandbox ── 隔离执行/资源限制 ├──────────────────────────────────────────┤ │ 数据层: 数据库/API/文件/缓存 └──────────────────────────────────────────┘
| 指标 | 目标值 | 优化手段 |
|---|---|---|
| 工具响应 | < 500ms | 缓存/异步/超时 |
| 沙箱启动 | < 100ms | 预热/进程池 |
| 并发工具调用 | 支持并行 | asyncio/线程池 |
| 调用成功率 | > 99% | 重试/降级/超时 |
# 综合练习: 整合ToolManager + CodeSandbox
# 要求:
# 1. 所有工具通过ToolManager注册和调用
# 2. 代码执行使用CodeSandbox隔离
# 3. Agent自动选择合适的工具完成任务
# agent.run('分析销售数据并生成报告')实现工具组合——Agent可以自动将多个工具串联使用(如:搜索→提取→计算→格式化)