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:
| Capa | Trigger | Duración | Cobertura | Efecto |
|---|---|---|---|---|
| Smoke | Cada PR | <2 min | 10 queries balanceadas | Bloquea PR si falla |
| Full | PR a main, nightly | 10-15 min | 100+ queries | Bloquea merge a main |
| Exhaustive | Pre-release tag | 30-60 min | 500+ queries con gpt-4o judge | Bloquea release |
| Drift watch | Weekly | 30 min | Producción samples | Alerta, 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:
pathsfilter: 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_TESTevita 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:
| Tipo | Ejemplo | Uso |
|---|---|---|
| Secret | OPENAI_API_KEY | Credenciales que dan acceso a recursos pagos |
| Secret | PINECONE_API_KEY | Acceso a base de datos |
| Secret | SLACK_WEBHOOK_URL | URL con token embebido |
| Variable | PINECONE_INDEX_TEST | Nombre del índice (no sensible) |
| Variable | PINECONE_REGION | Configuració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:
| Estrategia | Costo 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:
[](...)
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
| Criterio | CI tradicional sin eval | CI con eval RAG |
|---|---|---|
| Detección de degradación de calidad | Inexistente | Inmediata en PR |
| Confianza para refactorizar pipeline | Baja: temor a romper | Alta: red de seguridad |
| Onboarding de nuevos devs | Riesgoso | Seguro |
| Conversaciones sobre quality | Subjetivas | Cuantitativas |
| Visibilidad de calidad histórica | Inexistente | Reportes versionados |
| Madurez operacional | Standard | RAG-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
- GitHub Actions Documentation - Referencia oficial completa.
- Workflow Syntax - Sintaxis YAML detallada.
- Encrypted Secrets - Manejo seguro de credenciales.
- Sticky PR Comment Action - Comments que se actualizan.
- Slack GitHub Action - Notificaciones a Slack.
- Reusable Workflows - DRY entre múltiples repos.
Creado: Marzo 13, 2026
Versión: 2.0