# Designing tools LLMs can actually use: five failure modes I keep seeing

> Source: <https://tengli.dev/posts/tool-design-for-llms.html>
> Published: 2026-07-12 00:00:00+00:00

# Designing tools LLMs can actually use: five failure modes I keep seeing

I integrate tools into a production AI agent for a living — first-party ones,
third-party ones, HTTP APIs wrapped as tools, MCP servers. After enough
integrations, you notice something uncomfortable: **whether a model uses a tool
correctly has surprisingly little to do with how well the tool is engineered.**

A tool can have clean code, solid auth, perfect uptime — and the model still picks the wrong one, invents arguments, or ignores it entirely. The failures cluster into a handful of patterns. Here are the five I keep seeing.

## 1. The parameter with no description

The single most common failure, by a wide margin.

```
"url": { "type": "string" }
```

Your type system knows this is a string. The model needs to know *which* URL —
the full page URL? just the domain? with or without protocol? URL-encoded?
A model facing an undocumented parameter doesn't stop and ask. It guesses.
Sometimes it guesses right. In production, "sometimes" is a bug report.

The root cause is almost always schema generation: zod, Pydantic, or an
OpenAPI converter produces structurally perfect schemas with zero semantics,
and nobody goes back to add `.describe()`

. The type checker is satisfied.
The model is starving.

**The fix costs an hour:** every parameter gets a description with format and
one example value. It is the highest-leverage hour you can spend on agent
reliability, and nobody spends it.

## 2. Tools that compete for the same intent

`get_user`

and `get_users`

. `search_docs`

and `query_docs`

. `send_message`

and `post_message`

.

To you, the difference is obvious — you wrote them. To a model doing tool selection over a catalog it has never seen before, two near-identical names with near-identical descriptions is a coin flip. And a coin flip at step 2 of a 6-step task is a 50% failure rate you'll never reproduce locally.

Two rules of thumb: if two tools' descriptions could be swapped without
anyone noticing, the model can't tell them apart either. And every
description should answer *when to use this instead of the neighbors* — the
best catalogs cross-reference ("for bulk queries, use X instead").

## 3. Prose constraints instead of schema constraints

```
"status": {
  "type": "string",
  "description": "Must be one of: active, inactive, banned"
}
```

That constraint lives in prose, which means the model *can* violate it.
Move it into `"enum": ["active", "inactive", "banned"]`

and it *can't* —
most inference stacks enforce enums at decoding time.

The general principle: **anything the schema can express, the schema should
express.** Descriptions are for semantics; constraints belong in the type
system. Every constraint you leave in prose is a runtime error you've chosen
to discover later.

## 4. The catalog tax

Every tool you expose gets serialized into every single request. A catalog
with 25 tools and generous schemas can cost several thousand tokens *before
the user has typed a word* — you pay it on every turn, forever.

Worse than the money: selection accuracy degrades as catalogs grow. The model attends over everything; ten sharply-differentiated tools reliably beat thirty overlapping ones on both cost and correctness.

If your catalog is large, split it by domain, or gate rarely-used tools behind a mode. Your token bill and your error rate will both thank you.

## 5. Errors that strand the model

When a model calls your tool wrong, your error message is the only feedback loop it has. Compare:

`Internal Server Error`

`Missing required parameter "query". Expected a free-text search string, e.g. "login bug".`

The first strands the model — it will retry the same call, or give up, or
hallucinate a workaround. The second lets it self-correct *in one turn*.
Error messages are prompts. Write them like prompts.

There's a nastier variant: the tool that silently *accepts* an invalid call —
declared `required`

fields that aren't actually enforced, so the call
"succeeds" with `undefined`

in the payload. A model can recover from a good
error. It cannot recover from a lie.

## The common thread

None of these are engineering problems in the traditional sense. The code is
fine. They're *writing* problems — descriptions, names, error messages — the
parts that don't show up in code review because they don't break tests.

The mental model that helps: your tool definition is not an API contract,
**it's a prompt**. It gets read by a language model under time pressure with
no documentation open in another tab. Write it the way you'd write for a
sharp intern on their first day: what this does, when to use it, what comes
back, and what to do when it fails.

The teams that internalize this ship tools that agents use correctly on the first try. The teams that don't file bugs against the model.

*Next up: how widespread are these failure modes across the MCP ecosystem,
actually? I've been collecting data. It is not flattering. Stay tuned.*
