引言

Agent 的能力边界由工具决定。2024年,工具是硬编码在 Agent 中的;2025年,工具变成可配置的插件;2026年,Anthropic 的 MCP(Model Context Protocol)让工具发现变成动态的、标准化的。本文梳理工具发现机制的完整演进路径。

一、工具发现的三代演进

第一代:静态注册 (2023-2024)
┌─────────────────────────────┐
│         Agent 代码           │
│  ┌──────┐ ┌──────┐ ┌──────┐│
│  │search│ │calc  │ │write ││  ← 硬编码在代码中
│  └──────┘ └──────┘ └──────┘│
└─────────────────────────────┘
问题:添加工具需要改代码、重新部署

第二代:配置化注册 (2024-2025)
┌─────────────────────────────┐
│         Agent 运行时         │
│  ┌─────────────────────┐   │
│  │   Tool Registry     │   │  ← 从配置文件/YAML加载
│  │  ┌──┐┌──┐┌──┐┌──┐ │   │
│  │  │S ││C ││W ││E │ │   │
│  │  └──┘└──┘└──┘└──┘ │   │
│  └─────────────────────┘   │
└─────────────────────────────┘
问题:工具集固定,无法按需扩展

第三代:动态发现 (2025-2026)
┌─────────────────────────────┐
│         Agent 运行时         │
│  ┌─────────────────────┐   │
│  │  Tool Discovery     │   │
│  │  ┌────────────────┐ │   │
│  │  │  MCP Registry  │ │   │  ← 标准化协议
│  │  │  ┌─┐┌─┐┌─┐┌─┐ │ │   │
│  │  │  │S││C││W││?│ │ │   │  ← 动态发现
│  │  │  └─┘└─┘└─┘└─┘ │ │   │
│  │  └────────────────┘ │   │
│  └─────────────────────┘   │
└─────────────────────────────┘
优势:即插即用、运行时扩展、生态共享

二、第一代:静态注册

# 硬编码工具——最简单但最不灵活
class SimpleAgent:
    def __init__(self, llm):
        self.llm = llm
        self.tools = {
            "search": self._search,
            "calculate": self._calculate,
            "write_file": self._write_file,
        }
    
    async def run(self, query: str) -> str:
        # 将工具定义注入 Prompt
        tools_desc = "\n".join(
            f"- {name}: {func.__doc__}" 
            for name, func in self.tools.items()
        )
        
        prompt = f"""Tools available:
{tools_desc}

To use a tool, respond with JSON: {{"tool": "name", "args": {{...}}}}

User: {query}"""
        
        response = await self.llm.invoke(prompt)
        # 解析并执行工具
        ...

问题:添加工具需要修改代码、测试、部署,周期以天计。

三、第二代:配置化注册

from pydantic import BaseModel
import yaml

class ToolDefinition(BaseModel):
    name: str
    description: str
    input_schema: dict       # JSON Schema
    endpoint: str            # HTTP endpoint
    method: str = "POST"
    auth: dict | None = None
    timeout_ms: int = 10000
    tags: list[str] = []

class ConfigurableToolRegistry:
    """从配置文件加载工具"""
    
    def __init__(self):
        self._tools: dict[str, ToolDefinition] = {}
    
    def load_from_yaml(self, path: str):
        with open(path) as f:
            config = yaml.safe_load(f)
        
        for tool_config in config["tools"]:
            tool = ToolDefinition(**tool_config)
            self._tools[tool.name] = tool
    
    def load_from_directory(self, dir_path: str):
        """从目录中加载所有工具配置"""
        import os
        for filename in os.listdir(dir_path):
            if filename.endswith((".yaml", ".yml", ".json")):
                self.load_from_file(os.path.join(dir_path, filename))
    
    async def execute(self, tool_name: str, args: dict) -> dict:
        tool = self._tools.get(tool_name)
        if not tool:
            raise ToolNotFoundError(tool_name)
        
        # 参数校验
        self._validate_args(args, tool.input_schema)
        
        # HTTP 调用
        async with httpx.AsyncClient(timeout=tool.timeout_ms/1000) as client:
            response = await client.request(
                tool.method,
                tool.endpoint,
                json=args,
                headers=self._build_auth_headers(tool.auth)
            )
            return response.json()
    
    def get_openai_tools_schema(self) -> list[dict]:
        """生成 OpenAI Function Calling 格式"""
        return [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.input_schema,
                }
            }
            for tool in self._tools.values()
        ]

