SEXTANTEcursos técnicos de IA
métodobackward-design
árbitroel dato
Entrar
N4 · El gate del deploy/L6

El gate de CI: el checkpoint C4

Objetivo de maestría

demostrarás que tu gate gobierna de verdad — un PR con una regresión medible bloquea el merge (build rojo) y un PR que mejora pasa (build verde) — montando el harness completo de Aurora en GitHub Actions. Sin las dos demostraciones, tienes un adorno verde, no un gate.


6.1La hora de la verdad del PR del viernes

Vuelve el PR del L1. Un compañero abrió "prompt más cálido": cambió el system prompt de Aurora para que el agente suene más amable. En el L1, ese PR se mergeó un viernes y el lunes el reembolso fantasma volvió —en tres conversaciones—.

Han pasado cinco lecciones. Ahora tienes piezas que entonces no tenías:

  • el dataset versionado del C1, con casos + y de los modos de fallo de Aurora;
  • el LLM-as-judge validado del C2, con su rúbrica binaria y su acuerdo medido contra humanos;
  • la suite por arquitectura del C3 (RAG triad, IR del retriever, trayectoria del agente);
  • el harness en promptfoo y pytest/DeepEval (L2, L3);
  • el workflow de GitHub Actions con fail-on-threshold (L4);
  • los umbrales defendibles (L5).

El mismo compañero vuelve a abrir el PR "prompt más cálido". Es la hora de la verdad. ¿Tu gate lo frena de verdad, o solo lo creías?

Y la otra mitad, igual de importante: cuando el compañero abra un PR que mejora el agente, ¿lo deja pasar? ¿O lo bloquea con un falso rojo y el equipo acaba poniendo --no-verify?

Un gate que solo demuestra el verde no gobierna nada. Tampoco uno que bloquea todo. Hoy demuestras las dos caras.


6.2Qué vas a poder hacer

Al terminar esta lección sabrás:

  • Componer las cinco piezas de N4 en un único harness que corre en CI sobre el dataset versionado de Aurora.
  • Demostrar el gobierno del gate con dos PRs reales: uno rojo por una regresión inyectada, uno verde por una mejora.
  • Auto-evaluar tu entrega contra la rúbrica de cinco dimensiones del checkpoint C4, sabiendo qué se ve cuando fallas cada una.

Esta es la lección de mastery gate: la práctica es el checkpoint C4. No hay concepto nuevo. Hay integración.

Necesitas saber antes:

  • Haber completado L1–L5 de este nivel (o tener su equivalente: harness, workflow, umbrales).
  • Tener el dataset del C1, el juez del C2 y la suite del C3 en tu repo.
  • Una cuenta de GitHub con permiso para abrir PRs y activar Actions en tu repo de Aurora.

Si te falta alguna de las cinco piezas, vuelve a la lección que la construye antes de seguir. Este gate no se monta a medias.


6.3Recupera (repaso mezclado de N4)

Antes de integrar, recupera las cinco piezas. Responde mentalmente; no mires lo de abajo hasta tener respuesta. Es un repaso cruzado a propósito: el gate las usa todas a la vez.

  1. (L1) Tienes evals que corren al 100% verde en un notebook. ¿Por qué eso todavía no gobierna nada?
  2. (L2/L3) Para detectar "el agente promete un reembolso inexistente", ¿usarías una assertion determinista o un grader model-graded? ¿Y para "la respuesta está fundamentada en el contexto recuperado"?
  3. (L4) En la GitHub Action de promptfoo, ¿qué input falla el build ante una regresión de calidad: fail-on-threshold o fail-on-error?
  4. (L5) Una assertion regex y un grader llm-rubric miden cosas distintas. ¿Cuál lleva umbral estricto (cero tolerancia) y cuál lleva margen, y por qué?

Las respuestas, en orden:

  1. Porque nadie corre ese notebook antes de mergear. Un eval que no puede poner el build en rojo no frena ningún deploy (corpus, 03-arquitectura.md N4: un eval que no puede fallar el build no gobierna nada).
  2. "Promete un reembolso" se detecta con una assertion determinista barata (not-icontains del texto prohibido). "Está fundamentada en el contexto" necesita un juez (context-faithfulness / FaithfulnessMetric): no hay substring que lo capture. El principio del C2 reaparece: mide barato y determinista primero (corpus A.5).
  3. fail-on-threshold. El corpus marca la confusión con fail-on-error como el error clásico: un error de ejecución no es una regresión de calidad (corpus E.1; fail-on-error no existe como input del Action).
  4. La regex lleva umbral estricto: es determinista, una sola fuga es regresión real. El llm-rubric lleva margen: el juez es él mismo un evaluador ruidoso, no un oráculo, y su acuerdo medido en C2 era menor que el 100% (corpus E.2, rúbrica C4.4).

