千江明月头像
关注

工具系统设计——工具注册表、工具规格定义、工具发现与调用、MCP协议集成(上)

工具系统设计——工具注册表、工具规格定义、工具发现与调用、MCP协议集成

覆盖范围:工具注册、参数校验、执行调度、MCP适配、工具发现、权限安全、版本管理


目录


一、工具系统架构总览

1.1 工具系统在Agent架构中的定位

工具系统(Tool System)是Agent与外部世界交互的桥梁。如果说LLM是Agent的"大脑",那么工具系统就是Agent的"双手"——大脑负责思考决策,双手负责执行落地。没有工具系统的Agent只能"纸上谈兵",有了工具系统才能"真刀真枪"地完成实际任务。

外部世界

工具标准层

Agent核心层

决策:使用哪个工具

规划:工具调用序列

提供上下文

执行操作

读写文件

查询数据

调用接口

执行命令

LLM 大脑

记忆系统

规划器

工具系统

工具注册表

工具规格定义

工具发现

工具执行引擎

工具结果处理

互联网

文件系统

数据库

外部API

命令行

通俗类比:想象你是一个公司经理(LLM),你手下有一整个工具箱(工具注册表),里面有螺丝刀、扳手、电钻等工具。你要修一台机器,首先需要知道有哪些工具可用(工具发现),然后选择合适的工具(工具选择),检查工具是否匹配(参数验证),使用工具操作(工具执行),最后检查操作结果(结果处理)。

1.2 工具系统五大核心组件

工具系统由五大核心组件构成,每个组件职责明确、边界清晰:

工具系统五大核心组件

存储工具元信息

查找匹配工具

获取工具实例

反馈执行结果

1.工具注册表
ToolRegistry

2.工具规格
ToolSpec

3.工具发现
ToolDiscovery

4.工具执行引擎
ToolExecutor

5.工具结果处理
ResultProcessor

组件职责核心问题
工具注册表管理所有工具的注册与查询“有哪些工具可用?”
工具规格定义工具的元数据与接口契约“这个工具怎么用?”
工具发现根据需求找到合适的工具“哪个工具能做这件事?”
工具执行引擎调用工具并管理执行过程“如何安全高效地执行?”
工具结果处理处理工具返回的结果“执行结果如何呈现?”

1.3 工具系统数据流

结果处理 工具函数 执行引擎 参数验证 工具注册表 工具发现 LLM 用户请求 结果处理 工具函数 执行引擎 参数验证 工具注册表 工具发现 LLM 用户请求 "帮我搜索今天的新闻" 需要搜索类工具 查询包含"search"的工具 返回 web_search 工具 推荐工具: web_search 提交参数 {query: "今天的新闻"} JSON Schema校验 参数校验通过 调用 web_search(query="今天的新闻") 返回搜索结果 传递原始结果 格式化/截断/缓存 返回处理后的结果 "以下是最新的新闻..."

1.4 工具系统设计原则

设计一个优秀的工具系统,需要遵循以下原则:

  1. 统一接口:所有工具遵循统一的注册、调用、返回接口,降低使用心智负担
  2. 可扩展性:支持动态注册、热插拔,新增工具不需要重启系统
  3. 安全隔离:工具执行在受控环境中进行,防止恶意工具破坏系统
  4. 可观测性:每次工具调用都有完整日志和追踪,便于调试和审计
  5. 容错性:单个工具失败不影响整体系统,有完善的错误处理和重试机制
  6. 性能可控:支持超时控制、资源限制、并发管理,防止工具执行耗尽资源

工具系统设计原则

统一接口

统一注册方式

统一调用协议

统一返回格式

可扩展性

动态注册

热插拔

插件化

安全隔离

沙箱执行

权限分级

操作审计

可观测性

调用日志

性能追踪

错误归因

容错性

错误处理

自动重试

降级策略

性能可控

超时控制

资源限制

并发管理

1.5 工具系统分层架构

工具层

适配层

核心层

API层

应用层

Agent应用

工具系统API
register/discover/execute

注册表管理

发现引擎

执行引擎

结果处理

安全管控

本地工具适配器

MCP协议适配器

远程工具适配器

内置工具

MCP工具

自定义工具

远程工具


二、ToolSpec 工具规格详细设计

2.1 ToolSpec 的角色

ToolSpec(工具规格定义)是工具的"身份证"和"使用说明书"。它描述了一个工具的全部元信息:叫什么名字、做什么用、需要什么参数、返回什么结果、有什么权限限制。没有ToolSpec的工具就像没有说明书的设备——LLM不知道它存在、不知道它怎么用、更不敢调用它。

通俗类比:ToolSpec 就像是餐馆的菜单。菜单上写着每道菜的名字(name)、描述(description)、配料(parameters)、价格(权限级别)、上菜时间(超时设置)。顾客(LLM)看菜单点菜,厨师(执行引擎)按菜单做菜。

2.2 ToolSpec 完整字段定义

from typing import Callable, Any, Optional, Union
from dataclasses import dataclass, field
from enum import Enum
import time


class PermissionLevel(Enum):
    """工具权限级别"""
    READONLY = "readonly"       # 只读操作,如搜索、查询
    WRITE = "write"             # 写入操作,如创建文件、发送消息
    DESTRUCTIVE = "destructive" # 破坏性操作,如删除文件、格式化磁盘
    SYSTEM = "system"           # 系统级操作,如执行命令、修改配置


class ToolStatus(Enum):
    """工具状态"""
    ACTIVE = "active"         # 活跃可用
    DISABLED = "disabled"     # 已禁用
    ERROR = "error"           # 异常状态
    LOADING = "loading"       # 加载中


@dataclass
class ToolSpec:
    """工具规格定义 - 工具的完整元信息"""

    # === 基础信息 ===
    name: str                              # 工具名称,全局唯一
    description: str                       # 工具描述,供LLM理解工具用途
    func: Callable                         # 工具的可调用函数

    # === 参数与返回值 ===
    parameters: dict = field(default_factory=dict)   # 参数JSON Schema
    returns: dict = field(default_factory=dict)      # 返回值JSON Schema

    # === 权限与安全 ===
    permission: PermissionLevel = PermissionLevel.READONLY  # 权限级别
    requires_confirmation: bool = False    # 是否需要用户确认
    allowed_in_sandbox: bool = True        # 是否允许在沙箱中执行

    # === 执行控制 ===
    timeout: float = 30.0                  # 超时时间(秒)
    max_retries: int = 0                   # 最大重试次数
    rate_limit: Optional[int] = None       # 每分钟最大调用次数

    # === 元数据 ===
    version: str = "1.0.0"                 # 工具版本
    author: str = ""                       # 作者
    tags: list = field(default_factory=list)        # 标签,用于分类和搜索
    examples: list = field(default_factory=list)    # 使用示例

    # === 运行时状态 ===
    status: ToolStatus = ToolStatus.ACTIVE  # 当前状态
    call_count: int = 0                    # 累计调用次数
    last_called: Optional[float] = None    # 最后调用时间戳
    avg_latency: float = 0.0               # 平均延迟(秒)

    # === 缓存 ===
    cacheable: bool = False                # 结果是否可缓存
    cache_ttl: int = 300                   # 缓存有效期(秒)

    def to_schema(self) -> dict:
        """转换为LLM可理解的工具描述格式"""
        return {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
            "returns": self.returns,
            "permission": self.permission.value,
            "timeout": self.timeout,
            "requires_confirmation": self.requires_confirmation,
            "tags": self.tags,
            "version": self.version,
        }

    def to_public_schema(self) -> dict:
        """转换为公开的工具描述(不含内部实现细节)"""
        return {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
        }

2.3 ToolSpec 字段说明表

字段分类字段名类型说明
基础信息namestr工具名称,全局唯一标识
基础信息descriptionstr工具描述,LLM据此理解工具用途
基础信息funcCallable工具的实际执行函数
参数返回parametersdict参数的JSON Schema定义
参数返回returnsdict返回值的JSON Schema定义
权限安全permissionPermissionLevel权限级别枚举
权限安全requires_confirmationbool是否需要用户确认才执行
权限安全allowed_in_sandboxbool是否允许在沙箱环境中执行
执行控制timeoutfloat执行超时时间(秒)
执行控制max_retriesint失败后最大重试次数
执行控制rate_limitint/None每分钟最大调用次数限制
元数据versionstr工具版本号
元数据tagslist分类标签,用于搜索和过滤
元数据exampleslist使用示例
运行时状态statusToolStatus工具当前状态
运行时状态call_countint累计调用次数
运行时状态avg_latencyfloat平均执行延迟
缓存cacheablebool结果是否可缓存
缓存cache_ttlint缓存有效期(秒)

2.4 参数JSON Schema设计

参数Schema是ToolSpec中最关键的部分,它定义了工具需要哪些参数、每个参数的类型和约束。LLM根据这个Schema生成调用参数。