配置文件示例:

# tools/research.yaml
tools:
  - name: web_search
    description: "Search the web for current information"
    input_schema:
      type: object
      properties:
        query:
          type: string
          description: "Search query"
        max_results:
          type: integer
          default: 5
      required: [query]
    endpoint: "https://api.search.example.com/v1/search"
    method: POST
    auth:
      type: api_key
      header: "X-API-Key"
      secret_ref: "search-api-key"
    timeout_ms: 5000
    tags: [research, read-only]

  - name: send_email
    description: "Send an email to specified recipients"
    input_schema:
      type: object
      properties:
        to:
          type: array
          items: {type: string}
        subject: {type: string}
        body: {type: string}
      required: [to, subject, body]
    endpoint: "https://internal-mail.example.com/send"
    method: POST
    auth:
      type: bearer
      secret_ref: "mail-token"
    timeout_ms: 10000
    tags: [communication, write]

四、第三代:动态发现(MCP 协议)

4.1 MCP 协议概述

Anthropic 在 2024年底发布的 Model Context Protocol (MCP) 在 2026年已成为工具发现的事实标准。MCP 定义了三个核心原语:

┌───────────────────────────────────────────┐
│              MCP 协议原语                  │
├───────────┬───────────┬───────────────────┤
│  Tools    │ Resources │    Prompts        │
│ (可执行)  │ (可读取)  │   (可使用)        │
├───────────┼───────────┼───────────────────┤
│ 搜索网页  │ 读取文件  │ 代码审查模板      │
│ 执行代码  │ 查询数据库│ 分析报告模板      │
│ 发送邮件  │ 访问API   │ 调试指南          │
└───────────┴───────────┴───────────────────┘

4.2 MCP Server 实现

from mcp import Server, Tool
from mcp.types import Tool as ToolType, TextContent

class FileSystemMCPServer(Server):
    """文件系统 MCP 服务器"""
    
    def __init__(self, root_path: str):
        super().__init__("filesystem-server", "1.0.0")
        self.root = Path(root_path)
    
    @self.list_tools()
    async def list_tools(self) -> list[ToolType]:
        """动态返回可用工具列表"""
        tools = [
            ToolType(
                name="read_file",
                description="Read the contents of a file",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Relative path to the file"
                        }
                    },
                    "required": ["path"]
                }
            ),
            ToolType(
                name="list_directory",
                description="List files in a directory",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "default": "."
                        }
                    }
                }
            ),
            ToolType(
                name="search_files",
                description="Search for files matching a pattern",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "pattern": {"type": "string"},
                        "path": {"type": "string", "default": "."}
                    },
                    "required": ["pattern"]
                }
            ),
        ]
        
        # 根据权限动态添加写入工具
        if self._has_write_permission():
            tools.append(ToolType(
                name="write_file",
                description="Write content to a file",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "path": {"type": "string"},
                        "content": {"type": "string"}
                    },
                    "required": ["path", "content"]
                }
            ))
        
        return tools
    
    @self.call_tool()
    async def call_tool(
        self,
        name: str,
        arguments: dict
    ) -> list[TextContent]:
        """执行工具调用"""
        if name == "read_file":
            path = self.root / arguments["path"]
            if not path.is_relative_to(self.root):
                raise PermissionError("Path traversal detected")
            content = path.read_text()
            return [TextContent(type="text", text=content)]
        
        elif name == "list_directory":
            path = self.root / arguments.get("path", ".")
            entries = [str(p.relative_to(self.root)) for p in path.iterdir()]
            return [TextContent(type="text", text="\n".join(entries))]
        
        elif name == "search_files":
            import fnmatch
            pattern = arguments["pattern"]
            search_path = self.root / arguments.get("path", ".")
            matches = [
                str(p.relative_to(self.root))
                for p in search_path.rglob(pattern)
            ]
            return [TextContent(type="text", text="\n".join(matches))]

