DEV Community

Cover image for Designing Tools Your AI Agent Will Actually Use Correctly
Sham Prakash K
Sham Prakash K

Posted on

Designing Tools Your AI Agent Will Actually Use Correctly

The previous article showed how to wire tools into a Spring AI agent. The mechanics are straightforward — annotate a method, pass it to the ChatClient, done. What's less obvious is that the quality of your tools determines whether the agent works correctly.

A poorly described tool gets called at the wrong time, with the wrong arguments, or not at all. A well-designed tool gets called exactly when it should, with exactly the right input.

The description is an instruction to the model

When you write @Tool(description = "..."), that string isn't documentation for your team. It's a runtime instruction that the model reads before every single request.

The model's decision process looks like this:

  1. What did the user ask?
  2. What tools do I have available? (reads each description)
  3. Do I need a tool to answer this? Which one?
  4. What arguments should I pass? (reads each @ToolParam description)

If your description is vague, the model makes a guess. Sometimes the guess is right. Often it isn't — and because it's a language model, it won't tell you it was guessing. It will just proceed with what seemed most reasonable.

Vague description:

@Tool(description = "Get weather")
public String getWeather(String city) { ... }
Enter fullscreen mode Exit fullscreen mode

The model doesn't know: Is this current weather or a forecast? What does it return — temperature? conditions? Both? What format is the city name expected in?

Clear description:

@Tool(description = "Get the current weather for a city in India. " +
    "Returns temperature in Celsius and sky conditions (sunny, cloudy, rainy). " +
    "Use this when the user asks about current weather or whether to carry an umbrella.")
public String getWeather(
        @ToolParam(description = "City name in English, e.g. Mumbai, Chennai, Bengaluru") 
        String city) { ... }
Enter fullscreen mode Exit fullscreen mode

Now the model knows exactly when to call it, what it returns, and how to pass the city name. Same method, entirely different instruction.


What to include in a tool description

A useful tool description answers four questions:

What does it do? State the action clearly. "Fetches", "returns", "calculates", "queries" — be specific about the operation.

When should the model use it? This is the most commonly missed part. Add a "use this when..." clause. Without it, the model may skip the tool in cases where it should call it, because it isn't sure the tool applies.

What does it return? Describe the shape of the output. If it returns a price, say what currency. If it returns a date, say what format. If it returns a list, say roughly how many items.

What are the constraints? If the tool only works for certain inputs — cities in India, dates in the future, IDs that exist in the database — say that. The model will avoid calling it in unsupported cases, which prevents unnecessary tool failures.

Example applying all four:

@Tool(description = "Get the cheapest available bus or train ticket price between two cities in India. " +
    "Returns the price in INR and the transport type (bus or train). " +
    "Use this when the user asks about travel cost, ticket price, or how to get between cities. " +
    "Only works for cities with bus or rail connections — does not cover flights.")
