This llamaindex research agent n4n.ai tutorial walks through building a tool-using agent that combines live web lookups with a local vector index. You’ll configure LlamaIndex to call models through an OpenAI-compatible gateway, then wire up function tools and a query engine. By the end you’ll have a runnable script that researches a topic and cites both Wikipedia and your own documents.
Prerequisites
- Python 3.10 or newer
llama-indexandrequestsinstalled (pip install llama-index requests)- An API key from an OpenAI-compatible provider — we’ll use n4n.ai’s endpoint
- Familiarity with Python functions and basic async (not required but helpful)
Set your key in the environment before running anything:
export N4N_API_KEY="sk-..."
Configure the LLM
LlamaIndex’s OpenAI class speaks the OpenAI chat protocol, so any compliant endpoint works. We point it at the n4n.ai OpenAI-compatible endpoint (https://api.n4n.ai/v1) to get automatic fallback when a provider is degraded and per-token usage metering without changing our code.
import os
from llama_index.llms.openai import OpenAI
llm = OpenAI(
model="gpt-4o-mini",
api_key=os.environ["N4N_API_KEY"],
api_base="https://api.n4n.ai/v1",
temperature=0,
)
The model string can be any of the 240+ models the gateway routes to; here we use a stable OpenAI model. If you later switch to anthropic/claude-3-haiku or similar, nothing else changes.
Define a Wikipedia Tool
A research agent needs live facts. We wrap a simple requests call to the Wikipedia REST API in a FunctionTool. LlamaIndex serializes the function signature and docstring to build the tool schema the model sees.
import requests
from llama_index.core.tools import FunctionTool
def wikipedia_summary(title: str) -> str:
"""Fetch a short summary of a topic from Wikipedia."""
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{title}"
resp = requests.get(url, timeout=10)
if resp.status_code != 200:
return f"Failed to fetch {title}: {resp.status_code}"
data = resp.json()
return data.get("extract", "No extract available")
wiki_tool = FunctionTool.from_defaults(
fn=wikipedia_summary,
name="wikipedia_summary",
description="Get a concise Wikipedia summary for a given title",
)
Test it directly to confirm the shape:
print(wikipedia_summary("Printing_press"))
Expected output (truncated):
The printing press is a mechanical device for applying pressure to an inked surface...
Build a Local Knowledge Tool
External sources aren’t enough; you often need internal docs. We create an in-memory vector index over two sentences and expose it as a QueryEngineTool.
from llama_index.core import VectorStoreIndex, Document
from llama_index.core.tools import QueryEngineTool
docs = [
Document(text="The printing press was invented by Gutenberg in 1440."),
Document(text="Steam-powered presses emerged in the 19th century."),
]
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()
local_tool = QueryEngineTool.from_defaults(
query_engine=query_engine,
name="local_knowledge",
description="Answer questions from internal documents about printing history",
)
Run a quick check:
print(query_engine.query("When was the printing press invented?"))
Expected output:
The printing press was invented by Gutenberg in 1440.
Assemble the Agent
OpenAIAgent drives the ReAct-style loop: it picks a tool, observes the result, and continues until it can answer. Pass both tools and the configured LLM.
from llama_index.core.agent import OpenAIAgent
agent = OpenAIAgent.from_tools(
[wiki_tool, local_tool],
llm=llm,
verbose=True,
)
verbose=True prints the thought traces so you can see tool calls. For production, swap to a callback handler.
Run and Observe
Now ask a compound question that requires both tools:
response = agent.chat(
"Research the printing press: get a Wikipedia overview and combine "
"with our internal notes about its invention."
)
print(str(response))
With verbose=True you’ll see logs similar to:
Thought: I need a general overview and internal context.
Action: wikipedia_summary
Action Input: {"title": "Printing_press"}
Observation: The printing press is a mechanical device...
Thought: Now the internal doc.
Action: local_knowledge
Action Input: printing press invention
Observation: The printing press was invented by Gutenberg in 1440.
Final printed response:
The printing press, per Wikipedia, is a mechanical device for applying pressure
to an inked surface to transfer text and images. Our internal records specify
that Gutenberg invented it in 1440, with steam-powered models appearing in the
19th century.
That’s a working research agent. It planned, called two disparate sources, and synthesized.
Prompt Design for Research Tasks
The default agent prompt is decent, but for research you want explicit instructions on citation and scope. Override the system prompt:
from llama_index.core.agent import OpenAIAgent
SYSTEM_PROMPT = (
"You are a research assistant. Use the provided tools to gather facts. "
"Always state which source (Wikipedia or local) each fact came from. "
"If tools fail, say so explicitly."
)
agent = OpenAIAgent.from_tools(
[wiki_tool, local_tool],
llm=llm,
system_prompt=SYSTEM_PROMPT,
verbose=True,
)
This reduces hallucination because the model is forced to attribute.
Streaming Responses
For CLI or chat UI, stream tokens:
from llama_index.core.llms import ChatMessage, MessageRole
streaming_llm = OpenAI(
model="gpt-4o-mini",
api_key=os.environ["N4N_API_KEY"],
api_base="https://api.n4n.ai/v1",
streaming=True,
)
agent = OpenAIAgent.from_tools(
[wiki_tool, local_tool],
llm=streaming_llm,
)
stream = agent.stream_chat("Summarize printing press history using both tools")
for chunk in stream.response_gen:
print(chunk, end="", flush=True)
The agent still executes tools before streaming the final answer; intermediate steps appear in verbose logs only.
Error Handling and Resilience
Network calls fail. Wrap the Wikipedia function with retry logic so a transient 503 doesn’t kill the run:
import time
def wikipedia_summary(title: str, retries: int = 3) -> str:
"""Fetch a short summary of a topic from Wikipedia."""
for i in range(retries):
try:
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{title}"
resp = requests.get(url, timeout=10)
if resp.status_code == 200:
return resp.json().get("extract", "No extract")
time.sleep(2 ** i)
except requests.RequestException:
time.sleep(2 ** i)
return "Wikipedia fetch failed after retries"
Because the LLM gateway already provides fallback across providers when a model is rate-limited, the only remaining failure mode is the tool itself. Handle that at the tool boundary.
Extending the Agent
Real research agents usually need:
- A web search tool with pagination (
duckduckgoorserpapiwrappers) - A file-reading tool that loads PDFs into a temporary index
- A citation formatter that post-processes the agent output
Each is just another FunctionTool or QueryEngineTool. The agent loop stays identical; you only grow the tool list.
Key Takeaways
- LlamaIndex agents are tool orchestrators; the hard part is clean tool boundaries.
- Pointing the
OpenAIclass at an OpenAI-compatible gateway keeps your code provider-agnostic. - Always test tools in isolation before handing them to the agent.
- Explicit system prompts dramatically improve attribution and reduce drift.
The script we built is ~60 lines and runs end-to-end. From here, add persistent storage for the vector index and a loop that accepts user queries from stdin.