4.3 MCP Client(Agent 端)

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

class MCPToolDiscovery:
    """MCP 动态工具发现客户端"""
    
    def __init__(self):
        self.servers: dict[str, ClientSession] = {}
        self.discovered_tools: dict[str, DiscoveredTool] = {}
    
    async def connect_server(
        self,
        name: str,
        command: str,
        args: list[str] = None,
        env: dict = None
    ):
        """连接 MCP 服务器"""
        server_params = StdioServerParameters(
            command=command,
            args=args or [],
            env=env
        )
        
        # 建立连接
        stdio_transport = await stdio_client(server_params)
        session = ClientSession(*stdio_transport)
        await session.initialize()
        
        self.servers[name] = session
        
        # 发现工具
        await self._refresh_tools(name)
    
    async def _refresh_tools(self, server_name: str):
        """刷新服务器的工具列表"""
        session = self.servers[server_name]
        result = await session.list_tools()
        
        for tool in result.tools:
            discovered = DiscoveredTool(
                name=tool.name,
                description=tool.description,
                input_schema=tool.inputSchema,
                server=server_name
            )
            self.discovered_tools[f"{server_name}.{tool.name}"] = discovered
    
    async def execute_tool(
        self,
        full_name: str,
        args: dict
    ) -> str:
        """执行动态发现的工具"""
        tool = self.discovered_tools.get(full_name)
        if not tool:
            raise ToolNotFoundError(f"Unknown tool: {full_name}")
        
        session = self.servers[tool.server]
        result = await session.call_tool(tool.name, args)
        
        # 合并返回内容
        return "\n".join(
            content.text for content in result.content
            if hasattr(content, 'text')
        )
    
    def get_all_tools_schema(self) -> list[dict]:
        """获取所有已发现工具的 Schema"""
        return [
            {
                "type": "function",
                "function": {
                    "name": full_name,
                    "description": tool.description,
                    "parameters": tool.input_schema,
                }
            }
            for full_name, tool in self.discovered_tools.items()
        ]
    
    async def auto_discover(self):
        """自动发现网络中的 MCP 服务器"""
        # 通过 mDNS / 服务发现找到 MCP 服务器
        discovered = await self.service_discovery.find_services("_mcp._tcp")
        
        for service in discovered:
            try:
                await self.connect_server(
                    name=service.name,
                    command="npx",
                    args=["-y", "@mcp/remote", f"--url={service.url}"]
                )
                logger.info(f"Discovered and connected: {service.name}")
            except Exception as e:
                logger.warning(f"Failed to connect {service.name}: {e}")

4.4 动态工具集成到 Agent

class DynamicAgent:
    """支持动态工具发现的 Agent"""
    
    def __init__(self, llm, mcp_discovery: MCPToolDiscovery):
        self.llm = llm
        self.mcp = mcp_discovery
    
    async def run(self, query: str, max_iterations: int = 10) -> str:
        messages = [{"role": "user", "content": query}]
        
        for i in range(max_iterations):
            # 每次迭代刷新可用工具
            tools = self.mcp.get_all_tools_schema()
            
            # LLM 决策
            response = await self.llm.invoke(
                messages=messages,
                tools=tools,
                tool_choice="auto"
            )
            
            if not response.tool_calls:
                return response.content
            
            messages.append(response)
            
            # 执行工具
            for tool_call in response.tool_calls:
                try:
                    result = await self.mcp.execute_tool(
                        tool_call.function.name,
                        json.loads(tool_call.function.arguments)
                    )
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": result
                    })
                except Exception as e:
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": f"Error: {e}"
                    })
        
        return "Max iterations reached"

