Skip to content
Mihir AI · Hybrid RAG case study

How I Built Mihir AI

A secure portfolio chatbot that searches verified content about my background before answering, with hybrid retrieval, citation checks, and a feature-flagged rollout.

Mihir AI is not a generic chatbot pasted onto a résumé. It retrieves relevant passages from a controlled portfolio corpus, fuses semantic and keyword search, optionally reranks candidates, generates an answer from that evidence, and validates citation metadata server-side before the response returns to the widget.

5 source documents 21 indexed chunks 768-dim BGE embeddings Hybrid vector + keyword retrieval Top 5 passages to generation Recall@5 1.0000 (controlled eval) Hybrid + rerank MRR 0.9821 Security-first Cloudflare edge

Why I built it

A portfolio is usually a static brochure. Recruiters and hiring managers still have to hunt for answers: which project matches this role, which certification is current, is he available for contract work. I wanted a product that does that work for them.

Ask, don’t search

Visitors can ask a focused question instead of scanning the entire site for one fact.

Intent → lead capture

When hiring or collaboration intent shows up, the chatbot can offer a lightweight contact form in context.

Cost, safety, traceability

Free-tier cost control, abuse protection, and answers grounded in a controlled corpus, not the open web.

Two constraints shaped every decision: run at effectively $0/month on free tiers, and treat a public AI endpoint with storage and email as a security surface from day one.

RAG in plain English

Retrieval-Augmented Generation is an open-book exam for the model. Instead of answering only from training memory or from one giant profile stuffed into every prompt, Mihir AI first searches a small library of verified portfolio passages, selects the most relevant ones, then writes an answer using that evidence.

Important: the system searches Mihir’s controlled portfolio corpus (Markdown source files I maintain), not the public internet.

How a question becomes an answer

  1. 1 Question A visitor asks something about experience, skills, or availability.
  2. 2 Search verified portfolio Semantic and keyword search run over indexed passages.
  3. 3 Select evidence Fusion and optional reranking choose the top passages.
  4. 4 Generate answer The model answers from those passages with citation IDs.
  5. 5 Check citation IDs Server-side validation keeps only IDs that were actually retrieved.
Technical detail: what “evidence” means here

Each passage is a chunk with a stable ID such as experience#0. The generation prompt includes the retrieved chunk text framed as reference material. The API returns structured citations and retrieved metadata alongside the reply text.

From v1 to hybrid RAG

The first production chatbot put a structured professional profile directly into the system prompt. That was a deliberate stage-one design: small profile, fast to ship, easy to audit, and enough to build sessions, CSRF, rate limits, Turnstile, and lead capture around a real product.

As the system matured, selective context, source IDs, and measurable retrieval quality became more valuable than sending the full profile on every turn. Hybrid RAG was introduced behind a RAG_ENABLED feature flag so rollout and rollback stayed safe. When the flag is off, the legacy profile-in-prompt path remains available.

V1: profile in prompt

  • Full structured profile in every system prompt
  • Simple and auditable
  • No retrieval evaluation loop
  • Fixed prompt cost each turn
  • No citation metadata

Current: hybrid RAG

  • Retrieve only relevant passages
  • Hybrid semantic + keyword search
  • Reranking + citation metadata
  • Recall and ranking evaluation
  • Feature-flag rollback to legacy path

This is architectural evolution, not a rewrite of the product. The original security, session, rate-limit, Turnstile, lead-capture, and cost-conscious design still surrounds the retrieval pipeline.

How the knowledge base is built

Mihir’s experience, skills, projects, education, and certifications live as readable Markdown files. An offline script turns their sections into searchable passages, embeds them, and stores them in D1.

  1. Markdown5 source files
  2. Chunk## headings
  3. Heading context[Title > Heading]
  4. HashSHA-256
  5. EmbedBGE base
  6. NormalizeL2 unit length
  7. StoreD1 + FTS5
