Agents in production
Agent infrastructure
How to write tool descriptions for LLM agents: a 7-step guide covering schemas, parameter hints, disambiguation, docstrings, and evals that actually work.

Tool descriptions are the interface between your agent and your product. The model doesn't read your API docs. It reads the description string in the tool schema, then picks — or picks wrong. This guide walks through writing descriptions that hold up in production.
By the end you'll be able to write descriptions that reduce mis-selection, cut retry loops, and survive a real eval run. You need: a working LLM tool schema (OpenAI function calling, Anthropic tool use, or MCP), one or more tools already defined, and an eval harness or at least a set of test prompts. Time required: 40 minutes to rewrite a small tool set, longer if you're auditing a portfolio.
Before rewriting anything, dump the exact JSON your agent receives at inference time. Not the source. The serialised schema after your framework has processed it.
Run the tool list through the same path your agent uses:
import json
tools = client.get_registered_tools()
print(json.dumps(tools, indent=2))
Count tokens. A tool set that fits in 800 tokens leaves room for context; one that runs to 4,000 tokens is already eating your budget. If you don't know why that matters, agent context window management covers the arithmetic.
Expected result: a JSON blob per tool with name, description, and parameters. That's the surface. Everything else is invisible to the model.
The description's job is to help the model answer one question: should I call this tool right now, or a different one? Not what the tool does in general. What it's for, relative to its neighbours.
Open with the verb and the object. Then the condition that makes it the right pick.
Bad:
"description": "Handles user account operations including creation, updates, and lookups."
Better:
"description": "Look up a user by email address. Returns account status, plan, and last login. Use when the caller has an email and needs account details; do not use to search by name or ID."
The second one tells the model when to reach for it and when not to. That's the selection decision.
Rules that hold up:
search_users for partial matches; use this tool only when you have an exact email."The parameters block is where most agents fail silently. The model fills in fields based on the description of each field, not the field name. user_id with no description gets guessed. user_id with "UUID from the users table; get it from search_users if you only have an email" gets populated correctly.
Write each parameter description as if the model has no other context:
{
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "The account UUID, format: acc_XXXXXXXX. Obtain from list_accounts or the current session. Not the customer's email or company name."
},
"amount_cents": {
"type": "integer",
"description": "Amount in cents (integer). $12.50 is 1250. Do not pass dollar amounts or decimals."
}
},
"required": ["account_id", "amount_cents"]
}
The amount example prevents the single most common tool call failure — the model passing 12.5 when the API expects 1250. That's not the model being wrong. That's the description not doing its job.
Use enum where the value space is bounded. The model handles enums far better than free-form strings with rules buried in prose.
When you have tools that look similar, the description has to disambiguate them by name. Don't hope the model figures it out from the shape of the schema.
Suppose you have send_email and send_notification. Left to itself, the model will use them interchangeably. Force the split in the description:
"send_email": "Send an email to a specific address. Use for one-off messages to a known email. Do not use for in-app alerts or push — use send_notification for those."
"send_notification": "Send an in-app notification or push to a user by user_id. Use for product alerts, status updates, and reminders. Do not use for external email — use send_email for that."
Each description names the other tool. The model uses that cross-reference to make the right pick. This is the single highest-leverage change most tool sets need.
If you have more than 20 tools, you have a different problem — how many tools should an AI agent have covers where the ceiling is and what to do about it.
If you're using a framework that generates the tool schema from a Python or TypeScript function signature (LangChain, OpenAI Agents SDK, Anthropic's SDK), the agent tool docstring is the description. Treat it that way.
def refund_charge(charge_id: str, amount_cents: int, reason: str) -> dict:
"""Refund a specific charge, in full or in part.
Use when the user has confirmed they want to refund a known charge_id.
Do not use to cancel a subscription — use cancel_subscription for that.
Do not use to refund based on order_id — call get_charge_for_order first.
Args:
charge_id: The charge identifier, format ch_XXXXXXXX. Get from list_charges.
amount_cents: Refund amount in cents. Must be <= original charge amount.
For a full refund, pass the original charge total, not 0.
reason: One of 'duplicate', 'fraudulent', 'requested_by_customer'.
"""
One framework-specific gotcha: in LangChain, pass parse_docstring=True to @tool (or supply a Pydantic args_schema) so the Args section actually reaches the model. The default drops it, and your parameter descriptions vanish from the generated schema.
The docstring drives everything the model sees. Reviewers who wouldn't blink at a two-line docstring on an internal function should push back hard here. This one is the interface.
Descriptions get most of the attention, but return payloads shape the next turn. If the tool returns {"status": "ok", "data": {...}} with no schema hint, the model has to guess at what fields it can use downstream.
Add a short response schema or an example in the description:
"description": "Look up a user by email. Returns: { user_id, email, plan ('free'|'pro'|'enterprise'), created_at, last_login_at }."
Better still: keep responses small and predictable. Every extra field is tokens the model has to hold, and half of them will get ignored. Cap large lists. Truncate long strings. If the model needs pagination, say so in the response itself: {"results": [...], "has_more": true, "next_cursor": "..."}.
A description that reads well to you isn't a description that works. Run it through evals before it goes near production.
Minimum eval set per tool:
Grade tool selection accuracy, not final output. If the model picks the wrong tool 2 out of 13 times, the description is broken — the model can't recover from a wrong pick. Agent evals covers how to structure this properly.
Run the eval against at least two models. A description tuned to Claude sometimes underperforms on GPT and vice versa. If you're only shipping on one model today, still test on a second — you'll want the option later.
Descriptions written for humans. "This endpoint is part of our v2 accounts API and provides comprehensive account management." The model doesn't care about your API structure. Cut everything that isn't decision-relevant.
Missing negative guidance. Telling the model when not to use a tool matters as much as when to use it. Every description should have at least one "do not use for X" line if any adjacent tool exists.
Assuming the model reads parameter names. It does, but not reliably. email and email_address and user_email all get filled the same way — from the parameter description, not the name.
Silent drift. Your API changes, the description doesn't. The model keeps calling with the old shape until it breaks. Treat tool descriptions like any other API contract — OpenAPI spec drift applies here too, and the failure mode is worse because there's no compiler to catch it.
Over-explaining. A 400-token description for a simple tool crowds out everything else. If you can't say it in three sentences, the tool is probably doing too much — split it.
Skipping the response shape. The model uses the response of tool N to decide what to do at turn N+1. If it can't predict the shape, it wastes turns exploring. Document what comes back.
Rewrite one tool this afternoon. Run the eval. Then do the next one.
Stay up to date on the ever changing agentic landscape.