Build a Research Agent with Ollama
Build a ReAct research agent from scratch and compare it to single-shot AI.
Introduction
30 Second Summary
When you ask an AI a question, it generates an answer from what it learned during training. Sometimes that answer is outdated, confidently wrong, or entirely made up.
In this project, you will build two AI systems from scratch: a simple generative AI script and a full research agent that can search the web, read pages, and reason about what it finds before answering.
What You'll Build
A Python research agent that takes a question, autonomously searches the web, reads pages, reasons about what it found, and produces a grounded research report.
By the end of this project, you'll have:
- A working generative AI script that demonstrates single-shot LLM inference and its limitations.
- A working agentic AI system that implements the ReAct pattern with tool calling, web search, and page reading.
- A side-by-side comparison showing the same question answered by both approaches, demonstrating why agentic AI exists.
- Secret Mission: Add conversation memory so the agent can do follow-up research, refining its findings across multiple questions in a session.
Are there any prerequisites?
You need Python and Ollama installed on your machine before starting. You also need at least 16 GB of RAM to run the local AI model.
Set Up Your Environment
Before you can compare generative and agentic AI, you need the right model running locally and a clean project directory to build in.
In this step, you will pull a language model that supports tool calling, install the Python packages your agent will use, and create the file structure for the project.
In this step, get ready to:
- Pull the qwen2.5:7b model and verify it supports tool calling.
- Create a project directory and install Python dependencies.
- Create the project files and verify your setup works.
Pull the qwen2.5:7b model
Your research agent needs a model that can do more than generate text. It needs a model that can decide when to call external tools like web search. The qwen2.5:7b model supports native tool calling, which means Ollama can pass function definitions to it and receive structured tool-call responses back.
- In your IDE's integrated terminal, pull the model by running this command:
ollama pull qwen2.5:7b
What does this command do?
This downloads the qwen2.5:7b model weights to your machine. The download is approximately 4.7 GB, so it may take a few minutes depending on your internet speed.
- Once the download completes, verify that the model supports tool calling by running:
ollama show qwen2.5:7b
- Look for the Capabilities section in the output. You should see tools listed there.
What should I see?
The output displays model metadata in sections. Under the Capabilities section, you should see completion and tools listed. The tools entry confirms this model can receive function definitions and return structured tool-call requests.
Don't see tools in Capabilities?
Make sure Ollama is running in the background. If it is not started, open it from your applications before retrying.
If the Capabilities section is missing entirely or shows null, your Ollama version may be too old. Update Ollama to version 0.6.4 or newer, then re-run the ollama show command.
Help me troubleshoot why ollama show does not display tools in Capabilities for qwen2.5:7b
Create your project directory and install dependencies
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir research-agent
cd research-agent
- Install the Python packages your project needs by running:
pip install ollama==0.6.2 duckduckgo-search==8.1.1 httpx
What are these packages?
- ollama is the official Python SDK for interacting with Ollama models.
- duckduckgo-search provides free web search with no API key required.
- httpx is an HTTP client your agent will use to read web pages.
- You should see Successfully installed messages in the terminal output for each package.
Seeing permission errors or install failures?
If you see a permission error, try adding --user to the end of the pip command. If pip is not found, try pip3 instead.
Help me fix pip install errors for my research agent project
Create the project files and verify your setup
Your project will use three Python files, each with a distinct responsibility. You will fill these in over the next steps.
- Create the three project files by running this command:
touch generative.py tools.py agent.py
Windows users
If touch is not recognized, create the files manually in your IDE by right-clicking the research-agent folder in the sidebar and selecting New File for each: generative.py, tools.py, and agent.py.
- Confirm the files exist by running:
ls
- You should see agent.py, generative.py, and tools.py in the output.
Now verify that the ollama Python package can connect to your running Ollama instance and chat with the model.
- Run a quick test call by running this command:
python3 -c "from ollama import chat; r = chat(model='qwen2.5:7b', messages=[{'role': 'user', 'content': 'Say hello in one word'}]); print(r.message.content)"
- You should see a short response from the model printed in your terminal (something like "Hello" or "Hi").
Getting a connection error?
Make sure Ollama is running. Open a separate terminal and run ollama serve if the Ollama app is not already active in your system tray or menu bar.
If python3 is not found, try python instead. The exact command depends on your system's Python configuration.
Help me troubleshoot my Ollama Python connection
Your environment is ready. Next up, you will build the generative AI baseline and see exactly what a single-shot LLM call can and cannot do.
Build the Generative AI Baseline
Your environment is set up with the model pulled and packages installed. Now it's time to write your first AI script and see generative AI in action.
In this step, you'll build a simple Python script that sends a single question to the qwen2.5:7b model and prints whatever it responds with. This is generative AI at its most basic: one input, one output, no tools, no verification.
In this step, get ready to:
- Write a Python script that makes a single-shot call to a local LLM.
- Add timing to measure how long the model takes to respond.
- Run the script and observe the limitations of generative AI without tools.
Write the single-shot generative script
Generative AI works by taking a prompt, running it through the model once, and returning whatever the model generates. There is no second pass, no fact-checking, and no access to external information.
- In your IDE, open generative.py from the file explorer sidebar.
- Copy the following code into the file:
import time
from ollama import chat
QUESTION = "What are the top 3 AI agent frameworks in 2026 and how do they compare?"
def run_generative():
print("=" * 60)
print("GENERATIVE AI - Single Shot (No Tools, No Verification)")
print("=" * 60)
print(f"\nQuestion: {QUESTION}\n")
response = chat(
model='qwen2.5:7b',
messages=[{'role': 'user', 'content': QUESTION}]
)
print("Answer:")
print(response.message.content)
if __name__ == "__main__":
run_generative()
What does this code do?
QUESTION stores the prompt you'll send to the model. It asks about AI agent frameworks in 2026, a topic the model's training data likely doesn't cover accurately.
chat(model='qwen2.5:7b', messages=[...]) sends your question to the locally-running Ollama model as a single message. The response comes back in response.message.content as plain text.
The if __name__ == "__main__" guard ensures run_generative() only runs when you execute the file directly.
- Save generative.py.
Seeing a red underline on the import?
If your IDE shows an error on from ollama import chat, the IDE may not be using the Python environment where you installed the packages. Make sure your IDE's Python interpreter is set to the same one you used when running pip install in Step 1.
Need more help? Help me fix the import error for the ollama package in my IDE
Add timing and performance stats
Right now the script prints the answer, but you can't see how long the model took or understand the shape of the interaction. Adding timing makes the single-call pattern visible.
- In generative.py, find this line inside run_generative():
response = chat(
model='qwen2.5:7b',
messages=[{'role': 'user', 'content': QUESTION}]
)
- Add start = time.time() on a new line directly above it, so the line reads:
start = time.time()
response = chat(
model='qwen2.5:7b',
messages=[{'role': 'user', 'content': QUESTION}]
)
- Directly below the chat() call, add a line to capture the elapsed time:
elapsed = time.time() - start
- Now find the two print lines at the bottom of the function:
print("Answer:")
print(response.message.content)
- Add the following stats block directly below them:
print(f"\n--- Stats ---")
print(f"Time: {elapsed:.1f}s")
print(f"Single call, no tools, no verification.")
print(f"The model may hallucinate or provide outdated information.")
What do the stats reveal?
The timing captures the full round-trip: how long the model takes to generate its entire response from a single prompt. The stats summary reinforces what just happened architecturally: one input, one output, zero verification.
This is the simplest possible pattern for using an LLM. Later, you'll compare this directly against the agentic pattern that loops, searches, and verifies.
- Save generative.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved generative.py and move on to running it.
ⓧ I'd like to double check the full code
Here's the complete generative.py file. Make sure yours matches exactly:
import time
from ollama import chat
QUESTION = "What are the top 3 AI agent frameworks in 2026 and how do they compare?"
def run_generative():
print("=" * 60)
print("GENERATIVE AI - Single Shot (No Tools, No Verification)")
print("=" * 60)
print(f"\nQuestion: {QUESTION}\n")
start = time.time()
response = chat(
model='qwen2.5:7b',
messages=[{'role': 'user', 'content': QUESTION}]
)
elapsed = time.time() - start
print("Answer:")
print(response.message.content)
print(f"\n--- Stats ---")
print(f"Time: {elapsed:.1f}s")
print(f"Single call, no tools, no verification.")
print(f"The model may hallucinate or provide outdated information.")
if __name__ == "__main__":
run_generative()
Run the script and observe the limitations
Now you'll see generative AI answer your question in real time. Pay attention to what the model claims and whether it has any way to back those claims up.
- Run the script by entering this command in your terminal:
python generative.py
You should see the model print a confident-sounding answer about AI agent frameworks. It may name specific frameworks, describe features, and compare them as if it knows the current landscape.
What should I look for in the output?
Notice three things about the model's response:
- It answers confidently, even though it has no access to real-time data about 2026.
- It cannot verify any of its claims. There is no web search, no source citation, no fact-checking step.
- It may hallucinate frameworks, features, or comparisons that don't exist.
This is the fundamental limitation of generative AI. The model generates plausible text based on patterns in its training data. It has no mechanism to know what is actually true right now.
Seeing a connection error?
If you see an error mentioning a connection to localhost:11434, Ollama's server is not running. Open a separate terminal and run ollama serve to start it.
If the script runs but takes a very long time (over 2 minutes), the model may be running on CPU instead of GPU. This is normal for machines without a dedicated GPU. Let it finish.
Still stuck? Help me troubleshoot my generative.py script
You've now seen generative AI at its simplest. One question, one answer, no verification. Next, you'll build the tools that let an agent actually search the web and read pages to ground its answers in real data.
Build the Agent Tools
In the previous step, you saw generative AI in action. One call, one answer, no verification. The model confidently responded, but it had no way to check its own claims against real-world data.
Now you will build the tools that turn a basic LLM into something more powerful. These tools give the model the ability to search the web and read web pages. In the next step, you will wire these tools into a full agent loop.
In this step, get ready to:
- Write a web search tool using DuckDuckGo.
- Write a page reader tool using httpx.
- Test both tools independently before wiring them into the agent.
Write the web search tool
For an agent to answer questions about the real world, it needs access to current information. The first tool you will build lets the model search the web using DuckDuckGo.
The key to making a Python function callable as a tool by Ollama is writing it with proper type hints and Google-style docstrings. The Ollama SDK auto-parses these into tool calling schemas, so the model knows exactly what arguments to pass.
- Switch back to tools.py in your editor and add the following code:
from duckduckgo_search import DDGS
import httpx
def web_search(query: str, max_results: int = 5) -> str:
"""Search the web for current information on a topic.
Args:
query: The search query to look up
max_results: Maximum number of results to return
Returns:
A formatted string of search results with titles, URLs, and snippets
"""
results = DDGS().text(query, max_results=max_results)
if not results:
return "No results found."
output = []
for r in results:
output.append(f"Title: {r.get('title', 'N/A')}")
output.append(f"URL: {r.get('href', 'N/A')}")
output.append(f"Snippet: {r.get('body', 'N/A')}")
output.append("---")
return "\n".join(output)
What does this code do?
The imports at the top pull in the two libraries you will use for both tools in this file.
- DDGS().text() calls the DuckDuckGo search API and returns a list of dictionaries. Each dictionary contains a title, a URL (href), and a body snippet.
- The function returns a single formatted string rather than raw data. This is intentional. The model receives text, not Python objects.
- The Google-style docstring (Args: and Returns: sections) is what Ollama reads to build the tool schema automatically.
- Save tools.py.
- You will not see a change yet. The next chunk adds the second tool, and then you will test both together.
Seeing a red underline on the import?
If your editor highlights from duckduckgo_search import DDGS as an error, confirm the package is installed by running pip install duckduckgo-search==8.1.1 in your terminal again. Make sure you are using the same Python environment where you installed the packages in Step 1.
Help me fix an import error in tools.py for duckduckgo_search.
Add the page reader tool
Searching the web returns titles and snippets, but sometimes the agent needs to read an entire page to find detailed information. This second tool fetches a URL and extracts readable text from the HTML.
- In tools.py, add the following function below web_search:
def read_page(url: str) -> str:
"""Read the text content of a web page.
Args:
url: The URL of the page to read
Returns:
The text content of the page, truncated to 3000 characters
"""
try:
response = httpx.get(url, follow_redirects=True, timeout=10.0)
response.raise_for_status()
text = response.text
# Simple HTML tag removal
import re
text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
text = re.sub(r'<[^>]+>', ' ', text)
text = re.sub(r'\s+', ' ', text).strip()
return text[:3000]
except Exception as e:
return f"Error reading page: {str(e)}"
What does this code do?
- httpx.get() fetches the URL with redirect-following enabled and a 10-second timeout to avoid hanging on slow pages.
- The regex patterns strip <script> and <style> blocks first (they add noise), then remove all remaining HTML tags.
- The result is truncated to 3000 characters. This keeps the tool response small enough for the model's context window without losing the most important content at the top of the page.
- The try/except block ensures the agent gets a clean error message instead of a crash if a URL is unreachable.
- Save tools.py.
- You will not see a change yet. In the next substep, you will test both functions from the terminal.
Getting a syntax error on the regex lines?
Make sure you are using raw strings (r'...') for regex patterns. Without the r prefix, Python interprets backslashes as escape characters rather than regex symbols.
Help me fix a regex syntax error in my read_page function.
✔️ Awesome, I've got everything!
Great. Double check you have saved tools.py with both functions before moving on.
ⓧ I'd like to double check the full code
from duckduckgo_search import DDGS
import httpx
def web_search(query: str, max_results: int = 5) -> str:
"""Search the web for current information on a topic.
Args:
query: The search query to look up
max_results: Maximum number of results to return
Returns:
A formatted string of search results with titles, URLs, and snippets
"""
results = DDGS().text(query, max_results=max_results)
if not results:
return "No results found."
output = []
for r in results:
output.append(f"Title: {r.get('title', 'N/A')}")
output.append(f"URL: {r.get('href', 'N/A')}")
output.append(f"Snippet: {r.get('body', 'N/A')}")
output.append("---")
return "\n".join(output)
def read_page(url: str) -> str:
"""Read the text content of a web page.
Args:
url: The URL of the page to read
Returns:
The text content of the page, truncated to 3000 characters
"""
try:
response = httpx.get(url, follow_redirects=True, timeout=10.0)
response.raise_for_status()
text = response.text
# Simple HTML tag removal
import re
text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
text = re.sub(r'<[^>]+>', ' ', text)
text = re.sub(r'\s+', ' ', text).strip()
return text[:3000]
except Exception as e:
return f"Error reading page: {str(e)}"
Test both tools independently
Before wiring these tools into the agent, you should confirm each one works on its own. This saves you from debugging the agent loop and the tools at the same time.
- Test the web_search function by running this command in your terminal:
python -c "from tools import web_search; print(web_search('Python AI frameworks 2026', max_results=2))"
You should see output with titles, URLs, and snippets from DuckDuckGo search results. The exact results will vary depending on what is currently indexed.
- Test the read_page function by running this command:
python -c "from tools import read_page; print(read_page('https://example.com'))"
You should see plain text content from the example.com page. The HTML tags are stripped, leaving only readable text.
Why test before integrating?
When you build an agent, bugs can hide in the loop logic, the tool functions, or the message formatting. Testing each tool in isolation means you know the tools work correctly. If something breaks later in the agent loop, you can rule out the tools as the source of the problem.
Getting a network error or empty results?
The web_search function requires an internet connection. Check that you are connected to the internet and try a different search query if you get empty results.
If you see an import error, make sure you are running the command from inside your project directory (the folder containing tools.py).
Help me troubleshoot my tools.py test commands.
Both tools are working and returning real data from the internet. Next up, you will build the agent itself, wiring these tools into a ReAct loop that lets the model reason, search, read, and iterate its way to a grounded answer.
Build the Research Agent with the ReAct Loop
Your tools are built and tested. Now it's time to build the brain that decides when and how to use them.
In this step, you'll implement the ReAct pattern from scratch. ReAct stands for Reason, Act, Observe. Instead of generating one answer and hoping it's correct, the agent thinks about what it needs, takes an action, observes the result, and repeats until it has enough grounded information to respond.
In this step, get ready to:
- Write the system prompt that teaches the model the ReAct workflow.
- Implement the agent loop with tool calling and iteration control.
- Run the agent and compare its output against the generative baseline.
Set up the agent's system prompt and tool registry
The system prompt is what transforms a generic LLM into a research agent. It teaches the model to follow the ReAct loop instead of guessing an answer in one shot.
- Open agent.py in your editor and add the following code:
import time
from ollama import chat
from tools import web_search, read_page
QUESTION = "What are the top 3 AI agent frameworks in 2026 and how do they compare?"
MAX_ITERATIONS = 10
SYSTEM_PROMPT = """You are a research agent. Your job is to answer questions accurately using real, current information.
You have access to tools:
- web_search: Search the web for current information
- read_page: Read the full content of a specific URL
Your process (ReAct pattern):
1. REASON: Think about what information you need
2. ACT: Use a tool to gather that information
3. OBSERVE: Look at what the tool returned
4. REPEAT: If you need more information, go back to step 1
When you have enough information to answer the question comprehensively and accurately, provide your final answer as a well-structured research report. Do NOT guess or make up information. Only state facts you found through your tools."""
AVAILABLE_TOOLS = {
'web_search': web_search,
'read_page': read_page,
}
What does this code do?
- from tools import web_search, read_page imports the two tool functions you built in the previous step.
- QUESTION uses the same question as generative.py so you can compare outputs directly.
- MAX_ITERATIONS = 10 prevents the agent from looping forever if it can't find a satisfying answer.
- SYSTEM_PROMPT is the instruction set that teaches the model to follow the ReAct pattern: reason about what it needs, act with a tool, observe the result, and repeat.
- AVAILABLE_TOOLS maps tool names (strings) to the actual Python functions so the agent loop can look up and call them by name.
- Save agent.py.
You won't see a change yet. The next chunk adds the function that uses all of this configuration.
Seeing a red underline on the imports?
If your editor flags from tools import web_search, read_page as an error, make sure tools.py is saved in the same folder as agent.py. Both files must be in the same directory for the import to resolve.
help me fix a ModuleNotFoundError when importing from tools.py
Implement the agent loop
The agent loop is where generative AI becomes agentic AI. Instead of one call and one answer, the loop calls the model, checks if it wants to use a tool, executes that tool, feeds the result back, and repeats.
What is tool calling?
Tool calling is how an LLM requests to use an external function. When you pass tools=[web_search, read_page] to Ollama, the SDK reads the function docstrings and automatically generates a tool schema the model can understand.
The model doesn't execute the tool itself. It returns a structured request saying "I want to call web_search with this query." Your code handles the actual execution and feeds the result back.
- Add the run_agent() function below your AVAILABLE_TOOLS dictionary by copying the following code:
def run_agent():
print("=" * 60)
print("AGENTIC AI - ReAct Pattern (Tools + Reasoning Loop)")
print("=" * 60)
print(f"\nQuestion: {QUESTION}\n")
start = time.time()
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': QUESTION},
]
tools = [web_search, read_page]
for iteration in range(1, MAX_ITERATIONS + 1):
print(f"\n--- Iteration {iteration} ---")
response = chat(
model='qwen2.5:7b',
messages=messages,
tools=tools,
)
# If no tool calls, the agent is done
if not response.message.tool_calls:
print("\nAgent finished reasoning. Final answer:")
print(response.message.content)
break
# Process tool calls
messages.append(response.message)
for tool_call in response.message.tool_calls:
func_name = tool_call.function.name
func_args = tool_call.function.arguments
print(f" Tool: {func_name}({func_args})")
if func_name in AVAILABLE_TOOLS:
result = AVAILABLE_TOOLS[func_name](**func_args)
print(f" Result: {result[:200]}...")
else:
result = f"Error: Unknown tool '{func_name}'"
print(f" {result}")
messages.append({
'role': 'tool',
'content': str(result),
'tool_name': func_name,
})
else:
print(f"\nReached max iterations ({MAX_ITERATIONS}). Requesting final answer...")
messages.append({
'role': 'user',
'content': 'Please provide your final answer based on what you have found so far.'
})
response = chat(model='qwen2.5:7b', messages=messages)
print(response.message.content)
elapsed = time.time() - start
print(f"\n--- Stats ---")
print(f"Time: {elapsed:.1f}s")
print(f"Iterations: {iteration}")
print(f"The agent searched, read, reasoned, and produced a grounded answer.")
if __name__ == "__main__":
run_agent()
What does this code do?
- messages starts with the system prompt and the user's question. This list grows as the agent acts and observes.
- chat(model='qwen2.5:7b', messages=messages, tools=tools) sends the full conversation to the model with the tool schemas attached.
- if not response.message.tool_calls checks whether the model wants to use a tool. If it doesn't, the agent is done reasoning and the loop breaks.
- The inner for tool_call loop executes each tool the model requested, then appends the result with role='tool' so the model can see what the tool returned on the next iteration.
- The else clause on the for loop triggers only if the loop completes all 10 iterations without breaking. It forces the model to give a final answer with whatever it has found so far.
Why this approach instead of LangChain?
Frameworks like LangChain abstract the ReAct loop behind helper classes. Building it from scratch teaches you the transferable pattern: message threading, tool dispatch, and loop control. These fundamentals work the same way whether you later use cloud APIs, different models, or production frameworks.
- Save agent.py.
You won't see a change yet. You'll run the agent in the next substep to see the ReAct loop in action.
✔️ Awesome, I've got everything!
Great. Double check you've saved agent.py before moving on.
ⓧ I'd like to double check the full code
import time
from ollama import chat
from tools import web_search, read_page
QUESTION = "What are the top 3 AI agent frameworks in 2026 and how do they compare?"
MAX_ITERATIONS = 10
SYSTEM_PROMPT = """You are a research agent. Your job is to answer questions accurately using real, current information.
You have access to tools:
- web_search: Search the web for current information
- read_page: Read the full content of a specific URL
Your process (ReAct pattern):
1. REASON: Think about what information you need
2. ACT: Use a tool to gather that information
3. OBSERVE: Look at what the tool returned
4. REPEAT: If you need more information, go back to step 1
When you have enough information to answer the question comprehensively and accurately, provide your final answer as a well-structured research report. Do NOT guess or make up information. Only state facts you found through your tools."""
AVAILABLE_TOOLS = {
'web_search': web_search,
'read_page': read_page,
}
def run_agent():
print("=" * 60)
print("AGENTIC AI - ReAct Pattern (Tools + Reasoning Loop)")
print("=" * 60)
print(f"\nQuestion: {QUESTION}\n")
start = time.time()
messages = [
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': QUESTION},
]
tools = [web_search, read_page]
for iteration in range(1, MAX_ITERATIONS + 1):
print(f"\n--- Iteration {iteration} ---")
response = chat(
model='qwen2.5:7b',
messages=messages,
tools=tools,
)
# If no tool calls, the agent is done
if not response.message.tool_calls:
print("\nAgent finished reasoning. Final answer:")
print(response.message.content)
break
# Process tool calls
messages.append(response.message)
for tool_call in response.message.tool_calls:
func_name = tool_call.function.name
func_args = tool_call.function.arguments
print(f" Tool: {func_name}({func_args})")
if func_name in AVAILABLE_TOOLS:
result = AVAILABLE_TOOLS[func_name](**func_args)
print(f" Result: {result[:200]}...")
else:
result = f"Error: Unknown tool '{func_name}'"
print(f" {result}")
messages.append({
'role': 'tool',
'content': str(result),
'tool_name': func_name,
})
else:
print(f"\nReached max iterations ({MAX_ITERATIONS}). Requesting final answer...")
messages.append({
'role': 'user',
'content': 'Please provide your final answer based on what you have found so far.'
})
response = chat(model='qwen2.5:7b', messages=messages)
print(response.message.content)
elapsed = time.time() - start
print(f"\n--- Stats ---")
print(f"Time: {elapsed:.1f}s")
print(f"Iterations: {iteration}")
print(f"The agent searched, read, reasoned, and produced a grounded answer.")
if __name__ == "__main__":
run_agent()
Run the agent and compare with the generative baseline
Time to see the difference between generative AI and agentic AI in action. You'll run the agent with the same question you asked in generative.py and watch it search, read, reason, and iterate before answering.
- Run the research agent by running this command in your terminal:
python agent.py
You should see iteration logs printing in your terminal. Each iteration shows which tool the agent chose to call and a preview of the result it received back.
What's happening during the run?
The agent is cycling through the ReAct loop. In each iteration, it reasons about what information it still needs, calls web_search or read_page, observes the result, and decides whether it has enough to answer.
This will take longer than generative.py because the agent makes multiple model calls and network requests. That's the tradeoff: more time in exchange for grounded, verified information.
Agent not producing output or erroring out?
- If you see a connection error, make sure Ollama is still running. Open a new terminal tab and run ollama list to confirm it's active.
- If you see a ModuleNotFoundError for tools, make sure you're running the command from the same directory that contains tools.py and agent.py.
- If the agent loops without making tool calls and produces an empty answer, double check that the SYSTEM_PROMPT string was pasted correctly with no missing lines.
Help me debug why my research agent isn't working
- Once the agent finishes, run the generative script again to see the difference side by side:
python generative.py
Compare both outputs. The generative script produced its answer from memory in a few seconds. The agent took longer, but its answer cites real sources it found on the web.
What is the architectural difference?
Generative AI produces content from its training data in a single pass. It cannot verify, update, or ground its claims. Agentic AI plans, acts, observes, and iterates toward a goal. It uses tools to interact with the real world.
The same model powers both scripts. The difference is entirely in the architecture you built around it: the system prompt, the tool registry, and the loop.
You've built a fully functional research agent from scratch. It reasons, searches the web, reads pages, and produces grounded answers. Next up, you'll extend it with conversation memory so it can handle follow-up questions.
Secret mission
Add Conversation Memory
Your agent answers one question and forgets everything. What if it could remember prior research and build on it across multiple questions? Add conversation memory so your agent becomes a true research assistant that refines its findings over a session.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources for future projects or remove them to free up disk space.
Resources you used:
- qwen2.5:7b model (approximately 4.7 GB of disk space).
- Project files (generative.py, tools.py, agent.py).
- Python packages (ollama, duckduckgo-search, httpx).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with Ollama or building AI agents.
- The qwen2.5:7b model stays on your machine at no cost. It takes up approximately 4.7 GB of disk space.
- Your project files remain on your Desktop, ready to run anytime.
- The Python packages stay available in your environment for future scripts.
Pause - I'll come back to this later
No running processes to stop for this project. Everything is file-based and stays on disk until you decide to remove it.
- Your model, project files, and packages remain installed and ready for when you return.
- If you want to free the largest chunk of space while keeping your code, remove only the model using the Delete tab instructions and re-pull it later.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Remove the Ollama model by running:
ollama rm qwen2.5:7b
- Uninstall the Python packages by running:
pip uninstall ollama duckduckgo-search httpx
Confirm each uninstall when prompted.
- Delete your project folder (the one containing generative.py, tools.py, and agent.py) from your Desktop.
macOS
- In your terminal, navigate to your Desktop and remove the project folder by running:
cd ~/Desktop
ls
Find your project folder name in the output, then run:
rm -rf YOUR_PROJECT_FOLDER_NAME
Windows
- In PowerShell, navigate to your Desktop and remove the project folder by running:
cd ~\Desktop
ls
Find your project folder name in the output, then run:
Remove-Item -Recurse -Force YOUR_PROJECT_FOLDER_NAME
That's a Wrap!
That's a Wrap!
Nice work! You just built a research agent from scratch that autonomously searches the web, reads pages, and reasons its way to a grounded answer.
You've learned how to:
- Build a generative AI system with single-shot inference and observe its limitations firsthand, including hallucination and lack of access to current information.
- Build an agentic AI system from scratch using the ReAct pattern (Reason, Act, Observe) with tool calling, web search, and page reading.
- Experience the architectural difference: generative AI generates content in a single pass, while agentic AI plans, acts, observes, and iterates toward a goal.
- Secret Mission: Added conversation memory so the agent can conduct multi-turn research sessions with context preservation.
Ready to quiz yourself?