DEV Community

Cover image for Why Your AI Agents Keep Calling the Wrong Tools (And How We Built Toolsmith to Fix Them)
Harish Kotra (he/him)
Harish Kotra (he/him)

Posted on AI-assisted

Why Your AI Agents Keep Calling the Wrong Tools (And How We Built Toolsmith to Fix Them)

If you have built an autonomous AI agent using OpenAI, Claude, LangChain, or CrewAI, you have inevitably encountered this scenario:

Your agent needs to look up a customer order. Instead of calling fetch_customer_order(order_id), it calls search_database(query="order"). Or worse, it hallucinates an argument like status="completed_order_v2" because the schema simply said "status": { "type": "string" } without defining permitted enum values. Or it enters an infinite loop, toggling back and forth between two sibling tools that do almost the same thing.

When this happens, developers usually react by tweaking the system prompt:

"IMPORTANT: You MUST always call fetch_customer_order when the user mentions an order! Do NOT call search_database!"

This is the Prompt Engineering Trap.

The root cause of tool-calling failures is almost never the model’s reasoning capacity. It is broken, sloppy tool contracts.

In this article, I will explain why agent tool schemas fail in production, break down the architecture of Toolsmith—a developer tool we built to lint, score, and harden AI tool definitions—and share the algorithms and engineering decisions behind it.


1. Anatomizing the Broken Tool Contract

Let’s look at a schema typical of many agent tutorials:

