How to Fix 'duplicate tool calls' in LlamaIndex (Python)

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-callsllamaindexpython

What the error means

If you’re seeing ValueError: duplicate tool calls or a similar duplicate tool calls detected error in LlamaIndex, the agent is trying to register or execute the same tool more than once in a single run. This usually shows up when you wire tools into an agent incorrectly, reuse the same function/tool wrapper twice, or mix manual tool execution with agent-managed execution.

In practice, it happens most often with FunctionTool, QueryEngineTool, ReActAgent, or OpenAIAgent when the tool list contains duplicates or your loop re-adds tools on every request.

The Most Common Cause

The #1 cause is passing the same underlying callable into multiple tool wrappers, then giving all of them to the agent. LlamaIndex sees distinct Python objects, but the tool registry ends up with duplicate names or duplicate call signatures.

Here’s the broken pattern:

from llama_index.core.tools import FunctionTool
from llama_index.core.agent import ReActAgent

def get_account_balance(account_id: str) -> str:
    return f"Balance for {account_id}: $1,250"

# WRONG: same function wrapped twice with same default name
balance_tool_1 = FunctionTool.from_defaults(fn=get_account_balance)
balance_tool_2 = FunctionTool.from_defaults(fn=get_account_balance)

agent = ReActAgent.from_tools([balance_tool_1, balance_tool_2], verbose=True)
response = agent.chat("Check account 12345")
print(response)

And here’s the fixed version:

from llama_index.core.tools import FunctionTool
from llama_index.core.agent import ReActAgent

def get_account_balance(account_id: str) -> str:
    return f"Balance for {account_id}: $1,250"

# RIGHT: one tool instance, one name, one registration
balance_tool = FunctionTool.from_defaults(
    fn=get_account_balance,
    name="get_account_balance",
    description="Fetches the current balance for a bank account."
)

agent = ReActAgent.from_tools([balance_tool], verbose=True)
response = agent.chat("Check account 12345")
print(response)
Broken patternFixed pattern
Wrap the same function multiple timesCreate one tool instance
Let default names collideSet an explicit unique name
Pass duplicate tools into from_tools()Deduplicate before building the agent

The same issue happens with query tools too:

# WRONG
tool_a = QueryEngineTool.from_defaults(query_engine=qe)
tool_b = QueryEngineTool.from_defaults(query_engine=qe)

agent = OpenAIAgent.from_tools([tool_a, tool_b])

Use one wrapper per underlying capability:

# RIGHT
tool = QueryEngineTool.from_defaults(
    query_engine=qe,
    name="policy_search",
    description="Searches policy documents."
)

agent = OpenAIAgent.from_tools([tool])

Other Possible Causes

1. You append tools on every request

If you build your agent inside a request handler and keep appending to a shared list, duplicates accumulate.

# WRONG
TOOLS = []

def build_agent():
    TOOLS.append(balance_tool)
    TOOLS.append(balance_tool)
    return ReActAgent.from_tools(TOOLS)

Fix it by rebuilding from a clean list:

# RIGHT
def build_agent():
    tools = [balance_tool]
    return ReActAgent.from_tools(tools)

2. You mix manual tool calls with agent tool execution

If you call a tool directly and also let the agent call it in the same flow, some setups will surface duplicate execution behavior.

# WRONG
result = get_account_balance("12345")
response = agent.chat(f"Use this result: {result}")

Prefer one execution path:

# RIGHT
response = agent.chat("Check account 12345")

Or if you want full control, don’t expose that tool to the agent.

3. Two tools share the same name

LlamaIndex routes by tool name in several agent implementations. If two tools both resolve to get_account_balance, you’ll hit collisions.

# WRONG
tool_1 = FunctionTool.from_defaults(fn=fn_a, name="lookup")
tool_2 = FunctionTool.from_defaults(fn=fn_b, name="lookup")

Rename them explicitly:

# RIGHT
tool_1 = FunctionTool.from_defaults(fn=fn_a, name="policy_lookup")
tool_2 = FunctionTool.from_defaults(fn=fn_b, name="claims_lookup")

4. You reuse serialized/loaded objects across sessions

If you cache an agent or persist state and reload it into a new runtime without clearing old tool metadata, stale registrations can reappear.

# Example config smell
agent_cache_key = "support-agent-v1"
# reused across incompatible tool sets

Fix by versioning cache keys when tools change:

agent_cache_key = "support-agent-v2-tools-fixed"

How to Debug It

  1. Print every tool name before building the agent

    for t in tools:
        print(t.metadata.name)
    

    If you see duplicates, that’s your bug.

  2. Check whether you’re wrapping the same function more than once Search for repeated FunctionTool.from_defaults(...) or QueryEngineTool.from_defaults(...) calls against the same callable or engine.

  3. Log where your tool list is built In web apps and notebooks, this often happens in code that runs multiple times. Make sure your list is local to the request or rebuilt from scratch.

  4. Reduce to one tool Start with a single known-good FunctionTool. If the error disappears, add tools back one by one until it breaks again.

Prevention

  • Give every tool an explicit unique name.
  • Build tool lists locally; don’t mutate shared global lists.
  • Add a small startup check that asserts unique names:
    names = [t.metadata.name for t in tools]
    assert len(names) == len(set(names)), f"Duplicate tools: {names}"
    

If you’re using ReActAgent, OpenAIAgent, or any LlamaIndex class that accepts from_tools(), treat tool registration like API route registration: one capability, one name, one place in the list. That prevents most duplicate-tool failures before they reach production.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides