Módulo 8: RAG Evaluation + Proyecto Integrador

CI/CD de Evaluación con GitHub Actions

Descripción de la cápsula

Tienes pipeline de evaluación, golden dataset y quality gates calibrados. Pero hay una ley de la naturaleza humana en software: si la evaluación no corre automáticamente, eventualmente se deja de ejecutar. Tres semanas después de implementarla, alguien tiene una entrega urgente, salta la evaluación "una vez", se descubre que pasó algo, se vuelve hábito. Tres meses después nadie corre la evaluación y vuelves al estado inicial sin red de seguridad.

La solución es eliminar la opción de saltarse la evaluación. La forma profesional es integrarla en CI/CD: cada pull request dispara automáticamente la evaluación, los resultados se comentan en el PR, las degradaciones bloquean el merge. No depende de que alguien recuerde — es parte de la infraestructura.

En esta cápsula vas a configurar GitHub Actions para que tu sistema RAG tenga el mismo nivel de rigor operacional que un sistema de software tradicional: smoke tests en cada PR, evaluación completa nightly, reportes publicados como artefactos consultables, comentarios automáticos con cambios métricos vs main.

Al terminar tendrás un repositorio con evaluación de calidad RAG corriendo automáticamente, sin posibilidad de saltársela. Es el cierre operacional que separa "tengo evaluación" de "mi equipo opera con disciplina de evaluación".


Filosofía: capas de CI por velocidad y cobertura

No todos los checks pueden correr en cada PR. Costo, tiempo y rate limits importan. Estructura por capas:

CapaTriggerDuraciónCoberturaEfecto
SmokeCada PR<2 min10 queries balanceadasBloquea PR si falla
FullPR a main, nightly10-15 min100+ queriesBloquea merge a main
ExhaustivePre-release tag30-60 min500+ queries con gpt-4o judgeBloquea release
Drift watchWeekly30 minProducción samplesAlerta, no bloquea

Esta estructura significa que:

  • Developer tiene feedback en 2 min para PRs normales
  • Cambios riesgosos (a main) tienen evaluación más profunda
  • Releases pasan por evaluación con judge premium
  • Drift en producción se detecta semanalmente

Workflow base: smoke en cada PR

# .github/workflows/rag-eval-smoke.yml
name: RAG Eval - Smoke

on:
  pull_request:
    paths:
      - 'app/**'
      - 'eval/**'
      - 'golden_dataset/**'
      - 'pyproject.toml'

jobs:
  smoke:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install dependencies
        run: pip install -e .[eval]

      - name: Run smoke evaluation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
          PINECONE_INDEX: ${{ vars.PINECONE_INDEX_TEST }}
          OPENAI_SEED: '42'
        run: python scripts/evaluate.py --mode smoke --enforce-thresholds

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: smoke-report-${{ github.event.pull_request.number }}
          path: eval_reports/
          retention-days: 30

Decisiones clave:

  • paths filter: solo dispara si cambian directorios relevantes. Cambios a docs no necesitan correr eval (ahorra costos OpenAI).
  • timeout-minutes: 5: bloquea workflows infinitos por bug en código.
  • if: always() en upload: sube reporte incluso si falló, para poder debuggear.
  • OPENAI_SEED: '42': reproducibilidad entre runs.
  • Index separado para test: PINECONE_INDEX_TEST evita tocar índice de producción.

Workflow completo: full evaluation nightly

# .github/workflows/rag-eval-nightly.yml
name: RAG Eval - Nightly Full

on:
  schedule:
    - cron: '0 3 * * *'  # 3am UTC daily
  workflow_dispatch:  # manual trigger

jobs:
  full:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      - run: pip install -e .[eval]

      - name: Run full evaluation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
          PINECONE_INDEX: ${{ vars.PINECONE_INDEX_PROD }}
          OPENAI_SEED: '42'
        run: python scripts/evaluate.py --mode full --enforce-thresholds

      - name: Compare against baseline
        run: python scripts/compare_baseline.py --report eval_reports/latest.json

      - name: Upload to S3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          aws s3 cp eval_reports/ s3://rag-eval-reports/$(date +%Y-%m-%d)/ --recursive

      - name: Notify on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "🚨 RAG nightly evaluation failed",
              "channel": "${{ secrets.SLACK_CHANNEL }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Diferencias con smoke:

  • Schedule cron en lugar de PR trigger
  • Index de producción (PINECONE_INDEX_PROD) para evaluar contra estado real
  • Upload a S3 para retención larga (artifacts de GitHub se borran después de 90 días)
  • Slack notification en falla — porque pasaste la noche y nadie va a ver el rojo en GitHub

Workflow de comparación con main

Para mostrar al developer cómo cambió la calidad vs main, no solo si pasa thresholds:

# .github/workflows/rag-eval-compare.yml
name: RAG Eval - Compare with main

on:
  pull_request:

