第 23 章

Citations API:文档问答系统的引用溯源机制与结构化输出的不兼容处理

第二十三章:Tool Search Tool:动态工具发现与自适应能力扩展

23.1 工具发现的必要性

在传统的 Tool Use 模式中,工具列表在每次 API 请求时静态传入。这在工具数量少(5-10个)时是合理的,但当系统需要支持数百个工具时,会遇到严重问题:

问题一:上下文窗口限制 大量工具定义会消耗大量输入 token。50个详细工具定义可能消耗 10,000+ tokens,占据宝贵的上下文空间。

问题二:模型注意力分散 研究表明,当工具列表过长时,Claude 的工具选择准确性会下降。模型需要在大量工具中寻找正确的一个,容易产生混淆。

问题三:工具生态动态变化 企业环境中,工具会不断添加、更新和淘汰。静态工具列表无法适应这种动态变化。

解决方案:Tool Search Tool(工具搜索工具)

Tool Search Tool 是一种元工具(meta-tool)模式,它允许 Claude 在需要时动态搜索和发现可用工具,而不是在请求开始时接收所有工具。

23.2 Tool Search Tool 的设计原则

Tool Search Tool 的核心思想是:将工具发现本身作为一个工具

tool_search_tool = {
    "name": "search_tools",
    "description": """搜索可用工具列表。当你需要某种能力但不确定是否有对应工具时,
    或者需要浏览所有可用工具时,使用此工具。
    
    返回匹配的工具定义列表,每个工具包含名称、描述和参数 Schema。
    找到合适的工具后,可以直接使用该工具的名称和参数调用它。
    
    【注意】系统支持动态工具调用:搜索到工具后,即使它不在初始工具列表中,
    你也可以通过 execute_dynamic_tool 工具来调用它。""",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "搜索关键词,描述你需要的能力,如'发送邮件'、'查询数据库'、'处理图片'"
            },
            "category": {
                "type": "string",
                "description": "工具类别过滤(可选)",
                "enum": [
                    "communication",  # 通信工具:邮件、短信、通知
                    "data",           # 数据工具:数据库、文件、表格
                    "integration",    # 集成工具:第三方 API
                    "computation",    # 计算工具:数学、代码执行
                    "media",          # 媒体工具:图片、视频、音频
                    "utility"         # 通用工具
                ]
            },
            "max_results": {
                "type": "integer",
                "default": 5,
                "minimum": 1,
                "maximum": 20,
                "description": "返回工具数量上限"
            }
        },
        "required": ["query"]
    }
}

execute_dynamic_tool = {
    "name": "execute_dynamic_tool",
    "description": """执行通过 search_tools 发现的工具。
    在调用此工具前,必须先用 search_tools 确认工具存在并了解其参数格式。""",
    "input_schema": {
        "type": "object",
        "properties": {
            "tool_name": {
                "type": "string",
                "description": "要执行的工具名称(必须是 search_tools 返回的工具)"
            },
            "tool_input": {
                "type": "object",
                "description": "传给工具的参数(必须符合该工具的 input_schema)"
            }
        },
        "required": ["tool_name", "tool_input"]
    }
}

23.3 工具注册表的实现

import json
import re
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class ToolRegistration:
    """工具注册信息"""
    definition: dict
    func: callable
    category: str
    tags: List[str] = field(default_factory=list)
    usage_count: int = 0
    last_used: Optional[datetime] = None
    version: str = "1.0"


