[ blog · tutorial ]9 min read

Bangun Research Agent dengan Web Search + URL Extract (tool use Claude, dari awal sampai akhir)

Sarah ChoyDiterbitkan 3 Mei 20269 menit baca
Bangun Research Agent dengan Web Search + URL Extract (tool use Claude, dari awal sampai akhir)

Kebanyakan tutorial 'AI research agent' berhenti di 'ini definisi tool-nya'. Yang ini mengirimkan agent yang benar-benar berjalan: pertanyaan masuk, jawaban berkutipan keluar. Cari, ekstrak, bernalar, kutip — semuanya dalam kurang dari 120 baris Python.

TL;DR

  • Loop research agent terkecil yang berguna adalah: cari → pilih tautan → ekstrak isi → minta model menjawab dengan kutipan inline.
  • Dua tool — Web Search (15 kredit) dan URL Extract (2 kredit per URL) — mencakup 95% kasus penggunaan jawaban berdasar bukti.
  • Tool use Claude menangani orkestrasinya; Anda tinggal mengantar blok tool_use → panggilan API → blok tool_result sampai model berhenti.
  • Biaya dari awal sampai akhir: ~25 kredit + ~$0.02 token LLM per pertanyaan pada kedalaman yang umum.

Apa yang akan kita bangun

Sebuah research agent yang menerima satu input — pertanyaan dalam bahasa alami — dan mengembalikan jawaban dengan sumber. Secara arsitektur:

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] citations

Sekitar 120 baris Python. Dua tool — search dan extract — dan satu loop agent yang menangani apa pun yang Claude putuskan untuk dilakukan. Kita akan membangunnya dari nol dalam 4 langkah.

1Tarik tool schema-nya (tanpa menulis JSON manual)

Kedua endpoint menerbitkan rute tool-schema yang mengembalikan definisi tool Claude dalam bentuk persis seperti yang diharapkan 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.

Dua hal penting yang Anda dapatkan secara gratis dari sini: schema parameter (sehingga Claude tahu Anda menerima query, country_code, start_date, dll.) dan deskripsi yang jelas serta ramah-model tentang apa yang dilakukan tool tersebut.

2Tulis handler tool-nya

Ketika Claude mengembalikan blok tool_use, tugas Anda adalah memanggil API yang sebenarnya dan mengembalikan blok tool_result. Satu fungsi per tool, satu dispatcher yang mengarahkan berdasarkan nama yang dipilih 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,
    }

Tiga hal yang perlu diperhatikan. tool_use_id adalah cara Claude mengaitkan hasil dengan panggilannya yang sebelumnya — Anda harus mengembalikannya apa adanya. is_error: True memberi tahu Claude untuk pulih dengan anggun (sering kali dengan mencoba query lain). Dan kita meneruskan teks respons mentah — Claude nyaman dengan bentuk JSON yang dikembalikan API Pick, jadi tidak perlu transformasi.

3Loop agent-nya

Loop-nya pendek: kirim percakapannya, lihat setiap blok dalam respons, jalankan tool apa pun, tambahkan ke percakapan baik tool_use dari Claude maupun tool_result Anda, lalu ulangi sampai stop_reason menjadi 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}")

Perhatikan bahwa model dapat melepaskan beberapa panggilan tool dalam satu giliran — misalnya, ia mungkin mengeluarkan satu panggilan extract_urls dengan lima URL untuk memprosesnya secara batch. Kita menanganinya dengan mengumpulkan semua blok tool_use dari giliran asisten dan mengembalikan semua blok tool_result pada giliran user berikutnya.

4Jalankan

if __name__ == "__main__":
    answer = research("What were the major announcements at OpenAI DevDay this year?")
    print(answer)

Anda akan melihat kira-kira seperti ini:

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/...].

Itulah research agent. ~120 baris, dua tool, keluaran bersumber.

Berapa biayanya

Satu panggilan research yang umum:

  • 1 panggilan pencarian — 15 kredit ($0.015)
  • 1 panggilan ekstraksi mencakup 3–5 URL — 6–10 kredit ($0.006–$0.010)
  • ~3,000 token input + 800 output Claude — ~$0.02

Angka bulatnya: ~3 sen per jawaban yang diteliti. Pada 1,000 panggilan research per hari, itu sekitar ~$30/hari — kurang lebih seukuran anggaran kopi pagi seorang analis. Anggarkan untuk LLM, bukan API pencarian.

Tiga penyempurnaan untuk produksi

