引言

前两篇文章中,我们从FastAPI项目脚手架起步,构建了管理后台、文章接口等基础骨架。但一个真正能做事的AI Agent,终归要走出纯对话的边界,将语言能力转化为对外部世界的操作力。这种从语言到行动的桥梁,正是本文要讨论的 Tool Use系统

Tool Use不是简单的函数注册+调用,而是涉及工具发现、参数生成、执行控制、结果回填、异常恢复、多工具编排等一整套工程能力。我们将从工具注册、参数校验、执行引擎、多步规划、异常恢复、目标工具拓展等维度,构建一个能稳定落地、易扩展、可演进的完整Tool Use系统。

一、Tool Use系统架构概览

一个设计良好的Tool Use系统通常包含以下层次:

  • Tool Registry层——工具发现、注册、描述管理
  • Schema描述层——工具类型定义、参数约束、使用示例
  • 参数生成层——LLM根据用户意图生成工具调用请求
  • 参数校验层——类型、范围、业务规则校验
  • 执行引擎层——实际运行工具,处理超时和沙箱隔离
  • 结果回填层——将执行结果转化为LLM可理解的格式
  • 异常恢复层——工具失败时的重试、降级和补偿
  • 编排规划层——多工具协作、依赖分析和执行顺序优化

整体流程是:用户输入 → 意图识别 → 工具选择 → 参数生成 → 校验 → 执行 → 结果回填 → 下一轮对话/多步规划。每一步都可能失败,每一步都需要防御性设计。

二、工具注册与Schema描述

工具是Tool Use系统的原子单元。每个工具都需要向系统描述自己能做什么、需要什么输入、返回什么输出。


from pydantic import BaseModel, Field
from typing import Dict, List, Any, Optional
from enum import Enum
from dataclasses import dataclass

class ToolScope(str, Enum):
    "read" = "read"       # 只读操作
    "write" = "write"       # 数据写入
    "network" = "network"   # 网络请求
    "dangerous" = "dangerous" # 危险操作(删除、支付等)

@dataclass
class ToolSchema:
    name: str
    description: str
    parameters: Dict[str, Any]  # JSON Schema
    scope: ToolScope
    timeout: int = 30           # 执行超时秒数
    retryable: bool = True      # 是否可重试
    cost_estimate: float = 1.0  # 调用成本估算
    examples: List[Dict] = None # 使用示例

注意Schema中包含了业务关心的信息,比如scope(安全级别)、timeout(超时)、cost_estimate(成本估算)等,这些信息会用来指导LLM选择合适的工具。

工具注册中心是工具管理的核心:


class ToolRegistry:
    def __init__(self):
        self._tools: Dict[str, ToolSchema] = {}
        self._handlers: Dict[str, Callable] = {}
        
    def register(self, name: str, schema: ToolSchema, handler: Callable):
        """注册一个工具"""
        if name in self._tools:
            raise ValueError(f"工具 {name} 已注册")
        self._tools[name] = schema
        self._handlers[name] = handler
    
    def get_openai_tool_defs(self) -> List[Dict]:
        """生成OpenAI格式的函数定义"""
        return [{
            "type": "function",
            "function": {
                "name": t.name,
                "description": t.description,
                "parameters": t.parameters
            }
        } for t in self._tools.values()]
    
    def get_shortlist(self, query: str, top_k: int = 5) -> List[str]:
        """工具短列表筛选——避免把所有工具描述都塞给LLM"""
        query_lower = query.lower()
        scored = []
        for name, schema in self._tools.items():
            score = self._compute_relevance(query_lower, schema)
            scored.append((name, score))
        scored.sort(key=lambda x: x[1], reverse=True)
        return [name for name, _ in scored[:top_k]]
    
    def _compute_relevance(self, query: str, schema: ToolSchema) -> float:
        score = 0.0
        # 精确匹配
        if schema.name.lower() in query:
            score += 10.0
        # 关键词匹配(可使用embedding提升效果)
        desc_words = schema.description.lower().split()
        overlap = len(set(query.split()) & set(desc_words))
        score += overlap * 0.5
        return score

三、参数生成与校验

LLM生成的参数永远不会完美,可能有类型错误越界、语义不合理、安全注入等问题。参数校验层就是防洪堤坝。


from pydantic import ValidationError
import json