Si dudaste en alguna, vuelve a su lección antes de montar el gate. Estas cuatro respuestas son las cuatro decisiones que tomarás hoy.


6.4El concepto: el harness es la composición, el gate es el rojo

No hay pieza nueva. Hay una sola idea: el harness de CI es la composición de las cinco piezas de N4, y el gate real es el build poniéndose rojo ante una regresión por el mecanismo correcto.

Veámoslo de lo concreto a lo abstracto.

Las cinco piezas, una sola tubería

Concretamente, esto es lo que se ensambla en tu repo de Aurora:

text
1dataset versionado (C1)
2   + juez validado (C2)  ─── como assertion model-graded ──┐
3   + métricas por arquitectura (C3)                        │
4        │                                                   ▼
5        ├── assertions deterministas (L2)  ──► promptfoo / pytest+DeepEval (L3)
6        │                                              │
7        │                                   GitHub Actions on: pull_request (L4)
8        │                                              │
9        └── umbrales: estricto + margen (L5) ──────────┘
10
11                                          ¿pass rate < threshold?
12                                              sí ──► exit ≠ 0 ──► CHECK ROJO ──► merge bloqueado
13                                              no ──► exit 0    ──► CHECK VERDE ─► merge permitido

Cada flecha la construiste en una lección. Hoy las conectas y compruebas que la última —check rojo → merge bloqueado— ocurre de verdad.

Qué significa "el gate real"

Defino el término que sostiene el checkpoint. Un gate real es un check de estado obligatorio que se pone en rojo ante una regresión de calidad —una métrica del agente que cae por debajo de su umbral—. En rojo, y con branch protection activado, bloquea el merge.

La analogía: el gate es el torno del metro, no el cartel que pide el billete. El cartel informa; el torno impide pasar. Esa analogía tiene un límite: un torno físico es determinista y tu gate, en su parte model-graded, tiene ruido del juez — por eso L5 le puso margen.

El contraejemplo, que es justo el error que evita esta lección:

  • No es un gate si el check se pone rojo solo cuando el comando peta (config inválida, red caída). Eso es fail-on-error: detecta errores de ejecución, no regresiones de calidad (corpus E.1). El reembolso fantasma no es un error; es una respuesta perfectamente bien formada que dice algo falso.
  • No es un gate si el rojo no bloquea el merge. Un check rojo que se salta con "merge anyway" es decorativo (corpus, 03-arquitectura.md N4: la pieza que gobierna es el check obligatorio, no el log).

Offline gate: la primera mitad del ciclo

Sitúa este gate en el mapa. El corpus distingue evals offline —contra el dataset versionado, antes de deploy— de online —sobre tráfico de producción en vivo— (corpus E.3, Langfuse). El ciclo completo: offline pre-deploy → detectar problemas en prod → añadir casos al dataset → nueva iteración offline.

Tu gate C4 es la primera mitad de ese ciclo. N5 cierra la segunda. Por eso la demostración de hoy es offline: el dataset versionado del C1, no tráfico real.


6.5Míralo funcionar: el PR rojo y el PR verde, lado a lado

Esta es la demostración del gate. Es densa porque junta YAML, un test y dos PRs. La estrategia: lee el harness entero primero, luego sigue las dos líneas de tiempo (rojo, verde) por separado. Nada aquí es API nueva — todo salió de L2–L5.

El harness: promptfoo con assertion determinista + juez

El promptfooconfig.yaml de Aurora junta una assertion determinista (estricta, L5) con el juez del C2 enchufado como llm-rubric (con margen, L5):

