Todas las contribuciones
Ingenieríaevalspromptfoodeepeval

Evals de agentes en CI/CD: Promptfoo + DeepEval + Langfuse + GitHub Actions

Pipeline que corre la suite completa de evals en cada PR: dataset dorado versionado en Qdrant, detección automática de regresiones, métricas RAG con DeepEval, trazas en Langfuse y bloqueo de merge si el score baja.

Numoru EngineeringPublicado el 5 de julio de 202615 min de lectura
Compartir
Propuesta de implementacióngithub.com/numoru-ia/agent-evals-template

TL;DR

Un agente LLM sin evals en CI/CD es un deploy a producción sin tests unitarios: eventualmente se rompe, y nadie sabe qué commit lo rompió. En este artículo armamos un pipeline completo con Promptfoo para evals deterministas de prompts, DeepEval para métricas de RAG (faithfulness, context relevance, answer relevance), Langfuse para trazas y dataset dorado versionado, y GitHub Actions para correr todo en cada PR. Si el score cae más de 5% respecto a main, el merge se bloquea. Incluimos el pipeline YAML completo, los datasets de ejemplo y el script Go que calcula el diff semántico entre corridas.

$72
Costo mensual CI evals
30 PRs × $2.40
5%
Threshold de bloqueo
Caída de score mediana
60%
Incidentes LLM por regresiones
LangChain survey 2024
$8-22k
Ticket de implementación
Por equipo on-boarded

El problema

Los tests clásicos validan entradas/salidas exactas. Los LLMs son no deterministas — dos ejecuciones con el mismo input pueden diferir sintácticamente sin ser incorrectas. Por eso las tres métricas que importan en CI/CD son:

  1. Correctness: ¿la respuesta contiene la información correcta? (LLM-as-judge sobre rúbrica).
  2. Faithfulness: ¿la respuesta es fiel al contexto recuperado? (detección de alucinaciones).
  3. No-regression: ¿la score media del PR baja respecto al baseline?

Validar una sola no alcanza. Un prompt puede mejorar correctness pero empeorar faithfulness. Por eso corremos las tres en paralelo con umbrales independientes.

Arquitectura

┌─────────────────────────────────────────────────────────┐
│  GitHub Actions (CI)                                    │
│                                                         │
│  PR abierto                                             │
│   │                                                     │
│   ├─► job: promptfoo-eval                               │
│   │     └─► corre promptfoo run --config promptfoo.yml  │
│   │                                                     │
│   ├─► job: deepeval-rag                                 │
│   │     └─► python -m deepeval run                      │
│   │                                                     │
│   └─► job: regression-guard                             │
│         └─► descarga baseline de Langfuse               │
│             compara scores                              │
│             ✗ bloquea merge si delta < -5%              │
└─────────────────────────────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────────┐
│  Langfuse (observability + golden dataset)              │
│                                                         │
│  datasets/                                              │
│   ├── golden-support-v3        (120 casos curados)      │
│   ├── golden-rag-v2            (80 casos)               │
│   └── regression-baseline      (snapshots por release)  │
└─────────────────────────────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────────┐
│  Qdrant (versionado del dataset dorado)                 │
│   └─► cada ítem vectorizado permite detectar drift       │
└─────────────────────────────────────────────────────────┘

1. Dataset dorado versionado

