Skip to content
Blog

Tool-Selection Evals: Verifying the Agent Picks the Right Tool

Build eval suites that verify your agent selects the correct tool for each task, with precision metrics, confusion matrices, and regression testing for tool selection.

Published on September 15, 2026

AI Assistant

An agent with 20 tools is only useful if it picks the right one. Tool-selection evals measure whether your agent reliably maps user intent to the correct function, avoiding both false positives (calling unnecessary tools) and false negatives (missing tools that should be called).

Why Tool Selection Matters

Tool-selection errors are among the most expensive agent failures:

  • Wrong tool called: Wastes API calls, returns irrelevant data, confuses the user
  • No tool called when needed: Agent hallucinates instead of using available data
  • Wrong arguments: Tool is correct but parameters are incorrect
  • Unnecessary tool calls: Adds latency and cost without value

Building a Tool-Selection Eval Suite

Define the Ground Truth

For each eval case, specify which tool should be called and with what arguments:

eval_cases = [
    {
        "id": "ts-001",
        "user_input": "What's the current stock price of AAPL?",
        "expected_tool": "get_stock_price",
        "expected_args": {"symbol": "AAPL"},
        "should_not_call": ["search_web", "get_news"],
        "description": "Direct stock price query"
    },
    {
        "id": "ts-002",
        "user_input": "Summarize the latest earnings report for Apple",
        "expected_tool": "search_web",
        "expected_args": {"query": "Apple latest earnings report"},
        "should_not_call": ["get_stock_price"],
        "description": "Qualitative query requiring web search"
    },
    {
        "id": "ts-003",
        "user_input": "Compare Tesla and Ford's revenue this quarter",
        "expected_tools": ["get_financial_data", "get_financial_data"],
        "expected_args_list": [
            {"symbol": "TSLA", "metric": "revenue"},
            {"symbol": "F", "metric": "revenue"}
        ],
        "description": "Multi-tool comparison task"
    }
]

The Eval Runner

from dataclasses import dataclass
from typing import Optional
import json

@dataclass
class ToolSelectionResult:
    case_id: str
    predicted_tool: str
    expected_tool: str
    correct_tool: bool
    args_correct: bool
    extra_tools_called: list[str]
    missing_tools: list[str]
    score: float

def evaluate_tool_selection(agent, eval_cases: list[dict]) -> list[ToolSelectionResult]:
    results = []
    
    for case in eval_cases:
        # Run the agent
        response = agent.run(case["user_input"])
        
        # Extract tool calls from response
        tool_calls = extract_tool_calls(response)
        
        # Score tool selection
        predicted_tool = tool_calls[0]["name"] if tool_calls else None
        correct_tool = predicted_tool == case["expected_tool"]
        
        # Score arguments
        args_correct = False
        if correct_tool and tool_calls:
            args_correct = validate_args(
                tool_calls[0]["args"],
                case["expected_args"]
            )
        
        # Check for unnecessary tool calls
        called_tools = [tc["name"] for tc in tool_calls]
        extra = [t for t in called_tools if t not in case.get("expected_tools", [case["expected_tool"]])]
        missing = [t for t in case.get("expected_tools", [case["expected_tool"]]) if t not in called_tools]
        
        # Compute score
        score = compute_score(correct_tool, args_correct, extra, missing)
        
        results.append(ToolSelectionResult(
            case_id=case["id"],
            predicted_tool=predicted_tool,
            expected_tool=case["expected_tool"],
            correct_tool=correct_tool,
            args_correct=args_correct,
            extra_tools_called=extra,
            missing_tools=missing,
            score=score
        ))
    
    return results

def compute_score(correct_tool, args_correct, extra, missing):
    if not correct_tool:
        return 0.0
    base = 0.5
    if args_correct:
        base += 0.3
    penalty = len(extra) * 0.1 + len(missing) * 0.2
    return max(0, base - penalty)

Tool Confusion Matrix

Visualize which tools get confused with each other:

import numpy as np
from collections import defaultdict

def build_confusion_matrix(results: list[ToolSelectionResult], tool_names: list[str]):
    matrix = np.zeros((len(tool_names), len(tool_names)))
    tool_to_idx = {name: i for i, name in enumerate(tool_names)}
    
    for r in results:
        if r.predicted_tool in tool_to_idx and r.expected_tool in tool_to_idx:
            matrix[tool_to_idx[r.expected_tool]][tool_to_idx[r.predicted_tool]] += 1
    
    return matrix

