Bouw een research-agent met Web Search + URL Extract (Claude tool use, van begin tot eind)

De meeste tutorials over een 'AI-research-agent' stoppen bij 'hier is een tooldefinitie'. Deze levert een werkende agent op: vraag erin, antwoord met bronnen eruit. Zoeken, extraheren, redeneren, citeren — alles in minder dan 120 regels Python.
TL;DR
- •De kleinste bruikbare research-agentlus is: zoeken → links kiezen → bodies extraheren → het model vragen te antwoorden met inline bronvermeldingen.
- •Twee tools — Web Search (15 credits) en URL Extract (2 credits per URL) — dekken 95% van de gevallen waarin je een onderbouwd antwoord wilt.
- •Claude tool use regelt de orkestratie; jij verplaatst alleen tool_use-blokken → API-aanroepen → tool_result-blokken totdat het model stopt.
- •Kosten van begin tot eind: ~25 credits + ~$0.02 aan LLM-tokens per vraag bij typische diepte.
Wat we gaan bouwen
Een research-agent die één invoer aanneemt — een vraag in natuurlijke taal — en een antwoord met bronnen teruggeeft. Architectonisch:
question → [Claude]
↓ tool_use(search)
[API Pick Web Search] → ranked URLs
↓ tool_use(extract)
[API Pick URL Extract] → cleaned bodies
↓ Claude reads, decides if more is needed
↓ end_turn
answer with inline [source: URL] citationsOngeveer 120 regels Python. Twee tools — search en extract — en één agentlus die afhandelt wat Claude ook besluit te doen. We bouwen het vanaf nul in 4 stappen.
1Haal de toolschema's op (geen JSON met de hand)
Beide endpoints publiceren een tool-schema-route die een Claude-tooldefinitie teruggeeft in precies de vorm die messages.create verwacht.
import requests
API_KEY = "pk_yourkey"
def fetch_tool(tool_path: str) -> dict:
"""Fetch a Claude tool definition from API Pick's tool-schema endpoint."""
schema = requests.get(f"https://www.apipick.com{tool_path}/tool-schema").json()
return schema["claude"]
WEB_SEARCH_TOOL = fetch_tool("/api/search/web")
URL_EXTRACT_TOOL = fetch_tool("/api/extract")
# Cache these once at module load. They don't change between requests.Twee belangrijke dingen die je hiermee gratis krijgt: het parameterschema (zodat Claude weet dat je query, country_code, start_date enzovoort accepteert) en een heldere, modelvriendelijke beschrijving van wat de tool doet.
2Schrijf de tool-handler
Wanneer Claude een tool_use-blok teruggeeft, is het jouw taak om de echte API aan te roepen en een tool_result-blok terug te geven. Eén functie per tool, één dispatcher die routeert op basis van de naam die Claude koos.
def call_tool(tool_use_block) -> dict:
"""Execute the tool Claude asked for and return a tool_result block."""
name = tool_use_block.name
args = tool_use_block.input
if name == "web_search":
endpoint = "https://www.apipick.com/api/search/web"
elif name == "extract_urls":
endpoint = "https://www.apipick.com/api/extract"
else:
return {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": f"Unknown tool: {name}",
"is_error": True,
}
resp = requests.post(
endpoint,
headers={"x-api-key": API_KEY},
json=args,
timeout=30,
)
if resp.status_code != 200:
return {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": f"HTTP {resp.status_code}: {resp.text[:500]}",
"is_error": True,
}
return {
"type": "tool_result",
"tool_use_id": tool_use_block.id,
"content": resp.text,
}Drie dingen die het waard zijn om op te merken. tool_use_id is hoe Claude het resultaat koppelt aan zijn eerdere aanroep — je moet het ongewijzigd terugsturen. is_error: True vertelt Claude om gracieus te herstellen (vaak door een andere query te proberen). En we geven de ruwe responstekst door — Claude voelt zich op zijn gemak met de JSON-vorm die API Pick teruggeeft, dus is er geen transformatie nodig.
3De agentlus
De lus is kort: stuur het gesprek, bekijk elk blok in de respons, voer de tools uit, voeg zowel Claude's tool_use als jouw tool_result toe aan het gesprek, en herhaal totdat stop_reason gelijk is aan end_turn.
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = """You are a research assistant. When the user asks a question:
1. Use web_search to find relevant sources. Prefer recent (set start_date) for time-sensitive queries.
2. Use extract_urls to read the body of the top 3-5 most relevant URLs from the search results.
3. Answer the question concisely. After every factual claim, include an inline citation in the form [source: URL].
4. If you don't have enough information, say so — don't fabricate.
Be concise. Aim for 3-5 sentence answers unless the user asks for depth."""
def research(question: str) -> str:
messages = [{"role": "user", "content": question}]
while True:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM_PROMPT,
tools=[WEB_SEARCH_TOOL, URL_EXTRACT_TOOL],
messages=messages,
)
# Append the assistant's response (may contain tool_use blocks)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
# Pull out the text answer
return "\n".join(b.text for b in response.content if b.type == "text")
if response.stop_reason == "tool_use":
# Run every tool the model asked for in this turn
tool_results = [
call_tool(b)
for b in response.content if b.type == "tool_use"
]
messages.append({"role": "user", "content": tool_results})
continue
raise RuntimeError(f"Unexpected stop_reason: {response.stop_reason}")Merk op dat het model meerdere toolaanroepen in één beurt kan afvuren — het kan bijvoorbeeld één enkele extract_urls-aanroep met vijf URL's uitsturen om ze als batch te verwerken. We handelen dat af door alle tool_use-blokken uit de assistentbeurt te verzamelen en alle tool_result-blokken in de volgende gebruikersbeurt terug te geven.
4Voer het uit
if __name__ == "__main__":
answer = research("What were the major announcements at OpenAI DevDay this year?")
print(answer)Je krijgt iets als dit te zien:
OpenAI's DevDay focused on three themes: cheaper inference (a new gpt-mini
tier at roughly half the prior cost) [source: https://openai.com/devday-2026],
agent infrastructure (a managed agent runtime with persistent memory and
tool sandboxing) [source: https://techcrunch.com/...], and developer
tooling (a new Responses API replacing the legacy Assistants API)
[source: https://platform.openai.com/docs/...].Dat is een research-agent. ~120 regels, twee tools, uitvoer met bronnen.
Wat dit kost
Een typische research-aanroep:
- 1 zoekaanroep — 15 credits ($0.015)
- 1 extractie-aanroep die 3–5 URL's dekt — 6–10 credits ($0.006–$0.010)
- ~3,000 input- + 800 output-Claude-tokens — ~$0.02
Afgerond: ~3 cent per onderzocht antwoord. Bij 1,000 research-aanroepen per dag is dat ~$30/dag — ongeveer het budget voor de ochtendkoffie van één analist. Reserveer je budget voor het LLM, niet voor de zoek-API's.
Drie verfijningen voor productie
1. Cache op vraag
Als je product vergelijkbare vragen stelt, hash dan (question, today's date) en cache het uiteindelijke antwoord een uur lang. Het meeste door gebruikers gestuurde research heeft long-tail hitratio's boven de 30%.
2. Beperk wanneer actualiteit telt
Voor vragen over 'deze week' / 'vandaag' stel je start_date in de zoekaanroep in op de huidige datum of gisteren. Zonder die instelling kan de zoekopdracht artikelen van vorig jaar teruggeven. De eenvoudigste versie: een system-prompt-regel zoals 'neem altijd start_date op wanneer de vraag vandaag, deze week, recent of laatste bevat'.
3. Vangnetten tegen hallucinatie
De regel met de grootste individuele hefboomwerking: 'Als je niet genoeg geëxtraheerde content hebt om te antwoorden, geef dan terug: Ik kon hiervoor geen betrouwbare bron vinden. Gok niet.' Dit toevoegen aan de system prompt verlaagt verzonnen antwoorden met een orde van grootte in onze tests.
Waar je het hierna naartoe kunt brengen
Drie natuurlijke uitbreidingen:
- Maak het verticaal: vervang Web Search door Academic Search voor een literatuuronderzoek-agent, SEC Filings Search voor due diligence, of Clinical Search voor medisch onderzoek. De handler verandert niet — alleen de system prompt.
- Voeg gestructureerde uitvoer toe: vraag het model om JSON terug te geven met de velden
answerencitations[]zodat je ze als een UI-kaart kunt renderen. - Stream tokens: schakel over naar
client.messages.stream()voor incrementele uitvoer — handig wanneer het antwoord lang is. De tool-call-lus blijft hetzelfde.
Veelgestelde vragen
Waarom twee tools in plaats van één enkel 'answer'-endpoint?
Zoeken en extraheren scheiden geeft je controle over hoeveel URL's je leest, wanneer je stopt en welke je citeert. Eén gehost 'answer'-endpoint verbergt dit — je kunt de strategie niet eenvoudig wijzigen zonder opnieuw te architectureren. Met twee tools wordt dezelfde code een agent voor de ochtendbriefing, een literatuuronderzoeker of een scraper voor concurrentie-intelligentie, simpelweg door de prompt aan te passen.
Werkt dit ook met OpenAI Assistants / Responses API?
Ja. De architectuur is identiek — het enige wat verandert is hoe je de tool-call-blokken parset en de resultaten indient. De vorm van de handler wordt submit_tool_outputs(...) in plaats van een tool_result-contentblok, maar de agentlus en de tooldefinities zijn dezelfde JSON.
Hoe beslist de agent wanneer hij stopt?
Claude geeft stop_reason: end_turn terug wanneer hij genoeg context heeft om te antwoorden, en tool_use wanneer hij een tool wil aanroepen. De lus loopt gewoon door tot end_turn. In de praktijk doet het model meestal 1 tot 3 zoek-/extractiecycli voordat het antwoordt.
Hoe zorg ik ervoor dat hij echt bronnen citeert?
Twee hefbomen. Ten eerste: eis in de system prompt expliciet '[source: URL]' inline na elke bewering. Ten tweede: het antwoord van URL Extract bevat de URL, dus wanneer het model die in context heeft, citeert het van nature. Als bronvermeldingen toch wegvallen, voeg dan een laatste opmaakstap toe die het antwoord afwijst en het model vraagt de bronvermeldingen toe te voegen.
Hoe zit het met de latentie?
Elke zoekronde duurt ~1s; elke extractiebatch duurt 1–4s, afhankelijk van het aantal URL's en de JS-rendering. Een typische aanroep van 'onderzoek een vraag' komt uit op 5–15s kloktijd. Heb je het sneller nodig, parallelliseer dan de extractiestap (extract neemt al een lijst met URL's in één aanroep) en beperk het model tot één toolronde per vraag via instructies in de system prompt.
API's gebruikt in dit artikel
Sarah Choy is de CEO van API Pick. Ze schrijft over het bouwen van productieklare API's voor AI-agents en LLM-workflows.