Most agents break because the tool description leaves too much to inference. Writing tool descriptions AI agents follow means treating each function as a strict API contract: specify when to call, what args are legal, and what the response guarantees. Skip this and the model will hallucinate parameters or quietly bypass the tool.
Step 1: Pin down the tool contract before writing prose
Before you write a single description string, define the inputs, outputs, and side effects in code. If you can’t express the tool as a typed function, the model won’t infer it either. Decide which arguments are required, which are optional, and what enum values are permitted. Also decide whether the tool is read-only or mutates state—agents need to know if calling send_email actually sends something.
from dataclasses import dataclass
from enum import Enum
class Unit(Enum):
CELSIUS = "celsius"
FAHRENHEIT = "fahrenheit"
@dataclass
class WeatherArgs:
lat: float
lon: float
unit: Unit = Unit.CELSIUS
@dataclass
class WeatherResult:
temp: float
conditions: str
cached: bool
This dataclass forces you to confront required vs optional and prevents free-text units. The description later just mirrors this contract. I’ve reviewed dozens of agent codebases where the function accepted unit: str and the model passed "C", "celsius", and "Celcius" interchangeably. The enum would have killed that bug at the source.
Step 2: Write the description as an imperative directive
The description field is not a docstring for humans; it’s the primary instruction to the planner. Start with a verb. State explicit triggers and explicit non-triggers. Models treat the description as a mini-system-prompt for the tool, so ambiguity compounds.
{
"name": "get_weather",
"description": "Fetch current weather for a geographic coordinate. Use this when the user mentions weather, temperature, or conditions for a specific place. Do NOT use it for historical weather, forecasts beyond 1 hour, or vague locale references like 'here' without lat/lon provided by the user or a prior geocode call.",
"parameters": {}
}
Note the negative constraint. Models respect “Do NOT” far better than implied scope. Writing tool descriptions AI agents obey requires telling them what not to do as clearly as what to do. If the tool is side-effecting, add a sentence: “Calling this charges the user’s card.” That single line reduces accidental invocations more than any schema trick.
Step 3: Move hard constraints into JSON Schema, not prose
Prose is for intent; schema is for validation. Use required, enum, pattern, and minimum/maximum to forbid illegal calls at the protocol level. The model sees the schema and biases toward compliant args, and the gateway rejects the rest.
{
"parameters": {
"type": "object",
"required": ["lat", "lon"],
"properties": {
"lat": { "type": "number", "minimum": -90, "maximum": 90 },
"lon": { "type": "number", "minimum": -180, "maximum": 180 },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius" }
}
}
}
If you need a city name instead of coords, add a pattern for ISO country codes or a length limit. Anything you leave as type: string with no constraints will eventually receive “near the park” as input. Some model families still ignore schema hints, but none ignore both schema and a clear description. Layer them.
Step 4: Specify the return shape and failure modes
A tool description that omits what comes back forces the agent to guess. Include a concise statement of the return contract and what happens on error. The agent needs to know if a empty result means “no data” or “call failed.”
{
"description": "Fetch current weather for a geographic coordinate. Use this when the user mentions weather, temperature, or conditions for a specific place. Do NOT use it for historical weather or forecasts beyond 1 hour. Returns JSON with 'temp' (number), 'conditions' (string), 'cached' (bool). On upstream failure returns {'error': 'provider_unavailable'}; the agent should retry once or fall back to get_weather_cached."
}
This tells the agent how to handle the result and degrades gracefully. In my traces, adding the error clause cut “tool returned nothing” hallucinations by half. Agents default to synthesizing a plausible answer when the contract is silent.
Step 5: Assemble the full tool definition and call it
Here is the complete OpenAI-compatible tool spec and a minimal Python call using the official SDK. This is runnable against any OpenAI-compatible endpoint.
import openai
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Fetch current weather for a geographic coordinate. Use this when the user mentions weather, temperature, or conditions for a specific place. Do NOT use it for historical weather or forecasts beyond 1 hour. Returns JSON with 'temp' (number), 'conditions' (string), 'cached' (bool). On failure returns {'error': 'provider_unavailable'}.",
"parameters": {
"type": "object",
"required": ["lat", "lon"],
"properties": {
"lat": {"type": "number", "minimum": -90, "maximum": 90},
"lon": {"type": "number", "minimum": -180, "maximum": 180},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
}
}
}
}
]
client = openai.OpenAI(base_url="https://api.openai.com/v1", api_key="sk-...")
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What's the temperature in Paris at 48.85, 2.35?"}],
tools=tools,
tool_choice="auto"
)
print(resp.choices[0].message.tool_calls)
Swap the base_url to your gateway if you use one. Writing tool descriptions AI agents follow is easier when you can test the same spec across dozens of models without client changes.
Step 6: Test adherence with adversarial prompts
A description is only good if the model calls the tool correctly on edge cases. Build a small eval set:
- Direct request: “Weather at 51.5, 0.1”
- Indirect: “Should I wear a jacket in Berlin? Coordinates 52.5, 13.4”
- Negative: “What was the weather in London yesterday?” (must NOT call)
- Missing arg: “Weather here” (must ask for coords or call geocode first)
Run each through the model and assert tool_calls[0].function.name == "get_weather" for 1–2, and tool_calls is None for 3. For 4, expect either a clarification or a different tool.
def test_weather_tool(client, model):
cases = [
("Weather at 51.5, 0.1", "get_weather"),
("Should I wear a jacket in Berlin? Coordinates 52.5, 13.4", "get_weather"),
("What was the weather in London yesterday?", None),
]
for prompt, expected in cases:
resp = client.chat.completions.create(
model=model, messages=[{"role":"user","content":prompt}],
tools=tools, tool_choice="auto"
)
calls = resp.choices[0].message.tool_calls
name = calls[0].function.name if calls else None
assert name == expected, f"{prompt}: got {name}, expected {expected}"
If you route through n4n.ai, its single OpenAI-compatible endpoint addresses 240+ models, so you can run this suite against Claude, Llama, and Mistral variants without rewriting the client. That exposes description brittleness fast.
Verification of success: your tool is correctly invoked on ≥95% of positive prompts and zero hallucinated parameters on negative prompts across at least three model families. Until you hit that, the description is still ambiguous.
Step 7: Iterate using real agent traces
Production conversations will surface gaps no eval predicts. Log the raw tool_calls and the preceding message. When the model misuses the tool, ask: did the description lack a negative constraint? Was the enum too loose?
Common fix: add a one-line example to the description string. Examples pull the model toward the right arg shape more reliably than schema alone.
{
"description": "Fetch current weather for a geographic coordinate. Use this when the user mentions weather, temperature, or conditions for a specific place. Example: 'Is it raining at 40.7,-74.0?' → call with lat=40.7, lon=-74.0, unit=celsius. Do NOT use for historical weather."
}
Treat the description as code. Version it, diff it, and re-run Step 6 after every change.
Step 8: Keep descriptions lean but complete
Long descriptions aren’t automatically better. Overly verbose text dilutes the directive. Cap at 3–4 sentences: trigger, non-trigger, return contract, one example. If you need more, the tool is doing too much—split it.
Writing tool descriptions AI agents follow is iterative engineering, not prompt poetry. Ship the schema, test the calls, read the traces.