Siddhant Deval
Siddhant Deval
system design5 min read

Production Evals & Guardrails: Measuring Retrieval and Generation

Manually spot-checking AI prompts in development playgrounds fails to prevent regressions when system prompts, chunking algorithms, or embedding models change. This article implements automated CI evaluation pipelines using Ragas metrics — Faithfulness, Answer Relevance, Context Recall — and calibrated LLM-as-a-Judge grading to gate every pull request modifying AI components.

Series·Part 22 of 24

Full-Stack AI Architecture: From Streaming Foundations to Resilient Agentic Systems in Node.js & React

Production Evals & Guardrails: Ragas CI Pipeline

"It seems to be working" is not a quality metric. A RAG system that scores 94% on Faithfulness in week 1 might score 71% in week 8 — after three prompt changes, a chunk size adjustment, and an embedding model update — without anyone noticing until users start filing support tickets about hallucinated citations. Regression detection requires automated measurement.

This article implements a Ragas evaluation pipeline as a CI gate: automated test cases that score Faithfulness, Answer Relevance, and Context Recall on every pull request — blocking merges that regress RAG quality below configured thresholds. The evaluators run as LLM-as-a-Judge scoring functions, the test suite is deterministic and re-runnable, and the results are reported as GitHub PR annotations.

Architectural Note

Who this is for: Engineers who have shipped RAG features without objective quality metrics and want to prevent silent regression as the system evolves. After this article, RAG quality is a first-class CI signal alongside test coverage and type checks.


1. The Three Core RAG Metrics

Metric Question It Answers Formula Intuition
Faithfulness Does the answer contain only claims that can be verified in the retrieved context? Claims in answer that appear in context / Total claims in answer
Answer Relevance Is the answer actually answering the user's question? Avg cosine similarity of back-generated questions to original question
Context Recall Does the retrieved context contain all information needed for the ground-truth answer? Ground-truth sentences attributable to context / Total ground-truth sentences

2. Evaluation Dataset Structure

TYPESCRIPT
// ✅ Canonical evaluation dataset schema
// file: tests/evals/fixtures/rag-eval-dataset.ts

export interface EvalCase {
  id: string
  question: string
  groundTruthAnswer: string    // Written by human domain expert
  groundTruthChunks?: string[] // Expected relevant chunks (for Context Recall)
  metadata: {
    difficulty: 'easy' | 'medium' | 'hard'
    topic: string
    addedAt: string
  }
}

export interface EvalResult {
  caseId: string
  question: string
  retrievedChunks: string[]
  generatedAnswer: string
  faithfulnessScore: number     // 0.0 – 1.0
  answerRelevanceScore: number  // 0.0 – 1.0
  contextRecallScore: number    // 0.0 – 1.0 (when groundTruthChunks provided)
}

// Minimum acceptable scores — failure below these blocks the PR
export const QUALITY_THRESHOLDS = {
  faithfulness: 0.88,
  answerRelevance: 0.82,
  contextRecall: 0.80,
}

3. LLM-as-a-Judge Evaluators

3.1 Faithfulness Scorer

TYPESCRIPT
// ✅ Faithfulness evaluator — LLM-as-a-Judge
// file: tests/evals/evaluators/Faithfulness.ts

import { generateObject } from 'ai'
import { createAnthropic } from '@ai-sdk/anthropic'
import { z } from 'zod'

const anthropic = createAnthropic()

const ClaimsSchema = z.object({
  claims: z.array(z.object({
    claim: z.string(),
    supportedByContext: z.boolean(),
    evidence: z.string().optional(),
  })),
  faithfulnessScore: z.number().min(0).max(1),
})