# 搜索工具的参数Schema示例
search_tool_params = {
    "type": "object",
    "properties": {
        "query": {
            "type": "string",
            "description": "搜索关键词",
            "minLength": 1,
            "maxLength": 500
        },
        "num_results": {
            "type": "integer",
            "description": "返回结果数量",
            "default": 10,
            "minimum": 1,
            "maximum": 50
        },
        "language": {
            "type": "string",
            "description": "搜索语言",
            "enum": ["zh", "en", "ja", "ko"],
            "default": "zh"
        },
        "time_range": {
            "type": "string",
            "description": "时间范围过滤",
            "enum": ["day", "week", "month", "year", "all"],
            "default": "all"
        }
    },
    "required": ["query"],
    "additionalProperties": False
}

# 文件写入工具的参数Schema示例
file_write_params = {
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "description": "文件路径(绝对路径)",
            "pattern": "^/[^<>:|?*]*$"
        },
        "content": {
            "type": "string",
            "description": "写入内容"
        },
        "mode": {
            "type": "string",
            "description": "写入模式",
            "enum": ["write", "append"],
            "default": "write"
        },
        "encoding": {
            "type": "string",
            "description": "文件编码",
            "default": "utf-8"
        }
    },
    "required": ["path", "content"],
    "additionalProperties": False
}

2.5 权限级别设计

权限级别(从低到高)

无需确认

可选确认

必须确认

必须确认+审计

READONLY 只读
搜索/查询/读取

WRITE 写入
创建/修改/发送

DESTRUCTIVE 破坏性
删除/覆盖/清空

SYSTEM 系统级
执行命令/修改配置

自动执行

可能确认

用户确认

确认+审计日志

通俗类比:权限级别就像是银行账户的操作等级。READONLY是查看余额,WRITE是转账,DESTRUCTIVE是销户,SYSTEM是修改银行系统配置。级别越高,需要的确认和审计就越严格。

2.6 ToolSpec 创建示例

# 创建一个完整的搜索工具规格
search_spec = ToolSpec(
    name="web_search",
    description="在互联网上搜索信息,返回相关网页摘要和链接。适用于查找最新资讯、技术文档、产品信息等。",
    func=search_function,
    parameters=search_tool_params,
    returns={
        "type": "object",
        "properties": {
            "results": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "title": {"type": "string"},
                        "url": {"type": "string"},
                        "snippet": {"type": "string"}
                    }
                }
            },
            "total": {"type": "integer"}
        }
    },
    permission=PermissionLevel.READONLY,
    requires_confirmation=False,
    timeout=15.0,
    max_retries=2,
    rate_limit=60,
    version="2.1.0",
    author="system",
    tags=["search", "web", "information"],
    examples=[
        {"query": "Python asyncio教程", "num_results": 5},
        {"query": "最新AI新闻", "time_range": "day"}
    ],
    cacheable=True,
    cache_ttl=600
)

三、工具注册表深度设计

3.1 工具注册表的角色

工具注册表(ToolRegistry)是工具系统的"中央仓库",管理Agent可调用的全部工具。它负责工具的注册、注销、查询、列表展示等核心功能。

标准注册方式为装饰器模式:

from typing import Callable, Any
from dataclasses import dataclass

@dataclass
class ToolSpec:
    name: str
    description: str
    func: Callable
    parameters: dict

class ToolRegistry:
    def __init__(self):
        self._tools = {}

    def register(self, name, desc="", params=None):
        def wrapper(fn):
            self._tools[name] = ToolSpec(name, desc, fn, params or {})
            return fn
        return wrapper

    def schemas(self) -> list[dict]:
        return [{"name": t.name, "description": t.description, "parameters": t.parameters} for t in self._tools.values()]

registry = ToolRegistry()

@registry.register("web_search", "搜索互联网", {"type":"object","properties":{"query":{"type":"string"}}})
async def search(query: str):
    return f"模拟搜索:{query}"

3.2 注册表完整实现

import asyncio
import threading
import importlib
import inspect
from typing import Callable, Any, Optional
from pathlib import Path


class ToolRegistry:
    """工具注册表 - 管理所有工具的生命周期"""

    def __init__(self):
        self._tools: dict[str, ToolSpec] = {}
        self._lock = threading.RLock()  # 线程安全锁
        self._hooks: dict[str, list[Callable]] = {
            "before_register": [],
            "after_register": [],
            "before_unregister": [],
            "after_unregister": [],
        }
        self._tool_groups: dict[str, list[str]] = {}  # 工具分组

    # ========== 注册 ==========

    def register(self, name: str, description: str = "", parameters: dict = None,
                 **kwargs) -> Callable:
        """
        装饰器模式注册工具

        用法:
            @registry.register("my_tool", "我的工具", {"type": "object", ...})
            async def my_tool(param: str):
                return result
        """
        def wrapper(fn: Callable) -> Callable:
            spec = ToolSpec(
                name=name,
                description=description,
                func=fn,
                parameters=parameters or {},
                **kwargs
            )
            self._register_spec(spec)
            return fn
        return wrapper

    def register_spec(self, spec: ToolSpec) -> None:
        """直接注册一个ToolSpec对象"""
        self._register_spec(spec)

    def register_function(self, name: str, func: Callable, description: str = "",
                          parameters: dict = None, **kwargs) -> None:
        """直接注册一个函数(非装饰器方式)"""
        spec = ToolSpec(
            name=name,
            description=description,
            func=func,
            parameters=parameters or {},
            **kwargs
        )
        self._register_spec(spec)

    def _register_spec(self, spec: ToolSpec) -> None:
        """内部注册逻辑"""
        self._run_hooks("before_register", spec)

        with self._lock:
            if spec.name in self._tools:
                # 版本检查:允许高版本覆盖低版本
                existing = self._tools[spec.name]
                if self._version_compare(spec.version, existing.version) <= 0:
                    raise ValueError(
                        f"工具 '{spec.name}' 已存在且版本不低于当前版本 "
                        f"(existing: {existing.version}, new: {spec.version})"
                    )
            self._tools[spec.name] = spec

        self._run_hooks("after_register", spec)

    # ========== 注销 ==========

    def unregister(self, name: str) -> Optional[ToolSpec]:
        """注销工具"""
        with self._lock:
            if name not in self._tools:
                return None
            spec = self._tools[name]
            self._run_hooks("before_unregister", spec)
            del self._tools[name]
            # 从分组中移除
            for group_tools in self._tool_groups.values():
                if name in group_tools:
                    group_tools.remove(name)
            self._run_hooks("after_unregister", spec)
            return spec

    # ========== 查询 ==========

    def get(self, name: str) -> Optional[ToolSpec]:
        """按名称获取工具"""
        with self._lock:
            return self._tools.get(name)

    def has(self, name: str) -> bool:
        """检查工具是否存在"""
        with self._lock:
            return name in self._tools

    def list_tools(self, tag: str = None, permission: PermissionLevel = None,
                   status: ToolStatus = ToolStatus.ACTIVE) -> list[ToolSpec]:
        """列出工具,支持按标签、权限、状态过滤"""
        with self._lock:
            tools = list(self._tools.values())

        if tag:
            tools = [t for t in tools if tag in t.tags]
        if permission:
            tools = [t for t in tools if t.permission == permission]
        if status:
            tools = [t for t in tools if t.status == status]

        return tools

    def schemas(self) -> list[dict]:
        """返回所有工具的Schema(供LLM使用)"""
        with self._lock:
            return [t.to_schema() for t in self._tools.values()]

    def public_schemas(self) -> list[dict]:
        """返回公开Schema(精简版)"""
        with self._lock:
            return [t.to_public_schema() for t in self._tools.values()]

    # ========== 分组管理 ==========

    def create_group(self, group_name: str, tool_names: list[str]) -> None:
        """创建工具分组"""
        with self._lock:
            self._tool_groups[group_name] = list(tool_names)

    def get_group(self, group_name: str) -> list[ToolSpec]:
        """获取分组中的工具"""
        with self._lock:
            names = self._tool_groups.get(group_name, [])
            return [self._tools[n] for n in names if n in self._tools]

    # ========== 动态加载 ==========

    def load_from_module(self, module_path: str) -> int:
        """
        从Python模块动态加载工具
        模块中所有用@registry.register装饰的函数会自动注册
        返回注册的工具数量
        """
        module = importlib.import_module(module_path)
        count = 0
        for name, obj in inspect.getmembers(module):
            if callable(obj) and hasattr(obj, '_tool_registered'):
                count += 1
        return count

    def load_from_directory(self, dir_path: str) -> int:
        """从目录批量加载工具模块"""
        path = Path(dir_path)
        if not path.is_dir():
            raise FileNotFoundError(f"目录不存在: {dir_path}")

        count = 0
        for py_file in path.glob("*.py"):
            if py_file.name.startswith("_"):
                continue
            module_name = py_file.stem
            try:
                count += self.load_from_module(module_name)
            except Exception as e:
                print(f"加载模块 {module_name} 失败: {e}")
        return count

    # ========== 钩子系统 ==========

    def add_hook(self, event: str, callback: Callable) -> None:
        """添加钩子函数"""
        if event in self._hooks:
            self._hooks[event].append(callback)

    def _run_hooks(self, event: str, *args, **kwargs) -> None:
        """执行钩子"""
        for callback in self._hooks.get(event, []):
            try:
                callback(*args, **kwargs)
            except Exception as e:
                print(f"钩子 {event} 执行失败: {e}")

    # ========== 统计 ==========

    def stats(self) -> dict:
        """返回注册表统计信息"""
        with self._lock:
            total = len(self._tools)
            by_permission = {}
            by_status = {}
            total_calls = 0
            for tool in self._tools.values():
                perm = tool.permission.value
                by_permission[perm] = by_permission.get(perm, 0) + 1
                status = tool.status.value
                by_status[status] = by_status.get(status, 0) + 1
                total_calls += tool.call_count

            return {
                "total_tools": total,
                "by_permission": by_permission,
                "by_status": by_status,
                "total_calls": total_calls,
                "groups": len(self._tool_groups),
            }

    @staticmethod
    def _version_compare(v1: str, v2: str) -> int:
        """比较版本号,返回1(v1>v2)、0(相等)、-1(v1<v2)"""
        parts1 = [int(x) for x in v1.split(".")]
        parts2 = [int(x) for x in v2.split(".")]
        for p1, p2 in zip(parts1, parts2):
            if p1 > p2:
                return 1
            elif p1 < p2:
                return -1
        return 0