jobs:
  compare:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - run: pip install -e .[eval]

      - name: Run smoke on PR branch
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
        run: |
          python scripts/evaluate.py --mode smoke
          mv eval_reports/latest.json eval_reports/pr.json

      - name: Run smoke on main
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}
        run: |
          git checkout main
          python scripts/evaluate.py --mode smoke
          mv eval_reports/latest.json eval_reports/main.json

      - name: Generate comparison
        run: python scripts/diff_reports.py --pr eval_reports/pr.json --main eval_reports/main.json --output diff.md

      - name: Comment on PR
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          path: diff.md
          header: rag-eval-diff

El comment en PR se ve así:

## 📊 RAG Quality Diff

| Metric | main | this PR | Δ |
|--------|------|---------|---|
| Faithfulness | 0.91 | 0.93 | ↑ +2.2% |
| Answer Relevancy | 0.87 | 0.86 | ↓ -1.1% |
| Context Precision | 0.82 | 0.85 | ↑ +3.7% |
| Context Recall | 0.85 | 0.84 | ↓ -1.2% |

**Verdict**: ✅ Net improvement (no regressions beyond tolerance)

Esto es lo que hace que evaluation sea visceral para el developer — no es un test abstracto, es feedback directo sobre su PR.


Manejo de secrets y variables

GitHub distingue entre secrets (encriptados, valor no visible) y variables (visibles en logs). Usa apropiadamente:

TipoEjemploUso
SecretOPENAI_API_KEYCredenciales que dan acceso a recursos pagos
SecretPINECONE_API_KEYAcceso a base de datos
SecretSLACK_WEBHOOK_URLURL con token embebido
VariablePINECONE_INDEX_TESTNombre del índice (no sensible)
VariablePINECONE_REGIONConfiguración pública

Configuración:

# Repo settings → Secrets and variables → Actions
# Tab "Secrets":
gh secret set OPENAI_API_KEY
gh secret set PINECONE_API_KEY

# Tab "Variables":
gh variable set PINECONE_INDEX_TEST --body "rag-eval-test"
gh variable set PINECONE_INDEX_PROD --body "rag-prod-v1"

Anti-patrones críticos:

  • Hardcodear keys en YAML → expulsión automática del modelo y rotación de keys
  • Loguear secrets con echo $OPENAI_API_KEY → quedan en logs públicos del workflow
  • Usar el mismo OPENAI_API_KEY de producción para CI → CI puede agotar quota productiva. Usa key separada con quota propia.

Reducción de costos en CI

Evaluation con LLM-as-judge cuesta dinero por run. Para un repo activo con 30 PRs/mes:

EstrategiaCosto aprox/mes
Sin filtrado: full eval en cada push$300+
Smoke en PR (10 queries) + full nightly$25
Path filter (solo cambios relevantes)$15
+ judge gpt-4o-mini$8

Optimizaciones específicas:

# Cancelar runs viejos cuando llega push nuevo al mismo PR
concurrency:
  group: rag-eval-${{ github.ref }}
  cancel-in-progress: true

# Cache de pip para acelerar setup
- uses: actions/setup-python@v5
  with:
    python-version: '3.11'
    cache: 'pip'

# Cache de embeddings (si reusas queries del golden)
- uses: actions/cache@v4
  with:
    path: .cache/embeddings/
    key: embeddings-${{ hashFiles('golden_dataset/v1.0.0.json') }}

Conexión con el proyecto final

Tu Advanced RAG System debe entregar:

.github/workflows/
├── rag-eval-smoke.yml         # En cada PR
├── rag-eval-nightly.yml       # Cron diario
└── rag-eval-compare.yml       # Comment con diff vs main

scripts/
├── evaluate.py                # CLI de evaluación
├── compare_baseline.py        # Regression check
└── diff_reports.py            # Comparación entre runs

Y en el README:

## Quality Gates

