n4nAI

Writing tool descriptions for reliable function calling

Learn how writing tool descriptions function calling reliably works: a step-by-step guide to schema design, imperative phrasing, and cross-model testing.

n4n Team4 min read848 words

Audio narration

Coming soon — every post will get a voice note here.

Bad tool descriptions are the silent killer of function-calling pipelines. Writing tool descriptions function calling that the model actually respects requires treating the description as a strict API contract, not a place to dump natural-language vagaries. Get it wrong and you’ll see silent parameter omissions, hallucinated arguments, or the model calling the wrong tool entirely.

Step 1: Pin down the tool’s exact side effects

Before writing a single word of description, define what the function does and what it does not do. A tool that mutates state (sends an email, writes a row) needs different guardrails than a read-only lookup. List the preconditions, the required inputs, and the shape of the return value.

A tool definition for an OpenAI-compatible endpoint is a JSON object. Start with the skeleton:

{
  "type": "function",
  "function": {
    "name": "cancel_order",
    "description": "",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {"type": "string"}
      },
      "required": ["order_id"]
    }
  }
}

If you cannot fill in description with a precise sentence, stop. You do not yet understand the tool well enough to expose it to a model.

Step 2: Write the description as an imperative command

When writing tool descriptions function calling, phrase the first sentence as a command directed at the model. Models treat the description as instructions for when to act. Noun phrases like “Order cancellation utility” leave too much inference to the sampler.

Bad:

Better:

The better version states the action, the selector, the constraint, and the return. That reduces ambiguity about whether the tool should be called for a shipped order.

Common mistake: burying the trigger condition

If the tool should only be used under specific conditions, put that in the first two sentences. Models weight the start of the description heavily.

"Send a confirmation SMS to a user. Use this ONLY after the user has explicitly requested text notifications and provided a phone number."

Step 3: Encode hard constraints in the JSON schema

The description is for the model’s reasoning. The schema is for the parser. Do not write “latitude must be between -90 and 90” in prose and leave the schema open. Enforce it:

"lat": {
  "type": "number",
  "minimum": -90,
  "maximum": 90,
  "description": "Latitude in decimal degrees."
}

Use enum for closed sets. Use pattern for string formats like ISO dates or UUIDs. The model will still occasionally emit invalid values, but the schema lets your validation layer reject early instead of shipping garbage to your backend.

"status": {
  "type": "string",
  "enum": ["pending", "shipped", "cancelled"]
}

Step 4: Name parameters by semantic role

param1, arg0, or input force the model to guess from context. Name parameters exactly as they appear in your internal API or as a developer would expect: order_id, recipient_email, start_date.

If a parameter is optional but has a sane default, state the default in the description, not just in code:

"window_days": "Number of days to look back. Defaults to 7 if omitted."

And reflect that in the schema by omitting it from required.

Step 5: Document the return shape in the description

The model decides whether to call a tool based partly on what it expects to get back. If your function returns a structured object, say so.

"Returns JSON with keys: temperature_c (number), condition (string), observed_at (ISO8601 string)."

This helps the model plan multi-step calls. If it knows get_weather returns observed_at, it can decide whether to call get_forecast instead without asking the user.

Step 6: Force-call the tool to verify schema parsing

You cannot trust a tool definition until you have forced the model to emit arguments against it. Use tool_choice to compel a call, then inspect the tool_calls payload for schema validity.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.n4n.ai/v1",  # OpenAI-compatible endpoint
    api_key="YOUR_KEY",
)

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Fetch current weather for a given latitude and longitude. Returns temperature in Celsius and a short condition string.",
        "parameters": {
            "type": "object",
            "properties": {
                "lat": {"type": "number", "minimum": -90, "maximum": 90},
                "lon": {"type": "number", "minimum": -180, "maximum": 180},
            },
            "required": ["lat", "lon"],
        },
    },
}]

resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "What's the weather at 37.77, -122.41?"}],
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_weather"}},
)
print(resp.choices[0].message.tool_calls)

Because n4n.ai exposes one OpenAI-compatible endpoint addressing 240+ models, you can swap the model string and rely on automatic fallback if a provider is rate-limited or degraded. Run the same forced call against claude-3-5-sonnet, gemini-1.5-pro, and a local model to see which ones respect your enum and numeric bounds.

Step 7: Replay real failures against the exact tool def

In production, log the tools array and the preceding messages for every failed or corrected call. Store them as fixtures. When you change a description, replay those fixtures to confirm the regression is fixed.

A minimal replay harness:

import json

def replay(client, fixture_path):
    with open(fixture_path) as f:
        data = json.load(f)
    resp = client.chat.completions.create(
        model=data["model"],
        messages=data["messages"],
        tools=data["tools"],
    )
    return resp.choices[0].message.tool_calls

Keep a directory of .json fixtures named by failure mode: missing_required_arg.json, wrong_enum.json, called_when_shouldnt.json.

Step 8: Trim and harden the description

After testing, cut any phrase the model ignored. If the model still calls the tool when it shouldn’t, add a negative constraint: “Do NOT use this tool if the order is already shipped.” If it omits an optional param, move the default explanation to the start of the parameter description.

Writing tool descriptions function calling is iterative. The first version is a hypothesis; the eval set is the experiment.

Signal vs noise in descriptions

Remove marketing words (“efficiently”, “quickly”, “robustly”). They consume tokens and give the model no actionable signal. Every word should answer: when do I call this, with what, and what comes back.

Verify success

Build a fixed eval set of at least 50 user utterances where the correct tool and arguments are known. For each, run the model with tool_choice: "auto" and assert:

  1. The emitted function.name matches the expected tool.
  2. The arguments validate against the JSON schema (use jsonschema in Python).
  3. For optional params, the model does not invent values outside the documented default behavior.

Success means zero schema violations and correct tool selection on every case after your Step 8 edits. If you see a persistent failure on one model but not another, the description is under-specified for that model’s priors—tighten the schema or lead with the constraint earlier in the text.

Treat the description as code. Version it, diff it, and review it in PRs like any other interface.

Tagsfunction-callingtool-descriptionsprompt-designreliability

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All function calling fundamentals posts →