3.3 装饰器注册模式详解

装饰器模式是最优雅的工具注册方式。开发者只需在函数上方添加装饰器,工具就自动注册到注册表中:

# 创建全局注册表实例
registry = ToolRegistry()

# 方式1:基础装饰器注册
@registry.register(
    "web_search",
    "搜索互联网,返回相关网页信息",
    {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "搜索关键词"}
        },
        "required": ["query"]
    }
)
async def web_search(query: str):
    return f"模拟搜索结果: {query}"


# 方式2:带完整参数的装饰器注册
@registry.register(
    "file_read",
    "读取文件内容",
    {
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "文件路径"},
            "encoding": {"type": "string", "default": "utf-8"}
        },
        "required": ["path"]
    },
    permission=PermissionLevel.READONLY,
    timeout=10.0,
    tags=["file", "read"],
    version="1.2.0"
)
async def file_read(path: str, encoding: str = "utf-8"):
    with open(path, "r", encoding=encoding) as f:
        return f.read()


# 方式3:非装饰器直接注册
def calculate(expression: str):
    """计算数学表达式"""
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return {"expression": expression, "result": result}
    except Exception as e:
        return {"error": str(e)}

registry.register_function(
    name="calculator",
    func=calculate,
    description="计算数学表达式",
    parameters={
        "type": "object",
        "properties": {
            "expression": {"type": "string", "description": "数学表达式"}
        },
        "required": ["expression"]
    },
    permission=PermissionLevel.READONLY,
    tags=["math", "calculate"]
)

3.4 动态加载与热插拔

工具模块 文件系统 注册表 管理员 工具模块 文件系统 注册表 管理员 loop [每个文件] 热插拔:运行时动态增删工具 load_from_directory("./tools/") 扫描 .py 文件 返回文件列表 importlib.import_module() 模块加载成功 扫描已注册工具 返回加载工具数量 register("new_tool", ...) 添加到注册表 注册成功 unregister("old_tool") 从注册表移除 注销成功
# 动态加载示例
class ToolHotSwap:
    """工具热插拔管理器"""

    def __init__(self, registry: ToolRegistry):
        self.registry = registry
        self._watched_dirs: dict[str, float] = {}  # 目录 -> 上次修改时间

    def watch_directory(self, dir_path: str) -> None:
        """监控目录变化,自动加载新工具"""
        path = Path(dir_path)
        if not path.is_dir():
            return
        self._watched_dirs[dir_path] = self._get_dir_mtime(path)

    def check_updates(self) -> list[str]:
        """检查是否有更新,返回变化的工具列表"""
        changed = []
        for dir_path, last_mtime in self._watched_dirs.items():
            path = Path(dir_path)
            current_mtime = self._get_dir_mtime(path)
            if current_mtime > last_mtime:
                # 重新加载该目录
                new_count = self.registry.load_from_directory(dir_path)
                changed.append(f"{dir_path}: {new_count} tools reloaded")
                self._watched_dirs[dir_path] = current_mtime
        return changed

    @staticmethod
    def _get_dir_mtime(path: Path) -> float:
        """获取目录最后修改时间"""
        return max(f.stat().st_mtime for f in path.glob("*.py"))

3.5 注册表与LLM的交互

class ToolRegistryLLMAdapter:
    """工具注册表与LLM之间的适配器"""

    def __init__(self, registry: ToolRegistry):
        self.registry = registry

    def get_tools_for_llm(self, filter_tags: list[str] = None) -> list[dict]:
        """
        获取LLM可理解的工具列表
        可按标签过滤,只暴露相关工具给LLM
        """
        tools = self.registry.list_tools()
        if filter_tags:
            tools = [
                t for t in tools
                if any(tag in t.tags for tag in filter_tags)
            ]
        return [t.to_schema() for t in tools]

    def get_tool_descriptions(self) -> str:
        """生成工具描述文本(用于System Prompt)"""
        tools = self.registry.list_tools()
        lines = ["可用工具列表:"]
        for tool in tools:
            lines.append(f"\n- {tool.name}: {tool.description}")
            if tool.parameters.get("properties"):
                lines.append("  参数:")
                for param_name, param_info in tool.parameters["properties"].items():
                    required = param_name in tool.parameters.get("required", [])
                    req_str = "必填" if required else "可选"
                    lines.append(
                        f"    - {param_name} ({param_info.get('type', 'any')}): "
                        f"{param_info.get('description', '')} [{req_str}]"
                    )
        return "\n".join(lines)

通俗类比:注册表与LLM的交互就像图书馆的目录系统。注册表是图书管理员,LLM是来借书的读者。管理员不会把所有书都搬到读者面前,而是根据读者的需求(filter_tags),提供一份精选的书目清单(get_tools_for_llm),并附上每本书的简介和借阅要求(参数说明)。


四、工具发现机制

4.1 工具发现的核心问题

工具发现(Tool Discovery)解决的核心问题是:“给定一个任务需求,如何找到最合适的工具?” 这听起来简单,但当工具数量从几个增长到几百个甚至上千个时,这就变成了一个极具挑战性的问题。

工具发现三种模式

预定义映射

协议握手

语义匹配

静态注册发现
Static Discovery

注册表

MCP动态发现
MCP Discovery

MCP服务器

运行时搜索发现
Runtime Search

向量索引

本地工具

远程工具

全部工具池

4.2 静态注册发现

静态注册发现是最简单直接的方式:工具在系统启动时注册到注册表,使用时直接按名称或标签查找。

class StaticToolDiscovery:
    """静态工具发现 - 基于预注册的工具表"""

    def __init__(self, registry: ToolRegistry):
        self.registry = registry

    def discover_by_name(self, name: str) -> Optional[ToolSpec]:
        """按名称精确查找"""
        return self.registry.get(name)

    def discover_by_tag(self, tag: str) -> list[ToolSpec]:
        """按标签查找"""
        return self.registry.list_tools(tag=tag)

    def discover_by_permission(self, permission: PermissionLevel) -> list[ToolSpec]:
        """按权限级别查找"""
        return self.registry.list_tools(permission=permission)

    def discover_all(self) -> list[ToolSpec]:
        """列出所有可用工具"""
        return self.registry.list_tools()

    def discover_for_task(self, task_description: str) -> list[ToolSpec]:
        """
        根据任务描述发现工具(基于关键词匹配)
        这是最简单的匹配方式,适用于工具数量较少的场景
        """
        task_lower = task_description.lower()
        scored_tools = []

        for tool in self.registry.list_tools():
            score = 0
            # 名称匹配
            if tool.name.lower() in task_lower:
                score += 10
            # 描述匹配
            desc_words = tool.description.lower().split()
            for word in desc_words:
                if len(word) > 2 and word in task_lower:
                    score += 3
            # 标签匹配
            for tag in tool.tags:
                if tag.lower() in task_lower:
                    score += 5
            if score > 0:
                scored_tools.append((score, tool))

        scored_tools.sort(key=lambda x: x[0], reverse=True)
        return [tool for _, tool in scored_tools]

通俗类比:静态发现就像在自家工具箱里找工具。你知道扳手在哪个格子、螺丝刀在哪个抽屉,直接去拿就行。但如果工具箱有上千件工具,靠记忆找就太慢了。

4.3 MCP动态发现

MCP(Model Context Protocol)是一种标准化的工具发现协议。通过MCP,Agent可以在运行时动态发现远程工具服务器上提供的工具,无需预先注册。

import json
from typing import Any


class MCPDiscoveryClient:
    """MCP动态发现客户端"""

    def __init__(self):
        self._servers: dict[str, dict] = {}  # 服务器名 -> 服务器信息
        self._discovered_tools: dict[str, ToolSpec] = {}

    async def connect_server(self, server_name: str, server_config: dict) -> bool:
        """
        连接MCP工具服务器
        server_config 示例:
        {
            "transport": "stdio",  # 或 "http"
            "command": "python",
            "args": ["-m", "mcp_server_search"],
            "env": {"API_KEY": "..."}
        }
        """
        try:
            # 模拟MCP握手
            print(f"[MCP] 连接服务器: {server_name}")
            print(f"[MCP] 传输方式: {server_config.get('transport', 'stdio')}")

            # 发送initialize请求
            init_request = {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "initialize",
                "params": {
                    "protocolVersion": "2024-11-05",
                    "capabilities": {},
                    "clientInfo": {
                        "name": "agent-tool-system",
                        "version": "1.0.0"
                    }
                }
            }
            # 模拟响应
            init_response = {
                "protocolVersion": "2024-11-05",
                "capabilities": {"tools": {}},
                "serverInfo": {
                    "name": server_name,
                    "version": "1.0.0"
                }
            }

            self._servers[server_name] = {
                "config": server_config,
                "info": init_response,
                "connected": True
            }
            return True

        except Exception as e:
            print(f"[MCP] 连接失败: {e}")
            return False

    async def discover_tools(self, server_name: str) -> list[dict]:
        """从MCP服务器发现工具"""
        if server_name not in self._servers:
            raise ValueError(f"服务器 {server_name} 未连接")

        # 模拟发送 tools/list 请求
        list_request = {
            "jsonrpc": "2.0",
            "id": 2,
            "method": "tools/list",
            "params": {}
        }

        # 模拟响应
        mock_response = {
            "tools": [
                {
                    "name": "mcp_search",
                    "description": "MCP搜索工具",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "query": {"type": "string"}
                        },
                        "required": ["query"]
                    }
                },
                {
                    "name": "mcp_fetch",
                    "description": "MCP网页抓取工具",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "url": {"type": "string"}
                        },
                        "required": ["url"]
                    }
                }
            ]
        }

        discovered = []
        for tool_info in mock_response["tools"]:
            # 转换为ToolSpec
            spec = ToolSpec(
                name=f"{server_name}_{tool_info['name']}",
                description=tool_info["description"],
                func=self._create_mcp_proxy(server_name, tool_info["name"]),
                parameters=tool_info.get("inputSchema", {}),
                tags=["mcp", server_name],
                version="1.0.0"
            )
            self._discovered_tools[spec.name] = spec
            discovered.append(spec)

        return discovered

    def _create_mcp_proxy(self, server_name: str, tool_name: str) -> Callable:
        """创建MCP工具代理函数"""
        async def proxy(**kwargs):
            # 模拟发送 tools/call 请求
            call_request = {
                "jsonrpc": "2.0",
                "id": 3,
                "method": "tools/call",
                "params": {
                    "name": tool_name,
                    "arguments": kwargs
                }
            }
            # 模拟响应
            return {
                "content": [
                    {"type": "text", "text": f"[MCP] {server_name}.{tool_name} 执行结果"}
                ]
            }
        return proxy

    async def disconnect_server(self, server_name: str) -> None:
        """断开MCP服务器连接"""
        if server_name in self._servers:
            self._servers[server_name]["connected"] = False
            # 移除该服务器的工具
            to_remove = [
                name for name, spec in self._discovered_tools.items()
                if server_name in spec.tags
            ]
            for name in to_remove:
                del self._discovered_tools[name]
            del self._servers[server_name]

    def get_discovered_tools(self) -> list[ToolSpec]:
        """获取所有已发现的MCP工具"""
        return list(self._discovered_tools.values())

4.4 运行时语义搜索发现

当工具数量很多时,关键词匹配不够精确。运行时语义搜索使用向量嵌入和相似度计算来发现最相关的工具。

class SemanticToolDiscovery:
    """基于语义相似度的工具发现"""

    def __init__(self, registry: ToolRegistry):
        self.registry = registry
        self._embeddings: dict[str, list[float]] = {}  # 工具名 -> 嵌入向量
        self._initialized = False

    def _get_embedding(self, text: str) -> list[float]:
        """
        获取文本的嵌入向量
        实际实现中会调用embedding模型API
        这里使用简化的哈希模拟
        """
        # 模拟128维向量
        import hashlib
        hash_val = hashlib.md5(text.encode()).hexdigest()
        vector = [int(hash_val[i:i+2], 16) / 255 for i in range(0, 32, 1)]
        # 扩展到128维
        return (vector * 4)[:128]

    def build_index(self) -> None:
        """构建工具嵌入索引"""
        tools = self.registry.list_tools()
        for tool in tools:
            # 将名称、描述、标签组合成索引文本
            index_text = f"{tool.name} {tool.description} {' '.join(tool.tags)}"
            self._embeddings[tool.name] = self._get_embedding(index_text)
        self._initialized = True
        print(f"[SemanticDiscovery] 索引构建完成,共 {len(tools)} 个工具")

    def discover(self, query: str, top_k: int = 5) -> list[tuple[float, ToolSpec]]:
        """
        语义搜索发现工具
        返回 (相似度分数, 工具规格) 列表
        """
        if not self._initialized:
            self.build_index()

        query_embedding = self._get_embedding(query)
        results = []

        for tool_name, tool_embedding in self._embeddings.items():
            similarity = self._cosine_similarity(query_embedding, tool_embedding)
            tool = self.registry.get(tool_name)
            if tool:
                results.append((similarity, tool))

        results.sort(key=lambda x: x[0], reverse=True)
        return results[:top_k]

    @staticmethod
    def _cosine_similarity(v1: list[float], v2: list[float]) -> float:
        """计算余弦相似度"""
        if len(v1) != len(v2):
            return 0.0
        dot = sum(a * b for a, b in zip(v1, v2))
        norm1 = sum(a * a for a in v1) ** 0.5
        norm2 = sum(b * b for b in v2) ** 0.5
        if norm1 == 0 or norm2 == 0:
            return 0.0
        return dot / (norm1 * norm2)

4.5 三种发现模式对比

运行时语义搜索

构建向量索引

任务描述向量化

余弦相似度计算

返回Top-K工具

MCP动态发现

连接MCP服务器

协议握手

tools/list 发现工具

动态注册到本地

可调用

静态注册发现

启动时注册

按名称/标签查找

返回工具

维度静态注册发现MCP动态发现运行时语义搜索
发现时机启动时运行时按需运行时按需
匹配方式名称/标签精确匹配协议声明语义相似度
适用规模小规模(<50工具)中等规模大规模(>100工具)
延迟极低中等(需网络通信)较高(向量计算)
准确性高(精确匹配)高(协议保证)中(依赖嵌入质量)
动态性低(需重启)高(运行时发现)中(需重建索引)
实现复杂度低中高

通俗类比:

  • 静态发现 = 在通讯录里找电话号码(你认识的人)
  • MCP动态发现 = 在黄页上查找商家电话(你不认识但知道有这类服务)
  • 语义搜索 = 用搜索引擎搜索"附近修水管的人"(模糊需求,语义匹配)

五、工具选择策略

5.1 工具选择的核心挑战

工具发现找到了候选工具,工具选择则决定"用哪个"。当多个工具都能完成类似任务时,如何选择最优工具?

工具选择三种策略

LLM自主选择
LLM Selection

LLM根据工具描述
和任务上下文决策

规则匹配选择
Rule Matching

预定义规则映射
任务类型到工具

向量检索选择
Vector Retrieval

任务向量化
与工具向量匹配

选定工具

5.2 LLM自主选择

LLM自主选择是最灵活的方式:将所有可用工具的描述提供给LLM,让LLM根据任务上下文自主决策使用哪个工具。

class LLMToolSelector:
    """LLM自主工具选择器"""

    def __init__(self, registry: ToolRegistry, llm_client=None):
        self.registry = registry
        self.llm_client = llm_client

    def build_selection_prompt(self, task: str, available_tools: list[ToolSpec]) -> str:
        """构建工具选择提示词"""
        tool_descriptions = []
        for i, tool in enumerate(available_tools):
            params_str = ""
            if tool.parameters.get("properties"):
                params_str = "\n    参数: " + json.dumps(
                    tool.parameters.get("properties", {}),
                    ensure_ascii=False
                )
            tool_descriptions.append(
                f"  {i+1}. {tool.name}: {tool.description}{params_str}"
            )

        prompt = f"""你是一个智能助手,需要从以下工具中选择合适的工具来完成任务。

任务: {task}

可用工具:
{chr(10).join(tool_descriptions)}

请分析任务需求,选择最合适的工具,并以JSON格式输出:
{{
  "tool_name": "工具名称",
  "reason": "选择理由",
  "parameters": {{
    "参数名": "参数值"
  }}
}}
"""
        return prompt

    async def select(self, task: str, candidate_tools: list[ToolSpec] = None) -> dict:
        """
        LLM选择工具
        返回: {"tool_name": ..., "reason": ..., "parameters": ...}
        """
        if candidate_tools is None:
            candidate_tools = self.registry.list_tools()

        prompt = self.build_selection_prompt(task, candidate_tools)

        if self.llm_client:
            response = await self.llm_client.chat(prompt)
            # 解析LLM返回的JSON
            try:
                return json.loads(response)
            except json.JSONDecodeError:
                return {"error": "LLM返回格式错误", "raw": response}
        else:
            # 无LLM时的降级:返回所有工具供人工选择
            return {
                "candidates": [t.name for t in candidate_tools],
                "note": "无LLM可用,请手动选择"
            }

