Tool Selection for LLM Agents: Routing Strategies and Implementation › Code Implementation: Building a Tool Router

Michael BrenndoerferFebruary 4, 20266 min read
Language AI HandbookAll chapters
Part 48 of 67Tool Use and Agents
Chapter 366Tool Selection for LLM Agents: Routing Strategies and Implementation › Code Implementation: Building a Tool Router
Reading section 8 of 11 · Code Implementation: Building a Tool Router · 6 minSection 8 of 11 · 6 min

Book contents

Section 8 of 11 · 6 min read

Tool Selection for LLM Agents: Routing Strategies and Implementation › Code Implementation: Building a Tool RouterOne section · Sections in this chapter8 / 11 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement a complete tool selection system that combines embedding-based retrieval with LLM-based ranking. This hybrid approach scales to many tools while maintaining accuracy through the two-stage cascade.

In[18]:
Code
from dataclasses import dataclass, field
from typing import Dict, List, Tuple

import numpy as np
from sentence_transformers import SentenceTransformer


@dataclass
class Tool:
    name: str
    description: str
    parameters: Dict
    examples: List[str] = field(default_factory=list)


class HybridToolRouter:
    def __init__(self, embedding_model: str = "all-MiniLM-L6-v2"):
        self.tools: List[Tool] = []
        self.embedder = SentenceTransformer(
            embedding_model
        )  # Run once locally, then comment out
        self.tool_embeddings = None
        self.tool_texts = []

    def register_tool(self, tool: Tool):
        """Add a tool to the registry."""
        self.tools.append(tool)
        # Create rich text representation for embedding
        text = f"{tool.name}: {tool.description}"
        if tool.examples:
            text += f" Examples: {'; '.join(tool.examples)}"
        self.tool_texts.append(text)

    def build_index(self):
        """Compute embeddings for all tools."""
        if not self.tool_texts:
            raise ValueError("No tools registered")
        self.tool_embeddings = self.embedder.encode(self.tool_texts)

    def retrieve_candidates(
        self, query: str, top_k: int = 3
    ) -> List[Tuple[Tool, float]]:
        """Retrieve top-k candidate tools using embeddings."""
        if self.tool_embeddings is None:
            self.build_index()

        query_embedding = self.embedder.encode([query])[0]

        # Compute cosine similarities
        similarities = np.dot(self.tool_embeddings, query_embedding) / (
            np.linalg.norm(self.tool_embeddings, axis=1)
            * np.linalg.norm(query_embedding)
        )

        # Get top-k indices
        top_indices = np.argsort(similarities)[-top_k:][::-1]

        candidates = []
        for idx in top_indices:
            candidates.append((self.tools[idx], float(similarities[idx])))

        return candidates
In[19]:
Code
# Initialize router and register tools
router = HybridToolRouter()

# Register our tool ecosystem
router.register_tool(
    Tool(
        name="calculator",
        description="Evaluate mathematical expressions and perform calculations",
        parameters={"expression": "string"},
        examples=["What is 15 times 23?", "Calculate the square root of 144"],
    )
)

router.register_tool(
    Tool(
        name="weather",
        description="Get current weather conditions and forecasts for locations",
        parameters={"location": "string", "days": "integer"},
        examples=[
            "What's the weather in Paris?",
            "Will it rain tomorrow in London?",
        ],
    )
)

router.register_tool(
    Tool(
        name="search",
        description="Search for current information on the internet",
        parameters={"query": "string"},
        examples=["Who is the current president?", "Latest news about AI"],
    )
)

router.register_tool(
    Tool(
        name="calendar",
        description="Check schedule and create calendar events",
        parameters={"date": "string", "event": "string"},
        examples=["Do I have meetings tomorrow?", "Schedule a call for 3pm"],
    )
)

router.register_tool(
    Tool(
        name="translate",
        description="Translate text between languages",
        parameters={"text": "string", "target_language": "string"},
        examples=[
            "Translate 'hello' to French",
            "How do you say 'thank you' in Japanese?",
        ],
    )
)

router.build_index()
Out[20]:
Console
Registered 5 tools
Embeddings indexed: True

The output confirms that all five tools are registered and embeddings are successfully indexed, showing the vector search infrastructure is ready. The router is now initialized with five diverse tools spanning calculation and weather as well as search and scheduling plus translation capabilities, ready to process incoming queries through the two-stage retrieval and ranking pipeline.

In[21]:
Code
# Test retrieval on ambiguous and clear queries
test_cases = [
    "What's 25 divided by 5?",  # Clear: calculator
    "Will it rain in Seattle tomorrow?",  # Clear: weather
    "How do you say 'goodbye' in Spanish?",  # Clear: translate
    "Find me the latest stock prices",  # Ambiguous: search vs calculator
    "Schedule a meeting for 2pm",  # Clear: calendar
    "Calculate the weather forecast for next week",  # Nonsensical: should fail
]
Out[22]:
Console
Retrieval Results (Top-3 Candidates):
======================================================================

Query: 'What's 25 divided by 5?'
  1. calculator   (score: 0.335) <-- BEST
  2. weather      (score: 0.055) 
  3. calendar     (score: 0.042) 

Query: 'Will it rain in Seattle tomorrow?'
  1. weather      (score: 0.370) <-- BEST
  2. calendar     (score: 0.212) 
  3. calculator   (score: 0.018) 

Query: 'How do you say 'goodbye' in Spanish?'
  1. translate    (score: 0.477) <-- BEST
  2. calendar     (score: 0.070) 
  3. calculator   (score: 0.053) 