export async function scoreFaithfulness(
  question: string,
  answer: string,
  context: string[],
): Promise<{ score: number; breakdown: z.infer<typeof ClaimsSchema> }> {
  const contextText = context.map((c, i) => `[${i + 1}] ${c}`).join('\n\n')

  const { object } = await generateObject({
    model: anthropic('claude-3-5-sonnet-20241022'),
    schema: ClaimsSchema,
    temperature: 0,
    prompt: `Evaluate the faithfulness of the following answer to the retrieved context.

QUESTION: ${question}

RETRIEVED CONTEXT:
${contextText}

ANSWER TO EVALUATE:
${answer}

For each distinct factual claim in the answer:
1. Identify the claim
2. Determine if it is directly supported by the retrieved context
3. If supported, cite the evidence from context

Then compute faithfulnessScore = (supported claims) / (total claims).
If the answer makes no factual claims, score 1.0.`,
  })

  return { score: object.faithfulnessScore, breakdown: object }
}

3.2 Answer Relevance Scorer

TYPESCRIPT
// ✅ Answer Relevance — back-generation method
// file: tests/evals/evaluators/AnswerRelevance.ts

import { generateObject } from 'ai'
import { createAnthropic } from '@ai-sdk/anthropic'
import { z } from 'zod'
import { embedAndNormalise } from '../../../packages/db/src/embeddings'

const anthropic = createAnthropic()

export async function scoreAnswerRelevance(
  originalQuestion: string,
  answer: string,
  n = 3,  // Number of back-generated questions
): Promise<number> {
  const { object } = await generateObject({
    model: anthropic('claude-3-haiku-20240307'),
    schema: z.object({ questions: z.array(z.string()).length(n) }),
    temperature: 0.2,
    prompt: `Given this answer, generate ${n} different questions that this answer could be responding to.
ANSWER: ${answer}
Generate exactly ${n} questions.`,
  })

  // Embed original question and all back-generated questions
  const [origEmbedding, ...genEmbeddings] = await Promise.all([
    embedAndNormalise(originalQuestion),
    ...object.questions.map(q => embedAndNormalise(q)),
  ])

  // Compute cosine similarity (dot product of normalised vectors)
  const similarities = genEmbeddings.map(gen =>
    gen.reduce((sum, val, i) => sum + val * origEmbedding[i], 0)
  )

  return similarities.reduce((sum, s) => sum + s, 0) / similarities.length
}

4. CI Integration (GitHub Actions)

YAML
# .github/workflows/rag-evals.yml
name: RAG Quality Gate

on:
  pull_request:
    paths:
      - 'src/gateway/**'
      - 'packages/db/src/retrieval/**'
      - 'tests/evals/**'
      - 'content/**'

jobs:
  rag-evals:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: '22' }

      - run: npm ci --workspace packages/db

      - name: Run RAG evaluation suite
        run: node tests/evals/run-evals.mjs
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
          EVAL_DATASET: tests/evals/fixtures/rag-eval-dataset.json

      - name: Upload eval results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: rag-eval-results
          path: tests/evals/results/latest.json
TYPESCRIPT
// ✅ Eval runner — blocks CI on threshold violation
// file: tests/evals/run-evals.mjs

import { dataset } from './fixtures/rag-eval-dataset.json' with { type: 'json' }
import { retrieve } from '../../packages/db/src/retrieval/RetrievalPipeline.ts'
import { scoreFaithfulness } from './evaluators/Faithfulness.ts'
import { scoreAnswerRelevance } from './evaluators/AnswerRelevance.ts'
import { QUALITY_THRESHOLDS } from './fixtures/rag-eval-dataset.ts'

const results = []
let passed = 0
let failed = 0

for (const evalCase of dataset) {
  const chunks = await retrieve('ci-org', evalCase.question, { finalCount: 5 })
  const answer = await generateAnswer(evalCase.question, chunks)

  const [faithfulness, relevance] = await Promise.all([
    scoreFaithfulness(evalCase.question, answer, chunks.map(c => c.content)),
    scoreAnswerRelevance(evalCase.question, answer),
  ])

  const caseResult = {
    id: evalCase.id,
    faithfulness: faithfulness.score,
    answerRelevance: relevance,
    passed: faithfulness.score >= QUALITY_THRESHOLDS.faithfulness &&
            relevance >= QUALITY_THRESHOLDS.answerRelevance,
  }

  results.push(caseResult)
  if (caseResult.passed) passed++; else failed++

  console.log(`[${caseResult.passed ? '✓' : '✗'}] ${evalCase.id}: ` +
    `F=${caseResult.faithfulness.toFixed(2)} AR=${caseResult.answerRelevance.toFixed(2)}`)
}