{
  "name": "lookup_user",
  "description": "Used to find information about people in the company or app.",
  "parameters": {
    "type": "object",
    "properties": {
      "user": {
        "type": "string",
        "description": "The user"
      },
      "filter": {
        "description": "Optional search parameters"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why does an LLM fail when presented with this?

  1. Passive, Vague Description: "Used to find information..." does not give the model an imperative trigger. When should it call this instead of a general search tool? Under what constraints?
  2. Missing Imperative Verb: High-performing tool descriptions start with active verbs (Fetch, Query, Search, Validate). This matches the instruction fine-tuning patterns of modern frontier models.
  3. Untyped Parameter: "filter" has no "type". Is it a JSON object? A SQL string? A date range?
  4. No Enum Sets: If "status" or "mode" only supports three valid states, failing to declare an enum: ["active", "suspended", "archived"] guarantees model hallucinations.
  5. Missing required Array: The model has to guess whether "user" is mandatory or optional.

Multiply this by 15 or 20 tools in an enterprise agent runtime, and the model's tool-selection surface degrades into chaotic ambiguity.


2. Toolsmith System Architecture

To solve this, we designed Toolsmith: a tool that performs deterministic 0–100 linting of JSON tool schemas and provides an automated hardening loop powered by any OpenAI-compatible LLM endpoint (Ollama, Groq, OpenRouter, OpenAI) or Gemini.

Here is the high-level architecture:

┌────────────────────────────────────────────────────────────────┐
│                        USER INPUT                              │
│  - Raw Tool Schemas (tools wrapper, bare array, single tool)   │
└───────────────────────────────┬────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────┐
│                     INPUT NORMALIZER                           │
│  - Detects shape & standardizes into NormalizedTool[]          │
└───────────────────────────────┬────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────┐
│               DETERMINISTIC LINTING ENGINE                     │
│  (Pure TypeScript, Zero-Latency, No API calls required)        │
│                                                                │
│  ├── Description Clarity (Imperative verbs, min/max length)    │
│  ├── Parameter Hygiene (Types, enums, required arrays)         │
│  ├── Naming Conventions (snake_case, length, prefixing)        │
│  ├── Sibling Tool Overlap (Jaccard distance, lexical matches)  │
│  └── Token Economics (Byte size, token density, context cost)  │
└───────┬───────────────────────┬────────────────────────┬───────┘
        │                       │                        │
        ▼                       ▼                        ▼
┌──────────────┐        ┌───────────────┐        ┌───────────────┐
│ 0-100 Score  │        │ Tool Category │        │ Tool          │
│ & Grade Dial │        │ Breakdown     │        │ Complexity    │
│ (SVG arc)    │        │ (Progress)    │        │ (L / M / H)   │
└───────┬──────┘        └───────┬───────┘        └───────┬───────┘
        │                       │                        │
        └───────────────────────┼────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────┐
│                SESSION HISTORY ENGINE (Recharts)               │
│  - Tracks score improvement trajectory across analysis runs    │
└───────────────────────────────┬────────────────────────────────┘
                                │
                                ▼
┌────────────────────────────────────────────────────────────────┐
│               AI CONTRACT HARDENING ENGINE                     │
│  - Injects typed schemas, imperative verbs, enum constraints   │
│  - Connects to: Local Ollama, Groq, OpenRouter, or OpenAI API  │
│  - Side-by-Side Diff Inspector with field-by-field reasoning   │
└────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

3. The Deterministic Rule Engine

A critical design requirement for Toolsmith was determinism. Developers do not want to wait 8 seconds or spend $0.05 on API calls just to check if their JSON schema has typos or missing types.

The core linter runs synchronously in milliseconds:

export function runLinter(tools: NormalizedTool[]): LintReport {
  const findings: Finding[] = [];

  // 1. Description audits
  for (const tool of tools) {
    findings.push(...checkDescriptionQuality(tool));
    findings.push(...checkParameterHygiene(tool));
    findings.push(...checkNamingConventions(tool));
  }

  // 2. Cross-tool sibling overlap detection
  findings.push(...checkToolOverlap(tools));

  // 3. Score calculation
  const totalScoreDeductions = findings.reduce(
    (acc, f) => acc + f.scoreImpact,
    0
  );
  const overallScore = Math.max(0, Math.min(100, 100 - totalScoreDeductions));

  return {
    tools,
    findings,
    overallScore,
    grade: computeGrade(overallScore),
    categories: computeCategoryScores(findings),
    toolCount: tools.length,
  };
}
Enter fullscreen mode Exit fullscreen mode

Checking for Imperative Verbs

Models trigger tools most reliably when descriptions start with active, unambiguous command verbs. The linter enforces this check:

const IMPERATIVE_VERBS = new Set([
  "get", "fetch", "search", "query", "find", "retrieve", "list", "read",
  "create", "insert", "add", "post", "generate", "build", "make",
  "update", "modify", "set", "patch", "delete", "remove", "cancel",
  "send", "execute", "run", "calculate", "compute", "validate", "check"
]);

function checkDescriptionQuality(tool: NormalizedTool): Finding[] {
  const findings: Finding[] = [];
  const firstWord = tool.description.trim().split(/\s+/)[0]?.toLowerCase();

  if (!IMPERATIVE_VERBS.has(firstWord)) {
    findings.push({
      id: `${tool.name}-desc-verb`,
      category: "description",
      severity: "warn",
      toolName: tool.name,
      field: "description",
      message: `Description should begin with an active imperative verb (e.g., 'Fetch...', 'Search...'). Currently starts with: "${firstWord}".`,
      fix: `Rewrite the first sentence to start with an active imperative command verb.`,
      scoreImpact: 8,
    });
  }

  return findings;
}
Enter fullscreen mode Exit fullscreen mode

4. Measuring Tool Complexity

Not all tools carry equal cognitive load for an LLM. A tool with 8 top-level parameters and 3 layers of nested objects is significantly more likely to trigger execution errors than a simple key-value tool.

To expose this risk, we implemented a recursive tree walker that computes tool complexity:

export function computeToolComplexity(tool?: NormalizedTool): ToolComplexityInfo {
  if (!tool?.parameters?.properties) {
    return { level: "Low", paramCount: 0, nestedCount: 0, maxDepth: 0, summary: "0 params" };
  }

  let paramCount = 0;
  let nestedCount = 0;
  let maxDepth = 1;

  function walk(props: Record<string, any>, depth: number) {
    if (depth > maxDepth) maxDepth = depth;
    const keys = Object.keys(props || {});
    paramCount += keys.length;

    for (const key of keys) {
      const field = props[key];
      if (field?.type === "object" || field?.properties) {
        nestedCount++;
        if (field.properties) walk(field.properties, depth + 1);
      }
    }
  }

  walk(tool.parameters.properties, 1);

  let level: "Low" | "Medium" | "High" = "Low";
  if (paramCount >= 7 || nestedCount >= 2 || maxDepth >= 3) {
    level = "High";
  } else if (paramCount >= 4 || nestedCount >= 1 || maxDepth >= 2) {
    level = "Medium";
  }

  return {
    level,
    paramCount,
    nestedCount,
    maxDepth,
    summary: `${paramCount} param${paramCount === 1 ? "" : "s"}, ${nestedCount} nested`,
  };
}
Enter fullscreen mode Exit fullscreen mode

The UI visualizes this with high-contrast badges (Low in emerald, Medium in amber, High in rose) right alongside each audited tool.


5. Visualizing Iterative Improvement with Recharts

When developers iterate on their schemas—or run our AI hardening engine—they need to see if their contracts are getting measurably better.

We integrated Recharts to render a lightweight, zero-gradient trend line:

<ResponsiveContainer width="100%" height={140}>
  <LineChart data={history} margin={{ top: 10, right: 15, left: -25, bottom: 0 }}>
    <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
    <XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 10 }} />
    <YAxis domain={[0, 100]} stroke="#64748b" tick={{ fontSize: 10 }} />
    <Tooltip content={<CustomTooltip />} />
    <Line
      type="monotone"
      dataKey="score"
      stroke="#818cf8"
      strokeWidth={2}
      dot={{ fill: "#6366f1", r: 4, strokeWidth: 1, stroke: "#1e1b4b" }}
      activeDot={{ r: 6, fill: "#a5b4fc" }}
    />
  </LineChart>
</ResponsiveContainer>
Enter fullscreen mode Exit fullscreen mode

This immediately surfaces the delta between an unoptimized schema (e.g. 42/100, Grade F) and its hardened variant (e.g. 96/100, Grade A).


6. Flexible AI Hardening Without .env Hassles

Most developer tools force you to clone a repo, configure a .env file, and restart containers just to test an AI model.

In Toolsmith, the OpenAI-compatible client configuration lives entirely in browser state:

  • Local Ollama: Works out of the box (http://localhost:11434/v1, model llama3.1).
  • Groq Cloud: Blazing-fast hardening via llama-3.3-70b-versatile.
  • OpenRouter / OpenAI: Plug in your personal API key and model of choice.

When the target endpoint is localhost or 127.0.0.1, Toolsmith calls it directly from the user's browser, avoiding server container networking bridges. For remote endpoints, it uses a lightweight server proxy to bypass CORS restrictions safely.


7. Key Takeaways for Production AI Agent Developers

Building robust agents requires treating tool schemas with the same rigor we apply to OpenAPI specs and gRPC protobufs:

  1. Write imperative, trigger-focused descriptions: Never say "Used for search". Say: "Search customer orders by date range or customer ID. Call when the user explicitly requests order status."
  2. Always define explicit enum values: If a status is ["pending", "shipped", "delivered"], write it out. Never leave it as an unconstrained "string".
  3. Guard against overlapping tool scopes: If two tools have similar parameter names and verbs, merge them into a single parameterized tool or sharply distinguish their triggers.
  4. Audit schema token weight: Tool definitions are injected into every single prompt turn. Bloated tool schemas silently eat away your context window and inflate latency.

How this works - 1

How this works - 2

Code & more: https://www.dailybuild.xyz/project/252-toolsmith

Top comments (0)