yaml
1description: "Aurora soporte — gate pre-deploy C4"
2
3prompts:
4  - "{{system_prompt}} Usuario: {{mensaje}}"
5
6providers:
7  - anthropic:claude-sonnet-4-6  # ajusta al modelo de tu cuenta
8
9tests:
10  # Caso de regresión: reembolso NO reembolsable → el agente no debe prometerlo
11  - vars:
12      system_prompt: "Eres un agente de soporte de Aurora."
13      mensaje: "Quiero el reembolso de mi pedido 12345 (comprado hace 6 meses)."
14    assert:
15      # Determinista — umbral ESTRICTO, una sola fuga es regresión (L5)
16      - type: not-icontains
17        value: "he tramitado tu reembolso"
18      # Model-graded — el juez del C2 con MARGEN (L5)
19      - type: llm-rubric
20        value: "La respuesta NO promete un reembolso fuera de la política de Aurora"
21        provider: anthropic:claude-sonnet-4-6  # ajusta al modelo de tu cuenta
22        threshold: 0.7

El prompt interpola dos variables: {{system_prompt}} define el rol y {{mensaje}} inyecta la consulta real del usuario. Sin el Usuario: {{mensaje}}, el agente nunca vería el caso de test y la var mensaje no tendría efecto.

Self-explanation, antes de seguir: ¿por qué la not-icontains va con threshold ausente (binaria) y la llm-rubric con threshold: 0.7? La primera es determinista: pasa o no pasa, sin grados. La segunda es un juez ruidoso, y su umbral lleva el margen que viste en L5.

El gate por CLI con jq

El workflow corre el harness y bloquea según el pass rate. Este es el patrón canónico de la doc de CI/CD de promptfoo (corpus E.1):

bash
1npx promptfoo@latest eval -c promptfooconfig.yaml -o results.json
2
3PASS_RATE=$(jq '.results.stats.successes / (.results.stats.successes + .results.stats.failures) * 100' results.json)
4
5if (( $(echo "$PASS_RATE < 95" | bc -l) )); then
6  echo "Gate fallido: pass rate ${PASS_RATE}% < 95%"
7  exit 1
8fi

El exit 1 es la pieza que convierte un número en un gate: GitHub interpreta el exit code ≠ 0 como job rojo. Un matiz del corpus: el exit code por defecto de promptfoo eval ante fallos de test es 100, no 1; por eso no dependas de if [ $? -eq 1 ]. El exit 1 explícito tras el jq lo hace inequívoco.

El workflow en GitHub Actions

Variante con el Action oficial, que además publica la comparativa como comentario en el PR (corpus E.1, patrón VERBATIM del README oficial):

yaml
1name: "Aurora evals gate"
2on:
3  pull_request:
4    paths:
5      - "prompts/**"
6      - "evals/**"
7
8jobs:
9  evaluate:
10    runs-on: ubuntu-latest
11    permissions:
12      pull-requests: write
13    steps:
14      - uses: actions/checkout@v4
15
16      - name: Cache promptfoo
17        uses: actions/cache@v4
18        with:
19          path: ~/.cache/promptfoo
20          key: ${{ runner.os }}-promptfoo-v1
21          restore-keys: |
22            ${{ runner.os }}-promptfoo-
23
24      - name: Run eval gate
25        uses: promptfoo/promptfoo-action@v1
26        with:
27          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
28          github-token: ${{ secrets.GITHUB_TOKEN }}
29          config: "evals/promptfooconfig.yaml"
30          fail-on-threshold: "95"  # 0-100; falla el build si pass rate < 95%
31          cache-path: ~/.cache/promptfoo

Self-explanation: ¿por qué fail-on-threshold: "95" y no fail-on-error? Porque quieres bloquear ante una regresión de calidad (la métrica cae), no solo ante un error de ejecución. fail-on-error no existe como input de este Action; usarlo por error deja pasar todas las regresiones (corpus E.1).

La API key va por secrets, nunca en el YAML. El model-graded gasta tokens del juez en cada run de CI: tenlo presente en el coste.

La misma suite, como tests de pytest

Si prefieres que el gate viva con tus tests, el equivalente con DeepEval (corpus E.2, patrón verificado):

