Building a Query Engine Agent Over Your Own Data
A query engine answers one RAG question well.
Search across all documentation pages
A query engine answers one RAG question well.
An agent decides whether and when to use that engine among other tools (calculators, tickets, CRM, web).
This page wires your index into a QueryEngineTool and a LlamaIndex agent loop.
Build or load a query engine, wrap it with a precise name and description via QueryEngineTool.from_defaults, then pass it to FunctionAgent (or ReActAgent when function calling is weak).
query_engine = index.as_query_engine(...) with an explicit similarity_top_k.QueryEngineTool.from_defaults(query_engine, name=..., description=...).FunctionAgent for tool-calling models; ReActAgent for broader model support.agent.run(...) and log tool calls + source nodes for traces.import asyncio
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.tools import QueryEngineTool, FunctionTool
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI # verify imports at build
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(similarity_top_k=4)
docs_tool = QueryEngineTool.from_defaults(
query_engine,
name="company_docs",
description=(
"Search internal company policy and product documentation. "
"Use for refunds, support hours, SLAs, and feature behavior. "
"Input should be a full natural-language question."
),
)
def escalate(reason: str) -> str:
"""Open a human handoff ticket when docs are insufficient."""
return f"ESCALATED: {reason}"
tools = [
docs_tool,
FunctionTool.from_defaults(escalate),
]
llm = OpenAI(model="gpt-4o-mini") # verify at build
agent = FunctionAgent(
tools=tools,
llm=llm,
system_prompt=(
"You are a support agent. Prefer company_docs for policy facts. "
"If documentation is missing or conflicting, call escalate."
),
)
async def main() -> None:
answer = await agent.run(
"A customer bought a plan 12 days ago. Are they still eligible for a refund?"
)
print(str(answer))
if __name__ == "__main__":
asyncio.run(main())Optional: return the tool result without an extra rewrite when the docs answer is the product response:
docs_tool = QueryEngineTool.from_defaults(
query_engine,
name="company_docs",
description="...",
return_direct=True, # ends loop after tool; use carefully
)index.as_chat_engine() is great for a pure RAG chatbot.
Agents need optional retrieval: some turns are pure tool calls, math, or handoffs with no corpus hit.
QueryEngineTool makes the RAG pipeline look like any other API from the model's point of view.
The model selects tools from name + description + parameter schema.
Weak description: "docs tool".
Strong description: scope (which corpus), intended question types, and what not to use it for.
One tool per coherent corpus beats one mega-tool over unrelated folders.
| Agent | Mechanism | Use when |
|---|---|---|
FunctionAgent | Provider tool/function calling | Models with reliable tool APIs |
ReActAgent | Reason + act prompting | Models without solid function calling |
| Custom loop | Manual chat_with_tools | You need full control of retries/errors |
Typical production mix:
company_docs query engine toolKeep the system prompt short and role-specific.
Put operational policy in tools and data, not a 2,000-token sermon.
Pass a ChatMemoryBuffer (or current memory API) when multi-turn sessions must remember prior tool results.
Bound memory so long RAG dumps do not blow the context window.
Log for every turn:
Without this, “the agent ignored the docs” is guesswork.
You can expose the same query engine to LangGraph, CrewAI, or a custom runtime by wrapping query_engine.query as that framework’s tool type.
LlamaIndex does not have to own the outer loop if you only need its RAG stack.
return_direct=True skips synthesis and multi-tool plans. Use only for pure Q&A paths.| Approach | Pros | Cons |
|---|---|---|
| Query engine only | Simple, cheap | No multi-tool autonomy |
| QueryEngineTool + FunctionAgent | Natural tool use with RAG | Needs tool-calling model |
| RouterQueryEngine without agent | Good multi-index routing | Weaker multi-step tool plans |
| Custom agent loop | Full control | You own parsing and retries |
| External orchestrator + LI retriever | Best-of-breed graph control | Two stacks to operate |
No. A query engine runs a fixed retrieve-and-synthesize path. An agent chooses tools over multiple steps.
Yes. Register multiple QueryEngineTools with distinct names and descriptions, or put a router behind a single tool.
Prefer a full natural-language question so the underlying query engine can embed and synthesize correctly.
Inspect response.source_nodes from direct query-engine calls, or have the tool return node metadata/snippets in its string output for the agent to quote.
Use a tool-calling-capable model for FunctionAgent. Keep a cheaper model for pure retrieval synthesis if you split roles.
Enforce max iterations, timeouts, and product-level stop conditions; treat repeated identical tool calls as a failure.
Yes via OpenAI-compatible configuration (api_base / base URL patterns). See OpenRouter + LlamaIndex wiring in the OpenRouter production section.
When your model lacks reliable function calling or you need the explicit reason-act trace format.
Script golden tasks that must call company_docs, assert substrings or structured checks, and track tool-call rates.
No. Agents can call the tool correctly and still retrieve the wrong chunk. Evaluate retrieval separately.
Related: LlamaIndex Basics
Related: Ingesting and Chunking Documents for an Agent's Knowledge Base
Related: Combining Multiple Indexes with Router Query Engines
Related: Building a Retrieval Tool an Agent Can Call
Related: Combining OpenRouter with LangChain, CrewAI, and LlamaIndex
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026