Web Search + URL Extract ile bir araştırma ajanı oluşturun (Claude tool use, baştan sona)

Çoğu 'yapay zeka araştırma ajanı' eğitimi 'işte bir araç tanımı' noktasında durur. Bu eğitim çalışan bir ajan sunuyor: soru girer, kaynaklı yanıt çıkar. Aramak, çıkarmak, akıl yürütmek, kaynak göstermek — hepsi 120 satırın altında Python ile.
Özet
- •İşe yarayan en küçük araştırma ajanı döngüsü şudur: ara → bağlantıları seç → gövdeleri çıkar → modelden satır içi kaynaklarla yanıt vermesini iste.
- •İki araç — Web Search (15 kredi) ve URL Extract (URL başına 2 kredi) — temellendirilmiş yanıt senaryolarının %95'ini kapsar.
- •Orkestrasyonu Claude tool use üstlenir; sen yalnızca model durana kadar tool_use bloklarını → API çağrılarını → tool_result bloklarını taşırsın.
- •Baştan sona maliyet: tipik derinlikte soru başına ~25 kredi + LLM token'larında ~$0.02.
Ne inşa ediyoruz
Tek bir girdi alan — doğal dilde bir soru — ve kaynaklı bir yanıt döndüren bir araştırma ajanı. Mimari olarak:
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] citationsYaklaşık 120 satır Python. İki araç — search ve extract — ve Claude'un yapmaya karar verdiği her şeyi yöneten tek bir ajan döngüsü. Onu sıfırdan 4 adımda inşa edeceğiz.
1Araç şemalarını çek (elle JSON yazma)
Her iki endpoint de, messages.create'in beklediği tam biçimde bir Claude araç tanımı döndüren bir tool-schema yolu yayınlar.
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.Bunun sana bedavaya kazandırdığı iki önemli şey: parametre şeması (böylece Claude query, country_code, start_date vb. kabul ettiğini bilir) ve aracın ne yaptığına dair net, model dostu bir açıklama.
2Araç handler'ını yaz
Claude bir tool_use bloğu döndürdüğünde, senin işin gerçek API'yi çağırmak ve bir tool_result bloğu döndürmektir. Araç başına bir fonksiyon, Claude'un seçtiği ada göre yönlendiren bir dispatcher.
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,
}Dikkat edilmesi gereken üç şey. tool_use_id, Claude'un sonucu önceki çağrısıyla ilişkilendirme biçimidir — onu aynen geri döndürmen gerekir. is_error: True Claude'a zarif biçimde toparlamasını söyler (çoğu zaman farklı bir sorgu deneyerek). Ve ham yanıt metnini iletiyoruz — Claude, API Pick'in döndürdüğü JSON biçimiyle rahattır, dolayısıyla dönüştürmeye gerek yok.
3Ajan döngüsü
Döngü kısadır: konuşmayı gönder, yanıttaki her bloğa bak, araçları çalıştır, hem Claude'un tool_use'unu hem de senin tool_result'unu konuşmaya ekle, ve stop_reason end_turn olana kadar tekrarla.
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}")Modelin tek bir turda birden çok araç çağrısı tetikleyebileceğine dikkat et — örneğin, beş URL'yi yığın hâlinde işlemek için extract_urls'e tek bir çağrı yapabilir. Bunu, asistan turundaki tüm tool_use bloklarını toplayıp sonraki kullanıcı turunda tüm tool_result bloklarını döndürerek hallederiz.
4Çalıştır
if __name__ == "__main__":
answer = research("What were the major announcements at OpenAI DevDay this year?")
print(answer)Şöyle bir şey göreceksin:
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/...].İşte bir araştırma ajanı. ~120 satır, iki araç, kaynaklı çıktı.
Bunun maliyeti
Tipik bir araştırma çağrısı:
- 1 arama çağrısı — 15 kredi ($0.015)
- 3–5 URL'yi kapsayan 1 çıkarma çağrısı — 6–10 kredi ($0.006–$0.010)
- ~3,000 girdi + 800 çıktı Claude token'ı — ~$0.02
Yuvarlak rakam: araştırılan yanıt başına ~3 cent. Günde 1,000 araştırma çağrısında bu ~$30/gün eder — kabaca bir analistin sabah kahvesi bütçesi kadar. Arama API'lerine değil, LLM'e bütçe ayır.
Üç üretim iyileştirmesi
1. Soruya göre önbelleğe al
Ürünün benzer sorular soruyorsa, (question, today's date)'in hash'ini al ve nihai yanıtı bir saat boyunca önbelleğe al. Kullanıcı kaynaklı araştırmanın çoğunun uzun kuyruk isabet oranı %30'un üzerindedir.
2. Tazelik önemli olduğunda kısıtla
'Bu hafta' / 'bugün' soruları için arama çağrısındaki start_date'i geçerli tarihe veya düne ayarla. Bu olmadan arama, geçen yılın makalelerini döndürebilir. En basit hâli: 'soru bugün, bu hafta, son veya en son içerdiğinde her zaman start_date ekle' gibi bir system prompt kuralı.
3. Halüsinasyona karşı korumalar
Tek başına en yüksek kaldıraçlı kural: 'Yanıtlamak için yeterli çıkarılmış içeriğin yoksa şunu döndür: Bunun için güvenilir bir kaynak bulamadım. Tahmin yürütme.' Bunu system prompt'a eklemek, testlerimizde uydurma yanıtları bir kat azalttı.
Bundan sonra nereye götürülür
Üç doğal uzantı:
- Dikeyleştir: literatür gözden geçirme ajanı için Web Search'ü Academic Search ile, due diligence için SEC Filings Search ile, ya da tıbbi araştırma için Clinical Search ile değiştir. Handler değişmez — yalnızca system prompt değişir.
- Yapılandırılmış çıktı ekle: modelden, bir UI kartı olarak render edebilmen için
answervecitations[]alanlarına sahip JSON döndürmesini iste. - Token'ları akıt: artımlı çıktı için
client.messages.stream()'e geç — yanıt uzun olduğunda işe yarar. Araç çağrısı döngüsü aynıdır.
Sıkça Sorulan Sorular
Tek bir 'answer' endpoint'i yerine neden iki araç?
Aramayı ve çıkarmayı ayırmak sana kaç URL okuyacağın, ne zaman duracağın ve hangilerine atıf vereceğin üzerinde kontrol sağlar. Tek bir barındırılmış 'answer' endpoint'i bunu gizler — stratejiyi yeniden mimarileştirmeden kolayca değiştiremezsin. İki araçla aynı kod, yalnızca prompt'u ayarlayarak bir sabah brifing ajanına, bir literatür gözden geçiriciye ya da bir rekabet istihbaratı scraper'ına dönüşür.
Bu OpenAI Assistants / Responses API ile de çalışır mı?
Evet. Mimari aynıdır — değişen tek şey, araç çağrısı bloklarını nasıl parse edip sonuçları nasıl gönderdiğindir. Handler'ın biçimi bir tool_result içerik bloğu yerine submit_tool_outputs(...) olur, ama ajan döngüsü ve araç tanımları aynı JSON'dur.
Ajan ne zaman duracağına nasıl karar verir?
Claude, yanıtlamak için yeterli bağlama sahip olduğunda stop_reason: end_turn, bir araç çağırmak istediğinde ise tool_use döndürür. Döngü yalnızca end_turn olana kadar sürer. Pratikte model genellikle yanıtlamadan önce 1–3 arama/çıkarma döngüsü yapar.
Gerçekten kaynak gösterdiğinden nasıl emin olurum?
İki kaldıraç. Birincisi: system prompt'ta her iddianın ardından satır içi olarak '[source: URL]' biçimini açıkça zorunlu kıl. İkincisi: URL Extract yanıtı URL'yi içerir, bu yüzden model onu bağlamda gördüğünde doğal biçimde atıf verme eğilimindedir. Atıflar yine de kaçarsa, yanıtı reddedip modelden atıfları eklemesini isteyen son bir biçimlendirme adımı ekle.
Peki ya gecikme?
Her arama gidiş-dönüşü ~1s sürer; her çıkarma yığını URL sayısına ve JS render'ına bağlı olarak 1–4s sürer. Tipik bir 'bir soruyu araştır' çağrısı 5–15s duvar saati süresine oturur. Daha hızlı olmasını istiyorsan, çıkarma adımını paralelleştir (extract zaten tek çağrıda bir URL listesi alıyor) ve system prompt talimatlarıyla modeli soru başına tek araç turuna sınırla.
Bu makalede kullanılan API'ler
Sarah Choy, API Pick'in CEO'sudur. Yapay zeka ajanları ve LLM iş akışları için üretime hazır API'ler geliştirme üzerine yazılar yazar.