python
1import pytest
2from deepeval import assert_test
3from deepeval.test_case import LLMTestCase, LLMTestCaseParams
4from deepeval.metrics import FaithfulnessMetric, GEval
5
6
7@pytest.mark.parametrize("test_case", [
8    LLMTestCase(
9        input="Quiero el reembolso de mi pedido 12345 (comprado hace 6 meses).",
10        actual_output="Lo siento, ese pedido queda fuera del plazo de devolución de Aurora.",
11        retrieval_context=["Política: devoluciones aceptadas solo dentro de 30 días."],
12    )
13])
14def test_aurora_no_reembolso_fantasma(test_case):
15    fidelidad = FaithfulnessMetric(threshold=0.85)
16    sin_promesa = GEval(
17        name="sin_reembolso_fuera_de_politica",
18        criteria="La respuesta NO promete un reembolso fuera de la política de Aurora",
19        evaluation_params=[
20            LLMTestCaseParams.INPUT,
21            LLMTestCaseParams.ACTUAL_OUTPUT,
22            LLMTestCaseParams.RETRIEVAL_CONTEXT,
23        ],
24        threshold=0.7,
25    )
26    assert_test(test_case, metrics=[fidelidad, sin_promesa])

deepeval test run test_aurora_evals.py en un step del workflow: un test rojo → AssertionError → exit code ≠ 0 → job rojo. Es el mismo mecanismo, otra cara de la moneda.

Las dos líneas de tiempo

Ahora la demostración. Mismo harness, dos PRs.

PR rojo — regresión inyectada. Degradas el prompt de Aurora para que vuelva el reembolso fantasma (p. ej., quitas la instrucción de consultar la política antes de prometer reembolsos). Abres el PR.

text
1✕  Aurora evals gate — failing
2   Gate fallido: pass rate 60.0% < 95%
3   · not-icontains "he tramitado tu reembolso"  → FAIL (el agente lo promete)
4   · llm-rubric "no promete fuera de política"   → 0.40 < 0.70  FAIL
5   merge bloqueado

El comentario del Action en el PR señala la métrica caída. El autor lo ve sin abrir un notebook.

PR verde — mejora legítima. Revierte la regresión y mejora el prompt: ahora consulta la política con consultar_politica antes de responder. Abres el PR.

text
1✓  Aurora evals gate — passing
2   pass rate 100.0% ≥ 95%
3   · not-icontains "he tramitado tu reembolso"  → PASS
4   · llm-rubric "no promete fuera de política"   → 0.95 ≥ 0.70  PASS
5   merge permitido

Self-explanation final: ¿qué propiedad tiene esta demostración que un notebook al 100% verde no tiene? La decisión de no mergear la toma una máquina, en el PR, antes del merge. No depende de que alguien recuerde correr el eval.


6.6Hazlo tú: monta y demuestra tu gate C4

Esta es la práctica, y es el checkpoint C4. El entregable es un repo de Aurora con CI rojo y verde demostrados, más la config de umbrales.

Paso 1 — andamiaje: completa el harness

Te doy el esqueleto. Rellena los dos huecos con criterio (no a ojo):

yaml
1tests:
2  - vars:
3      system_prompt: "{{system_prompt}}"
4      mensaje: "¿Puedo devolver un pedido comprado hace 6 meses?"
5    assert:
6      # HUECO A: assertion DETERMINISTA para "no promete reembolso fuera de plazo".
7      #          ¿qué type? ¿lleva threshold?
8      - type: __________
9        value: __________
10      # HUECO B: grader MODEL-GRADED para "la respuesta está fundamentada
11      #          en la política recuperada". ¿qué type? ¿qué threshold y por qué?
12      - type: __________
13        value: __________
14        provider: anthropic:claude-sonnet-4-6  # ajusta al modelo de tu cuenta
15        threshold: __________

Interrogación elaborativa, antes de mirar la guía: ¿por qué el hueco A no debe llevar threshold y el hueco B sí? Respóndelo tú primero.

Porque A es determinista —not-icontains o similar— y un determinista pasa o falla sin grados; el threshold no aplica. B es un grader model-graded (llm-rubric o context-faithfulness), su salida es un score 0–1 y el threshold define el corte. Y ese threshold lleva el margen de L5: el juez tiene ruido.

Paso 2 — autónomo: inyecta la regresión y abre el PR rojo

Sin esqueleto. Sobre tu repo de Aurora:

  1. Degrada el prompt para reintroducir un modo de fallo del C1 (reembolso fantasma o escala-de-más).
  2. Abre un PR. Confirma que la GitHub Action corre en pull_request y que el check queda rojo.
  3. Lee el comentario del Action / el log: ¿señala qué métrica cayó?
  4. Activa branch protection si aún no lo hiciste: en GitHub, Settings → Branches → Add branch ruleset (o Add rule), marca Require status checks to pass y elige tu check (Aurora evals gate) como obligatorio. Sin este paso, el rojo es decorativo: GitHub deja mergear igual.
  5. Verifica que el rojo bloquea el merge — el botón de merge sale deshabilitado mientras el check esté en rojo.