5.3 规则匹配选择

规则匹配使用预定义的规则将任务类型映射到工具。速度快、可预测,但灵活性差。

class RuleBasedToolSelector:
    """基于规则的工具选择器"""

    def __init__(self, registry: ToolRegistry):
        self.registry = registry
        self._rules: list[dict] = []

    def add_rule(self, keywords: list[str], tool_name: str,
                 priority: int = 0, params_template: dict = None) -> None:
        """添加匹配规则"""
        self._rules.append({
            "keywords": keywords,
            "tool_name": tool_name,
            "priority": priority,
            "params_template": params_template or {}
        })
        # 按优先级排序
        self._rules.sort(key=lambda r: r["priority"], reverse=True)

    def select(self, task: str) -> Optional[dict]:
        """
        根据任务匹配工具
        返回: {"tool_name": ..., "parameters": ...}
        """
        task_lower = task.lower()
        for rule in self._rules:
            for keyword in rule["keywords"]:
                if keyword.lower() in task_lower:
                    return {
                        "tool_name": rule["tool_name"],
                        "parameters": rule["params_template"],
                        "matched_keyword": keyword
                    }
        return None

    def setup_default_rules(self) -> None:
        """设置默认规则集"""
        self.add_rule(
            keywords=["搜索", "查找", "查询信息", "search"],
            tool_name="web_search",
            priority=10
        )
        self.add_rule(
            keywords=["计算", "算数", "calculator"],
            tool_name="calculator",
            priority=10
        )
        self.add_rule(
            keywords=["读文件", "读取文件", "查看文件", "read file"],
            tool_name="file_read",
            priority=10
        )
        self.add_rule(
            keywords=["写文件", "保存文件", "write file"],
            tool_name="file_write",
            priority=10
        )
        self.add_rule(
            keywords=["执行代码", "运行代码", "run code", "execute"],
            tool_name="code_execute",
            priority=5
        )

5.4 向量检索选择

向量检索选择将任务描述和工具描述都转化为向量,通过相似度计算选择最匹配的工具。

class VectorToolSelector:
    """基于向量检索的工具选择器"""

    def __init__(self, registry: ToolRegistry):
        self.registry = registry
        self.semantic_discovery = SemanticToolDiscovery(registry)

    async def select(self, task: str, top_k: int = 3) -> list[dict]:
        """
        向量检索选择工具
        返回排序后的工具候选列表
        """
        results = self.semantic_discovery.discover(task, top_k=top_k)

        candidates = []
        for score, tool in results:
            candidates.append({
                "tool_name": tool.name,
                "description": tool.description,
                "similarity_score": round(score, 4),
                "parameters": tool.parameters
            })

        return candidates

5.5 混合选择策略

实际系统中,通常会组合多种策略以获得最佳效果:

class HybridToolSelector:
    """混合工具选择器 - 融合多种策略"""

    def __init__(self, registry: ToolRegistry, llm_client=None):
        self.registry = registry
        self.rule_selector = RuleBasedToolSelector(registry)
        self.rule_selector.setup_default_rules()
        self.vector_selector = VectorToolSelector(registry)
        self.llm_selector = LLMToolSelector(registry, llm_client)

    async def select(self, task: str) -> dict:
        """
        混合选择策略:
        1. 先用规则匹配(快速路径)
        2. 规则未命中时用向量检索缩小范围
        3. 用LLM从候选中做最终决策
        """
        # 步骤1: 规则匹配
        rule_result = self.rule_selector.select(task)
        if rule_result:
            return {
                "strategy": "rule_match",
                **rule_result
            }

        # 步骤2: 向量检索
        vector_candidates = await self.vector_selector.select(task, top_k=5)
        if not vector_candidates:
            return {"error": "未找到匹配工具", "task": task}

        # 步骤3: LLM决策
        candidate_tools = []
        for candidate in vector_candidates:
            tool = self.registry.get(candidate["tool_name"])
            if tool:
                candidate_tools.append(tool)

        llm_result = await self.llm_selector.select(task, candidate_tools)

        return {
            "strategy": "hybrid",
            "vector_candidates": vector_candidates,
            "llm_decision": llm_result
        }

命中

未命中

任务输入

规则匹配?

返回规则匹配结果

向量检索Top-5

LLM从候选中选择

返回最终选择

5.6 三种选择策略对比

维度LLM自主选择规则匹配向量检索
灵活性最高低中
响应速度慢(需LLM推理)极快较快
准确性高(理解上下文)高(精确匹配时)中(依赖嵌入质量)
可解释性中高低
维护成本低高(需维护规则)中(需重建索引)
适用场景复杂任务明确意图模糊需求

通俗类比:

  • LLM选择 = 让一个有经验的员工自主决定用什么工具(灵活但慢)
  • 规则匹配 = 查操作手册"遇到A情况用B工具"(快但不灵活)
  • 向量检索 = 在搜索引擎输入需求,看搜索结果推荐什么(折中方案)

六、工具参数生成与验证

6.1 参数生成的挑战

LLM决定使用某个工具后,需要生成该工具的调用参数。参数生成是工具调用中最容易出错的环节——LLM可能生成错误类型的参数、遗漏必需参数、或包含多余参数。

每工具执行前必须参数校验。使用JSON Schema或Pydantic:

def validate(schema: dict, args: dict):
    required = schema.get("required", [])
    for r in required:
        if r not in args:
            raise ValueError(f"缺少必需参数:{r}")
    return {k: v for k, v in args.items() if k in schema.get("properties", {})}

6.2 完整参数验证器

import re
from typing import Any, Union


class ParameterValidator:
    """工具参数验证器 - 基于JSON Schema"""

    def __init__(self):
        self._type_validators = {
            "string": self._validate_string,
            "integer": self._validate_integer,
            "number": self._validate_number,
            "boolean": self._validate_boolean,
            "array": self._validate_array,
            "object": self._validate_object,
        }

    def validate(self, schema: dict, args: dict) -> dict:
        """
        验证参数是否符合Schema
        返回验证后的参数(经过类型转换和默认值填充)
        异常时抛出 ValidationError
        """
        errors = []
        validated = {}

        properties = schema.get("properties", {})
        required = schema.get("required", [])

        # 检查必需参数
        for req in required:
            if req not in args:
                errors.append(f"缺少必需参数: {req}")

        # 验证每个参数
        for param_name, param_value in args.items():
            if param_name not in properties:
                if schema.get("additionalProperties", True) is False:
                    errors.append(f"未知参数: {param_name}")
                continue

            param_schema = properties[param_name]
            try:
                validated_value = self._validate_value(param_value, param_schema)
                validated[param_name] = validated_value
            except ValueError as e:
                errors.append(f"参数 {param_name} 验证失败: {e}")

        # 填充默认值
        for param_name, param_schema in properties.items():
            if param_name not in validated and "default" in param_schema:
                validated[param_name] = param_schema["default"]

        if errors:
            raise ValueError("; ".join(errors))

        return validated

    def _validate_value(self, value: Any, schema: dict) -> Any:
        """验证单个值"""
        expected_type = schema.get("type")
        if expected_type and expected_type in self._type_validators:
            return self._type_validators[expected_type](value, schema)
        return value

    def _validate_string(self, value: Any, schema: dict) -> str:
        """验证字符串"""
        if not isinstance(value, str):
            # 尝试转换
            value = str(value)

        # 最小长度
        min_len = schema.get("minLength")
        if min_len is not None and len(value) < min_len:
            raise ValueError(f"字符串长度不能小于 {min_len}")

        # 最大长度
        max_len = schema.get("maxLength")
        if max_len is not None and len(value) > max_len:
            raise ValueError(f"字符串长度不能超过 {max_len}")

        # 正则模式
        pattern = schema.get("pattern")
        if pattern and not re.match(pattern, value):
            raise ValueError(f"字符串不匹配模式: {pattern}")

        # 枚举值
        enum_values = schema.get("enum")
        if enum_values and value not in enum_values:
            raise ValueError(f"值必须是 {enum_values} 之一")

        return value

    def _validate_integer(self, value: Any, schema: dict) -> int:
        """验证整数"""
        try:
            value = int(value)
        except (ValueError, TypeError):
            raise ValueError(f"无法转换为整数: {value}")

        minimum = schema.get("minimum")
        if minimum is not None and value < minimum:
            raise ValueError(f"值不能小于 {minimum}")

        maximum = schema.get("maximum")
        if maximum is not None and value > maximum:
            raise ValueError(f"值不能大于 {maximum}")

        return value

    def _validate_number(self, value: Any, schema: dict) -> float:
        """验证数字"""
        try:
            value = float(value)
        except (ValueError, TypeError):
            raise ValueError(f"无法转换为数字: {value}")

        minimum = schema.get("minimum")
        if minimum is not None and value < minimum:
            raise ValueError(f"值不能小于 {minimum}")

        maximum = schema.get("maximum")
        if maximum is not None and value > maximum:
            raise ValueError(f"值不能大于 {maximum}")

        return value

    def _validate_boolean(self, value: Any, schema: dict) -> bool:
        """验证布尔值"""
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            if value.lower() in ("true", "1", "yes"):
                return True
            elif value.lower() in ("false", "0", "no"):
                return False
        if isinstance(value, (int, float)):
            return bool(value)
        raise ValueError(f"无法转换为布尔值: {value}")

    def _validate_array(self, value: Any, schema: dict) -> list:
        """验证数组"""
        if not isinstance(value, list):
            raise ValueError(f"期望数组类型,得到: {type(value)}")

        min_items = schema.get("minItems")
        if min_items is not None and len(value) < min_items:
            raise ValueError(f"数组至少需要 {min_items} 个元素")

        max_items = schema.get("maxItems")
        if max_items is not None and len(value) > max_items:
            raise ValueError(f"数组最多 {max_items} 个元素")

        item_schema = schema.get("items")
        if item_schema:
            validated_items = []
            for i, item in enumerate(value):
                try:
                    validated_items.append(self._validate_value(item, item_schema))
                except ValueError as e:
                    raise ValueError(f"数组第 {i} 个元素验证失败: {e}")
            return validated_items

        return value

    def _validate_object(self, value: Any, schema: dict) -> dict:
        """验证对象"""
        if not isinstance(value, dict):
            raise ValueError(f"期望对象类型,得到: {type(value)}")
        # 递归验证嵌套对象
        return self.validate(schema, value)