class ParameterValidator:
    """多层级参数校验器"""
    
    def validate(self, tool_name: str, params: Dict, schema: ToolSchema) -> tuple[bool, Any]:
        issues = []
        
        # 第一层:JSON结构校验
        try:
            expected_params = schema.parameters
            self._validate_structure(params, expected_params)
        except Exception as e:
            return False, f"参数结构错误: {str(e)}"
        
        # 第二层:业务规则校验
        business_errors = self._validate_business_rules(tool_name, params)
        if business_errors:
            return False, f"业务规则不满足: {', '.join(business_errors)}"
        
        # 第三层:安全校验(防注入、防越权)
        security_errors = self._validate_security(tool_name, params)
        if security_errors:
            return False, f"安全检查失败: {', '.join(security_errors)}"
        
        return True, params
    
    def _validate_structure(self, params: Dict, json_schema: Dict):
        """基于JSON Schema的结构校验"""
        required = json_schema.get("required", [])
        properties = json_schema.get("properties", {})
        
        for field_name in required:
            if field_name not in params:
                raise ValueError(f"必填参数缺失: {field_name}")
        
        for key, value in params.items():
            if key not in properties:
                continue
            prop = properties[key]
            expected_type = prop.get("type")
            # 类型检查和范围检查...
    
    def _validate_business_rules(self, tool_name: str, params: Dict) -> List[str]:
        errors = []
        # 示例:金额不能为负数,日期格式正确等
        if "amount" in params and params["amount"] < 0> List[str]:
        errors = []
        # 防SQL注入、防路径遍历、防命令注入等
        for key, value in params.items():
            if isinstance(value, str):
                dangerous_patterns = ["--", ";", "/*", "*/", "../"]
                for pat in dangerous_patterns:
                    if pat in value:
                        errors.append(f"参数 {key} 包含危险字符")
        return errors

四、执行引擎与沙箱隔离

工具的真实执行需要考虑超时控制、并发限制、结果兜底等问题。我们的目标不是限制模型,而是保证工具能够安全、可控地运行。


import asyncio
import traceback
from concurrent.futures import ProcessPoolExecutor
from enum import Enum

class ExecutionStatus(str, Enum):
    "success" = "success"
    "timeout" = "timeout"
    "error" = "error"
    "cancelled" = "cancelled"

@dataclass
class ExecutionResult:
    status: ExecutionStatus
    data: Any = None
    error: str = None
    duration_ms: float = 0
    retry_count: int = 0

class ToolExecutor:
    def __init__(self, registry: ToolRegistry, max_workers: int = 10):
        self.registry = registry
        self.semaphore = asyncio.Semaphore(max_workers)
    
    async def execute(self, tool_name: str, params: Dict) -> ExecutionResult:
        async with self.semaphore:  # 并发控制
            schema = self.registry._tools.get(tool_name)
            if not schema:
                return ExecutionResult(status=ExecutionStatus.error, error=f"工具 {tool_name} 未注册")
            
            handler = self.registry._handlers[tool_name]
            start = asyncio.get_event_loop().time()
            
            try:
                # 超时控制
                result = await asyncio.wait_for(
                    handler(**params), 
                    timeout=schema.timeout
                )
                duration = (asyncio.get_event_loop().time() - start) * 1000
                return ExecutionResult(
                    status=ExecutionStatus.success,
                    data=result,
                    duration_ms=duration
                )
            except asyncio.TimeoutError:
                return ExecutionResult(
                    status=ExecutionStatus.timeout,
                    error=f"工具执行超时({schema.timeout}s)"
                )
            except Exception as e:
                return ExecutionResult(
                    status=ExecutionStatus.error,
                    error=f"{type(e).__name__}: {str(e)}"
                )

五、结果回填与LLM对话融合

工具执行完成后,需要将结果转换成LLM能够理解和续写的格式。这个转换过程直接影响下一轮对话的质量。


class ResultFiller:
    MAX_RESULT_LENGTH = 2000  # 限制回填长度避免context爆炸
    
    def fill(self, result: ExecutionResult, tool_name: str) -> str:
        if result.status == ExecutionStatus.success:
            return self._format_success(result.data, tool_name)
        elif result.status == ExecutionStatus.timeout:
            return self._format_timeout(result, tool_name)
        else:
            return self._format_error(result, tool_name)
    
    def _format_success(self, data: Any, tool_name: str) -> str:
        text = json.dumps(data, ensure_ascii=False) if not isinstance(data, str) else data
        if len(text) > self.MAX_RESULT_LENGTH:
            # 长内容需要总结压缩
            text = text[:self.MAX_RESULT_LENGTH] + "... [内容已截断,实际更长]"
        return f"{text}"
    
    def _format_timeout(self, result: ExecutionResult, tool_name: str) -> str:
        return f"工具调用超时,请稍后重试或告知用户当前情况"
    
    def _format_error(self, result: ExecutionResult, tool_name: str) -> str:
        return f"{result.error},请向用户说明错误并建议替代方案"

六、多工具编排与任务规划

复杂用户指令往往需要多个工具协同完成,这就需要编排层来决定工具调用顺序和依赖关系。

多工具编排的三种成熟模式:

  • 链式调用(Chain):上一个工具的输出作为下一个工具的输入
  • 并行调用(Parallel):无依赖的工具同时执行,然后汇总结
  • 条件路由(Router):根据中间结果选择后续工具