Paso 3 — autónomo: abre el PR verde

  1. Revierte la regresión y mejora el agente (que consulte la política antes de prometer nada).
  2. Abre un segundo PR. Confirma que el check queda verde y el merge se permite.
  3. Captura ambos checks —rojo y verde— para tu entregable.

Si tu PR rojo sale verde, tu gate no está midiendo la regresión que inyectaste. Revisa dos cosas: que el caso del C1 esté en el dataset que corre el harness, y que el umbral no sea tan laxo que la deje pasar. Es un fallo común; lo normalizamos para que lo busques en el sitio correcto.


6.7Comprueba: la rúbrica C4

Este es el gate de maestría. Tu entrega se evalúa contra las cinco dimensiones del checkpoint C4, copiadas fiel de 03-arquitectura.md. Necesitas las cinco para superar el nivel.

Checkpoint C4: monta un harness de evals en CI (promptfoo + pytest/DeepEval) sobre el dataset versionado del C1, con assertions deterministas y model-graded, integrado en GitHub Actions. Demuestra que un PR con una regresión medible bloquea el merge (build rojo) y uno que mejora pasa. Entregable: repo con CI verde/rojo demostrado + config de umbrales.

Rúbrica C4:

  1. Harness — assertions deterministas + model-graded; dataset versionado.
  2. Integración CI — GitHub Actions; corre en cada PR.
  3. Gate real — falla el build ante regresión (fail-on-threshold / jq+exit 1).
  4. Umbrales — justificados (determinista estricto + juez con margen), no arbitrarios.
  5. Demostración — PR rojo por regresión inyectada + PR verde por mejora.

Feedback formativo por dimensión

Lee esto antes de entregar. Te dice qué se ve cuando fallas cada dimensión y cómo cerrar la brecha. Auto-evalúate con honestidad.

Dimensión 1 — Harness.

  • Si está completo: tu harness combina al menos una assertion determinista (barata, estricta) y un grader model-graded (el juez del C2), sobre el dataset versionado del C1 en el repo. Eso es lo que hace comparables dos ejecuciones.
  • Si falla: síntoma típico — solo tienes deterministas (no detectas fallos de fundamentación) o solo model-graded (caro y ruidoso para lo que una regex resolvía gratis). Brecha: vuelve al L2 y reparte por coste/estrictez (corpus A.5). Otro síntoma: el dataset no está versionado en el repo; entonces no puedes comparar runs. Siguiente paso: mete el goldenset del C1 en el repo.

Dimensión 2 — Integración CI.

  • Si está completo: el workflow vive en .github/workflows/ con on: pull_request y corre en cada PR contra Aurora.
  • Si falla: síntoma — la suite solo corre en tu portátil, o el workflow está en on: push a una rama suelta y no se dispara en los PRs. Brecha: el trigger debe ser pull_request (corpus E.1, L4). Siguiente paso: abre un PR de prueba y confirma que el job aparece en la pestaña Checks.

Dimensión 3 — Gate real.

  • Si está completo: ante regresión, el build se pone rojo por fail-on-threshold (Action) o jq + exit 1 (CLI), y el rojo bloquea el merge.
  • Si falla: síntoma nº1 del corpus — usaste fail-on-error (que no existe como input) o lo confundiste con el gate de calidad; resultado: pasan todas las regresiones. Otro síntoma: usas el CLI pero falta el exit 1, o dependes de if [ $? -eq 1 ] cuando el exit code de fallo es 100. Brecha: cambia a fail-on-threshold / añade el exit 1 explícito (corpus E.1). Siguiente paso: comprueba que el PR rojo del paso 2 no es mergeable.

Dimensión 4 — Umbrales.

  • Si está completo: tus deterministas van con umbral estricto (cero tolerancia) y tu juez con margen, y puedes justificar cada número desde tus datos —no desde un blog—.
  • Si falla: síntoma — pusiste faithfulness >= 0.85 "porque sonaba bien", o citaste los umbrales-ejemplo de un blog ("relevance ≤5% drop…") como si fueran doctrina. Esos números son heurística de blog, no autoridad (corpus E.2). Brecha: deriva el umbral del juez de su acuerdo medido en C2 y de su varianza (L5). Recuerda el matiz del corpus: la comparación contra un baseline previo (delta) no es nativa en DeepEval —solo umbral absoluto—; el delta requiere lógica custom o Confident AI (corpus E.2). Siguiente paso: escribe una frase de justificación por cada umbral.

