Tool-Using Research Agent
An agent that searches, reads, and produces a cited summary — stopping when it has enough.
The problem
The gap between "call an LLM" and "build an agent" is the loop, and the loop is where every hard problem lives: deciding what to do next, handling a tool that fails, and knowing when to stop. This is the smallest project that forces you to solve all three.
Who uses it: Producing a briefing on a topic with sources, where the value is in the citations rather than the prose.
Before you start
- Comfortable with Python
- Understand function calling / tool schemas
Stack
Architecture
- 01A loop: the model chooses a tool, the tool runs, the result is appended as an observation, repeat
- 02Two tools only — search(query) and fetch(url) — because a small tool surface is where correct tool use is learned
- 03A scratchpad the agent writes findings into, separate from the conversation, so context does not grow without bound
- 04A stopping condition the model can invoke explicitly: a finish(summary, sources) tool
- 05Hard caps on iterations and total tokens, enforced outside the model
Build it, in order
01Define two tools with precise schemas
The description is the model's entire basis for choosing. "Searches the web" produces poor calls; "Search the web for current information; returns 5 results with title, URL and snippet" produces good ones.
02Build the loop
Call the model, check for a tool call, execute it, append the result as an observation, repeat. Fifty lines. Do this before reaching for a framework so you know what the framework is doing.
03Make failure an observation
When a fetch returns 403, feed that back as an observation rather than raising. An agent that sees the failure adapts; one that gets an exception dies or retries identically forever.
04Add the scratchpad
Fetched pages are long. Summarise each into the scratchpad and drop the raw text from the context. Without this, context grows every iteration until the run becomes unaffordable and the model loses the earlier findings anyway.
05Add explicit stopping
Give the model a finish tool it must call to end. A loop with no completion signal runs until the iteration cap, which is not the same as being done.
How to know it works
- Ten research questions with known good sources
- Measure: did it find at least one authoritative source, are the citations real and reachable, did it stop on its own
- Track iterations and token spend per completed task — cost per task is the number that matters, not cost per call
- Check citations resolve. An agent that invents a plausible URL fails silently
What breaks on real input
- The same search repeated because nothing records what was already tried — keep a set of executed queries
- A model inventing a tool that does not exist; validate every call against the schema before executing
- Context growth making later iterations both expensive and worse
- Never stopping, because "enough information" was never defined
Deployment
- Run as a job, not a request — research takes minutes
- Log every step with its inputs and outputs; an agent you cannot replay is one you cannot debug
- Set a per-run spend ceiling and fail closed when it is hit
Repository structure
agent/loop.py — the decide-act-observe cycle agent/tools/ — search and fetch, each with its schema agent/scratchpad.py — accumulated findings eval/questions.yaml — the ten questions and expected sources README.md
A layout to create. There is no repository to clone — building it is the point.
Explaining Tool-Using Research Agent in an interview
Lead with the stopping condition and the iteration cap, because that is what separates someone who has run an agent from someone who has read about one. Explain the scratchpad as a context-management decision: fetched pages are long, context is quadratic in cost and attention, so summarise into a store and drop the raw text.
Resume lines
- Built a tool-using research agent with schema-validated tool calls, bounded iterations and explicit completion
- Reduced per-task token cost by summarising retrieved pages into a scratchpad instead of accumulating raw context
Take it further
- Add a critic step that checks the summary against the sources before finishing
- Parallelise independent searches and measure the latency change
- Add human-in-the-loop approval before any fetch of a domain not on an allowlist