class DynamicToolRegistry:
    """动态工具注册表"""
    
    def __init__(self):
        self.tools: Dict[str, ToolRegistration] = {}
        self._search_index: Dict[str, List[str]] = {}  # 关键词 -> 工具名列表
    
    def register(
        self,
        tool_definition: dict,
        func: callable,
        category: str = "utility",
        tags: List[str] = None
    ) -> "DynamicToolRegistry":
        """注册工具"""
        name = tool_definition["name"]
        
        registration = ToolRegistration(
            definition=tool_definition,
            func=func,
            category=category,
            tags=tags or []
        )
        self.tools[name] = registration
        
        # 更新搜索索引
        self._index_tool(name, tool_definition, tags or [])
        
        return self
    
    def _index_tool(self, name: str, definition: dict, tags: List[str]):
        """为工具建立搜索索引"""
        # 从名称和描述中提取关键词
        text = f"{name} {definition.get('description', '')} {' '.join(tags)}"
        
        # 简单的分词(实际生产中可用 jieba 等)
        keywords = set(re.findall(r'[\w]+', text.lower()))
        
        for keyword in keywords:
            if keyword not in self._search_index:
                self._search_index[keyword] = []
            if name not in self._search_index[keyword]:
                self._search_index[keyword].append(name)
    
    def search(
        self,
        query: str,
        category: Optional[str] = None,
        max_results: int = 5
    ) -> List[dict]:
        """搜索工具"""
        # 分词查询
        query_keywords = set(re.findall(r'[\w]+', query.lower()))
        
        # 计算每个工具的相关性分数
        tool_scores: Dict[str, float] = {}
        
        for keyword in query_keywords:
            # 完全匹配
            if keyword in self._search_index:
                for tool_name in self._search_index[keyword]:
                    tool_scores[tool_name] = tool_scores.get(tool_name, 0) + 1.0
            
            # 前缀匹配
            for indexed_keyword, tool_names in self._search_index.items():
                if indexed_keyword.startswith(keyword) and indexed_keyword != keyword:
                    for tool_name in tool_names:
                        tool_scores[tool_name] = tool_scores.get(tool_name, 0) + 0.5
        
        # 按类别过滤
        if category:
            tool_scores = {
                name: score
                for name, score in tool_scores.items()
                if self.tools.get(name) and self.tools[name].category == category
            }
        
        # 排序并返回前 N 个
        sorted_tools = sorted(tool_scores.items(), key=lambda x: x[1], reverse=True)
        
        results = []
        for tool_name, score in sorted_tools[:max_results]:
            if tool_name in self.tools:
                reg = self.tools[tool_name]
                results.append({
                    "name": tool_name,
                    "description": reg.definition.get("description", ""),
                    "category": reg.category,
                    "tags": reg.tags,
                    "input_schema": reg.definition.get("input_schema", {}),
                    "relevance_score": round(score, 2)
                })
        
        return results
    
    def execute(self, tool_name: str, tool_input: dict):
        """执行工具"""
        if tool_name not in self.tools:
            raise ValueError(f"工具不存在: {tool_name}")
        
        registration = self.tools[tool_name]
        registration.usage_count += 1
        registration.last_used = datetime.now()
        
        return registration.func(**tool_input)
    
    def list_categories(self) -> Dict[str, int]:
        """列出所有类别及工具数量"""
        categories = {}
        for reg in self.tools.values():
            categories[reg.category] = categories.get(reg.category, 0) + 1
        return categories
    
    def get_popular_tools(self, top_n: int = 10) -> List[dict]:
        """获取最常用的工具"""
        sorted_tools = sorted(
            self.tools.items(),
            key=lambda x: x[1].usage_count,
            reverse=True
        )
        return [
            {
                "name": name,
                "usage_count": reg.usage_count,
                "category": reg.category
            }
            for name, reg in sorted_tools[:top_n]
        ]

23.4 完整的动态工具发现 Agent

import anthropic

