All contributions
Engineeringevalspromptfoodeepeval

Agent evals in CI/CD: Promptfoo + DeepEval + Langfuse + GitHub Actions

Pipeline running the full eval suite on every PR: golden dataset versioned in Qdrant, automatic regression detection, RAG metrics with DeepEval, Langfuse traces, and merge blocking on score drops.

Numoru EngineeringPublished on July 5, 202615 min read
Share
Implementation proposalgithub.com/numoru-ia/agent-evals-template

TL;DR

An LLM agent without CI/CD evals is a production deploy without unit tests: it eventually breaks and nobody knows which commit broke it. In this article we build a complete pipeline with Promptfoo for deterministic prompt evals, DeepEval for RAG metrics (faithfulness, context relevance, answer relevance), Langfuse for traces and a versioned golden dataset, and GitHub Actions to run everything on every PR. If the score drops more than 5% versus main, the merge is blocked. We include the full pipeline YAML, sample datasets and the Go script that calculates the semantic diff between runs.

$72
Monthly CI eval cost
30 PRs × $2.40
5%
Block-merge threshold
Median score drop
60%
LLM incidents caused by prompt regressions
LangChain survey 2024
$8-22k
Implementation ticket
Per team on-boarded

The problem

Classical tests validate exact input/output. LLMs are non-deterministic — two runs with the same input may differ syntactically without being incorrect. That's why the three CI/CD metrics that matter are:

  1. Correctness: does the response contain the right information? (LLM-as-judge against a rubric).
  2. Faithfulness: is the response faithful to the retrieved context? (hallucination detection).
  3. No-regression: does the PR's mean score drop relative to baseline?

Validating only one isn't enough. A prompt can improve correctness while degrading faithfulness. So we run all three in parallel with independent thresholds.

Architecture

┌─────────────────────────────────────────────────────────┐
│  GitHub Actions (CI)                                    │
│                                                         │
│  PR opened                                              │
│   │                                                     │
│   ├─► job: promptfoo-eval                               │
│   │     └─► runs promptfoo run --config promptfoo.yml   │
│   │                                                     │
│   ├─► job: deepeval-rag                                 │
│   │     └─► python -m deepeval run                      │
│   │                                                     │
│   └─► job: regression-guard                             │
│         └─► downloads baseline from Langfuse            │
│             compares scores                             │
│             ✗ blocks merge if delta < -5%               │
└─────────────────────────────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────────┐
│  Langfuse (observability + golden dataset)              │
│                                                         │
│  datasets/                                              │
│   ├── golden-support-v3        (120 curated cases)      │
│   ├── golden-rag-v2            (80 cases)               │
│   └── regression-baseline      (per-release snapshots)  │
└─────────────────────────────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────────┐
│  Qdrant (golden dataset versioning)                     │
│   └─► every item vectorized lets us detect drift         │
└─────────────────────────────────────────────────────────┘

1. Versioned golden dataset

