· 8 min read
Building Reliable Tool-Use in LLM Agents
Tool calls are the most fragile part of agentic LLM workflows. Here are patterns that make them predictable: schema design, validation, and retries.
By Tanner Goins

Tool-use is what separates a chatbot from an agent. The moment an LLM can call functions (query a database, send an email, update a record, hit an API), it stops being a text generator and starts being a system that does things.
That’s exciting. It’s also where things get fragile.
I’ve built several agentic systems at this point, and the pattern is consistent: the LLM’s reasoning is usually fine. The tool calls are where everything breaks. Malformed arguments, wrong tool selection, missing required fields, hallucinated parameter values: these are the failure modes that dominate debugging sessions.
Here’s what I’ve learned about making tool-use reliable.
Why Tool Calls Break
Before getting into solutions, it helps to understand why LLM tool calls are inherently fragile.
The model is generating structured output from unstructured reasoning. When an LLM decides to call a function, it’s translating a natural language understanding of what needs to happen into a precise JSON schema. That translation step is where errors creep in. A field name gets slightly wrong, a type gets confused, an optional parameter gets hallucinated.
The model doesn’t understand your system. It knows what the tool description says, not how the tool works. If your tool description says “query the database” but doesn’t specify valid query formats, the model will guess. And it will guess confidently.
Context window pressure. As conversations get longer and tool schemas get more complex, the model has more opportunities to lose track of details. A tool call that works perfectly in a short conversation might fail in a long one because the model’s attention is spread thinner.
Schema Design: Your First Line of Defense
The tool schema is the contract between your application and the LLM. A well-designed schema prevents entire categories of errors.
Keep Schemas Narrow
Every parameter you add is a parameter the model can get wrong. Start with the minimum viable schema and add parameters only when you have evidence they’re needed.
Too broad:
{
"name": "search_products",
"parameters": {
"query": "string",
"category": "string",
"min_price": "number",
"max_price": "number",
"sort_by": "string",
"sort_order": "string",
"page": "number",
"per_page": "number",
"include_out_of_stock": "boolean",
"brand_filter": "string[]"
}
} Better:
{
"name": "search_products",
"parameters": {
"query": "string (required)",
"category": "string (optional, one of: electronics, clothing, home)",
"max_results": "number (optional, default 10)"
}
} The narrow schema gives the model fewer ways to fail and handles the remaining complexity in your application code, where you have full control.
Use Enums Aggressively
Whenever a parameter has a finite set of valid values, use an enum. Don’t let the model free-text a value that needs to match exactly.
{
"status": {
"type": "string",
"enum": ["pending", "approved", "rejected"],
"description": "The new status to set"
}
} Without the enum, the model might generate “Pending”, “PENDING”, “pending approval”, or “approve”. All plausible, all wrong.
Write Descriptions Like You’re Explaining to a New Hire
The model reads your tool descriptions literally. Ambiguous descriptions produce ambiguous behavior.
Vague:
"description": "Get user info" Precise:
"description": "Retrieve a user's profile by their unique user ID (a UUID string like '550e8400-e29b-41d4-a716-446655440000'). Returns name, email, and account creation date. Returns an error if the user ID doesn't exist." Include:
- What the tool does (and doesn’t do)
- The format of expected inputs (with examples)
- What the response looks like
- How errors are communicated
Validation: Trust But Verify
Even with perfect schemas, models will produce invalid tool calls. You need a validation layer between the LLM’s output and your tool execution.
Schema Validation
Before executing any tool call, validate the arguments against your schema. This catches structural errors: wrong types, missing required fields, values outside allowed ranges.
function validateToolCall(
call: ToolCall,
schema: ToolSchema,
): ValidationResult {
// Check required parameters are present
for (const required of schema.required) {
if (!(required in call.arguments)) {
return { valid: false, error: `Missing required parameter: ${required}` };
}
}
// Check enum values
for (const [key, value] of Object.entries(call.arguments)) {
const paramSchema = schema.parameters[key];
if (paramSchema?.enum && !paramSchema.enum.includes(value)) {
return {
valid: false,
error: `Invalid value for ${key}: "${value}". Must be one of: ${paramSchema.enum.join(", ")}`,
};
}
}
return { valid: true };
} Semantic Validation
Schema validation catches structural errors. Semantic validation catches logical errors, where the arguments are well-formed but don’t make sense in context.
Examples:
- A date range where the end date is before the start date
- A search query that’s suspiciously similar to the tool description (the model is parroting)
- A user ID that doesn’t match any user in the current conversation context
- A dollar amount that’s negative or unreasonably large
These checks are domain-specific and evolve as you discover new failure modes.
Feeding Validation Errors Back
When a tool call fails validation, don’t just error out. Feed the validation error back to the model and let it retry. Most of the time, the model will correct its mistake on the second attempt.
if (!validation.valid) {
// Feed the error back to the model as a tool result
return {
role: "tool",
content: `Tool call failed: ${validation.error}. Please fix the arguments and try again.`,
};
} This creates a self-correcting loop that handles most transient errors without human intervention.
Retry Strategies
Not all tool call failures are the model’s fault. External APIs time out. Databases have transient errors. Rate limits get hit. Your retry strategy needs to handle both model errors and infrastructure errors.
Model Errors: Retry With Feedback
When the model produces an invalid tool call, feed the error back and let it retry. Cap this at 2–3 attempts. If the model can’t produce a valid call after three tries, it’s not going to figure it out. Escalate to a fallback.
Infrastructure Errors: Retry With Backoff
When the tool itself fails (API timeout, rate limit, transient database error), retry with exponential backoff. The model doesn’t need to know about these. Just retry the same valid tool call.
async function executeWithRetry(
toolFn: () => Promise<ToolResult>,
maxRetries: number = 3,
): Promise<ToolResult> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await toolFn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
await sleep(Math.pow(2, attempt) * 1000); // 1s, 2s, 4s
}
}
} Know When to Stop
The hardest part of retry logic is knowing when to give up. Three retries is a reasonable default, but the right number depends on the cost of failure vs. the cost of retrying.
For a read-only query, retry generously. For a write operation that charges a customer’s credit card, don’t retry at all; surface the error and let a human decide.
The Confirmation Pattern
For high-stakes tool calls (anything that modifies data, sends communications, or spends money), add a confirmation step.
Instead of executing the tool immediately, return a summary of what the tool would do and ask the user (or the orchestrating system) to confirm.
Agent: I'd like to send the following email:
To: client@example.com
Subject: Invoice #4521
Body: [preview]
Should I go ahead and send this? This pattern costs one extra round trip but prevents the class of errors where the model does the right thing for the wrong reason: correct tool, correct format, wrong intent.
Observability: Seeing What the Model Sees
When tool calls go wrong, you need to reconstruct what happened. This requires logging that captures the full context of each tool interaction.
Log everything:
- The tool call as the model generated it (raw arguments)
- The validation result (pass/fail and why)
- The tool execution result (success/failure, response data, latency)
- The model’s interpretation of the result (what it did next)
This creates a trace that lets you diagnose failures after the fact. It’s the same principle behind the observability approach I described in AI products failing quietly: you need to see the full chain of reasoning, not just the final output.
Tracking Tool Call Success Rates
Over time, track per-tool success rates. If a specific tool fails more than others, the problem is usually in the schema. The description is ambiguous, the parameters are too complex, or the tool does too many things.
Architectural Patterns That Help
Tool Grouping
Don’t give the model access to every tool at every step. Instead, group tools by workflow phase and only expose relevant tools at each step.
In a customer support agent:
- Classification phase: Only expose the
classify_tickettool - Information gathering: Expose
search_knowledge_baseandget_customer_info - Resolution: Expose
update_ticket,send_response,escalate
This reduces the chance of the model calling the wrong tool because there are fewer wrong tools to call.
Tool Wrappers
Wrap your raw tool functions with a layer that handles validation, retries, logging, and error formatting. The model talks to the wrapper; the wrapper talks to the actual implementation.
This separation means you can change tool behavior, add guardrails, or fix edge cases without modifying your prompt or schema; the model’s interface stays stable while the implementation evolves.
Fallback Tools
For critical workflows, define fallback tools that the model can use when the primary tool fails. A search_knowledge_base tool might fall back to a simpler keyword_search if the semantic search index is down.
Start With These Principles
If you’re building agentic systems with tool-use, here’s the short version:
- Keep schemas narrow. Every parameter is a potential failure point.
- Validate before executing. Never pass unvalidated model output to a real system.
- Feed errors back. Let the model self-correct before you escalate.
- Confirm high-stakes actions. One extra round trip beats one wrong email.
- Log everything. You can’t fix what you can’t see.
Tool-use is what makes LLM agents genuinely useful. But “genuinely useful” requires “genuinely reliable,” and reliability isn’t a property of the model. It’s a property of the system you build around it. The model provides the reasoning. Your engineering provides the guardrails, the validation, and the safety net.
Build the safety net first. Then let the agent fly.