class DynamicToolAgent:
    """支持动态工具发现的 Agent"""
    
    def __init__(self, registry: DynamicToolRegistry, model: str = "claude-opus-4-5"):
        self.client = anthropic.Anthropic()
        self.registry = registry
        self.model = model
        
        # 初始工具集:只包含元工具
        self.meta_tools = [tool_search_tool, execute_dynamic_tool]
    
    def _handle_search_tools(self, inputs: dict) -> str:
        """处理 search_tools 调用"""
        results = self.registry.search(
            query=inputs["query"],
            category=inputs.get("category"),
            max_results=inputs.get("max_results", 5)
        )
        
        if not results:
            return json.dumps({
                "found": 0,
                "message": f"未找到与 '{inputs['query']}' 相关的工具",
                "suggestion": "尝试更换关键词,或查询 list_all_categories 了解可用工具类别"
            }, ensure_ascii=False)
        
        return json.dumps({
            "found": len(results),
            "tools": results
        }, ensure_ascii=False)
    
    def _handle_execute_dynamic_tool(self, inputs: dict) -> str:
        """处理 execute_dynamic_tool 调用"""
        tool_name = inputs["tool_name"]
        tool_input = inputs["tool_input"]
        
        try:
            result = self.registry.execute(tool_name, tool_input)
            return json.dumps(result, ensure_ascii=False, default=str)
        except ValueError as e:
            return json.dumps({
                "error": str(e),
                "suggestion": "请先用 search_tools 确认工具名称是否正确"
            }, ensure_ascii=False)
        except Exception as e:
            return json.dumps({
                "error": f"执行失败: {str(e)}",
                "tool_name": tool_name,
                "tool_input": tool_input
            }, ensure_ascii=False)
    
    def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        """处理工具调用"""
        if tool_name == "search_tools":
            return self._handle_search_tools(tool_input)
        elif tool_name == "execute_dynamic_tool":
            return self._handle_execute_dynamic_tool(tool_input)
        else:
            return json.dumps({"error": f"未知元工具: {tool_name}"})
    
    def run(self, user_message: str, system: str = "") -> str:
        """运行动态工具发现 Agent"""
        
        default_system = """你是一个能够动态发现和使用工具的助手。

【工具使用流程】
1. 接到任务后,先思考需要哪些类型的能力
2. 使用 search_tools 搜索相关工具
3. 查看搜索结果,了解工具的参数格式
4. 使用 execute_dynamic_tool 执行找到的工具
5. 分析工具结果,如需更多工具则继续搜索

【注意】
- 不要假设工具存在,必须先通过 search_tools 确认
- 如果第一次搜索没找到,尝试不同的关键词
- 工具执行失败时,检查参数是否符合工具的 input_schema"""
        
        messages = [{"role": "user", "content": user_message}]
        
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=system or default_system,
                tools=self.meta_tools,
                messages=messages
            )
            
            if response.stop_reason == "end_turn":
                return ' '.join(b.text for b in response.content if b.type == "text")
            
            if response.stop_reason == "tool_use":
                tool_results = []
                
                for block in response.content:
                    if block.type != "tool_use":
                        continue
                    
                    print(f"[元工具] {block.name}: {json.dumps(block.input, ensure_ascii=False)[:150]}")
                    
                    result = self._process_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
                
                messages.append({"role": "assistant", "content": response.content})
                messages.append({"role": "user", "content": tool_results})
            else:
                break
        
        return "任务完成"

23.5 构建工具生态系统

注册大量工具的实践

def build_enterprise_registry() -> DynamicToolRegistry:
    """构建企业级工具注册表(示例)"""
    registry = DynamicToolRegistry()
    
    # === 通信工具 ===
    registry.register(
        tool_definition={
            "name": "send_email",
            "description": "发送电子邮件给指定收件人",
            "input_schema": {
                "type": "object",
                "properties": {
                    "to": {"type": "string"},
                    "subject": {"type": "string"},
                    "body": {"type": "string"}
                },
                "required": ["to", "subject", "body"]
            }
        },
        func=lambda to, subject, body: {"status": "sent", "message_id": "msg_001"},
        category="communication",
        tags=["email", "通知", "邮件"]
    )
    
    registry.register(
        tool_definition={
            "name": "send_sms",
            "description": "发送短信到指定手机号",
            "input_schema": {
                "type": "object",
                "properties": {
                    "phone": {"type": "string"},
                    "message": {"type": "string", "maxLength": 160}
                },
                "required": ["phone", "message"]
            }
        },
        func=lambda phone, message: {"status": "sent", "cost": 0.05},
        category="communication",
        tags=["sms", "短信", "手机"]
    )
    
    # === 数据工具 ===
    registry.register(
        tool_definition={
            "name": "query_mysql",
            "description": "执行 MySQL 数据库查询",
            "input_schema": {
                "type": "object",
                "properties": {
                    "sql": {"type": "string"},
                    "database": {"type": "string"}
                },
                "required": ["sql", "database"]
            }
        },
        func=lambda sql, database: {"rows": [], "affected": 0},
        category="data",
        tags=["database", "mysql", "数据库", "查询", "sql"]
    )
    
    registry.register(
        tool_definition={
            "name": "read_excel",
            "description": "读取 Excel 文件并返回数据",
            "input_schema": {
                "type": "object",
                "properties": {
                    "file_path": {"type": "string"},
                    "sheet_name": {"type": "string"}
                },
                "required": ["file_path"]
            }
        },
        func=lambda file_path, sheet_name=None: {"rows": [], "columns": []},
        category="data",
        tags=["excel", "表格", "文件", "读取"]
    )
    
    # === 集成工具 ===
    registry.register(
        tool_definition={
            "name": "create_jira_ticket",
            "description": "在 Jira 中创建工单",
            "input_schema": {
                "type": "object",
                "properties": {
                    "project": {"type": "string"},
                    "summary": {"type": "string"},
                    "description": {"type": "string"},
                    "issue_type": {"type": "string", "enum": ["Bug", "Story", "Task"]}
                },
                "required": ["project", "summary", "issue_type"]
            }
        },
        func=lambda **kwargs: {"ticket_id": "PROJ-001", "url": "https://jira.example.com/PROJ-001"},
        category="integration",
        tags=["jira", "工单", "bug", "任务", "项目管理"]
    )
    
    registry.register(
        tool_definition={
            "name": "get_salesforce_contact",
            "description": "从 Salesforce CRM 获取联系人信息",
            "input_schema": {
                "type": "object",
                "properties": {
                    "email": {"type": "string"},
                    "name": {"type": "string"}
                }
            }
        },
        func=lambda **kwargs: {"contact_id": "003xx001", "name": "张三", "company": "示例公司"},
        category="integration",
        tags=["salesforce", "crm", "客户", "联系人"]
    )
    
    # 添加更多工具...(实际系统可能有数百个)
    print(f"工具注册表:{len(registry.tools)} 个工具")
    print(f"类别分布:{registry.list_categories()}")
    
    return registry