def print_confusion_matrix(matrix, tool_names):
    print(f"{'':>20}", end="")
    for name in tool_names:
        print(f"{name[:8]:>10}", end="")
    print()
    
    for i, name in enumerate(tool_names):
        print(f"{name[:18]:>20}", end="")
        for j in range(len(tool_names)):
            val = int(matrix[i][j])
            marker = "*" if i == j and val > 0 else " "
            print(f"{val:>9}{marker}", end="")
        print()

# Output might look like:
#                 get_stock search_web get_news  get_financial
#           get_stock       45*        3        1             1
#          search_web        2       38*        4             0
#             get_news        0        1       42*             0
#       get_financial        1        0        0            44*

Prompt Engineering for Better Tool Selection

Tool Description Quality

The single biggest factor in tool-selection accuracy is how well you describe your tools:

# Bad: Vague description
tools = [{
    "name": "search",
    "description": "Searches for information"
}]

# Good: Specific, with use-case guidance
tools = [{
    "name": "web_search",
    "description": "Search the web for current information. Use this when: the user asks about recent events, current prices, news, or anything that changes frequently. Do NOT use for: mathematical calculations, unit conversions, or definitions of established concepts.",
    "parameters": {
        "query": {
            "type": "string",
            "description": "The search query. Be specific and include relevant context."
        }
    }
}]

System Prompt Tool Guidance

system_prompt = """
You have access to the following tools. Choose the MINIMUM number of tools needed:

1. `get_stock_price(symbol)` - Current price and daily change. Use ONLY for real-time price checks.
2. `get_historical_data(symbol, start, end)` - Historical OHLCV data. Use for trends, charts, analysis.
3. `search_news(query)` - Recent news articles. Use for sentiment, events, announcements.
4. `get_financials(symbol)` - Quarterly/annual financial statements. Use for revenue, earnings, margins.

RULES:
- If the user asks about "current price" or "what's X trading at", use get_stock_price.
- If the user asks about "performance" or "trend", use get_historical_data.
- If the user asks about "earnings" or "revenue", use get_financials.
- If the user asks about "news" or "what happened", use search_news.
- Never call a tool "just in case". Only call tools you need.
"""

Running Evals in CI

import pytest

@pytest.fixture
def tool_eval_suite():
    return load_eval_cases("tool_selection_cases.json")

@pytest.mark.parametrize("case", tool_eval_suite(), ids=lambda c: c["id"])
def test_tool_selection(agent, case):
    response = agent.run(case["user_input"])
    tool_calls = extract_tool_calls(response)
    
    called_tools = [tc["name"] for tc in tool_calls]
    
    # Must call the expected tool
    assert case["expected_tool"] in called_tools, \
        f"Expected {case['expected_tool']}, got {called_tools}"
    
    # Must NOT call excluded tools
    for excluded in case.get("should_not_call", []):
        assert excluded not in called_tools, \
            f"Should not have called {excluded}"
    
    # Arguments must match
    if tool_calls:
        matching = [tc for tc in tool_calls if tc["name"] == case["expected_tool"]]
        if matching:
            assert validate_args(matching[0]["args"], case["expected_args"]), \
                f"Args mismatch: {matching[0]['args']} vs {case['expected_args']}"

Tracking Tool-Selection Drift

Monitor your tool-selection metrics over time:

# Weekly eval run
results = evaluate_tool_selection(agent, eval_cases)

metrics = {
    "tool_accuracy": sum(r.correct_tool for r in results) / len(results),
    "args_accuracy": sum(r.args_correct for r in results) / len(results),
    "avg_extra_calls": sum(len(r.extra_tools_called) for r in results) / len(results),
    "confusion_pairs": identify_confusion_pairs(results)
}

# Alert on regression
if metrics["tool_accuracy"] < 0.90:
    send_alert(f"Tool selection accuracy dropped to {metrics['tool_accuracy']:.1%}")

Tool-selection evals should be part of every agent’s CI pipeline. A confusion matrix reveals not just how often your agent fails, but which tools it confuses—guiding both prompt improvements and tool-description refinements.