const avgFaithfulness = results.reduce((s, r) => s + r.faithfulness, 0) / results.length
const avgRelevance = results.reduce((s, r) => s + r.answerRelevance, 0) / results.length

console.log(`\nSummary: ${passed}/${results.length} passed`)
console.log(`Avg Faithfulness: ${avgFaithfulness.toFixed(3)} (threshold: ${QUALITY_THRESHOLDS.faithfulness})`)
console.log(`Avg Answer Relevance: ${avgRelevance.toFixed(3)} (threshold: ${QUALITY_THRESHOLDS.answerRelevance})`)

if (failed > 0 || avgFaithfulness < QUALITY_THRESHOLDS.faithfulness) {
  console.error('\n❌ RAG quality gate FAILED')
  process.exit(1)
}

console.log('\n✅ RAG quality gate PASSED')

Summary

Metric Implementation CI Threshold
Faithfulness Claim extraction + context attribution (LLM-as-a-Judge) ≥ 0.88
Answer Relevance Back-generation + embedding cosine similarity ≥ 0.82
Context Recall Ground-truth attribution scoring ≥ 0.80

What's Next

Part 23 instruments the full Workspace stack with OpenTelemetry: LLM-specific spans (TTFT, ITL, token count), RAG retrieval spans (ANN latency, reranker scores, chunks selected), and per-tenant cost attribution — all wired to a Grafana dashboard via the GenAI semantic conventions.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Evaluation#Ragas#Guardrails#CI/CD#LLM#RAG#Production#Quality Assurance
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.

Series·Part 22 of 24

Full-Stack AI Architecture: From Streaming Foundations to Resilient Agentic Systems in Node.js & React

Curriculum Roadmap
01The Full-Stack AI Paradigm Shift: Beyond Toy Wrappers & Prompt Concatenation
02The Anatomy of an LLM Call: Tokens, Context Windows & Probabilistic Mechanics
03Structured Outputs & Formal Invariants: Taming Non-Determinism with Zod & TypeBox
04Full-Stack Streaming Protocols: SSE, Fetch Streams & WebSockets
05V8 Streams, Server-Side Backpressure & Cancellations in Node.js
06React 18/19 Streaming UI & Generative Components
07Generative UI & Interactive Canvas Synchronization
08High-Dimensional Vector Embeddings & Similarity Math
09Vector Storage & Indexing at Scale: HNSW vs IVF in PostgreSQL (pgvector)
10Hybrid Search: Dense Vectors Meet Sparse Lexical BM25
11Multi-Stage Retrieval & Cross-Encoder Reranking
12Advanced RAG Architectures: Contextual Retrieval & Parent-Document Splitting
13Query Transformation, Decomposition & Multi-Query Expansion
14Dynamic Context Window Management & Long-Context Packing
15Agent Loops & Deterministic State Machines
16Model Context Protocol (MCP) in Full-Stack Architecture
17Human-in-the-Loop & Durable Agent Workflows
18Multi-Agent Orchestration & Collaborative Swarms
19Semantic Caching & Provider Cost Optimization
20Multi-Provider AI Gateways: Fallbacks, Rate Limiting & Hedged Requests
21Client-Side AI & Edge Hybrid Execution: WebGPU & Transformers.js
22Production Evals & Guardrails: Measuring Retrieval and Generation
Reading
23Full-Stack AI Observability & Tracing with OpenTelemetry
24Production Capstone: Assembling The Enterprise Collaborative AI Workspace