工具搜索 — 替代方案
不同工具搜索策略的对比与实践
🌐 查看英文原文 | 源码 Notebook
Tool Search: Alternate Approaches
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:
- 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.
- Tools do not have to be passed in the request’s
toolslist 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.
Prerequisites
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-dotenvimport json
import anthropic
from dotenv import load_dotenv
load_dotenv()
MODEL = "claude-sonnet-4-6"
client = anthropic.Anthropic()
print("✓ Client initialized")Define Tool Library
We’ll define 5 simple tools. In production, this could be hundreds or thousands of tools stored in a database or configuration file.
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())}")The describe_tool Tool
Instead of semantic search, we give Claude a simple describe_tool tool. Claude calls this with a tool name to load that tool into context.
The system prompt lists all available tool names, so Claude knows what’s available without needing embeddings or search.
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)Mock Tool Execution
Simple mock responses for demonstration:
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")Conversation Loop with Dynamic Tool Loading
The key pattern here:
- Start with only
describe_toolin the tools list - When Claude calls
describe_tool, return atool_referenceAND add the tool toactive_toolswithdefer_loading=True defer_loading=Trueis critical - it keeps the tool definition out of the cached prompt prefix, avoiding cache invalidation when tools are discovered
Every time Claude sees a tool_reference in the conversation, the full tool definition is loaded into Claude’s context at that point in the conversation.
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")Example: Weather Query
Watch how Claude: 1. Sees get_weather in the system prompt’s tool list 2. Calls describe_tool("get_weather") to load it 3. Receives the tool_reference and can now use the tool
run_conversation("What's the weather in Tokyo?")Example: Multi-Tool Query
Claude can load multiple tools as needed:
run_conversation("Convert $100 to EUR, then calculate a 20% tip on a $85 dinner bill.")Why defer_loading=True Matters
When you add a tool to the tools list, the tool definition is normally loaded into the very beginning of Claude’s context window. If you add new tools, you will lose most of the cache because the very beginning of the context window has changed.
With defer_loading=True: - The tool definition is NOT included into the beginning of Claude’s context window. - Instead, it’s loaded into context when Claude sees the tool_reference - This means your system prompt and initial tools stay cached even as you discover new tools
The pattern:
# 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"}]This is essential for applications with hundreds or thousands of tools where you want to: - Keep initial request size small - Preserve prompt caching across tool discoveries - Only load tools Claude actually needs
Conclusion
The key insight from this cookbook: tools don’t need to be in the tools list until Claude needs them. Combined with defer_loading=True and tool_reference, this lets you scale to thousands of tools while keeping requests small and preserving prompt caching. However, in this case your client will need to provide a tool discovery mechanism.
The describe_tool approach shown here is just one flavor. Other patterns include: - list_tools - Returns tool names matching a category or keyword - Hierarchical discovery - Browse tool categories, then load specific tools - Hybrid - Combine listing with semantic search for large catalogs
The core pattern is always: 1. Return tool_reference when a tool is discovered 2. Add the tool with defer_loading=True to preserve caching 3. Claude can then use the tool immediately
See Tool Search with Embeddings for the embeddings-based approach.
中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。