1. Cache berdasarkan pertanyaan

Jika produk Anda mengajukan pertanyaan-pertanyaan serupa, hash-kan (question, today's date) dan cache jawaban akhirnya selama satu jam. Sebagian besar research yang digerakkan pengguna punya tingkat hit ekor-panjang di atas 30%.

2. Batasi ketika kesegaran penting

Untuk pertanyaan 'minggu ini' / 'hari ini', atur start_date pada panggilan pencarian ke tanggal hari ini atau kemarin. Tanpa itu, pencarian bisa mengembalikan artikel tahun lalu. Versi paling sederhana: satu aturan system prompt seperti 'selalu sertakan start_date ketika pertanyaan memuat kata hari ini, minggu ini, terbaru, atau terkini'.

3. Pengaman terhadap halusinasi

Aturan dengan daya ungkit tunggal tertinggi: 'Jika Anda tidak punya cukup konten yang diekstrak untuk menjawab, kembalikan: Saya tidak dapat menemukan sumber yang dapat diandalkan untuk ini. Jangan menebak.' Menambahkan ini ke system prompt menurunkan jawaban yang dikarang sebesar satu orde besaran dalam pengujian kami.

Ke mana melanjutkannya

Tiga perluasan yang alami:

  • Vertikalkan: tukar Web Search dengan Academic Search untuk agent tinjauan literatur, SEC Filings Search untuk uji tuntas, atau Clinical Search untuk riset medis. Handler-nya tidak berubah — hanya system prompt-nya.
  • Tambahkan keluaran terstruktur: minta model mengembalikan JSON dengan field answer dan citations[] agar Anda bisa merendernya sebagai kartu UI.
  • Streaming token: beralih ke client.messages.stream() untuk keluaran inkremental — berguna saat jawabannya panjang. Loop tool-call-nya sama saja.

Pertanyaan yang Sering Diajukan

Mengapa dua tool, bukan satu endpoint 'answer' saja?

Memisahkan pencarian dan ekstraksi memberi Anda kendali atas berapa banyak URL yang dibaca, kapan berhenti, dan mana yang dikutip. Satu endpoint 'answer' yang di-hosting menyembunyikan ini — Anda tidak bisa dengan mudah mengubah strategi tanpa merancang ulang arsitekturnya. Dengan dua tool, kode yang sama bisa menjadi agent ringkasan pagi, peninjau literatur, atau scraper intelijen kompetitif hanya dengan mengubah prompt.

Apakah ini juga bekerja dengan OpenAI Assistants / Responses API?

Ya. Arsitekturnya identik — satu-satunya yang berubah adalah cara Anda mem-parse blok tool-call dan mengirimkan hasilnya. Bentuk handler-nya menjadi submit_tool_outputs(...) alih-alih blok konten tool_result, tetapi loop agent dan definisi tool-nya adalah JSON yang sama.

Bagaimana agent memutuskan kapan berhenti?

Claude mengembalikan stop_reason: end_turn ketika sudah punya konteks cukup untuk menjawab, dan tool_use ketika ingin memanggil sebuah tool. Loop-nya terus berjalan sampai end_turn. Dalam praktiknya model biasanya melakukan 1–3 siklus cari/ekstrak sebelum menjawab.

Bagaimana memastikan agent benar-benar mengutip sumber?

Dua tuas. Pertama: di system prompt, wajibkan secara eksplisit '[source: URL]' inline setelah setiap klaim. Kedua: respons URL Extract menyertakan URL-nya, sehingga ketika model memilikinya dalam konteks, ia cenderung mengutip secara alami. Jika kutipan terlewat, tambahkan satu langkah pemformatan akhir yang menolak jawaban dan meminta model menambahkan kutipan.

Bagaimana dengan latensi?

Setiap putaran pencarian sekitar ~1d; setiap batch ekstraksi 1–4d tergantung jumlah URL dan rendering JS. Panggilan 'teliti satu pertanyaan' yang umum berakhir di 5–15d waktu nyata. Jika butuh lebih cepat, paralelkan langkah ekstraksi (extract sudah menerima daftar URL dalam satu panggilan) dan batasi model ke satu putaran tool per pertanyaan lewat instruksi di system prompt.

API yang digunakan dalam artikel ini

Sarah Choy
Ditulis oleh
Sarah Choy
CEO, API Pick

Sarah Choy adalah CEO API Pick. Ia menulis tentang membangun API siap produksi untuk AI agent dan alur kerja LLM.