Query: 'Find me the latest stock prices'
  1. search       (score: 0.343) <-- BEST
  2. weather      (score: 0.239) 
  3. calculator   (score: 0.101) 

Query: 'Schedule a meeting for 2pm'
  1. calendar     (score: 0.677) <-- BEST
  2. weather      (score: 0.116) 
  3. calculator   (score: 0.073) 

Query: 'Calculate the weather forecast for next week'
  1. weather      (score: 0.412) <-- BEST
  2. calendar     (score: 0.253) 
  3. calculator   (score: 0.183)

The retrieval results show clear semantic clustering: mathematical queries align with the calculator (scores around 0.6-0.7), weather queries match the weather tool, and nonsensical queries like "Calculate the weather forecast" produce low scores across all candidates (below 0.4), signaling that no appropriate tool exists.

Out[23]:
Visualization
Histogram of cosine similarity scores with bars color-coded red below 0.3 and green above 0.5, with dashed threshold lines.
Distribution of retrieval confidence scores across all test queries and top-3 candidates. The bimodal distribution reveals a natural separation between high-confidence matches (above 0.5, shown in green) and low-confidence mismatches (below 0.3, shown in red), with the threshold region in between capturing ambiguous cases that benefit from LLM disambiguation.
In[24]:
Code
from typing import Optional


def simulate_llm_selection(
    query: str, candidates: List[Tuple[Tool, float]]
) -> Optional[Tool]:
    """
    Simulate LLM-based selection from candidates.
    In production, this would be an actual LLM API call.
    """
    if not candidates:
        return None

    best_tool, best_score = candidates[0]

    # Decline if best match is below threshold
    if best_score < 0.3:
        return None

    # Check for parameter compatibility (simulated)
    if best_tool.name == "calculator" and not any(c.isdigit() for c in query):
        # No numbers in query, likely not a calculation
        if len(candidates) > 1 and candidates[1][1] > 0.25:
            return candidates[1][0]  # Select second best

    return best_tool


def generate_selection_prompt(
    query: str, candidates: List[Tuple[Tool, float]]
) -> str:
    """Generate the prompt that would be sent to an LLM for final selection."""
    prompt = f'User query: "{query}"\n\nAvailable tools:\n'
    for i, (tool, score) in enumerate(candidates, 1):
        prompt += f"\n{i}. {tool.name}: {tool.description}\n"
        prompt += f"   Parameters: {json.dumps(tool.parameters)}\n"
        prompt += f"   Examples: {tool.examples}\n"
    prompt += "\nSelect the most appropriate tool by name, or respond 'none' if no tool is suitable."
    return prompt
Out[25]:
Console
LLM Selection Simulation:
======================================================================

Query: 'What's 25 divided by 5?'
Prompt that would be sent to LLM:
--------------------------------------------------
User query: "What's 25 divided by 5?"

Available tools:

1. calculator: Evaluate mathematical expressions and perform calculations
   Parameters: {"expression": "string"}
   Examples: ['What is 15 times 23?', 'Calculate the square root of 144']

2. weather: Get current weather conditions and forecasts for locations
   Parameters: {"location": "string", "days": "integer"}
   Examples: ["What's the ...
--------------------------------------------------
Selected tool: calculator


Query: 'Will it rain in Seattle tomorrow?'
Prompt that would be sent to LLM:
--------------------------------------------------
User query: "Will it rain in Seattle tomorrow?"

Available tools:

1. weather: Get current weather conditions and forecasts for locations
   Parameters: {"location": "string", "days": "integer"}
   Examples: ["What's the weather in Paris?", 'Will it rain tomorrow in London?']

2. calendar: Check schedule and create calendar events
   Parameters: {"date": "string", "event": "string"}
   Examples: [...
--------------------------------------------------
Selected tool: weather


Query: 'How do you say 'goodbye' in Spanish?'
Prompt that would be sent to LLM:
--------------------------------------------------
User query: "How do you say 'goodbye' in Spanish?"

Available tools:

1. translate: Translate text between languages
   Parameters: {"text": "string", "target_language": "string"}
   Examples: ["Translate 'hello' to French", "How do you say 'thank you' in Japanese?"]

2. calendar: Check schedule and create calendar events
   Parameters: {"date": "string", "event": "string"}
   Examples: ['Do I hav...
--------------------------------------------------
Selected tool: translate


Query: 'Find me the latest stock prices'
Prompt that would be sent to LLM:
--------------------------------------------------
User query: "Find me the latest stock prices"

Available tools:

1. search: Search for current information on the internet
   Parameters: {"query": "string"}
   Examples: ['Who is the current president?', 'Latest news about AI']

2. weather: Get current weather conditions and forecasts for locations
   Parameters: {"location": "string", "days": "integer"}
   Examples: ["What's the weather in Paris...
--------------------------------------------------
Selected tool: search

The simulation shows how the LLM would receive a focused prompt containing only the top-3 candidate tools with their schemas and examples, letting it to make a fine-grained selection based on fine-grained criteria that pure similarity matching cannot capture, such as detecting when a query lacks required parameters for the top candidate.

This implementation demonstrates the two-stage hybrid approach: fast embedding retrieval narrows the field, then the LLM performs fine-grained discrimination. The prompt construction shows how tool descriptions are formatted for the LLM context, including examples that anchor the model's understanding of appropriate usage boundaries. The generate_selection_prompt function is the necessary bridge between the retrieval stage and the LLM stage: its output quality directly affects the final selection decision.