DEV Community

Cover image for My fix for LLM tool use issues was deleting half my tools, not writing a bigger prompt
Lars Winstand
Lars Winstand

Posted on Originally published at standardcompute.com

My fix for LLM tool use issues was deleting half my tools, not writing a bigger prompt

I fixed a flaky agent last week by removing tools.

Not by tuning the prompt.
Not by adding more examples.
Not by giving GPT-5.4 a longer lecture about how to behave.

I deleted half the tool list, tightened the schemas, and the agent immediately got less stupid.

That sounds obvious in hindsight, but I see teams make the same mistake over and over: when tool calling gets unreliable, they assume the model needs more instructions.

Usually it needs fewer choices.

The setup: too many tools that all kind of did the same thing

The agent had access to:

  • Slack
  • Notion
  • HubSpot
  • a couple of internal APIs

Pretty normal stack for an automation workflow.

The problem: GPT-5.4 kept calling the wrong function. Claude Opus did too, just with slightly better posture.

My first move was the classic bad move:

  • add rules
  • add exceptions
  • add examples
  • add anti-examples
  • add more prompt text explaining when to use which tool

The prompt got bigger.
The behavior did not get better.

That was the real clue: the prompt was no longer the bottleneck. The tool surface was.

The real bug: overlapping tools create semantic collisions

If you give a model 15 functions that all smell like “retrieve customer info,” you have not created flexibility.

You have created ambiguity.

Example:

  • find_contact_hubspot
  • get_account_notes_notion
  • search_customer_records_internal
  • search_slack_customer_thread

A human can see the differences.
A model sees four partially overlapping stories and has to rank them before it can even start solving the user request.

That’s where tool calling reliability starts to break.

Anthropic is pretty direct about this in their tool use docs: Claude chooses tools based on the user request and the tool description. They recommend good descriptions and input_examples.

That sounds like a prompt-writing problem.

It’s mostly a boundary-design problem.

If two tool descriptions overlap, the model has to guess which one you meant.

And OpenAI is pushing the same idea from another angle. Their function-calling guidance now recommends deferring rarely used tools with tool_search instead of stuffing everything into a single request.

That’s not subtle.

It’s basically: if your function list looks like a junk drawer, stop blaming the model.

Bigger toolboxes make agents worse faster than most people expect

Once you cross a certain number of tools, the model is doing semantic ranking work before it does task work.

That’s expensive in tokens, fragile in behavior, and hard to patch with prompt text.

OpenAI’s newer Agents building blocks made this more official, but most of us learned it the annoying way in production:

runtime tool selection beats front-loading everything.

The useful question is no longer:

How do I make the prompt smarter?

It’s:

How do I make the decision surface smaller?

Three approaches, and only two hold up in real systems

Approach What actually happens
OpenAI function tools JSON-schema-defined functions, strict: true for schema adherence, tool_search to defer rarely used tools
Anthropic client tools name, description, input_schema, optional input_examples, optional defer_loading, and disable_parallel_tool_use
Prompt-only tool disambiguation No hard enforcement, relies on instructions alone, gets worse as tool count and overlap increase

My opinion: prompt-only tool disambiguation is fine for demos and terrible for real automations.

If your n8n, Make, Zapier, or custom agent flow touches Slack, HubSpot, and Notion in production, you want contracts, not vibes.

Small schemas beat smart prompts

The boring weather-tool examples in the docs are actually great examples.

Why do they work?

Because they are narrow.

One required field. Maybe one enum. Clear intent. Very little room to improvise.

OpenAI-style tool

{
  "type": "function",
  "name": "get_weather",
  "description": "Get the current weather for a given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City and state, e.g. San Francisco, CA"
      }
    },
    "required": ["location"]
  },
  "strict": true
}
Enter fullscreen mode Exit fullscreen mode

Anthropic-style tool

{
  "name": "get_weather",
  "description": "Get the current weather for a given location.",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City and state, e.g. San Francisco, CA"
      },
      "unit": {
        "type": "string",
        "enum": ["celsius", "fahrenheit"],
        "description": "The unit of temperature"
      }
    },
    "required": ["location"]
  }
}
Enter fullscreen mode Exit fullscreen mode

That’s the whole trick in miniature.

Narrow tools are easier to choose and harder to misuse.

What I changed first

Here’s the order I’d use again.

1) Remove overlapping tools from the same turn

If multiple tools can answer basically the same request, don’t expose them all at once.

Bad:

  • find_customer_hubspot
  • lookup_customer_notion
  • search_customer_records

Better:

  • route first
  • then expose only the relevant subset

If you’re using OpenAI, use tool_search or equivalent runtime loading patterns.
If you’re using Anthropic, lean on defer_loading patterns and tighter tool registration.

2) Rename tools so they sound mutually exclusive

Tool names are not decoration.
They are part of the interface.

Bad:

customer_lookup_v2
record_search
account_tool
Enter fullscreen mode Exit fullscreen mode

Better:

get_customer_billing_status
read_customer_support_notes
find_recent_slack_thread_for_customer
Enter fullscreen mode Exit fullscreen mode

Those names create separation.

3) Shorten descriptions by increasing contrast

A long description is only useful if it defines boundaries.

Bad description:

Use this tool to retrieve customer information, account history, support context, messages, records, and related updates.
Enter fullscreen mode Exit fullscreen mode

Better description:

Use this tool only to fetch billing status from HubSpot. Do not use it for support notes, Slack history, or internal CRM records.
Enter fullscreen mode Exit fullscreen mode

That second one gives the model a much cleaner decision.