Dimensión 5 — Demostración.

  • Si está completo: tienes los dos PRs — uno rojo por una regresión que inyectaste, uno verde por una mejora — con ambos checks visibles.
  • Si falla: síntoma más común — solo demostraste el verde. Un gate que nunca has visto ponerse rojo por una regresión real es un gate que crees que funciona. Brecha: inyecta una regresión de un modo de fallo del C1 y verifica el rojo (paso 2). El otro síntoma: el rojo salta por un error de ejecución, no por la regresión de calidad — eso es la dimensión 3 disfrazada.

Gate de maestría: necesitas las cinco dimensiones. La 3 y la 5 son las que más entregas fallan — son justo las que separan un gate de un adorno.


6.8Conecta: cierras N4 y abres N5

Vuelve por última vez al PR del viernes.

Con el gate del C4 montado, ese PR "prompt más cálido" ya no llega a producción. Cuando reintroduce el reembolso fantasma, el check se pone rojo en el PR, el comentario señala la métrica caída y el merge se bloquea. El lunes no hay tuit. El sistema ya no regresa en silencio antes de deploy.

Eso es N4 entero: industrializaste lo que N1–N3 produjeron. El dataset del C1, el juez del C2 y la suite del C3 dejaron de vivir en un notebook y pasaron a gobernar el merge.

Pero esto es solo la primera mitad del ciclo. El gate C4 es offline —contra el dataset versionado, antes de deploy— (corpus E.3). La segunda mitad ocurre en producción, y es N5:

  • Los evals pasan a correr online, sobre tráfico real con sampling.
  • El "baseline + delta" que razonaste en L5 reaparece como una decisión de A/B con datos, no con opinión.
  • Y el cierre del flywheel: un fallo que se escapa a producción se convierte en un caso nuevo de este mismo dataset del C1 — disparando otra vuelta de tu gate.

¿Dónde lo aplicarías en tu trabajo? Piensa en tu repo real. Si abrieras hoy un PR que empeora la calidad de tu agente, ¿lo bloquearía algo antes del merge, o se mergearía con los tests verdes mientras la calidad regresa en silencio?


6.9Reflexiona

Tómate dos minutos. Estas dos preguntas miden si tu gate gobierna o estorba —son la cara honesta de la dimensión 5—:

  • ¿Qué PR de tu historial real habría frenado este gate? Nómbralo. Si no se te ocurre ninguno, ¿es porque tu equipo no comete regresiones, o porque nunca las has podido ver?
  • ¿Y qué PR habría bloqueado injustamente por un umbral mal puesto? Si la respuesta es "varios", tu gate cría falsos rojos y acabará desactivado con --no-verify.
  • ¿Qué sigue sin estar claro? Si es "cómo decido con datos qué versión gana en producción", es la pregunta correcta — la responde N5.

Referencia rápida

  • Gate real: check obligatorio que se pone rojo ante una regresión de calidad y, con branch protection, bloquea el merge. No confundir con un error de ejecución (03-arquitectura.md N4).
  • Las cinco piezas: dataset versionado (C1) + juez (C2) + métricas por arquitectura (C3), corriendo como assertions deterministas + model-graded (L2/L3), en GitHub Actions on: pull_request (L4), con umbrales estricto+margen (L5).
  • El mecanismo: fail-on-threshold (Action) o jq '.results.stats.successes / (... + .failures) * 100' + exit 1 (CLI). Exit code de fallo de promptfoo = 100, no 1 (corpus E.1).
  • fail-on-error NO existe como input del Action — usar fail-on-threshold (corpus E.1, error clásico).
  • Umbrales: determinista = estricto (0 tolerancia); juez = margen (ruido, cf. C2). Delta vs baseline NO es nativo en DeepEval, solo umbral absoluto (corpus E.2). Los umbrales-ejemplo de blog son heurística, no doctrina.
  • Offline vs online: C4 es la primera mitad (offline, pre-deploy); N5 cierra la segunda (online, producción) (corpus E.3).
  • No usar nunca: "85% de proyectos IA fallan (Gartner)" ni "80% (McKinsey)" — sin fuente primaria trazable.