Technical detail: indexing pipeline
  • Five Markdown files with YAML frontmatter (doc_id, source, title): experience, skills, projects, education, certifications.
  • Split on ## heading boundaries. Oversized sections can split at paragraph boundaries with roughly 15% trailing overlap.
  • Current corpus: 21 chunks.
  • Embedding input is prefixed with [Title > Heading] so title and section context influence the vector.
  • SHA-256 of the full embedding input supports incremental embedding reuse when content is unchanged.
  • Model: Cloudflare BGE @cf/baai/bge-base-en-v1.5768-dimensional vectors, L2-normalized.
  • Stored as little-endian Float32 BLOBs in D1 (3,072 bytes per embedding).
  • data/seed.sql populates chunks and the FTS5 virtual table chunks_fts.
  • Re-indexing is currently manual when source Markdown changes.

What happens when someone asks a question

Security checks run before expensive AI generation. Sessions are anonymous; there is no user login. CSRF proves the browser holds a token issued with that session; it is not authentication.

  1. 1
    Validate JSON and input limits

    Body shape, required fields, 2,000-character message cap.

  2. 2
    Load and validate the anonymous session

    D1 lookup; reject ended or missing sessions.

  3. 3
    Verify HMAC-signed CSRF

    HMAC-SHA256 over session id and secret; constant-time compare.

  4. 4
    Check D1 session message cap

    Hard ceiling so one conversation cannot drain quota.

  5. 5
    Apply global and per-IP KV rate limits

    Time-bucketed counters with TTL.

  6. 6
    Require Turnstile when threshold is reached

    Server-side siteverify after sustained use.

  7. 7
    Screen direct prompt-injection patterns

    Regex filter before any model call; canned refusal on match.

  8. 8
    Load up to six recent messages

    Short conversation history for generation context.

  9. 9
    Run RAG retrieval

    When RAG_ENABLED is on: hybrid retrieve → top 5 passages.

  10. 10
    Generate an answer

    Llama 3.1 8B FP8 via Workers AI.

  11. 11
    Validate citation metadata & log grounding

    Filter unknown IDs; lexical overlap is observability-only.

  12. 12
    Persist the turn to D1

    User message and assistant reply stored together.

  13. 13
    Return JSON to the widget

    reply, answer, citations, retrieved.

Mihir AI request architecture Browser widget posts to the Cloudflare Worker. Security checks run first, then hybrid RAG retrieval, generation, citation validation, D1 persistence, and a JSON response. Static frontend Cloudflare Worker Services Browser widget session + CSRF + message POST /api/chat CSRF · rate limits · Turnstile Injection filter Hybrid RAG retrieve Llama 3.1 generation Citation ID validation Persist turn · return JSON KV rate limits Turnstile D1 chunks + FTS5 Workers AI D1 sessions Resend (leads) JSON + citations metadata

How hybrid retrieval works

This is the centrepiece of the current architecture: two independent search methods, fused by rank (not raw score), then optionally reordered by an LLM before the top five passages reach generation.

  1. Questionuser message
  2. BGE embed768-dim query
  3. Exact vectordot product scan
  4. FTS5 / BM25keyword ranks
  5. RRF fusionk = 60
  6. LLM rerankoptional
  7. Top 5to generation
  8. Cited answerIDs validated

Query embedding

The question is embedded with the same BGE model used at index time and L2-normalized so that a dot product equals cosine similarity.

Exact vector search

With only 21 chunks, the Worker loads stored vectors and scores every one with a linear scan. A dedicated vector database would add operational cost without benefit at this scale. Something like Cloudflare Vectorize becomes appropriate only when the corpus grows materially larger. Mihir AI does not currently use Pinecone, pgvector, or Vectorize.

Keyword search

SQLite FTS5 with BM25 finds exact terms such as product names, company names, and certification codes that pure semantic search can miss.

Implementation accuracy: vector search and FTS5 are independent methods, but the current code awaits the vector path and then runs keyword search. They are not executed concurrently today.

Reciprocal Rank Fusion (RRF)

Vector similarity and BM25 scores live on different scales, so averaging them is meaningless. RRF combines rank positions instead:

score(chunk) = Σ  1 / (k + rank)
with k = 60

Small example

Suppose chunk A ranks 1st by vector and 4th by keyword; chunk B ranks 3rd and 1st. With k = 60, both get a contribution of roughly 1/61 + 1/64 and 1/63 + 1/61. The fused list reorders by those combined rank scores, not by averaging raw similarity numbers.

LLM reranking

Up to about 20 fused candidates can be sent to Llama 3.1 for a relevance reorder. The reranker must return up to five allowed passage IDs. If the call fails or returns invalid output, the system falls back to the RRF order, retrieval never fails solely because reranking failed.

Generation

The final five passages, up to six recent history messages, and the current question go to the generation model. The RAG system prompt requires the model to use only retrieved context, cite exact chunk IDs, avoid guessing, and refuse when evidence is insufficient. Retrieved text is framed as reference material, never as instructions.

Grounding and citation honesty

Traceability is a first-class design goal, and so is being precise about what the code actually guarantees.

What the server does today

  • Extracts citation IDs matching [doc_id#chunk_index] from the model reply.
  • Compares those IDs to the retrieved set for that request.
  • Excludes unknown IDs from the structured citations array in the API response.
  • Logs rag_hallucinated_citation when an unknown ID appears.
  • Runs a deterministic lexical-overlap assessment via assessGrounding().
  • Logs rag_ungrounded_claim when a citation-bearing sentence looks unsupported.

What it does not do:

  • It does not rewrite the answer text to remove invalid citation markers or unsupported claims. Filtering applies to structured citation metadata; reply is not post-edited.
  • assessGrounding() is not a blocking safety gate. It is observability-only. Citation-ID validation is the hard gate on metadata.

Honest limitations

  • A real citation ID does not prove every claim in that sentence is true; it only confirms that the ID was among the retrieved set.
  • An uncited factual statement is not deterministically blocked today.
  • The frontend currently renders reply only. It does not show a source panel, even though the backend already returns citations and retrieved.

These are transparent engineering limitations and logical next improvements, not hidden failures.

Security around the AI

RAG did not replace the security model; it sits behind it. Every mutating request still has to clear the same edge controls before any retrieval or generation cost is spent.

Transport & browser policy

  • Cloudflare TLS at the edge
  • Environment-aware CORS allowlist (not authentication; does not stop non-browser clients)

Session integrity

  • Anonymous D1 sessions
  • HMAC-SHA256 CSRF tokens
  • Constant-time comparison

Abuse & cost control

  • D1 per-session message cap
  • KV per-IP and global rate limits
  • Server-side Turnstile after threshold

AI-specific controls

  • Direct prompt-injection regex before model calls
  • Retrieved context framed as reference, not instructions
  • Citation-ID metadata validation
  • Log-only groundedness assessment

Privacy & secrets

  • Salted SHA-256 IP hashing
  • Secrets via Cloudflare secrets store

Side-effect isolation

  • No model-driven tools or side effects
  • Resend called only by deterministic lead route code

Input validation, body size limits, and control-character stripping still apply at the API boundary. Lead email is sent only to me from Worker code after an explicit form submit, never from free-form model output.

Tools I used and why

Each tool has a narrow job. The stack stayed small on purpose: static frontend, serverless Worker, no frontend framework, no runtime npm dependencies in production Worker code.

HTML, CSS, vanilla JS

Portfolio and chat widget with no framework or bundler.

GitHub Pages

Static hosting for the public site and this case study.

TypeScript

Strict typing for the Worker API surface.

Cloudflare Workers

Serverless edge compute for every API request.

Workers AI

Hosted inference for chat, embeddings, and reranking.

Llama 3.1 8B FP8

Generation and optional passage reranking model.

BGE Base English

768-dim embeddings for queries and chunks.

Cloudflare D1

SQLite for sessions, messages, leads, and vectors.

FTS5 + BM25

Keyword retrieval in the same database.

Cloudflare KV

TTL rate-limit counters for IP and global caps.

Turnstile

Server-verified bot protection after sustained use.

Resend

Transactional lead notification email.

Wrangler

Local dev, secrets, D1, and Worker deploys.

Markdown + YAML

Hand-editable RAG corpus with frontmatter IDs.

Node.js scripts

Offline chunking and embedding/index generation.

node:test

Built-in test runner with zero extra framework.

Miniflare

In-memory D1 for realistic integration tests.

esbuild

Bundles TypeScript modules for the test suite.

Git + GitHub

Two-repo history, reviews, and feature-flag rollouts.

How I built and validated it

The original product shipped in seven foundation phases. RAG did not exist in those phases; they built the protected product first.

Phase Historical foundation scope
0 Accounts and infrastructure: D1, KV, Turnstile, Resend, DNS
1-2 Worker skeleton, D1 schema, session creation with CSRF issuance
3 AI chat, persistence, KV rate limits, server-side Turnstile
4 Frontend widget shell and full backend integration
5 Lead capture, intent detection, Resend notifications
6 Production hardening, secret audit, deployment

Later stage: RAG expansion and production rollout

A separate expansion stage added the retrieval system without discarding the foundation:

  • Markdown corpus and deterministic chunking
  • Embedding and indexing scripts
  • Hybrid retrieval, RRF, and optional LLM reranking
  • Citation metadata validation and grounding observations
  • Evaluation harness with a golden dataset
  • RAG_ENABLED rollout flag with legacy fallback

AI-assisted development with human ownership

  • Architecture and acceptance criteria remained human-owned.
  • AI coding tools accelerated implementation and review.
  • Changes were independently reviewed and tested (unit, integration, eval).
  • Security posture and factual accuracy remained the engineer’s responsibility.

How I measured it

A controlled golden set drives retrieval and safety checks against the real retrieval code and live Workers AI (via the evaluation harness), not mock scores invented for marketing.

35Golden cases (retrieval, refusal, injection)
5 / 21Documents / chunks
1.0000Recall@5, all four modes
0.9821Best MRR (hybrid + rerank)
1.0000Refusal pass rate
1.0000Direct injection pass rate
Mode Recall@5 MRR
Vector-only 1.0000 0.9643
Keyword-only 1.0000 0.8988
Hybrid without reranking 1.0000 0.9583
Hybrid with reranking 1.0000 0.9821

What these numbers mean

  • Recall@5: of the passages that should be found for a question, what fraction appear in the top five results?
  • MRR (Mean Reciprocal Rank): on average, how close to rank 1 is the first correct passage? Always first → 1.0; always second → 0.5.

Caveat: this is a small, controlled, single-author corpus. These results validate the implementation and test set; they do not prove performance at enterprise scale.

Tradeoffs and what comes next

Current tradeoffs

  • Exact O(n) vector scan fits 21 chunks; not a large corpus.
  • Reranking adds another inference round trip.
  • KV rate-limit increments are not atomic.
  • Re-indexing is manual when Markdown changes.
  • RAG tables are seeded via data/seed.sql, outside the main migration path.
  • Citation UI is not implemented in the widget.
  • Grounding assessment is log-only.
  • No automated CI/CD currently deploys the Worker.

Logical next improvements

  • Frontend citation and source panel
  • Enforced, better-validated groundedness
  • Automated indexing on content change
  • Larger and more adversarial evaluation set
  • Vectorize or another ANN index when size warrants it
  • Richer structured observability
  • Streaming responses
  • CI/CD and migration automation

None of the “next” items are claimed as shipped. They are the natural follow-ons once you look honestly at the current system.

Try it or read the source

Non-technical visitor

Open the chatbot and ask a hard question about background, projects, or availability. The system should retrieve evidence, answer, and notice hiring intent when it appears.

Ask Mihir AI