public String getTicketPrice(
        @ToolParam(description = "Departure city name in English, e.g. Bengaluru") String from,
        @ToolParam(description = "Destination city name in English, e.g. Mysore") String to) {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Writing good parameter descriptions

Parameter descriptions are just as important as the tool description. The model constructs arguments from them.

Be specific about format:

// Bad — model doesn't know what format to use
@ToolParam(description = "Date") String date

// Good — model knows exactly what to pass
@ToolParam(description = "Travel date in YYYY-MM-DD format, e.g. 2026-10-15") String date
Enter fullscreen mode Exit fullscreen mode

Specify units:

// Bad
@ToolParam(description = "Budget") int amount

// Good
@ToolParam(description = "Maximum budget in INR, e.g. 5000") int amountInr
Enter fullscreen mode Exit fullscreen mode

Clarify expected values for enums or limited options:

@ToolParam(description = "Seat class: SLEEPER, AC_3_TIER, or AC_2_TIER") String seatClass
Enter fullscreen mode Exit fullscreen mode

When the model knows the exact expected values, it passes valid ones instead of guessing.


Returning results the model can reason about

Your tool's return value is what the model reads next. It uses that text to decide what to do — call another tool, ask a clarifying question, or produce the final answer.

Don't return raw JSON:

// Bad — the model has to parse this in its head
return "{\"temp\":28,\"condition\":\"PC\",\"humidity\":85}";
Enter fullscreen mode Exit fullscreen mode

The model can handle JSON, but it has to mentally parse it first. That's cognitive overhead that introduces errors. Write the return value as natural language.

Return readable sentences:

// Good — the model reads this like a human would
return "Current weather in Mumbai: 28°C, partly cloudy, humidity 85%.";
Enter fullscreen mode Exit fullscreen mode

Include context in the result:

If a tool fetches a price, include what the price is for. The model may call multiple tools and combine results — it needs context in each result to do that correctly.

// Without context — confusing when combined with other results
return "₹3,240";

// With context — clear when combined
return "Flight from Bengaluru to Goa on 2026-11-15: ₹3,240 (economy, non-stop, IndiGo 6E-502).";
Enter fullscreen mode Exit fullscreen mode

Common failure modes

The model answers without calling the tool

This happens when the description doesn't say when to use the tool, and the model happens to know something relevant from training data. It answers directly, skipping the tool entirely.

Fix: add "Use this when..." to the description. Add to the system prompt: "Always use available tools to get current information — do not answer from memory."

The model calls the tool with the wrong argument format

A date tool receiving "15th October 2026" instead of "2026-10-15". A city tool receiving "Blr" instead of "Bengaluru".

Fix: specify the exact format in @ToolParam. Add examples: "e.g. 2026-10-15", "e.g. Bengaluru".

The model calls the wrong tool

Two tools with similar descriptions cause the model to mix them up. A getWeather and a getForecast tool with overlapping descriptions leads to unpredictable choices.

Fix: make descriptions explicitly distinct. State what each tool does NOT do: "Returns current conditions — for forecasts, use getForecast."

The tool returns an error and the model hallucinates a result

The tool throws an exception, returns an empty string, or returns an error message the model didn't expect. The model sees this as an unhelpful result and fabricates what the result probably would have been.

Fix: return meaningful error strings, not exceptions. "Unable to fetch weather for this city — city not found." is something the model can relay honestly to the user. An uncaught exception that surfaces as an empty result is something the model will try to paper over.


How many tools is too many?

There's no hard rule, but there's a practical limit. A model given 20 tools spends more time deciding which one to use. The chance of picking the wrong tool increases. The context window fills with tool definitions.

Keep tools focused. One tool should do one thing. A tool that fetches weather AND attractions AND ticket prices is three tools masquerading as one — split it.

Name tools clearly. The tool name is part of what the model reads. getWeather, getAttractions, getTicketPrice are clear. getData, fetch, process are not.

Remove tools the agent doesn't need. If a tool exists but the current use case will never need it, don't register it. Fewer tools mean faster, more accurate decisions.


Testing your tools

Tool quality is easy to verify. Ask questions that should and shouldn't trigger each tool, then inspect whether the right tools were called with the right arguments.

Three test cases every tool should pass:

Positive case — ask something that clearly requires this tool. Verify it was called and returned a sensible result.

Negative case — ask something unrelated. Verify the tool was NOT called.

Edge case — ask something ambiguous or with unusual input. Verify the arguments the model constructed are valid.

Add logging to each tool method during development:

@Tool(description = "...")
public String getWeather(@ToolParam(description = "City name") String city) {
    log.info("getWeather called with city={}", city);
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The log shows you exactly what the model passed. Wrong argument format? Fix the @ToolParam description. Tool not called at all? Fix the tool description or the system prompt.


What's next

Tools are methods. The agent loop runs them in sequence or parallel. But there's a limit to what single tool calls can do — some problems require a model to plan across many steps, remember intermediate results, and course-correct when something goes wrong. That's where more structured agent patterns come in.

The next article covers memory in agents — how to give your agent context from previous turns so it can handle multi-turn conversations, remember what it learned from earlier tool calls, and build on its own previous answers.


Sham Prakash K — Backend Engineer, 4+ years in Java, Spring Boot, and distributed systems. Building AI backend infrastructure. Writing about what I actually learned, mistakes included.

Top comments (0)