AI & Machine Learning Bug Report

LLM agent calls the refund tool for order status questions: overlapping tool descriptions

Our support agent started opening refund requests when customers only asked where their order was. Tool names and descriptions overlapped, and the agent had 23 tools to choose from. Clear tool contracts, fewer tools per step and a tool-choice eval fixed it.

Ryan Pham Ryan Pham @ryan_bugs · July 08, 2026 · 7 min read · 22 Views
Banner: LLM agent choosing the wrong tool because tool descriptions overlap

The finance team noticed a jump in refund requests created by our support agent: 340 in one week, up from about 90. Reviewing a sample, most came from conversations like this:

User:  Hi, my order 55120 hasn't arrived yet, any update?
Agent: [tool_call] create_refund_request(order_id="55120", reason="order not received")
Agent: I've opened a refund request for order 55120. You'll hear from us within 3 business days.

The user asked for a status update. The agent started a refund. Refund requests are reviewed by humans, so no money moved automatically, but customers got confusing emails and the finance queue tripled.

Reproducing it

The change that week was the addition of four new tools for the returns team, bringing the agent's tool list to 23. With the previous 19 tools, the same conversation called get_order_status. With 23, it called create_refund_request about 30 percent of the time.

The relevant tool definitions:

{"name": "get_order_status", "description": "Get order info."}
{"name": "create_refund_request", "description": "Handle problems with an order, such as an order that was not received, damaged or wrong."}
{"name": "order_issue_lookup", "description": "Look up issues for an order."}

What we ruled out

  • Model change. Same pinned model snapshot before and after.
  • System prompt change. No edits that week.
  • A bug in tool routing code. The model's tool call itself named the refund tool; our code executed what it asked for.

Root cause

The new refund tool's description matched the user's words ("order that was not received") far better than the vague "Get order info." of the status tool. A third tool, order_issue_lookup, overlapped with both. With 23 tools in the prompt, the model chose based on surface similarity between the user's message and the descriptions. Nothing in the tool contract said that a refund should only be created when the customer asks for one.

What is happening under the hood

Tool definitions are part of the prompt. The model reads names, descriptions and parameter schemas and decides which one fits the conversation. Descriptions act as instructions. A specific, action-oriented description wins over a vague one, even when the vague tool is the correct choice. As the number of tools grows, accuracy tends to drop, especially when tools overlap in purpose.

Side-effecting tools make this worse: choosing a read tool by mistake wastes a call, choosing a write tool by mistake changes the world.

Environment

Component Version / value
Runtime Python 3.12
Model Large general model with tool calling, pinned snapshot
Tools 23 (11 read, 12 write)
Volume ~6k conversations per day
Safeguards Refunds reviewed by finance before payout

Minimal reproduction

A tiny eval with three tools reproduces the pattern: ask "my order 123 hasn't arrived, any update?" 50 times, count which tool is called. With the descriptions above, the refund tool wins a large share of calls.

The fix

1. Rewrite tool contracts: when to use, when not to use.

{
  "name": "get_order_status",
  "description": "Return shipping status, carrier, tracking link and estimated delivery for one order. Use this first for any question about where an order is, delivery dates, or whether it has shipped, including when the customer says it has not arrived."
}
{
  "name": "create_refund_request",
  "description": "Open a refund request for review by finance. Only use when the customer explicitly asks for a refund or return, after you have checked the order status. Never use for status or delivery questions."
}

We deleted order_issue_lookup; its job was covered by get_order_status plus a field for open issues.

2. Fewer tools per step. A small router classifies the conversation into one of five intents first, and only the tools for that intent are exposed to the agent. The order-tracking intent sees four tools, none of which write.

TOOLSETS = {
    "order_tracking": ["get_order_status", "get_tracking_events", "update_delivery_notes", "handoff_to_human"],
    "refund_return": ["get_order_status", "create_refund_request", "create_return_label", "handoff_to_human"],
    # ...
}

def tools_for(conversation) -> list[dict]:
    intent = classify_intent(conversation)        # small, fast model or rules
    return [TOOL_DEFS[name] for name in TOOLSETS[intent]]

3. Confirmation for write tools. Write tools now return a draft and require the customer to confirm in the chat before execution. The agent has to say "I can open a refund request for order 55120, would you like me to?" and act only on a yes.

4. Server-side preconditions. create_refund_request checks that get_order_status was called for the same order in the conversation, and that the order is eligible. If not, it returns a structured error explaining what is missing.

Alternatives and trade-offs

  • Force a tool with tool_choice. Useful when you know the step, like "always check status first", but removes flexibility.
  • Better system prompt only. Helped a little; tool descriptions carried more weight in our tests.
  • One giant tool with an action parameter. Fewer tools, but the action choice just moves into a parameter and validation gets harder.

How to test the fix

We built a tool-choice eval: 400 real conversation openings labelled with the correct first tool, including 120 "not arrived yet" cases.

config                               correct_first_tool  wrong_write_tool
23 tools, old descriptions                 0.71              0.19
23 tools, new descriptions                 0.88              0.04
router + 4-6 tools per intent              0.96              0.004

The eval runs in CI whenever a tool definition, the router or the model changes.

Confirming in production

  • Agent-created refund requests went back to about 85 per week, and finance confirmed the share of invalid ones dropped from roughly 70 percent to under 5 percent.
  • Average conversation length did not change; the router added about 150 ms per conversation.
  • We emailed the customers who had received refund notices by mistake with a correction.

Regression guard

  • Tool-choice eval in CI with a threshold on wrong write-tool calls.
  • New tools require a description review: purpose, when to use, when not to use, and which existing tool it might be confused with.
  • A dashboard of tool call counts per day; sudden shifts after a release trigger a review.

Security implications

Write tools are where agent mistakes become real-world actions. The confirmation step and server-side preconditions are the important controls here, because tool descriptions influence the model but do not enforce anything. The same controls also limit damage from prompt injection: a malicious message cannot make the agent issue a refund without the customer's explicit confirmation and an eligible order.

Prevention checklist

  • Write tool descriptions as contracts: what it does, when to use, when not to.
  • Remove overlapping tools instead of adding more description text.
  • Expose only the tools relevant to the current intent.
  • Confirm and validate write actions server-side.
  • Keep a tool-choice eval and run it on every change.

References

Add a label

#llm #agents #function-calling #evals #tool-design
Written by
Ryan Pham @ryan_bugs

I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.