Agentic RAG Chatbot

Agentic RAG · Cloudflare Edge · LLM Observability

This is not a demo. It is running on this site right now. Open the chat bubble on any page and ask about my work. Live metrics are on the /ops dashboard, and the source lives in the worker/ folder of this site's repository.

What it is

A production RAG chatbot that answers questions about me, grounded in this site's content: bio, experience, projects, publications, and the full text of every write-up. It is agentic: Claude holds a search_knowledge_base tool and decides per question whether to search or answer directly, then streams the answer token by token. The whole backend is one Cloudflare Worker at the edge, and the project covers the full lifecycle of an LLM system: retrieval design, evaluation, cost and latency tracking, and deployment.

Architecture

Pipeline: question goes to Claude, which decides via tool use whether to search; on search, Vectorize (semantic) and D1 FTS5 (keyword) run in parallel, are fused with Reciprocal Rank Fusion, reranked by Haiku to the top 6 chunks, and fed back for a grounded answer streamed as SSE

One Worker, four Cloudflare primitives (Workers, Workers AI, Vectorize, D1) and the Anthropic API. The dashed path is the no-search branch.

How a request flows

Each /chat request goes to Claude with a persona prompt and the search tool. Greetings and core facts are answered directly; anything deeper triggers a search, and the Worker feeds the results back and streams the grounded answer as Server-Sent Events. Retrieved text is wrapped as untrusted data so nothing inside a document can inject instructions, inputs are capped, CORS is locked to this origin, and the API key never leaves the Worker.

Hybrid retrieval

Two searches run in parallel: semantic (the query embedded with bge-base-en-v1.5 and matched by similarity in Vectorize) and keyword (BM25 via SQLite FTS5 in D1), which catches exact names that embeddings blur. The rankings are fused with Reciprocal Rank Fusion into 12 candidates, and a cheap Claude Haiku pass reranks them down to the 6 chunks the answer is grounded on. A slot is always reserved for the best project, about, and bio chunk so identity facts are never crowded out.

Design decisions

The small, critical facts (projects, publication, roles, contact) are pinned straight into the system prompt; retrieval is reserved for the ~350 chunks of write-up and project content that do not fit. That split came from a real failure: when full write-up bodies were first ingested, they crowded the projects and publication chunks out of the results entirely. A golden-question eval now guards that seam, failing below 90% retrieval recall and reporting every run to the /ops RAG tab.

The corpus itself is curated from the site: bio, project pages (parsed from the built HTML), and every write-up, split into overlapping chunks. npm run ingest purges both stores, then re-embeds and upserts everything so the semantic and keyword indexes stay in lockstep.

Cheatsheet

The whole system at a glance:

StageTech / modelRuns on
Embeddingsbge-base-en-v1.5 (768-dim)Workers AI
Semantic searchCosine similarity, top 12Cloudflare Vectorize
Keyword searchSQLite FTS5 (BM25)Cloudflare D1
FusionReciprocal Rank Fusion (k = 60)Worker
RerankClaude Haiku 4.5 → top 6 chunksAnthropic API
Search decision + generationClaude Haiku 4.5 with tool useAnthropic API
StreamingServer-Sent EventsWorker → browser
TracingPer-request traces; Langfuse spansD1 + Langfuse
Eval gateGolden-set recall ≥ 0.9npm run eval → /ops
Chat widgetReact, on every pageGitHub Pages

Deployment

The site is a Next.js static export on GitHub Pages; the backend deploys with one wrangler deploy. Secrets live in the Worker, CORS is pinned to the production origin, and there is no server to patch and no idle cost.

Cloudflare dashboard metrics for the ashim-chatbot Worker: invocations, zero errors, 1.33 ms CPU time, and the active deployment serving 100% of traffic, with subrequests going to api.anthropic.com and us.cloud.langfuse.com
The deployed Worker in Cloudflare: zero errors, ~1.3 ms median CPU time, and outbound calls only to the Anthropic API and Langfuse.

Observability

Every request is traced to D1: tokens, cost per pipeline stage, latency, retrieval quality, and the sources each answer cited. That feeds the live /ops dashboard, including the latest eval run with its PASS/FAIL badge.

The /ops RAG tab: average vector hits 12.0, keyword hits 8.7, fused 12.0, chunks used 5.0, overlap 3.3, similarity 0.657, and a most-cited sources list led by write-ups, the About page, and project pages
Live retrieval health on /ops: hit counts, fusion, similarity, and which sources answers actually cite.

Langfuse tracing

/ops shows the aggregate picture; Langfuse shows what happened inside one request: decision, rerank, and generation spans with per-span tokens, cost, and latency, plus retrieval stats attached to the trace. Events are posted straight to the Langfuse API and flushed with ctx.waitUntil(), so tracing adds zero latency for the visitor and no-ops entirely if the keys are unset.

Langfuse tracing view: a chat trace with decision, rerank, generation, and retrieval children, per-span token counts and costs, and retrieval metadata (avgScore, overlap, used, fused)
A real request in Langfuse: decision, rerank, and generation spans with per-span tokens and cost.

Tech Stack

Cloudflare WorkersWorkers AIVectorizeD1 + FTS5Claude Haiku 4.5Anthropic tool useReciprocal Rank FusionServer-Sent EventsLangfuseTypeScriptNext.jsGitHub Pages