6.3 参数生成流程

工具函数 参数验证器 LLM 工具函数 参数验证器 LLM 提交参数 {query: "天气", num: "10"} 检查必需参数 ✓ 类型转换 num: "10" → 10 范围检查 num: 10 (1~50) ✓ 填充默认值 language: "zh" 移除未知参数 ✓ 验证后参数 {query: "天气", num: 10, language: "zh"} 执行结果

6.4 Pydantic 验证方案

除了手写验证器,还可以使用Pydantic进行更优雅的参数验证:

from pydantic import BaseModel, Field, validator
from typing import Optional


class SearchParams(BaseModel):
    """搜索工具参数(Pydantic模型)"""
    query: str = Field(..., min_length=1, max_length=500, description="搜索关键词")
    num_results: int = Field(default=10, ge=1, le=50, description="返回结果数量")
    language: str = Field(default="zh", description="搜索语言")
    time_range: str = Field(default="all", description="时间范围")

    @validator("language")
    def validate_language(cls, v):
        allowed = ["zh", "en", "ja", "ko"]
        if v not in allowed:
            raise ValueError(f"语言必须是 {allowed} 之一")
        return v

    @validator("time_range")
    def validate_time_range(cls, v):
        allowed = ["day", "week", "month", "year", "all"]
        if v not in allowed:
            raise ValueError(f"时间范围必须是 {allowed} 之一")
        return v


# 使用示例
try:
    params = SearchParams(query="Python教程", num_results=5)
    print(f"验证通过: {params.dict()}")
except Exception as e:
    print(f"验证失败: {e}")

通俗类比:参数验证就像机场安检。你的行李(参数)必须通过尺寸检查(类型验证)、重量检查(范围验证)、禁带物品检查(枚举验证),通过后才能登机(执行工具)。安检人员(验证器)会告诉你哪里不合格,你需要修正后重新通过。


七、工具执行引擎

7.1 执行引擎的职责

工具调度层根据LLM输出解析工具名称与参数,走校验后执行:

async def route(tool_name: str, args: dict, registry: ToolRegistry):
    tool = registry._tools.get(tool_name)
    if not tool:
        return f"错误:工具{tool_name}不存在"
    validated = validate(tool.parameters, args)
    return await tool.function(**validated)

执行引擎不仅要调用工具函数,还要管理整个执行过程:超时控制、资源限制、错误处理、并发管理、沙箱隔离。

7.2 执行引擎完整实现

import asyncio
import signal
import resource
import traceback
from typing import Any
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from enum import Enum


class ExecutionStatus(Enum):
    """执行状态"""
    SUCCESS = "success"
    TIMEOUT = "timeout"
    ERROR = "error"
    CANCELLED = "cancelled"
    RATE_LIMITED = "rate_limited"


@dataclass
class ExecutionResult:
    """工具执行结果"""
    status: ExecutionStatus
    result: Any = None
    error: str = ""
    duration: float = 0.0
    tool_name: str = ""
    parameters: dict = None

    def to_dict(self) -> dict:
        return {
            "status": self.status.value,
            "result": self.result,
            "error": self.error,
            "duration": round(self.duration, 3),
            "tool_name": self.tool_name,
        }


class ToolExecutor:
    """工具执行引擎"""

    def __init__(self, registry: ToolRegistry, validator: ParameterValidator = None):
        self.registry = registry
        self.validator = validator or ParameterValidator()
        self._executor = ThreadPoolExecutor(max_workers=10)
        self._call_history: dict[str, list[float]] = {}  # 工具名 -> 调用时间戳列表
        self._result_cache: dict[str, tuple[Any, float]] = {}  # 缓存key -> (结果, 过期时间)

    async def execute(self, tool_name: str, args: dict,
                      context: dict = None) -> ExecutionResult:
        """
        执行工具
        这是执行引擎的主入口
        """
        import time
        start_time = time.time()

        # 1. 查找工具
        tool = self.registry.get(tool_name)
        if not tool:
            return ExecutionResult(
                status=ExecutionStatus.ERROR,
                error=f"工具不存在: {tool_name}",
                tool_name=tool_name,
                duration=time.time() - start_time
            )

        # 2. 检查工具状态
        if tool.status != ToolStatus.ACTIVE:
            return ExecutionResult(
                status=ExecutionStatus.ERROR,
                error=f"工具不可用(状态: {tool.status.value})",
                tool_name=tool_name,
                duration=time.time() - start_time
            )

        # 3. 限流检查
        if tool.rate_limit:
            if not self._check_rate_limit(tool_name, tool.rate_limit):
                return ExecutionResult(
                    status=ExecutionStatus.RATE_LIMITED,
                    error=f"工具调用频率超限(限制: {tool.rate_limit}/分钟)",
                    tool_name=tool_name,
                    duration=time.time() - start_time
                )

        # 4. 参数验证
        try:
            validated_args = self.validator.validate(tool.parameters, args)
        except ValueError as e:
            return ExecutionResult(
                status=ExecutionStatus.ERROR,
                error=f"参数验证失败: {e}",
                tool_name=tool_name,
                parameters=args,
                duration=time.time() - start_time
            )

        # 5. 权限检查
        if tool.requires_confirmation:
            if context and not context.get("confirmed", False):
                return ExecutionResult(
                    status=ExecutionStatus.ERROR,
                    error="此工具需要用户确认才能执行",
                    tool_name=tool_name,
                    duration=time.time() - start_time
                )

        # 6. 缓存检查
        cache_key = self._make_cache_key(tool_name, validated_args)
        if tool.cacheable and cache_key in self._result_cache:
            cached_result, expire_time = self._result_cache[cache_key]
            if time.time() < expire_time:
                return ExecutionResult(
                    status=ExecutionStatus.SUCCESS,
                    result=cached_result,
                    tool_name=tool_name,
                    duration=0.0,
                )

        # 7. 执行工具(带超时控制)
        try:
            result = await self._execute_with_timeout(
                tool.func, validated_args, tool.timeout
            )

            # 更新工具统计
            duration = time.time() - start_time
            self._update_tool_stats(tool, duration)

            # 缓存结果
            if tool.cacheable:
                self._result_cache[cache_key] = (result, time.time() + tool.cache_ttl)

            return ExecutionResult(
                status=ExecutionStatus.SUCCESS,
                result=result,
                tool_name=tool_name,
                parameters=validated_args,
                duration=duration
            )

        except asyncio.TimeoutError:
            return ExecutionResult(
                status=ExecutionStatus.TIMEOUT,
                error=f"工具执行超时({tool.timeout}秒)",
                tool_name=tool_name,
                duration=time.time() - start_time
            )
        except asyncio.CancelledError:
            return ExecutionResult(
                status=ExecutionStatus.CANCELLED,
                error="工具执行被取消",
                tool_name=tool_name,
                duration=time.time() - start_time
            )
        except Exception as e:
            return ExecutionResult(
                status=ExecutionStatus.ERROR,
                error=f"{type(e).__name__}: {str(e)}",
                tool_name=tool_name,
                duration=time.time() - start_time
            )

    async def _execute_with_timeout(self, func: Callable, args: dict,
                                    timeout: float) -> Any:
        """带超时控制执行工具"""
        if asyncio.iscoroutinefunction(func):
            # 异步函数直接等待
            return await asyncio.wait_for(func(**args), timeout=timeout)
        else:
            # 同步函数在线程池中执行
            loop = asyncio.get_event_loop()
            future = loop.run_in_executor(self._executor, func, **args)
            return await asyncio.wait_for(future, timeout=timeout)

    def _check_rate_limit(self, tool_name: str, rate_limit: int) -> bool:
        """检查调用频率"""
        import time
        now = time.time()
        window = 60.0  # 1分钟窗口

        if tool_name not in self._call_history:
            self._call_history[tool_name] = []

        # 清理过期记录
        self._call_history[tool_name] = [
            t for t in self._call_history[tool_name] if now - t < window
        ]

        if len(self._call_history[tool_name]) >= rate_limit:
            return False

        self._call_history[tool_name].append(now)
        return True

    @staticmethod
    def _make_cache_key(tool_name: str, args: dict) -> str:
        """生成缓存键"""
        return f"{tool_name}:{json.dumps(args, sort_keys=True)}"

    @staticmethod
    def _update_tool_stats(tool: ToolSpec, duration: float) -> None:
        """更新工具统计数据"""
        tool.call_count += 1
        tool.last_called = time.time()
        # 移动平均
        if tool.avg_latency == 0:
            tool.avg_latency = duration
        else:
            tool.avg_latency = (tool.avg_latency * 0.9 + duration * 0.1)

    async def execute_batch(self, calls: list[dict]) -> list[ExecutionResult]:
        """批量并行执行工具调用"""
        tasks = [
            self.execute(call["tool_name"], call.get("args", {}), call.get("context"))
            for call in calls
        ]
        return await asyncio.gather(*tasks, return_exceptions=True)

    def clear_cache(self) -> None:
        """清空结果缓存"""
        self._result_cache.clear()

    def get_cache_stats(self) -> dict:
        """获取缓存统计"""
        import time
        now = time.time()
        active = sum(1 for _, exp in self._result_cache.values() if exp > now)
        return {
            "total_entries": len(self._result_cache),
            "active_entries": active,
            "expired_entries": len(self._result_cache) - active,
        }

