Why I built this
I'm a CS graduate preparing for a Data Science/AI master's application, and I wanted
to go beyond the usual coursework projects — Kaggle competitions, Coursera
certificates — and actually build something that shows I understand how modern AI
systems work under the hood, not just how to call an API.
So I built three small agents, each demonstrating a different core pattern in how
LLMs interact with the world: web search, retrieval over your own data, and live
code execution. All three run entirely on Google Gemini's free tier — total cost: $0.
Agent 1: Research Assistant (tool-use)
The simplest and most foundational pattern: give the model a tool and let it decide,
on its own, when it actually needs to use it. I used Gemini 2.5 Flash as the LLM and
DuckDuckGo search (via the free ddgs Python package, no API key needed) as the tool.
The agent doesn't search for everything — it only calls the web_search tool when
it judges that it needs current or specific information it isn't confident about,
and answers directly from its own knowledge otherwise. This matters because a naive
"always search" agent wastes calls and can actually produce worse answers by
grounding itself in irrelevant search results for questions it already knew the
answer to. Getting this decision right — teaching the model when not to use a
tool — turned out to be as important as the tool integration itself.
Agent 2: RAG Q&A (retrieval-augmented generation)
The difference between an agent that "knows things" from training and one that
looks them up in real, specific source material. This agent ingests your own
documents (notes, articles, anything in .txt/.md format), splits them into
overlapping chunks, and embeds each chunk into a vector using Gemini's embedding
model. Those vectors get stored locally in ChromaDB — no hosted database, runs
entirely on your own machine.
When you ask a question, it's embedded the same way, and the database finds the
chunks whose meaning is closest to your question. Only those chunks are handed to
Gemini as context, and it's instructed to answer using only that context, citing
which file it came from. This grounding is what reduces hallucination — the model
isn't guessing from general training data, it's answering from your actual source
material, and it's honest when the answer isn't in there.
Agent 3: Data Analysis Agent (code execution)
Instead of trusting the model's own arithmetic (LLMs are notoriously unreliable at
exact calculations), this agent writes real pandas code and executes it against a
CSV you give it, then reports the actual computed result — not a guess. You ask a
question in plain English ("what's the average fare by passenger class?"), the
model translates that into pandas code, the code actually runs, and you get a
verifiable answer rather than a black-box one. This is the same underlying idea as
tools like ChatGPT's Code Interpreter, just built from scratch to understand how it
actually works.
What tied all three together
All three agents share the same underlying pattern: the model decides when to call
a tool, uses it, then reasons over the result. The main thing that surprised me was
how much of the real engineering effort wasn't the "AI" part — it was the boring
plumbing: handling free-tier rate limits gracefully, chunking documents sensibly,
deciding what to exclude from version control. The actual "make an LLM call a tool"
logic was often the easiest part; making it work reliably and cheaply was where the
real learning happened.
Try it yourself
All three repos are public on GitHub, along with an overview repo tying them together:
If you're building something similar on a student budget: Gemini's free tier is
genuinely usable for small projects like this — you don't need to spend anything to
learn how agentic systems actually work.
Top comments (7)
The line that stood out: "teaching the model when not to use a tool turned out to be as important as the tool integration." That's the lesson most tutorials never reach, and you got there on agent #1. An always-search agent doesn't just waste calls — it actively poisons answers it already knew by grounding them in mediocre retrieval, which is a subtle failure because it looks like the tool is "working."
One thing that'll bite once these leave your laptop: the code-execution agent is the one to sandbox hardest. Writing real pandas is the right call over trusting LLM arithmetic, but "execute model-written code" is also the widest blast radius in the whole stack — resource limits, no network, ephemeral filesystem, the works. Worth building that boundary in from the start rather than retrofitting. For the RAG agent, the next thing to measure is chunk retrieval quality itself (recall@k on a few hand-labeled questions) — grounding only helps if the right chunk actually comes back. Genuinely solid set of projects to reason about the patterns from; this is the right way to learn the stack.
Really appreciate this — especially the sandboxing point. You're right that "runs
locally on my own machine" is doing a lot of load-bearing work right now that
wouldn't hold if this ever left my laptop. I'll add it as a documented limitation
for now and look into a proper sandboxed execution environment (resource limits,
no network, ephemeral fs) as a next iteration.
The recall@k suggestion is great too — I've been evaluating the RAG agent by
eyeballing answer quality, which I now realize doesn't actually tell me whether
retrieval itself is working. Going to put together a small hand-labeled test set
this week.
Thanks for actually engaging with the technical details instead of just the surface
level — this is exactly the kind of feedback that's hard to get building solo.
The "$0" framing is the honest one, and it's the part most agent tutorials skip. Free-tier APIs get you to "it works in the demo" fast, but the moment you care about cost you're back to a bill — just a smaller one.
The thing that actually changes the math is running the model on your own hardware. I keep a Qwen 3.8 27B on a 2x3090 box and it handles the day-to-day agent work — the tool calls, the RAG answers, the code execution — without a per-token charge at all. The catch nobody warns you about: a local model's real cost is power, not tokens, and if you don't measure it you can't optimize it. I track what each model actually burns in energy per run (the Costs tab in homelab-monitor prices every run against your real tariff). That's what turned "I'm saving on API fees" into a number I can act on — which agent work is worth running local and which is cheaper to just pay for.
The "teach it when not to call the tool" part is the one I'd have underestimated too, and it's worth saying that on a free tier it's a quota decision as much as an answer-quality one.
One thing worth knowing if anyone reproduces this: Google no longer publishes the free-tier RPD/RPM numbers for Gemini - they sit behind a login now. When I swept the free tiers on 2026-08-31 it was the one provider where I couldn't record a rate limit at all, so you can't budget an agent's tool calls against a published figure the way you can with, say, OpenRouter's 20 req/min.
Doesn't change anything about your build. It does mean "runs entirely on the free tier" is currently unverifiable from the outside for Gemini specifically, which is a slightly awkward property for a portfolio project someone else might try to re-run.
The three-pattern split is a good map of what agents actually do: reach out (tools), reach in (retrieval), and act (execution). The free-tier forcing function probably taught more than a paid one would - rate limits make you think about what each call is for. The pattern that pays off longest is usually the boring middle one, retrieval over your own data; it's where the under-the-hood understanding you were after actually lives.
Solid breakdown. The tool-use vs code-execution tradeoff is real — code exec is more capable but much harder to sandbox safely, which is why so many zero-budget setups still leak secrets or run unbounded. Curious how you handled sandboxing at $0.
The code-execution split makes sense. I’d log the tool call, input, and output separately too, since replaying a failed run gets painful once an agent makes several calls in a row.