DEV Community

Nitish kumar
Nitish kumar

Posted on

Your agent doesn't need more tools, it needs better tool descriptions

Most agent failures I've debugged weren't reasoning failures. The model reasoned fine. It just picked the wrong tool, because the tool description didn't tell it what it needed to know.

This is an under-discussed problem, and it gets worse the more integrations you add. Here's what we've learned.

The setup

Say your agent has four calendar-ish tools available:

[
  { "name": "calendar_create_event",   "description": "Creates a calendar event" },
  { "name": "calendar_quick_add",      "description": "Adds an event from text" },
  { "name": "scheduling_book_slot",    "description": "Books a scheduling slot" },
  { "name": "meeting_schedule",        "description": "Schedules a meeting" }
]
Enter fullscreen mode Exit fullscreen mode

Now: "Book me 30 minutes with Sarah on Thursday."

Which one? You don't know either, and you have context the model doesn't. The model will pick one, it'll probably work about 60% of the time, and when it's wrong the failure will be silent — an event created in the wrong system that nobody notices for a week.

Roughly 40% of our agent failures traced back to tool selection, not reasoning. That was surprising and it changed where we spent engineering time.

Why OpenAPI schemas aren't enough

A schema tells you what a tool accepts. It doesn't tell you:

What the tool is for — the use case, not the signature
When it's appropriate versus the four adjacent tools that look identical
Whether it's reversible — can you undo it, and how
What it costs — a rate-limited call and a free one shouldn't be equally attractive
What it assumes — does it require the user's calendar to be connected first?

Documentation doesn't have this either, because docs are written for humans who already know which product they're integrating with. The model is choosing between products.

What a description for an agent looks like

Here's the template we converged on:

name: calendar_create_event
purpose: >
  Create an event on the user's primary Google Calendar when you already
  know the exact date, time, and duration.
use_when:
  - The time is fully specified ("Thursday 2pm for 30 minutes")
  - The user owns the calendar being written to
do_not_use_when:
  - The time is approximate or needs negotiation → use scheduling_book_slot
  - Inviting external participants who need to pick → use scheduling_book_slot
  - The user said "find a time" rather than naming one
reversible: yes — event can be deleted via calendar_delete_event
side_effects: sends invitations to all attendees immediately
cost: low
requires: google_calendar connection active
Enter fullscreen mode Exit fullscreen mode

The do_not_use_when block with explicit redirects is doing most of the work. It's not describing this tool — it's describing the boundary between this tool and its neighbours, which is exactly what the model is uncertain about.

Generating these at scale

Writing these by hand doesn't scale past a few dozen tools. Our loop:

Generate a first draft from the schema and whatever docs exist, using a model.
Ship it. It'll be mediocre.
Log every selection, along with what the agent was asked and what it picked.
When a selection is wrong, don't patch the prompt — patch the description of the tool that should have been chosen and the one that was.
Over time the descriptions become a record of every mistake the system has made.

def record_misselection(task, chosen_tool, correct_tool):
    # The fix goes in the tool metadata, not the system prompt.
    # System prompt fixes don't generalize; description fixes do.
    boundary_note = infer_distinguishing_feature(task, chosen_tool, correct_tool)
    tools[correct_tool].use_when.append(boundary_note)
    tools[chosen_tool].do_not_use_when.append(
        f"{boundary_note} → use {correct_tool}"
    )
Enter fullscreen mode Exit fullscreen mode

That step 5 is the important one. Every instinct says to fix selection errors by adding rules to the system prompt. Resist it. Prompt fixes are global, they collide with each other, and they don't survive a model upgrade. Description fixes are local to the tools involved and they compose.

A cheap win: prune before you send

Related, and it saves real money: don't send every tool schema on every step of the loop. Schemas can be a large share of your token spend, because they're re-sent on every iteration, not once per run.

def relevant_tools(context, all_tools, k=8):
    # Cheap pre-filter — embedding similarity between the current
    # goal and each tool's `purpose` field
    scored = rank_by_similarity(context.goal, all_tools, key="purpose")
    return scored[:k] + always_include(all_tools)
Enter fullscreen mode Exit fullscreen mode

Fewer tools in context also means better selection accuracy, so this helps twice. Most frameworks encourage handing the agent everything up front; that's the wrong default past about a dozen tools.

The bigger point

The industry is competing on model quality and integration count. Neither is where agent reliability actually comes from right now.

The semantic layer — what each tool means, when it applies, how it differs from its neighbours — is unglamorous curation work that nobody ships as a feature. It's also, in our experience, the difference between an agent that demos well and one that runs unattended.

If I were starting again I'd treat it as core product from day one instead of infrastructure to get through.

Building DeskFerry — agents across 1,500+ integrations, which is how we ended up caring about this. Questions welcome below.

Top comments (0)