五、工具发现的安全考量

class ToolSecurityValidator:
    """动态工具安全验证"""
    
    async def validate_discovered_tool(
        self,
        tool: DiscoveredTool
    ) -> SecurityAssessment:
        """验证动态发现的工具安全性"""
        
        risks = []
        
        # 1. 检查工具来源可信度
        if not await self._is_trusted_server(tool.server):
            risks.append(Risk(
                level="high",
                reason=f"Untrusted MCP server: {tool.server}"
            ))
        
        # 2. 分析工具能力
        dangerous_patterns = [
            ("delete", "remove", "drop"),  # 破坏性操作
            ("execute", "eval", "system"),  # 代码执行
            ("send", "upload", "post"),     # 数据外传
        ]
        
        tool_desc_lower = tool.description.lower()
        tool_name_lower = tool.name.lower()
        
        for patterns in dangerous_patterns:
            if any(p in tool_name_lower or p in tool_desc_lower for p in patterns):
                risks.append(Risk(
                    level="medium",
                    reason=f"Tool may perform sensitive operation: {tool.name}"
                ))
        
        # 3. Schema 分析
        if self._has_unbounded_input(tool.input_schema):
            risks.append(Risk(
                level="medium",
                reason="Tool accepts unbounded input"
            ))
        
        return SecurityAssessment(
            tool=tool.name,
            risks=risks,
            requires_approval=any(r.level == "high" for r in risks),
            safe_to_use=all(r.level != "high" for r in risks)
        )

六、工具发现的最佳实践

工具命名空间

# 避免命名冲突:使用 server.tool 命名空间
"filesystem.read_file"      # 文件系统服务器的读文件
"github.read_file"          # GitHub 服务器的读文件
"database.read_file"        # 数据库服务器的读文件

# 工具别名:为常用工具提供短名
TOOL_ALIASES = {
    "search": "filesystem.search_files",
    "read": "filesystem.read_file",
}

工具缓存与预加载

class ToolCache:
    """工具描述缓存,减少每次请求的工具列表长度"""
    
    async def get_relevant_tools(
        self,
        query: str,
        all_tools: list[DiscoveredTool]
    ) -> list[DiscoveredTool]:
        """根据查询语义筛选相关工具"""
        
        # 1. 关键词匹配
        keyword_matches = [
            t for t in all_tools
            if any(kw in query.lower() for kw in t.keywords)
        ]
        
        # 2. 语义匹配
        query_embedding = await self.embedder.embed(query)
        tool_embeddings = await self._get_tool_embeddings(all_tools)
        
        similarities = cosine_similarity(query_embedding, tool_embeddings)
        semantic_matches = [
            all_tools[i] for i in np.argsort(similarities)[-10:]
            if similarities[i] > 0.5
        ]
        
        # 3. 合并去重
        relevant = list(set(keyword_matches + semantic_matches))
        
        # 4. 总是包含核心工具
        core_tools = [t for t in all_tools if "core" in t.tags]
        
        return list(set(relevant + core_tools))

七、工具发现 Checklist

□ 工具配置与代码分离(YAML/JSON/数据库)
□ 支持 MCP 协议的标准化工具接口
□ 动态发现的工具经过安全验证
□ 工具命名空间避免冲突
□ 工具列表按相关性筛选(避免 Context 膨胀)
□ 工具可用性健康检查
□ 工具版本兼容性管理
□ 工具权限分级(只读/写入/危险)
□ 工具调用审计日志
□ 支持工具热加载(不重启 Agent)

结语

工具发现机制的演进映射了软件架构的演进——从单体到微服务,从硬编码到服务发现。MCP 协议让 Agent 的工具生态从"内置应用"进化为"应用商店"。2026年,一个 Agent 的能力不再由开发者决定,而由它连接的工具生态决定。构建开放的工具发现机制,让你的 Agent 拥有无限扩展的可能。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。