23.6 自适应工具组合:处理未知任务

Tool Search Tool 的真正威力在于处理系统设计时未预见的任务:

def demonstrate_adaptive_capability():
    """演示自适应工具发现能力"""
    
    registry = build_enterprise_registry()
    agent = DynamicToolAgent(registry)
    
    # 任务1:系统设计时预见的任务
    result1 = agent.run("给 [email protected] 发一封邮件,通知他明天的会议取消了")
    
    # 任务2:需要组合多个工具的复杂任务
    result2 = agent.run("""
    从 Excel 文件 /data/customers.xlsx 读取客户列表,
    在 Salesforce 中查找每个客户的联系人信息,
    然后为每个客户创建一个 Jira 跟进工单,
    并发送邮件通知销售团队。
    """)
    
    # 任务3:完全未预见的组合需求
    result3 = agent.run("""
    我需要给最近购买了超过 $10000 的客户发短信优惠券。
    先查询数据库获取客户列表,然后逐一发送短信。
    """)
    
    return result1, result2, result3

23.7 工具版本化与能力演进

随着业务发展,工具能力需要演进。Tool Search 模式天然支持工具的无缝更新:

class VersionedToolRegistry(DynamicToolRegistry):
    """支持工具版本管理的注册表"""
    
    def register_versioned(
        self,
        tool_definition: dict,
        func: callable,
        version: str = "1.0",
        deprecated_versions: List[str] = None,
        **kwargs
    ):
        """注册带版本的工具"""
        name = tool_definition["name"]
        
        # 检查是否有旧版本
        if name in self.tools:
            old_version = self.tools[name].version
            print(f"更新工具 {name}: v{old_version} -> v{version}")
        
        # 添加版本信息到描述
        description = tool_definition.get("description", "")
        if deprecated_versions:
            description += f"\n(当前版本:{version},废弃版本:{', '.join(deprecated_versions)})"
        else:
            description += f"\n(版本:{version})"
        
        enhanced_definition = {**tool_definition, "description": description}
        
        reg = self.register(enhanced_definition, func, **kwargs)
        self.tools[name].version = version
        
        return reg

小结

Tool Search Tool 模式将工具使用从"静态配置"提升到"动态发现",适合以下场景:

  1. 工具数量大(50个以上):避免每次请求传递全量工具定义
  2. 工具生态动态变化:新工具上线后无需更改 Agent 代码
  3. 处理未预见任务:让 Claude 自主发现完成任务所需的能力
  4. 多团队工具共享:不同团队的工具统一注册,按需发现和使用

核心实现要素:

下一章将探讨 Advisor Tool——让 Claude 在执行任务前进行元认知规划的高级模式。

本章评分
4.8  / 5  (9 评分)

💬 留言讨论