El dataset vive en dos sitios: texto plano en el repo (evals/datasets/*.yml) para diffs legibles en PR, y vectorizado en Qdrant para detección de drift (si alguien "mejora" un caso alterándolo, queremos saber).

# evals/datasets/golden-support-v3.yml
- id: SUP-001
  tags: [refund, mexico, regulated]
  input: |
    Hola, compré un dispositivo médico hace 3 días y no me sirve.
    ¿Puedo devolverlo? RFC XAXX010101000, factura F-20456.
  expected_facts:
    - "período de devolución es de 7 días naturales"
    - "requiere factura original"
    - "reembolso en 10 días hábiles al método original"
  forbidden_phrases:
    - "no puedo ayudarte"
    - "contacta a un humano"
  rubric:
    - criterion: "¿Menciona el período correcto (7 días)?"
      weight: 0.3
    - criterion: "¿Valida RFC y factura antes de confirmar?"
      weight: 0.3
    - criterion: "¿Tono profesional y empático?"
      weight: 0.2
    - criterion: "¿No fabrica políticas que no están en el contexto?"
      weight: 0.2

2. Promptfoo para evals deterministas

promptfoo.yml:

description: "Agente de soporte — suite de regresión"

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 corre los 120 casos, calcula score por assertion, agrega a nivel de suite, y genera un reporte HTML.

3. DeepEval para métricas RAG

Cuando el agente usa RAG (nuestro mcp-qdrant-rag), los errores no son de prompt sino de retrieval. DeepEval tiene métricas específicas:

# 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)

Cada métrica tiene su propio umbral. Un PR que rompa uno — típicamente ContextualRecallMetric cuando alguien "optimiza" el chunking — falla CI.

4. Regression guard: comparar contra baseline

El paso más importante. Corremos la suite en main y guardamos el score agregado en Langfuse. Cada PR corre su propia suite y compara.

// 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, "❌ regresión detectada: bloqueando merge")
        os.Exit(1)
    }

    // Lista casos que regresionaron individualmente
    for _, c := range pr.Cases {
        base := baseline.CaseByID(c.ID)
        if base != nil && c.Score < base.Score-0.10 {
            fmt.Printf("⚠️  caso %s bajó de %.2f a %.2f\n", c.ID, base.Score, c.Score)
        }
    }
}

5. El workflow de GitHub Actions

# .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: cómo detectarlo

Cuando alguien edita evals/datasets/*.yml, queremos saber si cambió un caso existente o añadió uno nuevo. Hook pre-commit:

#!/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

El script vectoriza cada caso y compara contra la versión en Qdrant. Si un caso cambió semánticamente más de 5% sin cambiar su id, pide confirmación explícita (y abre una issue automáticamente).

7. Métricas en Langfuse

Configuramos evals automáticos en Langfuse sobre producción, no solo en CI. Cada 1000 interacciones reales, un worker las muestrea y corre las mismas rúbricas. El dashboard resultante permite:

  • Detectar drift de modelo (cuando el provider actualiza Claude y el comportamiento cambia).
  • Detectar drift de datos (nuevos casos que el dataset dorado no cubre).
  • Priorizar qué ejemplos reales agregar al dataset dorado.

8. Ejemplo real de falla detectada

Hace dos semanas un PR cambió el prompt del sistema para "hacer respuestas más cortas". En CI:

promptfoo:        score 0.812 → 0.807   ✓ ok
deepeval rag:     faithfulness 0.88 → 0.76  ❌ bajó
regression-guard: delta -0.12 en caso SUP-047  ❌ bloqueado

La raíz: respuestas más cortas omitían el disclaimer "en base a la documentación disponible...", lo que bajó la faithfulness porque el juez interpretaba que el agente estaba afirmando hechos sin marcar su fuente. Se añadió una regla al prompt y el score recuperó sin perder concisión.

9. Costos reales

SuiteCasosLlamadas LLMCosto/run
promptfoo support120120 × 1 = 120~0.40 USD
deepeval rag8080 × 5 = 400~1.30 USD
judge (claude-sonnet)200200~0.70 USD
Total por PR~2.40 USD

Con ~30 PRs por mes, cuesta 72 USD/mes tener un red de seguridad completa. Un incidente en producción por regresión cuesta (típicamente) un orden de magnitud más.

Costo de una regresión de prompt — detectada en CI vs detectada por usuarios

Comparación de orden de magnitud del costo esperado según dónde se detecta la regresión. Basado en post-mortems del reporte LangChain State of AI Agents 2024 y engagements Numoru.

$0$20,000$40,000$60,000$80,000Detectada en CI(este pipeline)Detectada enstagingDetectada por QAinternoDetectada porusuarios (soporte)Detectada trasincidente público

LangChain State of AI Agents 2024 + logs de incidentes de clientes Numoru.

Impacto de negocio y casos

Business & commercial impact

Vender evals-as-a-service

Toda empresa shipeando agentes en 2026 necesita este pipeline. La mayoría no tiene el músculo para armar Promptfoo + DeepEval + Langfuse + GitHub Actions por sí misma. El patrón de engagement es simple: setup de 2-4 semanas que deja la suite cableada en su CI, más retainer opcional que cura el golden dataset conforme evoluciona el producto.

Quién lo compra

Ticket del pipeline de evals por comprador (Numoru, USD)

SaaS AI-native (Series A/B)
Primer pipeline de CI para su producto de agente.
$12,000 – 22,000
One-time + $600 / mes
Plataforma IA enterprise
Infra de evals compartida para 5+ equipos internos.
$35,000 – 80,000
One-time + $2,400 / mes
Fintech / salud (reguladas)
Evals audit-ready + suite de bias.
$25,000 – 55,000
One-time + $1,500 / mes
Agencias con bots de cliente
Template reusable en proyectos de cliente.
$8,000 – 18,000
One-time + $400 / mes / proyecto
Consultoría interna (F500)
Política + training para 10-50 equipos.
$60,000 – 180,000
Retainer anual

Benchmarks públicos

Public case studyFramework IA · Global · 2024

LangChain — State of AI Agents 2024

Challenge
Encuestar equipos de ingeniería sobre qué rompe sus agentes productivos.
Solution
LangChain encuestó a 1,300+ ingenieros IA de startups y enterprise sobre reliability de agentes y gaps de tooling.
Results
Regresiones de prompt son la causa top
60%
De los equipos
Tienen golden dataset / evals formal
23%
Versionado
Usan LangSmith / Langfuse / Promptfoo
47%
Observabilidad
Public case studyTooling de evals · Global · 2024-2025

Promptfoo — casos enterprise

Challenge
Documentar outcomes al adoptar CI/CD para prompts LLM.
Solution
Promptfoo publica casos de estudio mostrando catch rates de bugs e impacto en developer velocity.
Results
Bugs detectados pre-prod
3-5× más
Vs QA manual
Dev velocity
+28%
Equipos shippean con más confianza
Tasa de incidentes
-47%
Post-rollout de evals

Caso ilustrativo — SaaS AI-native Series B

Illustrative caseSaaS de tooling de soporte · 42 empleados · $9M ARR · EE.UU. + base LATAM

SaaS AI-native de soporte Series B, 12 ingenieros, 180 clientes pagos

Baseline
Tweaks semanales de prompt. Sin evals en CI. 3-5 regresiones reportadas por cliente al mes. NPS cayendo 52 → 38 en 6 meses. Horas eng en respuesta a incidentes: ~18 / semana.
Intervention
Setup Numoru de 3 semanas: Promptfoo en GitHub Actions, DeepEval para RAG, datasets Langfuse (120 casos sobre 3 superficies), regression guard con threshold 5%. Rutina semanal de curación de dataset.
Projected outcome (12 mo)
Regresiones reportadas
5 / mes → 0.7 / mes
-86%
Horas eng de incidente
18 → 4 / sem
-78%
NPS
38 → 51
Recuperado en 4 mo
Costo del engagement
$16,500
One-time + $600 / mes
Tiempo eng ahorrado (año)
+$64,400
14 h × $92 × 50 sem
Churn evitado
+$280,000
7 logos en riesgo retenidos
Deltas de tasa de incidentes anclados a las customer stories de Promptfoo y al survey de LangChain. Caso sintético — no representa a un cliente específico de Numoru.

Calculadora ROI — traer evals in-house

SaaS AI-native: sin evals vs pipeline Numoru (12 meses)

Payback: 1 months
Assumptions
Ingenieros del equipo10
Incidentes de prompt / mes (baseline)4
Horas eng por incidente12
Eng hourly blended$92
Impacto al cliente por incidente$2,400 avg
Tasa post-install0.8 / mes
Costo infra CI evals$72 / mes
Costo de setup$16,500 one-time
Setup (one-time)−$16,500
Retainer (12 mo × $600)−$7,200
Infra CI evals (12 mo × $72)−$864
Tiempo eng evitado (3.2 × 12 × 12 × $92)+$42,393
Impacto al cliente evitado (3.2 × 12 × $2,400)+$92,160
Reducción de churn+$180,000
Contribución neta año 1+$289,989

Tiers de pricing Numoru

Starter install
$8,500one-time
2 semanas. Un agente, 40 casos.
  • Wiring Promptfoo + DeepEval
  • Golden dataset (40 casos)
  • Pipeline GitHub Actions
  • Regression guard threshold 5%
  • Warranty 30 días
Team install
$16,500one-time + $600 / mes
3 semanas. Hasta 3 agentes, 120 casos.
  • Todo lo del Starter
  • 3 agentes / 120 casos dorados
  • Datasets Langfuse + alertas drift
  • Curación semanal de dataset
  • Alertas Slack en regresión
  • Revisión evals trimestral
Platform
$45,000+one-time + $2,400 / mes
5-8 sem. Multi-equipo enterprise.
  • Infra evals compartida multi-equipo
  • Dataset curado 300+ casos
  • Adendo bias + safety
  • Config custom de judge-model
  • Artefactos compliance-ready
  • CSM dedicado

10. Anti-patrones

  • Usar el mismo modelo como judge y como respondedor. Sesgo circular. Judge siempre debe ser un modelo distinto (ideal: otro proveedor).
  • Dataset que crece sin curaduría. Más ≠ mejor. 100 casos curados valen más que 1000 autogenerados.
  • Umbrales globales. Algunos casos son críticos (compliance, cobros); deben tener umbral mayor individual, no diluirse en el promedio.
  • Sin trace-back a casos reales. Cada caso del dataset dorado debe poder vincularse al incidente o feature request que lo originó.

FAQ

¿Puedo correr esto en monorepo con varios agentes?Sí. Cada agente tiene su propio promptfoo.yml y dataset. El regression-guard acepta un flag --agent-id y compara contra la baseline correspondiente.

¿Qué pasa si el LLM del judge tiene un día malo?Por eso la suite corre 3 muestras por caso y usa mediana, no media. Si la varianza del judge supera 0.10 para un caso, se marca como "flaky" y se excluye del cálculo de delta.

¿Cómo migro desde pruebas manuales?Exporta 50 casos recientes desde tu Langfuse de producción. Clasifica manualmente por "respuesta buena/mala" y usa esos labels como seed del dataset dorado. 2-3 días de trabajo, meses de tranquilidad.

¿Funciona con agentes multi-step?Promptfoo sólo evalúa output final. Para evaluar tool calls intermedios, DeepEval tiene ToolCorrectnessMetric. Para evaluar trayectorias completas, usamos deepeval.metrics.ConversationalMetric con el trace completo de Langfuse como input.

¿Detecta prompt injection?Indirectamente: si el adversarial input cambia el comportamiento, los casos golden fallan. Para detección directa, añadir una suite específica con garak o rebuff.

Próximos pasos

El repo github.com/numoru-ia/agent-evals-template tiene el pipeline completo listo para hacer fork. Incluye datasets doradas seed para: soporte al cliente, agente de ventas B2B, RAG documentación técnica y recepcionista IA. Cada una con 50-100 casos curados con casos reales de clientes Numoru (anonimizados).

¿Quieres resultados así para tu empresa?

Iniciar conversación
Compartir