Agnost AIAgnost AIBook a demo
Open +
← All articles

How to Detect and Stop Duplicate Tool Calls in AI Agents

Find repeated tool calls without flagging legitimate retries or polling. Includes a scoped detector, latency accounting, and a test plan.

Detect duplicate tool calls by comparing their purpose, inputs, authorization scope, data version, and results within one task. The same tool running twice is not enough. The second call may be a retry after failure, a status poll, or a lookup after the underlying state changed.

A useful duplicate finding explains why the later call added no information and what it cost.

Separate repetition from wasted work

Consider an illustrative agent that searches for a return policy three times. Each call asks the same question, reads the same policy version, and returns the same result. The first takes 0.4 seconds; the next two take 0.4 and 0.8 seconds.

If those calls run sequentially and block the answer, the two later calls add 1.2 seconds of tool time on the critical path. If they overlap with useful work, adding their durations does not establish the user-visible delay. Report duplicate tool time and end-to-end latency separately.

Repeated activity Usually a duplicate? Evidence needed
Same successful read, unchanged data, same purpose Candidate duplicate Inputs, result fingerprint, data version
Retry after a timeout Not automatically Attempt status and whether the earlier result was usable
Polling a pending job Not automatically Explicit polling purpose, interval, terminal condition
Same query under a different tenant No shared-cache assumption Authorization scope
Recheck after a mutation Not automatically State change or freshness requirement

Use a scoped fingerprint

Start with a narrow read-only detector. This example runs on normalized records prepared by your application. It does not automatically classify every MCP or framework event.

import json

def repeated_successful_reads(calls):
    seen = set()
    duplicates = []
    for call in calls:
        if not call["read_only"] or not call["ok"] or call["purpose"] == "poll":
            continue
        key = (
            call["tool"], call["scope"], call["version"], call["purpose"],
            json.dumps(call["args"], sort_keys=True), call["result_hash"]
        )
        if key in seen:
            duplicates.append(call["id"])
        seen.add(key)
    return duplicates

Use this within a bounded task, not a global history. Define ok as a usable successful result, not just a transport status. Keep fingerprints internal; hashing sensitive content does not automatically make it safe to disclose. Normalize JSON key order, but do not casually lowercase, remove arguments, or merge superficially similar requests.

A matching result helps detect redundant work after it happens. Preventing the call requires a cache or reuse policy whose freshness and authorization checks are valid before execution.

Fix the reason the agent repeats itself

Inspect whether the first result reached the next model turn. A tracing pipeline may record the result even when the agent’s context omitted it. A truncated observation, wrong message format, or lost graph state can make another lookup reasonable from the model’s perspective.

If the result is present and sufficient, test an instruction to reuse it, a task-scoped read cache, or a tool budget. Prefer a fix aimed at the actual cause. A hard “one call per tool” limit will break workflows that legitimately need multiple objects or pages.

For calls with external side effects, use idempotency. Read deduplication and duplicate payment prevention are different problems.

Verify the change with controls

Replay at least these cases: identical successful reads, a failed first attempt, a changed policy version, a different authorization scope, and a job that requires polling. Compare correctness as well as call counts. A faster agent that stops checking newly changed information is a regression.

Our synthetic monitoring lab includes repeated reads and healthy controls. Its published results demonstrate detector behavior on deliberately constructed examples, not expected savings in a production application.

In Agnost, start with Tool Calls, then open the surrounding conversation to understand why the repeated execution occurred. If your agent uses graph resumes or retries, the LangGraph monitoring guide explains the identity boundaries to preserve.

What should you report?

Report the fraction of eligible runs with confirmed redundant reads, extra calls per affected run, and measured end-to-end latency. For cost, use actual provider billing units rather than assuming every tool call has the same price. Keep polling and retries as separate categories so an availability incident does not get mistaken for a reasoning loop.