Blog
Published on

Too Many Tools: Testing Agent Tool Discovery

Authors
  • avatar
    Name
    Jared Chung
    Twitter

Introduction

Giving an agent more tools sounds like giving it more capability. At some point the opposite happens: tool definitions consume context, similar names compete for selection, and large intermediate results bury the information the model needs.

Agents are increasingly connected to company context, tools, and repeatable workflows. The engineering question is no longer only how to call a tool. It is how to make the right tool discoverable without loading an entire software catalog into every prompt.

The comparison below uses a controlled experiment instead of assuming that deferred loading or code-based orchestration always helps.

Tool-context laboratory

TOOL CONSTELLATION / SEMANTIC DISCOVERYissues.searchissues.readrepository.filescomments.createAGENTISSUES4%CONTEXT640 tokens4 loaded / 120 available
Deferred discovery exposes 4 tool definitions, using about 640 context tokens in this synthetic model.
See a tool catalog compete with the task. Compare loading every definition with retrieving a compact task-specific set. Values are illustrative and intended to expose the tradeoff.

Tool Catalogs Compete for Attention

Each tool usually contributes a name, description, and input schema to the model's context. Ten compact tools may be easy to distinguish. Fifty tools from several services can overlap:

slack_send_message
slack_send_channel_message
teams_send_message
email_send_message
email_create_draft
crm_create_note
crm_create_task
project_create_task

The problem has three parts:

  1. Context cost: definitions consume tokens before work begins.
  2. Selection ambiguity: similar tools can be confused.
  3. Result pollution: large results accumulate in the conversation.

Start by measuring the serialized size of the catalog and grouping tools by purpose. Tool count alone is misleading: one complex schema may cost more context than several small tools.

Design Tools for Agent Decisions

A tool schema is an interface for a probabilistic caller. Names should expose the service and action. Descriptions should explain when to use the tool and any important boundary.

{
  "name": "github_issues_search",
  "description": "Search issues in repositories the user can access. Use for issue titles, bodies, labels, and status; do not use for code search.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {"type": "string"},
      "repository": {"type": "string"},
      "state": {"enum": ["open", "closed", "all"]}
    },
    "required": ["query", "repository"]
  }
}

Avoid exposing every underlying API endpoint. A higher-level get_customer_context tool may serve an agent better than separate tools that return transactions, notes, and contact details in incompatible shapes. The right boundary depends on representative tasks and evaluation.

Compare Three Loading Strategies

Build a harmless catalog of roughly 40 mock tools across source control, messaging, documents, and project tracking. Include intentional naming collisions and irrelevant tools.

Evaluate three policies:

PolicyInitial contextExtra stepMain risk
Always loadedEvery definitionNoneContext cost and confusion
Deferred discoverySearch tool plus common toolsSearch before uncommon useSearch miss or latency
Curated bundlesTask-specific subsetBundle selectionWrong bundle or maintenance

Keep three to five common tools loaded in the deferred condition. Index the remaining names and descriptions for search.

def load_tools(task, catalog, common_tools, search):
    selected = list(common_tools)
    selected.extend(search(task, catalog, limit=5))
    return deduplicate_by_name(selected)

For a real OpenAI Responses API baseline, pass the selected function definitions to the Python client. The repository pins openai==2.24.0 so the example remains reproducible. It accepts the model ID through configuration so the experiment records the exact model used:

import os

from openai import OpenAI


client = OpenAI()

issue_search_tool = {
    "type": "function",
    "name": "github_issues_search",
    "description": (
        "Search issue titles, bodies, labels, and status in one repository. "
        "Do not use this function to search source code."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "repository": {"type": "string"},
            "state": {"type": "string", "enum": ["open", "closed", "all"]},
        },
        "required": ["query", "repository", "state"],
        "additionalProperties": False,
    },
    "strict": True,
}

response = client.responses.create(
    model=os.environ["OPENAI_TOOL_MODEL"],
    input="Find open payment bugs in example/support-api.",
    tools=[issue_search_tool],
    tool_choice="auto",
    metadata={"tool_policy": "always-loaded-baseline"},
)

for item in response.output:
    if item.type == "function_call":
        print(item.name, item.arguments)

The application must parse and validate item.arguments before executing the function, then return the tool result using the call identifier. The snippet stops before execution so that the experiment can score selection and arguments independently from the mock tool's behavior.

Use the same task set and model settings for every policy. Tasks should require one tool, several tools, no tool, and a deliberate choice between similarly named tools.

Keep Intermediate Work Out of Context

Tool discovery controls definitions. Programmatic orchestration controls results. When an agent needs to search many records, code can filter and aggregate them before returning a small result to the model.

async def summarize_failed_jobs(api, project_id):
    jobs = await api.list_jobs(project_id=project_id, limit=500)
    failed = [job for job in jobs if job.status == "failed"]

    return {
        "total_jobs": len(jobs),
        "failed_count": len(failed),
        "top_error_codes": count_top(job.error_code for job in failed),
        "sample_job_ids": [job.id for job in failed[:5]],
    }

This reduces context usage and inference round trips, but it also moves logic into code. Test that filters and aggregation do not hide cases the agent needs. Preserve links or identifiers that allow deeper inspection.

Evaluate Selection, Arguments, and Outcomes

End-to-end task success is the primary measure. Add diagnostic metrics to show why a policy succeeds or fails:

  • correct tool selected;
  • required tool discovered in the top results;
  • arguments valid and semantically correct;
  • unnecessary calls;
  • input and output tokens;
  • time to final answer;
  • task rubric passed.
task: find_open_payment_bug
expected_tools:
  - github_issues_search
forbidden_tools:
  - github_code_search
checks:
  - repository argument is correct
  - state equals open
  - final answer cites returned issue IDs

Run multiple trials. Tool choice is probabilistic, and small differences may disappear across repetitions. Report the task set, model, catalog revision, search method, and full distribution.

Provider-reported tool-search improvements are useful motivation, not results for your catalog. Deferred discovery adds latency and may offer little value for fewer than ten compact, frequently used tools.

Conclusion

An agent's tool catalog should grow only when evaluation shows that new capability outweighs context and selection costs. Clear interfaces, deferred discovery, curated bundles, and code-based aggregation address different bottlenecks.

Measure the smallest useful configuration first. More tools expand what an agent could do; good discovery determines what it can do reliably.

References