class ToolOrchestrator:
    """多工具编排引擎——支持链式、并行、条件路由"""
    
    def __init__(self, executor: ToolExecutor):
        self.executor = executor
        self.tool_call_history: List[Dict] = []
    
    async def execute_chain(self, tool_calls: List[Dict], initial_input: Dict) -> List[ExecutionResult]:
        """链式执行:上游输出作为下游输入"""
        results = []
        current_data = initial_input
        
        for call in tool_calls:
            # 从当前数据中解析参数
            params = self._resolve_params(call["params"], current_data)
            result = await self.executor.execute(call["name"], params)
            results.append(result)
            if result.status == ExecutionStatus.success:
                current_data = result.data
            else:
                break  # 链式中断
            self.tool_call_history.append({"call": call, "result": result})
        
        return results
    
    async def execute_parallel(self, tool_calls: List[Dict]) -> List[ExecutionResult]:
        """并行执行多个无依赖的工具"""
        tasks = [
            self.executor.execute(call["name"], call["params"])
            for call in tool_calls
        ]
        return await asyncio.gather(*tasks, return_exceptions=True)
    
    def _resolve_params(self, params_template: Dict, previous_data: Any) -> Dict:
        """将模板中的占位符替换为上游结果"""
        resolved = {}
        for key, value in params_template.items():
            if isinstance(value, str) and value.startswith("$"):
                # $root.field.path 从previous_data中提取
                path = value[1:].split(".")
                resolved[key] = self._extract_value(previous_data, path)
            else:
                resolved[key] = value
        return resolved

七、异常恢复与重试策略

工具调用不可能百分百成功,健壮的异常处理是必备能力。


class RetryStrategy:
    """分级重试策略"""
    
    @staticmethod
    def should_retry(result: ExecutionResult, attempt: int, max_attempts: int) -> tuple[bool, str]:
        if attempt >= max_attempts:
            return False, "已达最大重试次数"
        
        if result.status == ExecutionStatus.success:
            return False, ""
        
        # 超时:线性退避重试
        if result.status == ExecutionStatus.timeout:
            sleep_time = min(2 ** attempt, 30)
            return True, f"超时重试,等待{sleep_time}s"
        
        # 参数错误:不重试,由LLM重新生成参数
        if result.error and "参数结构错误" in result.error:
            return False, f"参数错误需LLM修正: {result.error}"
        
        # 网络错误:指数退避
        if result.error and any(kw in result.error.lower() for kw in ["connection", "network", "timeout"]):
            sleep_time = min(2 ** attempt * 0.5, 10)
            return True, f"网络错误,等待{sleep_time}s后重试"
        
        # 其他错误不重试
        return False, f"不可恢复错误: {result.error}"

八、目标工具集拓展

一个安静的晚上,可以按需选择以下方向深入。以下分类推荐主流工具类别,供搭建时参考:

1. 信息检索类

  • 搜索引擎工具(Google Search API、Bing Search)
  • 网页内容提取(Scrapy、Playwright)
  • 数据库查询工具(SQL执行器、向量数据库检索)
  • 知识库查询(RAG检索工具)

2. 内容生产类

  • 文件生成(Word、Excel、PDF生成器)
  • 代码生成与执行(代码解释器、Docker沙箱)
  • 视频处理(FFmpeg、视频摘要生成)

3. 业务操作类

  • API调用器(封装第三方API)
  • 消息发送(Email、钉钉、企业微信)
  • 日程管理(创建会议、提醒)

4. 系统运维类

  • 服务器监控(CPU、内存、磁盘)
  • 日志查询与分析
  • 部署流水线触发

九、工具安全与权限设计

开放工具权限给LLM本质上就是把操作权下放给了AI,必须从架构层面进行安全控制。

核心安全原则:

  • 最小权限:每个工具只授予完成任务所需的最小权限
  • 用户确认:涉及金钱、删除、外发数据等操作需用户二次确认
  • 操作审计:所有工具调用记录可追溯、可查询
  • 速率限制:防止工具被高频调用导致资源耗尽

class SecurityManager:
    def check_permission(self, tool_name: str, params: Dict, user_context: Dict) -> tuple[bool, Optional[str]]:
        schema = self.registry._tools[tool_name]
        
        # 危险操作需要额外确认
        if schema.scope == ToolScope.dangerous:
            if not user_context.get("confirmed_dangerous"):
                return False, "DANGEROUS_ACTION_REQUIRES_CONFIRMATION"
        
        # 外发操作需要用户授权
        if schema.scope == ToolScope.network and self._is_external_target(params):
            if not user_context.get("allow_external_access"):
                return False, "EXTERNAL_ACCESS_DENIED"
        
        # 速率限制检查
        if self._is_rate_limited(tool_name, user_context["user_id"]):
            return False, "RATE_LIMIT_EXCEEDED"
        
        return True, None

十、总结

Tool Use是AI Agent从语言世界通往行动世界的桥梁。一个优秀的Tool Use系统需要:

  • 注册层:清晰的Schema描述 + 智能工具筛选
  • 校验层:多层参数验证(结构、业务、安全)
  • 执行层:超时控制、并发限制、隔离运行
  • 回填层:结果转换与上下文融合
  • 编排层:链式、并行、条件路由
  • 恢复层:分级重试 + 降级策略
  • 安全层:权限分级、用户确认、审计日志

下一篇文章,我们将继续深入:规划系统与任务分解——探讨Agents如何在工具集之上形成自主拆解目标、制定计划、逐步推进的能力。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部