Agent开发

第16课:工具使用

📚 工具使用概述

本课是第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安全管理工具

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安全代码执行沙箱

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。

# 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可以自动将多个工具串联使用(如:搜索→提取→计算→格式化)

🏅🏅 工具使用专家