Capstone: research agent
A marketing research agent with retrieval, tools and evals.
Create a free account Try part of it here
Why it matters
This is all of Phase 1 in one program. The marketing team asks questions that need both your documents and your data. "Which active campaign has the best return on ad spend?" "Can we call the new fleece eco-friendly?" A useful assistant has to look things up rather than guess. It has to say where each fact came from, stay inside its limits, and prove it works on more than the one question you tried. You've already built every piece. The work now is putting them together and measuring the result.
What you'll be able to do
- Combine retrieval, tools and an agent loop into one assistant
- Make every claim traceable to a source the agent actually looked at
- Grade the agent automatically with code checks on a small eval set
- Decide what to improve next from the eval's failures
What it covers
- What you're building
- The tools on their own
- Making citations checkable
- Grade one answer, all three checks
- After the exercise: what to improve
- Read the failures before you change anything
- Judge on many questions
What you're building
Think of a good research assistant. They check the files before answering, they note where each fact came from, and they tell you when they couldn't find something.
You'll build one for the shop's marketing team. It has four tools over the practice API:
| Tool | What it returns | From lesson |
|---|---|---|
search_docs(query) | The 3 best-matching help-center and brand-guide articles (id, title, score) | Similarity search, RAG |
get_doc(doc_id) | One article's full text | RAG |
list_campaigns(status) | Campaign ids and names | Tool use |
get_campaign(campaign_id) | Spend, revenue and return on ad spend (ROAS) for one campaign | Tool use |
Around the tools:
- An agent loop with a turn limit, because different questions need different numbers of steps (the agent loop lesson).
- A grounded system prompt. Use the tools, never answer from memory, and cite every fact with the id it came from, such as
[doc_voice]or[cmp_104]. Say so when the tools don't have the answer (the RAG and hallucination lessons). - Source tracking. Your code records every id that appears in a tool result. Then a citation to anything else is provably invented (the making RAG good lesson).
- An eval. Test questions, each with the source the answer must cite and a fact it must contain, graded by code (the evals lesson).
question ─► agent loop ─► model ─► tool_use ─► your functions ─► practice API
▲ │
└──────── tool_result ◄──────────────┘ (ids recorded in "seen")
answer with [citations] ─► grade(): citations ⊆ seen? required source? required fact?
Each part comes from a lesson in this phase. Here they finally join up.
The tools on their own
Before wiring the tools to a model, check that each one returns what you expect. These are the exact functions the agent will call.
import requests API = "https://api.gradient.test/v1" def search_docs(query): return requests.get(f"{API}/docs", params={"q": query, "limit": 3}).json()["data"] def get_campaign(campaign_id): response = requests.get(f"{API}/campaigns/{campaign_id}") response.raise_for_status() c = response.json() roas = round(c["revenue"] / c["spend"], 2) if c["spend"] else None return {"id": c["id"], "name": c["name"], "spend": c["spend"], "revenue": c["revenue"], "roas": roas} print(search_docs("can we say eco-friendly in ads")) print(get_campaign("cmp_104"))
The tools return small, focused results: ids, titles and the numbers the agent needs, with ROAS already worked out. Doing the arithmetic in code, instead of asking the model, is cheaper and removes a source of mistakes.
Show the answer
Code does arithmetic exactly and for free; models can slip on numbers
Anything code can do exactly, such as arithmetic, is cheaper and more reliable in code. Let the model do the parts that need judgment.
Making citations checkable
The system prompt asks for citations in one fixed format, so code can find them:
Cite every fact with the id it came from in square brackets, like [doc_returns] or [cmp_104].
While the agent runs, your code collects every id that appears in any tool result. When it finishes, three checks grade the answer:
- The citations are valid. There's at least one, and every cited id is in the set the agent actually saw. A citation to a document it never saw is invented. Recording every id the agent looked at is what makes this check possible: each citation is checked against that set.
- The right source is cited. Each test case names the document or campaign a correct answer must rely on.
- The key fact is there. Each test case names a phrase a correct answer must contain, such as "recycled polyester" or "cmp_104".
import re answer = "Say 'made with recycled polyester' instead of eco-friendly [doc_voice]. Our best campaign is cmp_104 [cmp_104] [doc_ads]." seen = {"doc_voice", "doc_sustainability", "cmp_104"} cited = re.findall(r"\[((?:doc|cmp)_[a-z0-9_]+)\]", answer) print("cited:", cited) print("invented:", [c for c in cited if c not in seen])
It prints cited: ['doc_voice', 'cmp_104', 'doc_ads'], then invented: ['doc_ads']. The agent never saw doc_ads, so that citation is made up. The pattern finds anything in square brackets that starts with doc_ or cmp_.
These checks don't prove an answer is perfect. They catch invented sources, missing evidence and missing facts, cheaply, on every run. For tone or completeness, you'd add an LLM judge.
doc = "Returns are free for 30 days." cited = "free for 30 days" print(doc.find(cited) >= 0)
Show the answer
True
find returns the position where the text starts, which is 0 or more when it's there, and -1 when it isn't. "free for 30 days" is in doc, so the test gives True. A citation is checkable if code can find that exact text in the source.
Grade one answer, all three checks
Here's one answer graded the way your eval will grade every answer. The test case says a correct answer must cite doc_voice and must mention "recycled polyester".
import re answer = "Say 'made with recycled polyester' instead of eco-friendly [doc_voice]." seen = {"doc_voice", "doc_sustainability"} cited = re.findall(r"\[((?:doc|cmp)_[a-z0-9_]+)\]", answer) checks = { "valid citations": bool(cited) and all(c in seen for c in cited), "right source": "doc_voice" in cited, "key fact": "recycled polyester" in answer, } print(checks) print(all(checks.values()))
It prints {'valid citations': True, 'right source': True, 'key fact': True}, then True: this answer passes. all(...) is True only when every check passes, so one failure fails the whole case. Change the citation to [doc_ads] and the first two checks turn False.
Show the answer
The citation is invented: the agent never saw that document
Tracking which ids the agent actually saw turns a made-up source into a bug you can detect.
After the exercise: what to improve
When your eval reports failures, read them before changing anything. Each kind of failure points somewhere different:
- The right source never appears in "seen". Retrieval or the choice of tool failed. Improve the tool descriptions or the search query, or add hybrid search.
- The source was seen but not cited. Tighten the citation instruction, or add an example.
- The fact is missing. The answer may be right but worded differently. Decide whether the check or the answer is wrong. For open-ended facts, use an LLM judge.
- The turn limit was reached. The agent is going in circles. Look at its steps. Often a tool returns too little information, or an error it can't recover from.
Then extend it with the rest of Phase 1. Cache the system prompt and tools. Log every run with a trace of its tool calls. Add an approval step before any tool that changes data. And put it behind a small backend with rate limits. With a real API key, run the eval after every change to the prompt or the model.
For marketers: you now know the design of most AI products you'll be pitched: a model, a few tools over company data, a loop, citations and an eval. Ask vendors how their version handles each part, especially the eval.
Show the answer
The citation instruction in the system prompt: tighten it or add an example
The agent found the document, so retrieval worked. It just didn't cite it, which is about how the prompt asks for citations.
Read the failures before you change anything
When an eval case fails, read why before touching the prompt. The cause may be retrieval, or a tool, and a prompt change won't fix those. And let code do the arithmetic.
Judge on many questions
One hand-picked question proves very little.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Why does the agent record every id it sees in tool results?