How to Fix 'duplicate tool calls' in LlamaIndex (Python)
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 pattern | Fixed pattern |
|---|---|
| Wrap the same function multiple times | Create one tool instance |
| Let default names collide | Set 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
- •
Print every tool name before building the agent
for t in tools: print(t.metadata.name)If you see duplicates, that’s your bug.
- •
Check whether you’re wrapping the same function more than once Search for repeated
FunctionTool.from_defaults(...)orQueryEngineTool.from_defaults(...)calls against the same callable or engine. - •
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.
- •
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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