LlamaIndex agents — ReActAgent, FunctionCallingAgent tool descriptions and step limits (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LlamaIndex Agent tool errors
LlamaIndex Agents · ReAct + FunctionCalling Severity: High HTTP n/a

LlamaIndex ReActAgent / FunctionCallingAgent — tool desc and step-limit failures

LlamaIndex agents attach tools (including query engines) to a chat loop. When tool descriptions are sparse, when step limits are missing, or when the agent picks the wrong tool, cost balloons and users see irrelevant answers.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: LlamaIndex offers ReActAgent (text-based ReAct) and FunctionCallingAgent (provider-native tool calls). Both take tools=[QueryEngineTool | FunctionTool]. Fix issues by (a) writing crisp tool_description for every tool, (b) setting max_iterations to prevent loops, (c) picking FunctionCallingAgent for OpenAI/Anthropic/Google and ReActAgent only for models without tool calling, and (d) using AgentWorkflow (the Workflow-based agent) for anything complex.

Real error messages you'll see

These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

Agent picks the wrong tool
# tools = [get_weather_tool, search_docs_tool]
# get_weather description: "gets weather"
# search_docs description: "searches"
# User: "What is our refund policy?"
# Agent calls get_weather_tool → returns error → gives up
Max iterations reached
ValueError: Reached max_iterations (10) without producing a final answer.
# Agent loops between the same two tools; no forward progress
ReAct parse error
ValueError: Could not parse output: ```json
{"action": "search", "action_input": "policy"}
```
# Model emitted JSON code fence; ReAct parser wanted plain text format

Reference

Agent classes and when to use each

ClassWhen to useNotes
FunctionCallingAgentOpenAI, Anthropic, Google, BedrockPreferred — uses native tool calls
ReActAgentLocal / open-source models without tool callingText-based; brittle parser
OpenAIAgentLegacy OpenAI-specificDeprecated in favor of FunctionCallingAgent
AgentWorkflowComplex multi-step agentsWorkflow-based; most flexible
ReActChatEngineReAct as chat engineFor chat-shaped ReAct

Root causes, ranked by frequency

Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.

  • 26%
    Sparse tool descriptions. "gets data" tells the agent nothing about when to call it.
  • 18%
    Overlapping tool descriptions. Two tools sound similar; agent flips between them.
  • 14%
    No max_iterations set. Agent loops; cost balloons.
  • 10%
    Tool errors returned as normal text. Agent retries with variations.
  • 8%
    Wrong agent class for provider. ReActAgent used with GPT-4 or Claude — needlessly fragile.
  • 7%
    QueryEngineTool without name. Auto-derived name conflicts or is undescriptive.
  • 7%
    Streaming lost through agent. Agent buffers output; UI shows nothing.
  • 10%
    Agent state grows unbounded. Every turn adds tool outputs to context; hits LLM limit mid-conversation.

Fixes — copy-paste solutions

Fix #1

Use FunctionCallingAgent with crisp QueryEngineTool metadata

Every tool needs a name and a description the model can act on.

Wrap query engines in QueryEngineTool with explicit ToolMetadata. Same idea for arbitrary Python functions via FunctionTool.

agent_with_tools.py
from llama_index.core.agent import FunctionCallingAgentWorker, AgentRunner
from llama_index.core.tools import QueryEngineTool, ToolMetadata, FunctionTool
from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o-mini")

# --- Wrap a query engine as a tool ---
policy_engine = policy_index.as_query_engine(similarity_top_k=6)
policy_tool = QueryEngineTool(
    query_engine=policy_engine,
    metadata=ToolMetadata(
        name="company_policies",
        description=(
            "Search company policies, including refund, returns, warranty, "
            "shipping, and terms of service. Use this when the user asks about "
            "any company policy or procedure. Do NOT use for product features."
        ),
    ),
)

products_engine = products_index.as_query_engine(similarity_top_k=6)
products_tool = QueryEngineTool(
    query_engine=products_engine,
    metadata=ToolMetadata(
        name="product_catalog",
        description=(
            "Search product catalog for features, specifications, availability, "
            "and pricing. Use this when the user asks about a specific product "
            "or product feature. Do NOT use for policies."
        ),
    ),
)

# --- FunctionTool for arbitrary Python code ---
def get_order_status(order_id: str) -> str:
    """Fetch order status from the orders API.

    Args:
        order_id: The order identifier, e.g. "ORD-12345"
    """
    return f"Order {order_id}: shipped"

order_tool = FunctionTool.from_defaults(fn=get_order_status)

# --- Build the agent ---
agent_worker = FunctionCallingAgentWorker.from_tools(
    tools=[policy_tool, products_tool, order_tool],
    llm=llm,
    verbose=True,
    max_iterations=8,       # cap the loop
    system_prompt=(
        "You are a customer support assistant. Use the tools to answer accurately. "
        "When you have enough information, produce the final answer."
    ),
)
agent = AgentRunner(agent_worker)

response = agent.chat("What is the refund policy for damaged items?")
print(response)

# --- Multi-turn conversation ---
response = agent.chat("What about for the ACME-5000 product specifically?")
print(response)
"Do NOT use for X" phrases in tool descriptions are surprisingly effective. They tell the model explicitly when NOT to pick a tool, reducing wrong-tool errors dramatically.
Fix #2

Set max_iterations and detect stuck loops

Never ship an agent without an iteration cap.

Configure max_iterations at agent construction. Add a callback that detects repeated tool calls and short-circuits before hitting the cap.

agent_iteration_cap.py
from llama_index.core.agent import FunctionCallingAgentWorker, AgentRunner
from llama_index.core.callbacks import CallbackManager, CBEventType

# --- Basic cap ---
agent_worker = FunctionCallingAgentWorker.from_tools(
    tools=tools,
    llm=llm,
    max_iterations=8,           # ← never more than 8 tool calls
    verbose=True,
)
agent = AgentRunner(agent_worker)

# --- Detect loop pattern ---
class LoopDetector:
    def __init__(self, max_same_tool_in_row: int = 3):
        self.recent_tools = []
        self.max_repeat = max_same_tool_in_row

    def check(self, tool_name: str) -> bool:
        self.recent_tools.append(tool_name)
        if len(self.recent_tools) > 5:
            self.recent_tools.pop(0)
        if self.recent_tools.count(tool_name) >= self.max_repeat:
            return True
        return False

# --- Wrap the agent with a safety loop ---
def run_agent_safely(agent, query: str, max_wall_time_sec: int = 60):
    import time
    detector = LoopDetector()
    start = time.time()

    # For deeper control, use the step-based API
    task = agent.create_task(query)
    for step_idx in range(agent.agent_worker.max_iterations):
        if time.time() - start > max_wall_time_sec:
            return "Task aborted: wall-time limit exceeded."

        step_output = agent.run_step(task.task_id)

        # Inspect what happened this step
        for tool_call in getattr(step_output, "tool_calls", []) or []:
            if detector.check(tool_call.tool_name):
                return f"Task aborted: {tool_call.tool_name} called repeatedly."

        if step_output.is_last:
            return agent.finalize_response(task.task_id).response

    return "Task aborted: iteration cap reached."

result = run_agent_safely(agent, "very tricky query", max_wall_time_sec=45)
print(result)
For production, budget total agent cost per request. A per-call token counter callback that raises above a threshold is the ultimate safety net.
Fix #3

Prefer AgentWorkflow for complex multi-step agents

The Workflow-based agent primitive is more flexible than the classic agents.

AgentWorkflow uses LlamaIndex Workflow (event-driven state machine) under the hood. Better for agents with multi-step orchestration, human-in-the-loop, or complex control flow.

agent_workflow.py
from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o-mini")

# --- Single-agent workflow (simplest replacement for FunctionCallingAgent) ---
async def get_weather(city: str) -> str:
    """Get current weather for a city."""
    return f"{city}: 32°C sunny"

async def search_docs(query: str) -> str:
    """Search company documentation."""
    return f"Search results for: {query}"

agent = FunctionAgent(
    tools=[
        FunctionTool.from_defaults(fn=get_weather),
        FunctionTool.from_defaults(fn=search_docs),
    ],
    llm=llm,
    system_prompt="You are helpful. Use tools when they help.",
)

# --- Run the workflow-based agent ---
async def main():
    result = await agent.run(user_msg="What is the weather in Karachi?")
    print(result)

    # Multi-turn with state
    ctx = agent.new_context()   # persistent context across turns
    r1 = await agent.run(user_msg="What is the weather in Karachi?", ctx=ctx)
    r2 = await agent.run(user_msg="And in London?", ctx=ctx)
    print(r1, r2)

import asyncio
asyncio.run(main())

# --- Multi-agent workflow (root agent delegates to sub-agents) ---
from llama_index.core.agent.workflow import AgentWorkflow

policy_agent = FunctionAgent(
    name="policy_agent",
    description="Handles policy questions",
    tools=[policy_tool],
    llm=llm,
    system_prompt="You handle company policy questions.",
)

product_agent = FunctionAgent(
    name="product_agent",
    description="Handles product questions",
    tools=[products_tool],
    llm=llm,
    system_prompt="You handle product catalog questions.",
)

workflow = AgentWorkflow(
    agents=[policy_agent, product_agent],
    root_agent="policy_agent",     # entry point
)

async def run():
    result = await workflow.run(user_msg="What is the refund policy?")
    print(result)

asyncio.run(run())
For any agent workflow beyond simple tool-calling, AgentWorkflow is the recommended path. LlamaIndex is actively developing this API; the classic FunctionCallingAgent still works but is less feature-rich.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Use FunctionCallingAgent (or AgentWorkflow) for tool-capable models; only use ReActAgent for models without native tool calling.
  • Every tool needs an explicit name and rich description.
  • Include "Do NOT use for X" phrasing in tool descriptions to prevent wrong-tool selection.
  • Always set max_iterations — 5-10 is typical.
  • Add a loop detector and a wall-time cap for production agents.
  • Log intermediate_steps to observability; alert on high iteration counts.
  • For complex workflows, migrate from classic agents to AgentWorkflow.

Frequently asked questions

AgentRunner is LlamaIndex's current runner class for step-based agents. Older code uses OpenAIAgent or ReActAgent as top-level classes; those still work. AgentRunner + AgentWorker is the more flexible split.
Not directly. Wrap the LangChain tool call in a Python function and expose it as a FunctionTool. The reverse (LangChain agent using LlamaIndex query engine) works the same way.
Use agent.stream_chat("...") which returns an async generator. Streaming works cleanly for FunctionCallingAgent; ReActAgent has a non-streaming reasoning step and streams only the final answer.
SubQuestionQueryEngine is deterministic — it decomposes ONE query into sub-queries against tools. An agent is iterative — it decides tools + reasoning per step. Use SubQuestion when you know upfront the query needs decomposition; use agents for open-ended interactions.
Wrap the tool with a closure or class that carries user context. Alternatively, use Context in AgentWorkflow to pass per-request auth downstream to tools.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.