Case

LLM Evaluation PipelineNothing is ever dropped silently.

2025Internal toolDelivered

An internal QA tool for a company in a highly regulated industry. It automatically scores every AI support chatbot conversation on three dimensions, routes uncertain verdicts to a human review queue, and surfaces trends in an analytics dashboard.

Story

Separate upload from evaluation. Judge against real source material.

AI support chatbots in regulated industries can produce wrong answers, and the only way to know is to read every conversation yourself. There is no systematic way to measure quality degradation, catch a hallucination before it reaches a user, or give a QA team anything actionable. The chatbot ships; monitoring is hope.

Build a two-stage pipeline that separates upload speed from evaluation speed. Upload parses and queues immediately, returning 202 Accepted in under a second, no AI in the loop. Async cron handles evaluation: embed the question, retrieve the relevant knowledge base articles from the same RAG index the chatbot uses, and have Claude score the answer against actual source material, not general world knowledge. Verdicts feed a human review queue; the review queue feeds aggregate analytics.

Pipeline
UploadCSV export is parsed and deduplicatedPapaParse reads the chatbot export and MD5 content hashing deduplicates overlapping re-exports. Rows go into a queue table and the upload returns 202 Accepted before any AI runs.
QueueCron evaluates the oldest rowsEach run fetches the oldest unevaluated rows and runs five concurrently via Promise.allSettled. Upload latency and AI latency are fully decoupled.
RetrieveEmbed the question, pull the KB articlesThe evaluator retrieves the same knowledge base articles the chatbot was trained on before scoring.
JudgeClaude scores Honest, Helpful, HarmlessWeights are 40%, 35%, and 25%. The verdict formula lives in application code, not the prompt.
ReviewUncertain verdicts go to a humanReviewers accept or reject with notes. Evaluation errors surface here too rather than disappearing into the pipeline.
Shipped
  • RAG-grounded judge: judgment rests on actual documentation rather than world knowledge
  • Auditable thresholds: the verdict formula lives in application code, so they can be adjusted without a prompt engineering session
  • Harmless as an independent gate: an answer that scores well overall but poorly on harmless fails regardless, and no weighted average overrides it
  • Human review queue with accept or reject and reviewer notes
  • Analytics dashboard in Recharts: daily, weekly, and monthly volume, resolution rates by question category, peak hour, and week-over-week trends
Stack
FrameworkNext.js 16 · App Router · Vercel
LanguageTypeScript strict mode: enforced types on all evaluation shapes
UIReact 19 · Tailwind CSS v4 · shadcn/ui (card, table, tabs, badge)
ChartsRecharts 3.8: volume trends, hourly distribution, resolution rates
DatabaseSupabase: PostgreSQL, shared KB index with production chatbot
AI: judgeClaude Sonnet 4.5: structured output via Vercel AI SDK v6 Output.object()
AI: embeddingsOpenAI text-embedding-3-small: 1536-dim, same model as KB index
CSV parsingPapaParse 5.5.3: chatbot export format, malformed row handling
Outcome

Upload to verdict in under 30 seconds per pair. The QA team gets a scored, reviewable record of every conversation instead of sampling from a queue and hoping the sample is representative.

The distinction between “did the chatbot know this” and “is this generally true” is the whole point of RAG-grounded evaluation. An LLM judge without retrieved context applies world knowledge, which is the wrong standard for a chatbot that only covers one company's documentation. Grounding the judge in the same source of truth as the chatbot is what separates a useful eval tool from a confidence score nobody can act on.

Next caseContent PipelineTopic in, publication-ready article out.All work →

Have a problem that fits?

Get a plan by tomorrow →