工具搜索 — 替代方案

不同工具搜索策略的对比与实践

🌐 查看英文原文 | 源码 Notebook


工具搜索:替代方案

Recommend first reading see the cookbook: Tool Search with Embeddings.

The goal of this cookbook is to show of some alternate approaches to using tool search (or really “tool discovery”) with Claude. In this cookbook we’ll demonstrate two useful techniques:

  1. Tools can be discovered without “Search”. In this cookbook, we’ll include all of the tool names in Claude’s system prompt and provide Claude with decribe_tool_tool to load the tool fully into Claude’s context.
  2. Tools do not have to be passed in the request’s tools list if they have not been loaded into Claude’s context yet. This can be a bit more application complexity to manage, but can allow your application to keep requests small, even while Claude has potential access to thousands of tools.

Users have a lot of flexibility to design tool search to keep Claude’s context (and Messages requests) as focused as possible.

前置条件

Before following this guide, ensure you have:

Required Knowledge - Python fundamentals - comfortable with functions, dictionaries, and basic data structures - Basic understanding of Claude tool use - we recommend reading the Tool Use Guide first

Required Tools - Python 3.11 or higher - Anthropic API key (get one here)

%pip install -q anthropic python-dotenv
import json

import anthropic
from dotenv import load_dotenv

load_dotenv()

MODEL = "claude-sonnet-4-6"
client = anthropic.Anthropic()

print("✓ Client initialized")

定义工具库

我们将定义 5 个简单的工具。在生产环境中,这可能是存储在数据库或配置文件中的数百或数千个工具。

TOOL_LIBRARY = {
    "get_weather": {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name"},
            },
            "required": ["city"],
        },
    },
    "get_stock_price": {
        "name": "get_stock_price",
        "description": "Get current stock price for a ticker symbol",
        "input_schema": {
            "type": "object",
            "properties": {
                "ticker": {"type": "string", "description": "Stock ticker (e.g., AAPL)"},
            },
            "required": ["ticker"],
        },
    },
    "convert_currency": {
        "name": "convert_currency",
        "description": "Convert amount between currencies",
        "input_schema": {
            "type": "object",
            "properties": {
                "amount": {"type": "number"},
                "from_currency": {"type": "string"},
                "to_currency": {"type": "string"},
            },
            "required": ["amount", "from_currency", "to_currency"],
        },
    },
    "calculate_tip": {
        "name": "calculate_tip",
        "description": "Calculate tip amount for a bill",
        "input_schema": {
            "type": "object",
            "properties": {
                "bill_amount": {"type": "number"},
                "tip_percent": {"type": "number", "default": 20},
            },
            "required": ["bill_amount"],
        },
    },
    "send_email": {
        "name": "send_email",
        "description": "Send an email to a recipient",
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Recipient email"},
                "subject": {"type": "string"},
                "body": {"type": "string"},
            },
            "required": ["to", "subject", "body"],
        },
    },
}

print(f"✓ Defined {len(TOOL_LIBRARY)} tools: {list(TOOL_LIBRARY.keys())}")

describe_tool 工具

我们不给 Claude 做语义搜索,而是给它一个简单的 describe_tool 工具。Claude 用工具名称调用它,将该工具加载到上下文中。

系统提示列出了所有可用的工具名称,因此 Claude 知道有哪些工具可用,而无需 embedding 或搜索。

DESCRIBE_TOOL = {
    "name": "describe_tool",
    "description": "Load a tool's full definition into context. Call this before using any tool for the first time.",
    "input_schema": {
        "type": "object",
        "properties": {
            "tool_name": {
                "type": "string",
                "description": "Name of the tool to load",
            },
        },
        "required": ["tool_name"],
    },
}

# Build system prompt with tool catalog
tool_names = list(TOOL_LIBRARY.keys())
SYSTEM_PROMPT = f"""You are a helpful assistant with access to various tools.

Available tools: {", ".join(tool_names)}

Before using any tool, you must first call describe_tool with the tool name to load it."""

print("System prompt:")
print(SYSTEM_PROMPT)

模拟工具执行

用于演示的简单模拟响应:

def execute_tool(name: str, inputs: dict) -> str:
    """Mock tool execution."""
    if name == "get_weather":
        return json.dumps({"city": inputs["city"], "temp": "72°F", "conditions": "Sunny"})
    elif name == "get_stock_price":
        return json.dumps({"ticker": inputs["ticker"], "price": 185.50, "change": "+1.2%"})
    elif name == "convert_currency":
        rate = 0.92 if inputs["to_currency"] == "EUR" else 1.0
        converted = inputs["amount"] * rate
        return json.dumps({"converted": round(converted, 2), "to": inputs["to_currency"]})
    elif name == "calculate_tip":
        tip = inputs["bill_amount"] * (inputs.get("tip_percent", 20) / 100)
        return json.dumps({"tip": round(tip, 2), "total": round(inputs["bill_amount"] + tip, 2)})
    elif name == "send_email":
        return json.dumps({"status": "sent", "to": inputs["to"]})
    return json.dumps({"error": f"Unknown tool: {name}"})


print("✓ Mock execution ready")

带有动态工具加载的对话循环