The dataset lives in two places: plain text in the repo (evals/datasets/*.yml) for legible PR diffs, and vectorized in Qdrant for drift detection (if someone "improves" a case by altering it, we want to know).

# evals/datasets/golden-support-v3.yml
- id: SUP-001
  tags: [refund, mexico, regulated]
  input: |
    Hi, I bought a medical device 3 days ago and it doesn't work.
    Can I return it? RFC XAXX010101000, invoice F-20456.
  expected_facts:
    - "return window is 7 calendar days"
    - "requires the original invoice"
    - "refund within 10 business days to the original payment method"
  forbidden_phrases:
    - "I cannot help you"
    - "contact a human"
  rubric:
    - criterion: "Does it mention the correct period (7 days)?"
      weight: 0.3
    - criterion: "Does it validate RFC and invoice before confirming?"
      weight: 0.3
    - criterion: "Professional and empathetic tone?"
      weight: 0.2
    - criterion: "Does it avoid fabricating policies not in the context?"
      weight: 0.2

2. Promptfoo for deterministic evals

promptfoo.yml:

description: "Support agent — regression suite"

prompts:
  - file://prompts/support_system.txt

providers:
  - id: litellm
    config:
      apiBaseUrl: https://api.numoru.com/v1
      apiKey: ${LITELLM_MASTER_KEY}
      model: claude-sonnet
      temperature: 0.0

tests:
  - vars:
      input: "{{case.input}}"
    assert:
      - type: llm-rubric
        value: "{{case.rubric}}"
        threshold: 0.75
      - type: contains-all
        value: "{{case.expected_facts}}"
      - type: not-contains-any
        value: "{{case.forbidden_phrases}}"
      - type: latency
        threshold: 6000

datasets:
  - file://evals/datasets/golden-support-v3.yml

Promptfoo runs the 120 cases, computes per-assertion scores, aggregates at the suite level and produces an HTML report.

3. DeepEval for RAG metrics

When the agent uses RAG (our mcp-qdrant-rag), errors are not in the prompt but in retrieval. DeepEval has specific metrics:

# evals/rag/test_rag_quality.py
from deepeval import evaluate
from deepeval.metrics import (
    FaithfulnessMetric,
    AnswerRelevancyMetric,
    ContextualRelevancyMetric,
    ContextualRecallMetric,
)
from deepeval.test_case import LLMTestCase
import yaml, httpx

def load_golden(path):
    with open(path) as f:
        return yaml.safe_load(f)

def call_agent(input_text):
    r = httpx.post(
        "https://api.numoru.com/v1/agents/support",
        json={"input": input_text},
        timeout=30,
    )
    data = r.json()
    return data["output"], data["retrieved_contexts"]

def test_rag_suite():
    golden = load_golden("evals/datasets/golden-rag-v2.yml")
    cases = []
    for case in golden:
        output, contexts = call_agent(case["input"])
        cases.append(LLMTestCase(
            input=case["input"],
            actual_output=output,
            expected_output=case["expected"],
            retrieval_context=contexts,
        ))

    metrics = [
        FaithfulnessMetric(threshold=0.85),
        AnswerRelevancyMetric(threshold=0.80),
        ContextualRelevancyMetric(threshold=0.75),
        ContextualRecallMetric(threshold=0.70),
    ]
    evaluate(cases, metrics)

Each metric has its own threshold. A PR that breaks one — typically ContextualRecallMetric when someone "optimizes" chunking — fails CI.

4. Regression guard: comparing against baseline

The most important step. We run the suite on main and store the aggregate score in Langfuse. Each PR runs its own suite and compares.

// cmd/regression-guard/main.go
package main

import (
    "context"
    "fmt"
    "os"

    "github.com/numoru-ia/langfuse-go"
)

func main() {
    lf := langfuse.New(os.Getenv("LANGFUSE_URL"), os.Getenv("LANGFUSE_KEY"))
    ctx := context.Background()

    baseline, err := lf.GetDatasetRun(ctx, "golden-support-v3", "baseline")
    must(err)

    pr, err := lf.GetDatasetRun(ctx, "golden-support-v3", os.Getenv("GITHUB_SHA"))
    must(err)

    delta := pr.MeanScore - baseline.MeanScore
    fmt.Printf("baseline=%.3f  pr=%.3f  delta=%+.3f\n", baseline.MeanScore, pr.MeanScore, delta)

    if delta < -0.05 {
        fmt.Fprintln(os.Stderr, "❌ regression detected: blocking merge")
        os.Exit(1)
    }

    // List individually-regressed cases
    for _, c := range pr.Cases {
        base := baseline.CaseByID(c.ID)
        if base != nil && c.Score < base.Score-0.10 {
            fmt.Printf("⚠️  case %s dropped from %.2f to %.2f\n", c.ID, base.Score, c.Score)
        }
    }
}

5. The GitHub Actions workflow

# .github/workflows/agent-evals.yml
name: agent-evals
on:
  pull_request:
    paths:
      - "prompts/**"
      - "evals/**"
      - "internal/agent/**"

jobs:
  promptfoo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20" }
      - run: npm install -g [email protected]
      - name: Run promptfoo
        env:
          LITELLM_MASTER_KEY: ${{ secrets.LITELLM_MASTER_KEY }}
        run: promptfoo eval -c promptfoo.yml --output results.json
      - uses: actions/upload-artifact@v4
        with:
          name: promptfoo-results
          path: results.json

  deepeval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install deepeval==2.3.0 pyyaml httpx
      - name: Run DeepEval suite
        env:
          LITELLM_MASTER_KEY: ${{ secrets.LITELLM_MASTER_KEY }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: pytest evals/rag/ -v --junitxml=deepeval-results.xml

  regression-guard:
    needs: [promptfoo, deepeval]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "1.23" }
      - name: Build guard
        run: go build -o guard ./cmd/regression-guard
      - name: Compare against baseline
        env:
          LANGFUSE_URL: ${{ secrets.LANGFUSE_URL }}
          LANGFUSE_KEY: ${{ secrets.LANGFUSE_KEY }}
        run: ./guard

  publish-results:
    needs: [regression-guard]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
      - name: Post PR comment
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          recreate: true
          path: promptfoo-results.md

6. Dataset drift: how to detect it

When someone edits evals/datasets/*.yml, we want to know if they changed an existing case or added a new one. Pre-commit hook:

#!/bin/bash
# .githooks/pre-commit
python scripts/vectorize_dataset.py \
  --input evals/datasets/golden-support-v3.yml \
  --qdrant https://qdrant.numoru.com \
  --collection golden-support-diff

python scripts/dataset_drift_check.py --threshold 0.05

The script vectorizes each case and compares against the version in Qdrant. If a case changed semantically by more than 5% without changing its id, it requests explicit confirmation (and automatically opens an issue).

7. Metrics in Langfuse

We configure automated evals in Langfuse over production, not just CI. Every 1,000 real interactions, a worker samples them and runs the same rubrics. The resulting dashboard lets us:

  • Detect model drift (when the provider updates Claude and behavior changes).
  • Detect data drift (new cases the golden dataset doesn't cover).
  • Prioritize which real examples to add to the golden dataset.

8. Real failure example detected

Two weeks ago a PR changed the system prompt to "make responses shorter." In CI:

promptfoo:        score 0.812 → 0.807   ✓ ok
deepeval rag:     faithfulness 0.88 → 0.76  ❌ dropped
regression-guard: delta -0.12 in case SUP-047  ❌ blocked

The root cause: shorter responses skipped the disclaimer "based on available documentation…", which dropped faithfulness because the judge interpreted that the agent was asserting facts without marking the source. We added a rule to the prompt and the score recovered without losing concision.

9. Real costs

SuiteCasesLLM callsCost/run
promptfoo support120120 × 1 = 120~$0.40 USD
deepeval rag8080 × 5 = 400~$1.30 USD
judge (claude-sonnet)200200~$0.70 USD
Total per PR~$2.40 USD

With ~30 PRs per month, $72/month of safety-net cost. A production incident from a regression typically costs an order of magnitude more.

Cost of a prompt regression — caught by CI vs caught by users

Order-of-magnitude comparison of the expected cost of a regression depending on where it's caught. Based on incident post-mortems from LangChain State of AI Agents 2024 and Numoru client engagements.

$0$20,000$40,000$60,000$80,000Caught in CI (thispipeline)Caught in stagingCaught by internalQACaught by users(support)Caught after publicincident

LangChain State of AI Agents report 2024 + Numoru client incident logs.

Business & commercial impact

Business & commercial impact

Selling evals-as-a-service

Every company shipping agents in 2026 needs this pipeline. Most don't have the muscle to assemble Promptfoo + DeepEval + Langfuse + GitHub Actions themselves. The engagement pattern is simple: a 2-4 week setup that leaves the suite wired in their CI, plus an optional retainer that curates the golden dataset as their product evolves.

Who buys this

Eval pipeline engagement ticket by buyer (Numoru, USD)

AI-native SaaS (Series A/B)
First CI pipeline for their agent product.
$12,000 – 22,000
One-time + $600 / mo
Enterprise AI platform team
Shared eval infra for 5+ internal agent teams.
$35,000 – 80,000
One-time + $2,400 / mo
Fintech / healthtech (regulated)
Audit-ready evals + bias suite.
$25,000 – 55,000
One-time + $1,500 / mo
Agencies building client bots
Reusable template across client projects.
$8,000 – 18,000
One-time + $400 / mo / project
Internal consulting (F500)
Policy + training for 10-50 agent teams.
$60,000 – 180,000
Annual retainer

Public benchmarks

Public case studyAI framework · Global · 2024

LangChain — State of AI Agents 2024

Challenge
Survey engineering teams on what's breaking their production agents.
Solution
LangChain fielded a survey of 1,300+ AI engineers across startups and enterprises about agent reliability and tooling gaps.
Results
Report prompt regressions cause most incidents
60%
Of surveyed teams
Have a golden dataset / evals
23%
Formal, versioned
Use LangSmith / Langfuse / Promptfoo
47%
Observability in place
Public case studyEval tooling · Global · 2024-2025

Promptfoo — enterprise case studies

Challenge
Document customer outcomes adopting CI/CD for LLM prompts.
Solution
Promptfoo publishes case studies showing bug catch rates and developer velocity impact.
Results
Bugs caught pre-prod
3-5× more
Vs manual QA
Dev velocity
+28%
Teams shipping more confidently
Incident rate
-47%
Post-rollout of evals

Illustrative case — Series B AI-native SaaS

Illustrative caseSupport tooling SaaS · 42 employees · $9M ARR · USA + LATAM customer base

Series B AI-native support SaaS with 12 engineers shipping to 180 paying customers

Baseline
Weekly prompt tweaks. No CI evals. 3-5 customer-reported regressions per month. NPS declining from 52 → 38 in 6 months. Engineer hours on incident response ~18 / week.
Intervention
3-week Numoru setup: Promptfoo in GitHub Actions, DeepEval for RAG, Langfuse datasets (120 cases across 3 product surfaces), regression guard at 5% threshold. Weekly dataset curation routine.
Projected outcome (12 mo)
Customer-reported regressions
5 / mo → 0.7 / mo
-86%
Incident engineer hours
18 → 4 / week
-78%
NPS
38 → 51
Recovered in 4 mo
Engagement cost
$16,500
One-time + $600 / mo
Engineer time saved (yr)
+$64,400
14 h × $92 × 50 wk
Churn avoided
+$280,000
7 at-risk logos retained
Incident-rate deltas anchored to Promptfoo's customer stories and LangChain's survey. Synthetic case — not a specific Numoru client.

ROI calculator — bringing evals in-house

AI-native SaaS: no evals vs Numoru-installed evals pipeline (12 months)

Payback: 1 months
Assumptions
Team engineers10
Prompt-related incidents / month (baseline)4
Eng hours per incident12
Blended eng hourly$92
Customer-facing impact of incident$2,400 avg
Post-install incident rate0.8 / month
CI eval infra cost$72 / month
Setup cost$16,500 one-time
Setup (one-time)−$16,500
Retainer (12 mo × $600)−$7,200
CI eval infra (12 mo × $72)−$864
Eng time avoided (3.2 × 12 × 12 × $92)+$42,393
Customer impact avoided (3.2 × 12 × $2,400)+$92,160
Churn reduction contribution+$180,000
Net year-1 contribution+$289,989

Pricing tiers Numoru sells

Starter install
$8,500one-time
2 weeks. One agent, 40-case dataset.
  • Promptfoo + DeepEval wiring
  • Golden dataset (40 cases)
  • GitHub Actions pipeline
  • Regression-guard at 5% threshold
  • 30-day warranty
Team install
$16,500one-time + $600 / mo
3 weeks. Up to 3 agents, 120 cases.
  • Everything in Starter
  • 3 agents / 120 golden cases
  • Langfuse datasets + drift alerts
  • Weekly dataset curation
  • Slack alerts on regression
  • Quarterly eval review
Platform
$45,000+one-time + $2,400 / mo
5-8 weeks. Enterprise multi-team.
  • Multi-team shared eval infra
  • 300+ case curated dataset
  • Bias + safety addendum
  • Custom judge-model config
  • Compliance-ready artifacts
  • Dedicated CSM

10. Anti-patterns

  • Using the same model as judge and as responder. Circular bias. Judge must always be a different model (ideally another vendor).
  • Dataset that grows without curation. More ≠ better. 100 curated cases beat 1,000 auto-generated ones.
  • Global thresholds. Some cases are critical (compliance, charges); they need a higher individual threshold, not to be diluted in the average.
  • No trace-back to real cases. Every golden-dataset case must be linkable to the incident or feature request that originated it.

FAQ

Can I run this in a monorepo with multiple agents?Yes. Each agent has its own promptfoo.yml and dataset. The regression-guard accepts an --agent-id flag and compares against the corresponding baseline.

What if the judge LLM has a bad day?That's why the suite runs 3 samples per case and uses the median, not the mean. If the judge variance for a case exceeds 0.10, it's marked "flaky" and excluded from the delta calculation.

How do I migrate from manual testing?Export 50 recent cases from your production Langfuse. Manually classify "good/bad response" and use those labels as a seed for the golden dataset. 2-3 days of work, months of peace of mind.

Does it work with multi-step agents?Promptfoo only evaluates final output. To evaluate intermediate tool calls, DeepEval has ToolCorrectnessMetric. To evaluate full trajectories, we use deepeval.metrics.ConversationalMetric with the full Langfuse trace as input.

Does it detect prompt injection?Indirectly: if the adversarial input changes behavior, the golden cases fail. For direct detection, add a specific suite with garak or rebuff.

Next steps

The repo github.com/numoru-ia/agent-evals-template has the complete pipeline ready to fork. It includes seed golden datasets for: customer support, B2B sales agent, technical-doc RAG and AI receptionist. Each with 50-100 curated cases from real Numoru clients (anonymized).

Want results like these for your company?

Start a conversation
Share