4) Constrain arguments hard

Use enums.
Use required fields sparingly.
Avoid giant free-form objects.

Bad schema:

{
  "type": "object",
  "properties": {
    "customer": { "type": "object" },
    "options": { "type": "object" },
    "mode": { "type": "string" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Better schema:

{
  "type": "object",
  "properties": {
    "customer_id": { "type": "string" },
    "source": {
      "type": "string",
      "enum": ["hubspot", "internal_crm"]
    }
  },
  "required": ["customer_id", "source"]
}
Enter fullscreen mode Exit fullscreen mode

5) Turn off parallel tool chaos when side effects matter

If a workflow can send Slack messages, update HubSpot, and write to Notion, parallel tool use is not always a feature.

Sometimes it’s just a faster way to create cleanup work.

For Anthropic-style control:

{
  "tool_choice": {
    "type": "auto",
    "disable_parallel_tool_use": true
  }
}
Enter fullscreen mode Exit fullscreen mode

I think this setting is underrated.

One clean action beats three half-right actions every time.

A sneaky failure mode: better descriptions that make selection worse

This one catches teams a lot.

You improve every tool description.
You add more detail.
You add more context.
You feel good about it.

And then tool selection still sucks.

Why?

Because you made every description richer in the same vocabulary.

Now all five tools mention:

  • customer
  • account
  • support
  • history
  • messages
  • updates

So yes, the descriptions are better written.
They are also more semantically similar.

That can make selection harder, not easier.

The fix is contrast.

A good tool description should say:

  • what this tool does
  • when to use it
  • when not to use it
  • which nearby tool should be used instead

That last one matters more than most people realize.

Prompts still matter, just not for this job

This is not an argument against prompts.

It’s an argument against using prompts to compensate for sloppy tool design.

Here’s the split I use now.

Use prompts for policy

Examples:

  • never issue a refund without manager approval
  • never message a customer after 9 PM local time
  • ask for confirmation before deleting a Notion page

Use schemas and tool boundaries for behavior

Examples:

  • valid enum values
  • required fields
  • mutually exclusive tools
  • one-tool-per-turn limits for risky actions

If you mix those up, you end up writing a 900-word prompt to solve a 9-line schema problem.

The safest pattern for side effects is slower on purpose

Anthropic’s client-tool flow gets this right.

The model emits a tool_use block.
Your app executes it.
You send back a tool_result.
Then the model continues.

That rhythm is slower than letting an agent freestyle through six actions in one shot.

It is also much safer for:

  • customer data
  • billing actions
  • internal systems
  • external APIs with side effects

I’d rather have GPT-5.4 or Claude Opus take one careful step than confidently do three wrong things in parallel.

A practical cleanup checklist

If your agent keeps choosing the wrong tool, this is the first pass I’d do.

# 1) List every tool exposed in a single turn
# 2) Group tools by overlapping intent
# 3) Remove or defer duplicates
# 4) Tighten names and descriptions
# 5) Add enums / required fields / strict schemas
# 6) Disable parallel tool use for side-effecting actions
Enter fullscreen mode Exit fullscreen mode

Or more explicitly:

[ ] Are multiple tools able to satisfy the same request?
[ ] Do tool names clearly signal exclusive intent?
[ ] Do descriptions say when NOT to use the tool?
[ ] Are optional fields actually necessary?
[ ] Can free-form inputs be replaced with enums?
[ ] Should this workflow allow parallel actions at all?
Enter fullscreen mode Exit fullscreen mode

Why this matters more in production automations

A toy demo with three tools can survive on vibes.

A real workflow can’t.

Especially if you’re running agents inside:

  • n8n
  • Make
  • Zapier
  • OpenClaw
  • custom internal agent frameworks

Once a workflow touches Slack, Notion, HubSpot, Discord, internal APIs, and customer data, tool ambiguity becomes an operational problem.

And it gets expensive fast if you’re paying per token while the model burns cycles deciding between five overlapping functions.

That’s one reason I’m bullish on Standard Compute’s model for this kind of work: if you’re iterating on agent behavior, testing tool schemas, and running automations continuously, flat-rate API access is a lot easier to live with than token anxiety on every debugging pass.

If your stack already speaks the OpenAI API, the swap is pretty painless.

The main takeaway

When LLM tool use starts failing, most people add things:

  • more prompt
  • more examples
  • more fallback logic
  • more tools

The highest-leverage fix is often subtraction:

  • fewer active tools
  • fewer overlapping descriptions
  • fewer optional fields
  • fewer chances for parallel side effects

That was the fix for me.

Not a smarter speech.
Not a giant system prompt.
Not another layer of “be careful” instructions.

Just fewer tools competing for attention.

If your agent keeps making bad tool choices, try deleting half the toolbox before you write another 500 tokens of prompt.

You might be solving a UI problem for the model, not an intelligence problem.

Top comments (1)

Collapse
 
autenai profile image
Auten •

One place where you can't delete the overlap: UI-control tools. Clicking an element by id and clicking at x,y coordinates are both "click", and you need both, because some apps expose nothing useful in the accessibility tree. What helped there was writing the coordinate tool's description as a precondition instead of a purpose: "use only when the element list has no match for the target". The model stopped reaching for coordinates first once the rule pointed at state it had already seen, not at intent it had to guess.

The other cheap lever: when the wrong tool gets called anyway, make its error name the right one ("no element 14 in the current list, call list_elements first"). A tool result steers the next turn more reliably than another prompt line.

Did tool_search / defer_loading cost you a noticeable extra round trip on the first call, or was that noise next to the misroutes it removed?