这里的关键模式:

  1. 仅以 describe_tool 开始在工具列表中
  2. 当 Claude 调用 describe_tool 时,返回一个 tool_reference 并使用 defer_loading=True 将工具添加到 active_tools
  3. defer_loading=True 至关重要——它使工具定义保持在缓存的提示前缀之外,避免在发现工具时缓存失效

每次 Claude 在对话中看到 tool_reference 时,完整的工具定义都会在该对话点被加载到 Claude 的上下文中。

def run_conversation(user_message: str, max_turns: int = 10):
    """Run a conversation with dynamic tool loading."""
    print(f"\n{'=' * 60}")
    print(f"USER: {user_message}")
    print(f"{'=' * 60}\n")

    messages = [{"role": "user", "content": user_message}]

    # Start with ONLY describe_tool - no other tools in the request
    active_tools = [DESCRIBE_TOOL]
    loaded_tools = set()  # Track which tools we've added

    for turn in range(max_turns):
        print(f"--- Turn {turn + 1} (tools in request: {len(active_tools)}) ---")

        response = client.messages.create(
            model=MODEL,
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            tools=active_tools,
            messages=messages,
            extra_headers={"anthropic-beta": "advanced-tool-use-2025-11-20"},
        )

        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            for block in response.content:
                if block.type == "text":
                    print(f"\nASSISTANT: {block.text}")
            break

        # Process tool calls
        tool_results = []
        for block in response.content:
            if block.type == "text" and block.text:
                print(f"ASSISTANT: {block.text}")

            elif block.type == "tool_use":
                tool_name = block.name
                tool_input = block.input

                if tool_name == "describe_tool":
                    requested_tool = tool_input["tool_name"]
                    print(f"🔍 describe_tool({requested_tool})")

                    if requested_tool in TOOL_LIBRARY:
                        # Add tool to active_tools with defer_loading=True
                        # This is critical for prompt caching!
                        if requested_tool not in loaded_tools:
                            tool_def = {**TOOL_LIBRARY[requested_tool], "defer_loading": True}
                            active_tools.append(tool_def)
                            loaded_tools.add(requested_tool)
                            print(f"   ✓ Added {requested_tool} to tools (defer_loading=True)")

                        # Return tool_reference so Claude can use it
                        tool_results.append(
                            {
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "content": [
                                    {"type": "tool_reference", "tool_name": requested_tool}
                                ],
                            }
                        )
                    else:
                        tool_results.append(
                            {
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "content": f"Tool '{requested_tool}' not found.",
                            }
                        )
                else:
                    # Execute discovered tool
                    print(f"🔧 {tool_name}({json.dumps(tool_input)})")
                    result = execute_tool(tool_name, tool_input)
                    print(f"   → {result}")
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result,
                        }
                    )

        if tool_results:
            messages.append({"role": "user", "content": tool_results})

    print(f"\n{'=' * 60}\n")


print("✓ Conversation loop ready")

示例:天气查询

观察 Claude 如何: 1. 在系统提示的工具列表中看到 get_weather 2. 调用 describe_tool("get_weather") 来加载它 3. 接收 tool_reference 并现在可以使用该工具

run_conversation("What's the weather in Tokyo?")

示例:多工具查询

Claude 可以根据需要加载多个工具:

run_conversation("Convert $100 to EUR, then calculate a 20% tip on a $85 dinner bill.")

为什么 defer_loading=True 很重要

当你向 tools 列表添加工具时,工具定义通常会被加载到 Claude 上下文窗口的最开始。如果你添加新工具,你将丢失大部分缓存,因为上下文窗口的最开始已经改变了。

使用 defer_loading=True 时: - 工具定义不会被包含在 Claude 上下文窗口的最开始 - 相反,它会在 Claude 看到 tool_reference 时被加载到上下文中 - 这意味着你的系统提示和初始工具保持缓存,即使你发现了新工具

模式:

# Initial request - only describe_tool, system prompt is cached
tools = [DESCRIBE_TOOL]

# After Claude calls describe_tool("get_weather")
# Add with defer_loading to preserve cache
tools.append({**TOOL_LIBRARY["get_weather"], "defer_loading": True})

# Return tool_reference so Claude knows it's available
tool_result = [{"type": "tool_reference", "tool_name": "get_weather"}]

这对于拥有数百或数千个工具的应用程序至关重要,你可能希望: - 保持初始请求大小较小 - 在工具发现过程中保持提示缓存 - 仅加载 Claude 实际需要的工具

结论

本教程的关键洞察:工具不需要在 Claude 需要它们之前就放入 tools 列表中。结合 defer_loading=True 和 tool_reference,这让你可以扩展到数千个工具,同时保持请求较小并保持提示缓存。不过,在这种情况下,你的客户端需要提供工具发现机制。

这里展示的 describe_tool 方法只是一种形式。其他模式包括: - list_tools - 返回匹配某个类别或关键词的工具名称 - 分层发现 - 浏览工具类别,然后加载特定工具 - 基于 embedding 的搜索 - 通过语义相似度查找工具(见 tool_search_with_embeddings.ipynb)

核心模式始终是: 1. 发现工具时返回 tool_reference 2. 使用 defer_loading=True 添加工具以保持缓存 3. Claude 然后可以立即使用该工具

有关基于 embedding 的方法,请参见基于 Embedding 的工具搜索。


Tip

中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。