Agent riset berbasis topik input yang mencari secara otomatis dari web, Wikipedia, dan arXiv, memvalidasi sumber, membaca konten, mengevaluasi kecukupan, lalu menyusun laporan Markdown yang di-stream ke UI secara real-time.
Catatan: Proyek ini dibuat sebagai catatan pembelajaran membangun aplikasi end-to-end berbasis LLM dengan LangGraph dan LCEL, bukan untuk kebutuhan produksi. Keputusan teknis, eksperimen, dan keterbatasan sistem didokumentasikan di folder
documentations/.
Mencari jawaban atas satu pertanyaan riset butuh waktu: membuka banyak tab, menilai sumber satu per satu, membaca, lalu merangkum. Bertanya langsung ke LLM memang cepat, tapi jawabannya tidak bisa ditelusuri ke sumber dan rawan mengarang.
Proyek ini dibangun untuk mencoba menjembatani keduanya: sebuah agent yang mengotomasi alur riset (mencari, membaca, dan merangkum dari web) sambil menjaga jawabannya tetap berdasarkan sumber yang benar-benar dibaca agent, sehingga hasilnya bisa diverifikasi. Di sisi lain, proyek ini juga menjadi eksperimen membangun agent LLM multi-langkah yang terkontrol dan transparan, dengan setiap langkah ditampilkan ke UI secara real-time.
Browser tidak pernah memanggil FastAPI langsung. Next.js berperan sebagai BFF (Backend for Frontend) yang menangani keamanan permukaan, lalu mem-proxy ke FastAPI.
Browser
│ POST /api/research/stream { question }
▼
Next.js BFF (Vercel)
• validasi input + filter pola injection
• rate limit per IP via Upstash Redis
• proxy ke FastAPI dengan header X-BFF-Token
• teruskan SSE stream apa adanya ke browser
│ POST /research/stream (+ X-BFF-Token)
▼
FastAPI (Render)
• verifikasi X-BFF-Token
• jalankan LangGraph agent
• scan output (leakage) sebelum dikirim
• stream hasil sebagai Server-Sent Events
│
▼
LangGraph Agent (6 node, lihat Alur di bawah)
Lima node berjalan berurutan, lalu evaluate_node memutuskan: cukup, atau ulangi fetch.
START
│
▼
plan_node tentukan query_type (general/technical) + maks. 3 query
│
▼
search_node Tavily + Wikipedia (+ arXiv jika technical)
│ gabung, dedup, batasi 11 URL
▼
validate_sources_node cek URL bisa diakses (HEAD/GET), buang yang gagal
│
▼
fetch_node ◄───────┐ baca isi halaman via Jina Reader (maks. 3 URL/iterasi)
│ │
▼ │
evaluate_node │ LLM nilai: informasi sudah cukup?
│ │
├─ belum cukup ───┘ (selama iterasi < 3 dan masih ada URL baru)
│
▼ cukup, atau iterasi = 3, atau URL habis
synthesize_node LLM susun laporan Markdown, token di-stream ke UI
│
▼
END
Penjelasan lengkap: documentations/ARCHITECTURE.md
- Bukan aplikasi instan. Satu riset butuh ~30-60 detik (1 iterasi) dan bisa beberapa menit jika loop fetch-evaluate berjalan sampai 3 iterasi, wajar untuk beberapa LLM call + validasi + fetch.
- Cold start di production. Backend Render free tier tidur setelah ~15 menit idle; request pertama setelahnya butuh tambahan beberapa puluh detik.
query_typecenderunggeneral. Pertanyaan gaya "bagaimana cara kerja X" sering dinilaigeneralmeski teknis, sehingga arXiv tidak dipanggil. Untuk sumber akademis, pakai kata kunci teknis yang eksplisit.- Tanda ✗ bukan berarti URL rusak. URL ditandai tidak dapat diakses ketika agent gagal membacanya, sering karena situs memblokir request otomatis (mis. DataCamp), bukan karena tautannya mati. Masih bisa dibuka manual di browser.
- Akurasi laporan bergantung pada sumber yang terbaca. Agent merangkum dari halaman yang berhasil di-fetch dan tidak menilai kredibilitas sumber. Daftar sumber dicantumkan di laporan agar bisa diverifikasi sendiri.
- Rate limit 5 request per IP per jam. Di Docker lokal semua request berbagi satu IP gateway; di production dihitung per user via
x-forwarded-for. - Tidak ada automated test. Pengujian manual via curl dan browser; lihat TESTING.md.
| Komponen | Tool |
|---|---|
| Backend framework | FastAPI + Uvicorn |
| Agent orchestration | LangGraph (state machine) + LangChain LCEL (komposisi chain) |
| LLM | OpenAI gpt-4o-mini (plan, evaluate, synthesize) |
| Web search | Tavily |
| Sumber akademis | Wikipedia API + arXiv API |
| Web reader | Jina Reader |
| HTTP client | httpx + certifi |
| Data validation | Pydantic v2 |
| Frontend framework | Next.js 14 App Router |
| Styling | Tailwind CSS v4 + DaisyUI v5 (theme night) |
| Markdown render | react-markdown + @tailwindcss/typography |
| Icons | lucide-react |
| Rate limiting | Upstash Redis via REST API |
| Infra lokal | Docker + Docker Compose |
| Deployment | Backend di Render (Docker), frontend di Vercel (Next.js) |
Yang dibutuhkan: Docker + Docker Compose, API key untuk OpenAI, Tavily, dan Upstash Redis. Jina API key opsional di lokal (lihat tabel Environment Variables).
# 1. Clone repo
git clone <repo-url>
cd web-research-agent
# 2. Siapkan environment variables
cp .env.example .env
# Isi semua variabel - lihat tabel Environment Variables di bawah
# 3. Jalankan
docker compose up --buildBuka http://localhost:3000, masukkan topik riset, tunggu laporan selesai di-stream.
Catatan rate limit: 5 request per IP per jam. Di Docker, semua request dari mesin yang sama berbagi kuota ini karena IP tercatat sebagai gateway Docker network (
172.31.0.1).
| Variable | Keterangan | Wajib |
|---|---|---|
OPENAI_API_KEY |
Untuk plan_node, evaluate_node, dan synthesize_node |
Ya |
TAVILY_API_KEY |
Web search via Tavily | Ya |
JINA_API_KEY |
Untuk Jina Reader (baca isi halaman). Opsional di lokal karena IP residential dilayani tanpa key, tapi wajib di production, karena Jina menolak IP datacenter (Render) dengan HTTP 401 tanpa key. Gratis di jina.ai/reader. | Ya* |
BFF_SHARED_SECRET |
Shared secret antara Next.js BFF dan FastAPI. Nilai bebas, tapi harus sama di kedua sisi. | Ya |
UPSTASH_REDIS_REST_URL |
REST URL dari Upstash Redis dashboard | Ya |
UPSTASH_REDIS_REST_TOKEN |
Token autentikasi Upstash Redis | Ya |
ALLOWED_ORIGINS |
Origin yang diizinkan CORS di backend (mis. URL Vercel). Default http://localhost:3000 untuk development. |
Tidak |
FASTAPI_URL |
URL backend FastAPI. Di Docker diset otomatis ke http://backend:8000 oleh docker-compose.yml. Diisi URL Render saat deploy ke production. |
Ya |
* JINA_API_KEY opsional untuk development lokal, wajib untuk deployment.
Tidak ada automated test; pengujian dilakukan manual.
- Uji fungsional & keamanan - panduan curl/browser lengkap (smoke test, auth, rate limit, injection, query teknis vs general) di documentations/TESTING.md.
- Observasi perilaku -
observations/run_observations.pymenjalankan 5 topik uji lewat BFF lalu menulis ringkasan (tipe query, jumlah iterasi, waktu, cuplikan laporan) keobservations/RESULT_OBSERVATIONS.md.
# aplikasi harus sedang berjalan; 5 query = batas rate limit per jam
python3 observations/run_observations.py5 query langsung menghabiskan kuota rate limit per jam, jadi untuk run ulang reset dulu key Upstash (perintahnya di TESTING.md).
web-research-agent/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app + CORS
│ │ ├── api/research.py # SSE endpoint, auth, leakage scan
│ │ ├── graph/
│ │ │ ├── state.py # ResearchState + Pydantic schemas
│ │ │ ├── nodes.py # node + LCEL chains
│ │ │ ├── edges.py # routing + batas iterasi
│ │ │ └── graph.py # rakit & compile graph
│ │ └── tools/ # search, wikipedia, arxiv, fetch, validate
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ ├── app/
│ │ ├── page.tsx # form input topik
│ │ ├── research/page.tsx # UI streaming (layout 30/70)
│ │ ├── api/research/stream/route.ts # BFF: proxy + rate limit + validasi
│ │ └── globals.css # Tailwind v4 + DaisyUI
│ └── Dockerfile
├── observations/ # script uji + hasilnya
├── documentations/ # ARCHITECTURE, DECISIONS, SECURITY, DATA_CONTRACT, TESTING
├── docker-compose.yml
└── .env.example
| Dokumen | Isi |
|---|---|
| ARCHITECTURE.md | Internal mechanics: ResearchState, tanggung jawab tiap node, edge routing, SSE event filtering |
| DECISIONS.md | Keputusan teknis dengan rationale dan trade-off |
| SECURITY.md | Kontrol keamanan dipetakan ke OWASP LLM Top 10 2025: prompt injection, output handling, system prompt leakage, unbounded consumption, plus BFF auth |
| DATA_CONTRACT.md | Schema request/response endpoint BFF dan FastAPI, format SSE event, Pydantic schemas |
| TESTING.md | Test manual: smoke test, backend langsung, BFF, rate limiting, query teknis vs general |
Fase eksperimen sebelum kode dipindah ke backend/. Setiap notebook memvalidasi satu bagian pipeline secara terpisah sebelum dirangkai jadi aplikasi.
| Notebook | Isi |
|---|---|
| 01 - LCEL in Node | Membangun LCEL chain (plan, evaluate, synthesize) dan membungkusnya menjadi node LangGraph |
| 02 - Agent Graph | Merangkai semua node + conditional loop fetch-evaluate menjadi graph end-to-end |
| 03 - Streaming | Menangkap event graph via astream_events() dan memilah step vs token untuk SSE |
LLM tidak mencari sumber, hanya menyusun dari yang ditemukan. Pencarian dilakukan API deterministik (Tavily, Wikipedia, arXiv) dan isi halaman diambil via Jina Reader. Peran LLM terbatas pada tiga hal: merencanakan query, menilai kecukupan, dan menyusun laporan dari konten yang di-fetch. Fakta dalam laporan berasal dari sumber yang dibaca, bukan pengetahuan internal LLM, sehingga sumbernya bisa ditelusuri. Jika tidak ada konten yang berhasil dibaca, agent menolak membuat laporan daripada mengarang (lihat D-05 di DECISIONS).
Tiga sumber dipilih karena saling melengkapi, bukan redundan. Tavily menangkap informasi terkini, Wikipedia memberi definisi dan konteks yang stabil, arXiv menyediakan rujukan akademis (hanya saat query_type = technical). Mengandalkan satu sumber membuat riset dangkal untuk sebagian jenis pertanyaan.
BFF bukan sekadar proxy. Next.js API route adalah tempat semua keamanan sisi permukaan masuk berada: validasi panjang input, injection pattern blocking, dan rate limiting. FastAPI hanya menerima request yang sudah lolos semua pemeriksaan ini dan membawa X-BFF-Token yang valid. URL FastAPI tidak pernah terekspos ke browser.
Dua lapis: LCEL untuk tiap langkah, LangGraph untuk alurnya. Tiap chain (plan, evaluate, synthesize) dibangun dengan LangChain Expression Language (LCEL) sebagai unit mandiri yang bisa diuji lepas dari graph (lihat Notebooks), lalu dibungkus menjadi node. LangGraph merangkai node-node itu sebagai state machine: semua kondisi berhenti agent (informasi cukup, iterasi maksimal, URL habis) ada di conditional edge, bukan di dalam node atau prompt LLM, sehingga perilaku agent bisa dibaca sebagai kode biasa dan tidak bisa diabaikan oleh output LLM.
Token streaming lewat satu jalur yang ketat. astream_events() menghasilkan event dari semua komponen dalam graph, termasuk token JSON internal dari plan_node dan evaluate_node. Filter berdasarkan metadata["langgraph_node"] memastikan hanya token dari synthesize_node yang diteruskan ke browser - token internal tidak pernah tampil di UI.
Spotlighting sebagai mitigasi prompt injection. Konten yang di-fetch dari web dibungkus dengan delimiter <<<EXTERNAL_CONTENT_START>>> sebelum masuk ke prompt synthesize_node. LLM diberitahu secara eksplisit bahwa konten di dalam delimiter adalah data eksternal tidak terpercaya, bukan instruksi. Teknik ini dari Microsoft Research (arXiv:2403.14720).
Semua keputusan teknis didokumentasikan dengan rationale dan trade-off di documentations/DECISIONS.md.