Every PR runs RAG smoke evaluation automatically. See latest results:
[![RAG Eval](https://github.com/org/repo/actions/workflows/rag-eval-smoke.yml/badge.svg)](...)

Nightly full evaluation: [latest report on S3](s3://rag-eval-reports/latest.html)

El badge en README es la señal visible de que el repo opera con disciplina de evaluación.


Comparación: CI sin evaluación vs CI con evaluación RAG

CriterioCI tradicional sin evalCI con eval RAG
Detección de degradación de calidadInexistenteInmediata en PR
Confianza para refactorizar pipelineBaja: temor a romperAlta: red de seguridad
Onboarding de nuevos devsRiesgosoSeguro
Conversaciones sobre qualitySubjetivasCuantitativas
Visibilidad de calidad históricaInexistenteReportes versionados
Madurez operacionalStandardRAG-specific operational excellence

Troubleshooting

Problema 1: "Workflow falla por timeout"

Causa: dataset grande en PR o concurrency baja en runner.
Solución: modo smoke en PR (no full). Sube timeout-minutes solo si la full eval realmente tarda más de 30 min — si es así, optimiza concurrency en execute_batch.

Problema 2: "Secret no encontrado en CI"

Causa: secret no configurado o nombre incorrecto.
Solución: verifica con gh secret list y compara nombres exactos. Recuerda que secrets de organization no se heredan automáticamente; deben otorgarse al repo.

Problema 3: "Métricas inestables: pasa local, falla en CI"

Causa: dependencias o seed distintos.
Solución: pin exacto en pyproject.toml (ragas==0.1.x, no ragas>=0.1). Fija OPENAI_SEED=42 y temperature=0 en código de eval. Verifica que dataset_version es la misma.

Problema 4: "Costos de OpenAI explotaron"

Causa: workflow corre en cada push (no solo PR), o full eval en lugar de smoke.
Solución: path filters, smoke en PR, full solo nightly. Usa key separada con quota cap para CI. Considera gpt-4o-mini como judge default.

Problema 5: "Diff comments saturan el PR"

Causa: múltiples runs comentan separadamente.
Solución: usa marocchino/sticky-pull-request-comment@v2 con header único — actualiza el comment existente en lugar de crear nuevos.

Problema 6: "Workflow corre en forks externos sin secrets"

Causa: PRs de forks no tienen acceso a secrets por seguridad.
Solución: workflow detecta esto y skip eval con mensaje claro. Para PRs de contributors externos, eval corre cuando un maintainer hace /eval o cuando se mergea a feature branch.


Ejercicios

Ejercicio 1: Workflow básico para PR

Crea workflow mínimo que corre smoke eval en cada PR.

Ver solución
# .github/workflows/rag-eval.yml
name: RAG Eval

on:
  pull_request:
    paths:
      - 'app/**'
      - 'eval/**'
      - 'golden_dataset/**'

jobs:
  smoke:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      - run: pip install -e .[eval]
      - run: python scripts/evaluate.py --mode smoke --enforce-thresholds
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          OPENAI_SEED: '42'

Explicación: path filter evita correr en cambios irrelevantes; timeout previene hangs; cache acelera setup.

Ejercicio 2: Subir reporte como artifact

Extiende el workflow para subir el reporte JSON con retención de 30 días.

Ver solución
- name: Upload report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: eval-report-pr-${{ github.event.pull_request.number }}
    path: |
      eval_reports/*.json
      eval_reports/*.md
    retention-days: 30

Explicación: if: always() sube reporte incluso si el step anterior falló (crucial para debug). El nombre con PR number facilita encontrar el reporte específico.

Ejercicio 3: Comment automático en PR con diff

Implementa job que postea diff vs main en cada PR.

Ver solución
- name: Post diff comment
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    header: rag-eval
    message: |
      ## 📊 RAG Eval Results
      
      | Metric | This PR | Threshold | Status |
      |--------|---------|-----------|--------|
      | Faithfulness | ${{ steps.eval.outputs.faithfulness }} | 0.85 | ${{ steps.eval.outputs.faithfulness_status }} |
      | Relevancy | ${{ steps.eval.outputs.relevancy }} | 0.80 | ${{ steps.eval.outputs.relevancy_status }} |

Para popular las outputs:

- id: eval
  run: |
    python scripts/evaluate.py --mode smoke --output-format github
    echo "faithfulness=$(jq .ragas.faithfulness eval_reports/latest.json)" >> $GITHUB_OUTPUT

Explicación: sticky comment se actualiza con cada push en lugar de crear ruido. Outputs permiten parametrizar el comment con valores reales.

Ejercicio 4: Schedule para nightly full eval

Crea workflow que corre full eval cada noche y notifica en Slack si falla.

Ver solución
name: RAG Eval Nightly

on:
  schedule:
    - cron: '0 3 * * *'
  workflow_dispatch:

jobs:
  full:
    runs-on: ubuntu-latest
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -e .[eval]
      - run: python scripts/evaluate.py --mode full --enforce-thresholds
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          PINECONE_API_KEY: ${{ secrets.PINECONE_API_KEY }}

      - if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "🚨 RAG nightly eval failed: ${{ github.workflow_url }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Explicación: workflow_dispatch permite trigger manual cuando necesitas re-correr sin esperar al cron. Slack notification asegura que regresiones nocturnas no esperan al lunes.


Resumen

  • CI/CD convierte evaluación en infraestructura permanente, no práctica voluntaria
  • Capas por velocidad/cobertura: smoke (PR), full (nightly), exhaustive (release), drift (weekly)
  • Path filters, concurrency cancellation y caching reducen costo de CI 10×
  • Diff comments en PR hacen evaluación visceral para el developer
  • Secrets vs variables: secrets para credenciales, variables para nombres
  • Slack notification en nightly failures: lo que no avisas, no se arregla
  • Badge en README es señal pública de madurez operacional

Recursos adicionales

  1. GitHub Actions Documentation - Referencia oficial completa.
  2. Workflow Syntax - Sintaxis YAML detallada.
  3. Encrypted Secrets - Manejo seguro de credenciales.
  4. Sticky PR Comment Action - Comments que se actualizan.
  5. Slack GitHub Action - Notificaciones a Slack.
  6. Reusable Workflows - DRY entre múltiples repos.

Creado: Marzo 13, 2026
Versión: 2.0