Zbuduj agenta badawczego z Web Search + URL Extract (tool use w Claude, od początku do końca)

Większość poradników o 'agencie badawczym AI' kończy się na 'oto definicja narzędzia'. Ten dostarcza działającego agenta: na wejściu pytanie, na wyjściu odpowiedź z cytowaniami. Szukaj, ekstrahuj, rozumuj, cytuj — wszystko w niecałych 120 liniach Pythona.
TL;DR
- •Najmniejsza użyteczna pętla agenta badawczego to: wyszukaj → wybierz linki → wyekstrahuj treści → poproś model o odpowiedź z cytowaniami w tekście.
- •Dwa narzędzia — Web Search (15 kredytów) i URL Extract (2 kredyty za URL) — pokrywają 95% przypadków odpowiedzi opartych na źródłach.
- •Tool use w Claude zajmuje się orkiestracją; ty tylko przerzucasz bloki tool_use → wywołania API → bloki tool_result, aż model się zatrzyma.
- •Koszt od początku do końca: ~25 kredytów + ~$0.02 w tokenach LLM na pytanie przy typowej głębokości.
Co budujemy
Agenta badawczego, który przyjmuje jedno wejście — pytanie w języku naturalnym — i zwraca odpowiedź ze źródłami. Architektonicznie:
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] citationsOkoło 120 linii Pythona. Dwa narzędzia — search i extract — oraz jedna pętla agenta, która obsługuje cokolwiek Claude zdecyduje się zrobić. Zbudujemy to od zera w 4 krokach.
1Pobierz schematy narzędzi (żadnego JSON-a ręcznie)
Oba endpointy udostępniają trasę tool-schema, która zwraca definicję narzędzia Claude w dokładnie takim kształcie, jakiego oczekuje messages.create.
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.Dwie ważne rzeczy, które dostajesz za darmo: schemat parametrów (aby Claude wiedział, że akceptujesz query, country_code, start_date itd.) oraz jasny, przyjazny dla modelu opis tego, co robi narzędzie.
2Napisz handler narzędzia
Gdy Claude zwraca blok tool_use, twoim zadaniem jest wywołać właściwe API i zwrócić blok tool_result. Jedna funkcja na narzędzie, jeden dispatcher kierujący na podstawie nazwy wybranej przez Claude.
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,
}Trzy rzeczy warte uwagi. tool_use_id to sposób, w jaki Claude koreluje wynik ze swoim wcześniejszym wywołaniem — musisz go odesłać z powrotem. is_error: True mówi Claude, by odzyskał równowagę z gracją (często próbując innego zapytania). A my przekazujemy surowy tekst odpowiedzi — Claude czuje się swobodnie z kształtem JSON zwracanym przez API Pick, więc żadna transformacja nie jest potrzebna.
3Pętla agenta
Pętla jest krótka: wyślij konwersację, przejrzyj każdy blok w odpowiedzi, uruchom wszelkie narzędzia, dołącz do konwersacji zarówno tool_use Claude, jak i swój tool_result, i powtarzaj, aż stop_reason będzie równe 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}")Zauważ, że model może wystrzelić wiele wywołań narzędzi w jednej turze — na przykład może wydać jedno wywołanie extract_urls z pięcioma adresami URL, aby przetworzyć je wsadowo. Obsługujemy to, zbierając wszystkie bloki tool_use z tury asystenta i zwracając wszystkie bloki tool_result w następnej turze użytkownika.
4Uruchom to
if __name__ == "__main__":
answer = research("What were the major announcements at OpenAI DevDay this year?")
print(answer)Zobaczysz coś w stylu:
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/...].Oto agent badawczy. ~120 linii, dwa narzędzia, wynik ze źródłami.
Ile to kosztuje
Typowe wywołanie badawcze:
- 1 wywołanie wyszukiwania — 15 kredytów ($0.015)
- 1 wywołanie ekstrakcji obejmujące 3–5 adresów URL — 6–10 kredytów ($0.006–$0.010)
- ~3,000 tokenów wejściowych + 800 wyjściowych Claude — ~$0.02
W zaokrągleniu: ~3 centy za zbadaną odpowiedź. Przy 1,000 wywołań badawczych dziennie to ~$30/dzień — mniej więcej tyle, ile budżet na poranną kawę jednego analityka. Budżetuj LLM, a nie API wyszukiwania.
Trzy usprawnienia produkcyjne
1. Buforuj według pytania
Jeśli twój produkt zadaje podobne pytania, policz hash z (question, today's date) i buforuj końcową odpowiedź przez godzinę. Większość badań napędzanych przez użytkowników ma współczynniki trafień długiego ogona powyżej 30%.
2. Ogranicz, gdy liczy się świeżość
Dla pytań o 'ten tydzień' / 'dzisiaj' ustaw start_date w wywołaniu wyszukiwania na bieżącą datę lub wczoraj. Bez tego wyszukiwanie może zwrócić zeszłoroczne artykuły. Najprostsza wersja: reguła w system prompcie typu 'zawsze dołączaj start_date, gdy pytanie zawiera dzisiaj, ten tydzień, niedawny lub najnowszy'.
3. Zabezpieczenia przed halucynacją
Reguła o największej dźwigni: 'Jeśli nie masz wystarczającej wyekstrahowanej treści, by odpowiedzieć, zwróć: Nie udało mi się znaleźć wiarygodnego źródła dla tego. Nie zgaduj.' Dodanie tego do system promptu zmniejsza liczbę zmyślonych odpowiedzi o rząd wielkości w naszych testach.
Dokąd zabrać to dalej
Trzy naturalne rozszerzenia:
- Zwertykalizuj to: zamień Web Search na Academic Search dla agenta do przeglądu literatury, SEC Filings Search dla due diligence lub Clinical Search dla badań medycznych. Handler się nie zmienia — zmienia się tylko system prompt.
- Dodaj ustrukturyzowany wynik: poproś model o zwrócenie JSON-a z polami
answericitations[], abyś mógł wyrenderować je jako kartę UI. - Strumieniuj tokeny: przełącz się na
client.messages.stream(), aby uzyskać wynik przyrostowy — przydatne, gdy odpowiedź jest długa. Pętla tool-call jest taka sama.
Najczęściej zadawane pytania
Dlaczego dwa narzędzia zamiast jednego endpointu 'answer'?
Rozdzielenie wyszukiwania i ekstrakcji daje ci kontrolę nad tym, ile adresów URL czytać, kiedy przestać i które cytować. Pojedynczy hostowany endpoint 'answer' to ukrywa — nie da się łatwo zmienić strategii bez przebudowy architektury. Mając dwa narzędzia, ten sam kod staje się agentem porannego briefingu, recenzentem literatury lub scraperem wywiadu konkurencyjnego — wystarczy zmienić prompt.
Czy to działa też z OpenAI Assistants / Responses API?
Tak. Architektura jest identyczna — zmienia się tylko sposób, w jaki parsujesz bloki tool-call i przesyłasz wyniki. Kształt handlera staje się submit_tool_outputs(...) zamiast bloku treści tool_result, ale pętla agenta i definicje narzędzi to ten sam JSON.
Jak agent decyduje, kiedy się zatrzymać?
Claude zwraca stop_reason: end_turn, gdy ma wystarczający kontekst, aby odpowiedzieć, oraz tool_use, gdy chce wywołać narzędzie. Pętla po prostu działa aż do end_turn. W praktyce model zwykle wykonuje od 1 do 3 cykli wyszukiwania/ekstrakcji przed udzieleniem odpowiedzi.
Jak mam się upewnić, że faktycznie cytuje źródła?
Dwie dźwignie. Po pierwsze: w system prompcie wyraźnie wymagaj '[source: URL]' w tekście po każdym stwierdzeniu. Po drugie: odpowiedź URL Extract zawiera URL, więc gdy model ma go w kontekście, ma tendencję do naturalnego cytowania. Jeśli cytowania się gubią, dodaj końcowy krok formatowania, który odrzuca odpowiedź i prosi model o dodanie cytowań.
A co z opóźnieniem?
Każda runda wyszukiwania to ~1s; każda partia ekstrakcji to 1–4s w zależności od liczby adresów URL i renderowania JS. Typowe wywołanie 'zbadaj pytanie' mieści się w 5–15s rzeczywistego czasu. Jeśli potrzebujesz szybciej, zrównolegl krok ekstrakcji (extract już przyjmuje listę adresów URL w jednym wywołaniu) i ogranicz model do jednej rundy narzędzi na pytanie za pomocą instrukcji w system prompcie.
API użyte w tym artykule
Sarah Choy jest CEO API Pick. Pisze o budowaniu produkcyjnych API dla agentów AI i przepływów pracy z LLM.