7.3 执行引擎架构

执行引擎

缓存命中

超限

需确认

execute 入口

1.查找工具

2.检查状态

3.限流检查

4.参数验证

5.权限检查

6.缓存检查

7.执行+超时控制

8.更新统计

9.缓存结果

返回结果

返回限流错误

返回需确认错误

7.4 沙箱隔离执行

对于不可信的工具或高权限操作,执行引擎应支持沙箱隔离:

class SandboxedExecutor:
    """沙箱隔离执行器"""

    def __init__(self, max_memory_mb: int = 256, max_cpu_seconds: int = 30):
        self.max_memory_mb = max_memory_mb
        self.max_cpu_seconds = max_cpu_seconds

    def _set_resource_limits(self):
        """设置进程资源限制"""
        try:
            # 内存限制
            mem_bytes = self.max_memory_mb * 1024 * 1024
            resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes))
            # CPU时间限制
            resource.setrlimit(resource.RLIMIT_CPU, (self.max_cpu_seconds, self.max_cpu_seconds))
        except (resource.error, AttributeError):
            # Windows不支持resource模块,跳过
            pass

    async def execute_sandboxed(self, func: Callable, args: dict,
                                timeout: float = 30.0) -> Any:
        """在沙箱中执行函数"""
        loop = asyncio.get_event_loop()

        def run_in_sandbox():
            self._set_resource_limits()
            return func(**args)

        try:
            future = loop.run_in_executor(None, run_in_sandbox)
            return await asyncio.wait_for(future, timeout=timeout)
        except asyncio.TimeoutError:
            raise TimeoutError(f"沙箱执行超时({timeout}秒)")
        except Exception as e:
            raise RuntimeError(f"沙箱执行失败: {e}")

通俗类比:执行引擎就像工厂的生产车间。工具函数是生产线上的机器,执行引擎是车间主任。主任负责:确认机器可用(状态检查)、确认工人有操作资格(权限检查)、控制生产节奏不要太快(限流)、检查原材料合格(参数验证)、设置安全护栏(超时控制)、记录生产数据(统计更新)。


八、工具结果处理

8.1 结果处理的重要性

工具执行完成后,原始结果可能格式不统一、过长、包含敏感信息或需要进一步加工。结果处理器负责将原始结果转化为LLM和用户可理解的形式。

8.2 结果处理器实现

import json
from typing import Any, Optional


class ResultProcessor:
    """工具结果处理器"""

    def __init__(self, max_length: int = 4000, max_array_items: int = 20):
        self.max_length = max_length
        self.max_array_items = max_array_items
        self._formatters: dict[str, Callable] = {}
        self._filters: list[Callable] = []

    def register_formatter(self, tool_name: str, formatter: Callable) -> None:
        """为特定工具注册自定义格式化器"""
        self._formatters[tool_name] = formatter

    def add_filter(self, filter_func: Callable) -> None:
        """添加结果过滤器(如脱敏)"""
        self._filters.append(filter_func)

    def process(self, result: Any, tool_name: str = "",
                context: dict = None) -> dict:
        """
        处理工具执行结果
        返回标准化的结果字典
        """
        context = context or {}

        # 1. 应用过滤器(脱敏等)
        for filter_func in self._filters:
            result = filter_func(result, tool_name)

        # 2. 自定义格式化
        if tool_name in self._formatters:
            result = self._formatters[tool_name](result)
        else:
            # 3. 默认格式化
            result = self._default_format(result)

        # 4. 截断处理
        result = self._truncate(result)

        # 5. 包装为标准格式
        return {
            "tool_name": tool_name,
            "result": result,
            "truncated": len(str(result)) > self.max_length,
            "timestamp": time.time(),
        }

    def _default_format(self, result: Any) -> str:
        """默认格式化"""
        if isinstance(result, str):
            return result
        elif isinstance(result, (dict, list)):
            # 截断数组
            if isinstance(result, list) and len(result) > self.max_array_items:
                result = result[:self.max_array_items]
                result.append({"_note": f"... 已截断,共 {len(result)} 条"})
            return json.dumps(result, ensure_ascii=False, indent=2)
        else:
            return str(result)

    def _truncate(self, result: str) -> str:
        """截断过长结果"""
        if len(result) <= self.max_length:
            return result
        return result[:self.max_length] + f"\n... [结果已截断,原始长度: {len(result)}]"

    def format_for_llm(self, processed: dict) -> str:
        """将处理后的结果格式化为LLM可读文本"""
        lines = [
            f"[工具: {processed['tool_name']}]",
            f"结果:",
            processed["result"],
        ]
        if processed.get("truncated"):
            lines.append("(注意: 结果已截断)")
        return "\n".join(lines)

    def format_for_user(self, processed: dict) -> str:
        """将处理后的结果格式化为用户可读文本"""
        return processed["result"]

8.3 结果缓存

class ResultCache:
    """工具结果缓存"""

    def __init__(self, max_size: int = 1000, default_ttl: int = 300):
        self._cache: dict[str, dict] = {}
        self._max_size = max_size
        self._default_ttl = default_ttl
        self._hits = 0
        self._misses = 0

    def get(self, tool_name: str, args: dict) -> Optional[Any]:
        """获取缓存结果"""
        key = self._make_key(tool_name, args)
        if key in self._cache:
            entry = self._cache[key]
            if time.time() < entry["expire"]:
                self._hits += 1
                return entry["result"]
            else:
                del self._cache[key]
        self._misses += 1
        return None

    def set(self, tool_name: str, args: dict, result: Any, ttl: int = None) -> None:
        """设置缓存"""
        if len(self._cache) >= self._max_size:
            self._evict()

        key = self._make_key(tool_name, args)
        self._cache[key] = {
            "result": result,
            "expire": time.time() + (ttl or self._default_ttl),
            "created": time.time(),
        }

    def invalidate(self, tool_name: str) -> int:
        """使某工具的所有缓存失效"""
        keys_to_remove = [k for k in self._cache if k.startswith(f"{tool_name}:")]
        for k in keys_to_remove:
            del self._cache[k]
        return len(keys_to_remove)

    def _evict(self):
        """LRU淘汰"""
        oldest_key = min(self._cache, key=lambda k: self._cache[k]["created"])
        del self._cache[oldest_key]

    @staticmethod
    def _make_key(tool_name: str, args: dict) -> str:
        return f"{tool_name}:{json.dumps(args, sort_keys=True)}"

    def stats(self) -> dict:
        """缓存统计"""
        total = self._hits + self._misses
        hit_rate = self._hits / total if total > 0 else 0
        return {
            "size": len(self._cache),
            "max_size": self._max_size,
            "hits": self._hits,
            "misses": self._misses,
            "hit_rate": round(hit_rate, 4),
        }

8.4 结果处理流程

结果处理流程

原始结果

1.过滤器/脱敏

2.格式化
自定义或默认

3.截断处理

4.包装标准格式

LLM可读格式

用户可读格式

缓存存储

通俗类比:结果处理就像新闻编辑的工作。记者(工具函数)发回原始素材(原始结果),编辑要做的:检查有无敏感信息需要打码(过滤脱敏)、把素材整理成文章(格式化)、太长的内容删减到版面能放下(截断)、最后排版成读者爱看的样式(用户可读格式)。


九、MCP协议集成

9.1 MCP协议概述

MCP(Model Context Protocol)是一种标准化的工具发现与调用协议,旨在解决"Agent如何发现和使用外部工具"的通用问题。MCP定义了工具服务器(Tool Server)和工具客户端(Tool Client)之间的通信规范,使任何Agent都能通过统一协议接入任何工具服务。

MCP是工具标准层(第7章深入)。

9.2 MCP架构

工具服务器侧

MCP协议层

Agent侧

MCP客户端

Agent

发现协议
tools/list

调用协议
tools/call

通知协议
notifications

搜索服务器

search工具

文件服务器

read/write工具

代码服务器

execute工具

9.3 MCP通信流程

MCP服务器 MCP客户端 Agent MCP服务器 MCP客户端 Agent 服务器可主动通知工具变更 需要搜索工具 initialize (握手) 返回服务器信息+能力声明 tools/list (发现工具) 返回工具列表 [search, fetch, ...] 可用工具: search, fetch 调用 search(query="AI") tools/call (name="search", args={query:"AI"}) 执行搜索 返回搜索结果 搜索结果 notification: toolsChanged tools/list (重新发现) 返回更新后的工具列表

9.4 MCP工具服务器实现

class MCPToolServer:
    """MCP工具服务器 - 提供工具给Agent使用"""

    def __init__(self, server_name: str, version: str = "1.0.0"):
        self.server_name = server_name
        self.version = version
        self._tools: dict[str, dict] = {}
        self._handlers: dict[str, Callable] = {}

    def register_tool(self, name: str, description: str,
                      input_schema: dict, handler: Callable) -> None:
        """注册MCP工具"""
        self._tools[name] = {
            "name": name,
            "description": description,
            "inputSchema": input_schema
        }
        self._handlers[name] = handler

    def handle_request(self, request: dict) -> dict:
        """处理MCP请求"""
        method = request.get("method")
        req_id = request.get("id")
        params = request.get("params", {})

        if method == "initialize":
            return self._handle_initialize(req_id, params)
        elif method == "tools/list":
            return self._handle_list(req_id, params)
        elif method == "tools/call":
            return self._handle_call(req_id, params)
        else:
            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "error": {"code": -32601, "message": f"未知方法: {method}"}
            }

    def _handle_initialize(self, req_id: int, params: dict) -> dict:
        """处理初始化请求"""
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {
                "protocolVersion": "2024-11-05",
                "capabilities": {"tools": {}},
                "serverInfo": {
                    "name": self.server_name,
                    "version": self.version
                }
            }
        }

    def _handle_list(self, req_id: int, params: dict) -> dict:
        """处理工具列表请求"""
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {
                "tools": list(self._tools.values())
            }
        }

    def _handle_call(self, req_id: int, params: dict) -> dict:
        """处理工具调用请求"""
        tool_name = params.get("name")
        arguments = params.get("arguments", {})

        if tool_name not in self._handlers:
            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "error": {"code": -32602, "message": f"工具不存在: {tool_name}"}
            }

        try:
            handler = self._handlers[tool_name]
            result = handler(**arguments)
            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "result": {
                    "content": [
                        {"type": "text", "text": str(result)}
                    ]
                }
            }
        except Exception as e:
            return {
                "jsonrpc": "2.0",
                "id": req_id,
                "error": {"code": -32603, "message": str(e)}
            }


# 创建MCP服务器示例
mcp_server = MCPToolServer("search-server", "1.0.0")

def search_handler(query: str, num: int = 10):
    """搜索处理器"""
    return f"搜索 '{query}' 的前 {num} 条结果"

mcp_server.register_tool(
    name="search",
    description="搜索互联网内容",
    input_schema={
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "搜索关键词"},
            "num": {"type": "integer", "default": 10}
        },
        "required": ["query"]
    },
    handler=search_handler
)

9.5 MCP客户端实现

class MCPClient:
    """MCP客户端 - 连接MCP服务器并使用其工具"""

    def __init__(self):
        self._connections: dict[str, dict] = {}
        self._tools: dict[str, dict] = {}
        self._tool_to_server: dict[str, str] = {}

    async def connect(self, server_name: str, transport: dict) -> bool:
        """
        连接MCP服务器
        transport: {"type": "stdio", "command": "...", "args": [...]}
                   {"type": "http", "url": "http://..."}
        """
        # 模拟连接和初始化
        init_response = {
            "protocolVersion": "2024-11-05",
            "capabilities": {"tools": {}},
            "serverInfo": {"name": server_name, "version": "1.0.0"}
        }

        self._connections[server_name] = {
            "transport": transport,
            "info": init_response,
            "connected": True
        }

        # 自动发现工具
        await self._discover_tools(server_name)
        return True

    async def _discover_tools(self, server_name: str) -> None:
        """发现服务器上的工具"""
        # 模拟 tools/list 请求
        mock_tools = [
            {
                "name": "search",
                "description": "搜索互联网",
                "inputSchema": {
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"]
                }
            }
        ]

        for tool in mock_tools:
            full_name = f"{server_name}.{tool['name']}"
            self._tools[full_name] = tool
            self._tool_to_server[full_name] = server_name

    async def call_tool(self, tool_name: str, arguments: dict) -> Any:
        """调用MCP工具"""
        if tool_name not in self._tools:
            raise ValueError(f"工具不存在: {tool_name}")

        server_name = self._tool_to_server[tool_name]
        if server_name not in self._connections:
            raise RuntimeError(f"服务器未连接: {server_name}")

        # 模拟 tools/call 请求
        short_name = tool_name.split(".")[-1]
        request = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "tools/call",
            "params": {"name": short_name, "arguments": arguments}
        }

        # 模拟响应
        mock_result = {
            "content": [
                {"type": "text", "text": f"工具 {tool_name} 执行结果"}
            ]
        }
        return mock_result

    async def disconnect(self, server_name: str) -> None:
        """断开服务器连接"""
        if server_name in self._connections:
            # 移除该服务器的工具
            to_remove = [
                name for name, srv in self._tool_to_server.items()
                if srv == server_name
            ]
            for name in to_remove:
                del self._tools[name]
                del self._tool_to_server[name]
            del self._connections[server_name]

    def list_tools(self) -> list[dict]:
        """列出所有已发现的MCP工具"""
        return list(self._tools.values())

9.6 MCP与本地注册表的融合

class UnifiedToolRegistry:
    """统一工具注册表 - 融合本地工具和MCP工具"""

    def __init__(self):
        self.local_registry = ToolRegistry()
        self.mcp_client = MCPClient()
        self._all_tools: dict[str, ToolSpec] = {}

    def register_local(self, name: str, description: str = "",
                       parameters: dict = None, **kwargs) -> Callable:
        """注册本地工具"""
        return self.local_registry.register(name, description, parameters, **kwargs)

    async def connect_mcp(self, server_name: str, transport: dict) -> bool:
        """连接MCP服务器并导入工具"""
        success = await self.mcp_client.connect(server_name, transport)
        if success:
            # 将MCP工具转换为ToolSpec并注册
            for tool_info in self.mcp_client.list_tools():
                full_name = f"{server_name}.{tool_info['name']}"
                spec = ToolSpec(
                    name=full_name,
                    description=tool_info["description"],
                    func=self._create_mcp_wrapper(full_name),
                    parameters=tool_info.get("inputSchema", {}),
                    tags=["mcp", server_name],
                )
                self._all_tools[full_name] = spec
        return success

    def _create_mcp_wrapper(self, tool_name: str) -> Callable:
        """创建MCP工具的本地包装函数"""
        async def wrapper(**kwargs):
            result = await self.mcp_client.call_tool(tool_name, kwargs)
            # 提取文本内容
            if isinstance(result, dict) and "content" in result:
                texts = [c["text"] for c in result["content"] if c.get("type") == "text"]
                return "\n".join(texts) if texts else str(result)
            return str(result)
        return wrapper

    def get(self, name: str) -> Optional[ToolSpec]:
        """获取工具(本地或MCP)"""
        if name in self._all_tools:
            return self._all_tools[name]
        return self.local_registry.get(name)

    def list_all(self) -> list[ToolSpec]:
        """列出所有工具"""
        local_tools = self.local_registry.list_tools()
        mcp_tools = list(self._all_tools.values())
        return local_tools + mcp_tools

    def schemas(self) -> list[dict]:
        """返回所有工具的Schema"""
        return [t.to_schema() for t in self.list_all()]

9.7 MCP协议消息格式

MCP消息格式(JSON-RPC 2.0)

请求消息

jsonrpc: 2.0

id: 请求ID

method: 方法名

params: 参数

响应消息

jsonrpc: 2.0

id: 对应请求ID

result: 结果 / error: 错误

通知消息

jsonrpc: 2.0

method: 方法名

params: 参数

无id: 不需要响应

MCP方法方向说明
initialize客户端→服务器初始化连接,交换能力信息
tools/list客户端→服务器请求工具列表
tools/call客户端→服务器调用指定工具
notifications服务器→客户端工具变更通知

通俗类比:MCP协议就像USB标准。以前每个设备都有自己的接口(串口、并口、PS/2…),现在统一用USB。MCP让所有工具服务都用同一种"语言"与Agent通信,Agent不需要为每个工具服务学一套新的对接方式。


转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/lza_csdn2019/article/details/167081697

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--