Index Ai Academy
Phase 1 · Building with LLMs · Quality & production

Capstone: research agent

A marketing research agent with retrieval, tools and evals.

About 45 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. What you're building
  2. The tools on their own
  3. Making citations checkable
  4. Grade one answer, all three checks
  5. After the exercise: what to improve
  6. Read the failures before you change anything
  7. 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:

ToolWhat it returnsFrom 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 textRAG
list_campaigns(status)Campaign ids and namesTool use
get_campaign(campaign_id)Spend, revenue and return on ad spend (ROAS) for one campaignTool 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?
Try it now

Each part comes from a lesson in this phase. Here they finally join up.

Worked example

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"))
It runs right here in your browser, with nothing to install.

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.

Quick check
Why does get_campaign work out ROAS in code instead of letting the model divide revenue by spend?
  1. ROAS is a secret
  2. Code does arithmetic exactly and for free; models can slip on numbers
  3. Models can't see numbers
  4. The API doesn't allow division
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:

  1. 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.
  2. The right source is cited. Each test case names the document or campaign a correct answer must rely on.
  3. 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.

Quick check
What does this print?
doc = "Returns are free for 30 days."
cited = "free for 30 days"
print(doc.find(cited) >= 0)
  1. -1
  2. True
  3. False
  4. 12
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.

Worked example

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()))
In the lesson you can change this and run it.

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.

Quick check
The agent's answer cites [doc_pricing], but no tool result during the run contained doc_pricing. What does that tell you?
  1. The agent needs more turns
  2. The document exists but loaded slowly
  3. The citation is invented: the agent never saw that document
  4. The citation format is wrong
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.

Quick check
Your eval shows the right document is in "seen" but the answer doesn't cite it. Where should you look first?
  1. The turn limit
  2. The citation instruction in the system prompt: tighten it or add an example
  3. The practice API
  4. The search tool: retrieval failed
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

Create a free account

Try a quiz question

Why does the agent record every id it sees in tool results?

  1. So citations can be checked against what it actually looked at
  2. To make the model faster, since it can skip tools it has already called
  3. Because the API requires each tool result to carry an id the agent has seen
  4. To reduce the number of tools, by dropping those whose ids never come back

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson