Módulo 7: Observability Latency And Evals In Production
6. Manos a la obra: el arnés de smoke test con manifiestos fijos
Descripción
Esta lección construye evals/manifest_extraction_smoke_test.py, corrido sobre evals/fixtures/sample_manifests.json — el arnés de smoke test de extract-shipment-manifest-fields, la pieza que la lección 5 prometió sin construir. La estructura de comparación —cargar cinco casos fijos, validar cada uno contra ShipmentFields, producir un reporte PASS/FAIL— es código real, ejecutado de verdad para escribir esta lección, reusando validate_shipment_fields() (Módulo 4, lección 6) sin modificar una sola línea. La respuesta del modelo que cada caso compara es un dict fijo, escrito a mano, etiquetado explícitamente como "REPRESENTATIVE" dentro del propio archivo de datos — nunca la salida de una invocación real, por las razones que la lección 7 desarrolla con precisión completa.
Conexión con el módulo
La lección 5 trazó la línea: forma (esta guía) frente a significado (AI Engineering). Esta lección construye exactamente del lado de "forma" de esa línea — cada aserción de este arnés pregunta "¿tiene esta respuesta las claves correctas, con valores no vacíos, weightKg numérico?", nunca "¿es esta la extracción correcta para este texto?". El SLI 3 de la lección 2 (tasa de bloqueo del guardrail) obtiene, aquí, su primer número real: dos de los cinco casos de este arnés fallan la validación, un 40% de bloqueo sobre este conjunto específico.
Analogía: el control de calidad de una línea de ensamblaje, corrido contra piezas de muestra
Una fábrica que quiere probar su línea de control de calidad —¿detecta de verdad una caja con un artículo faltante?— no espera a que lleguen cajas reales de producción para probarlo. Arma, a propósito, un conjunto fijo de cajas de prueba: algunas perfectas, algunas con un artículo faltante a propósito, algunas con un artículo de más que no debería estar ahí. Corre esas cajas de prueba por la línea, y confirma que el control de calidad las clasifica correctamente —perfectas como perfectas, defectuosas como defectuosas—. Esto prueba que el control de calidad funciona, no que la fábrica esté produciendo cajas perfectas hoy. manifest_extraction_smoke_test.py es exactamente ese ejercicio: las cinco "cajas" de este arnés no son extracciones reales de Bedrock —son construidas a mano, algunas deliberadamente completas, algunas deliberadamente incompletas—, para probar que validate_shipment_fields(), el control de calidad, las clasifica correctamente. El día que exista una cuenta real con Bedrock habilitado, este mismo arnés correría contra respuestas reales sin cambiar una sola línea de su lógica de comparación.
Paso 1 — El archivo de fixtures, cinco casos, extendiendo 4471/4472/4473
evals/fixtures/sample_manifests.json, en la raíz de andes-cargo-infra/:
[
{
"caseId": "4471",
"rawText": "Hi team,\n\nFollowing up on the shipment we discussed on the call. We're sending\n120kg of textile goods from our Lima warehouse to the distribution\ncenter in Santiago. AndesExpress is handling the pickup this Thursday.\nShipment reference on our side is AC-4471.\n\nRegards,\nLogistics Team\n",
"expectedFields": {
"shipmentId": "4471",
"originCountry": "Peru",
"destinationCountry": "Chile",
"carrier": "AndesExpress",
"weightKg": "120"
},
"representativeModelResponse": {
"shipmentId": "4471",
"originCountry": "Peru",
"destinationCountry": "Chile",
"carrier": "AndesExpress",
"weightKg": "120"
},
"note": "REPRESENTATIVE -- hand-built to match, never a real Bedrock response. Module 1, lesson 3 source."
},
{
"caseId": "4472",
"rawText": "Hello,\n\nWe have 85kg of goods ready to move from our Bogota facility to\nQuito. AndesExpress will handle the transport, pickup scheduled for\nnext week. Our reference for this one is CO-4472.\n\nThanks,\nWarehouse Team\n",
"expectedFields": {
"shipmentId": "4472",
"originCountry": "Colombia",
"destinationCountry": "Ecuador",
"carrier": "AndesExpress",
"weightKg": "85"
},
"representativeModelResponse": {
"shipmentId": "4472",
"originCountry": "Colombia",
"destinationCountry": "Ecuador",
"carrier": "AndesExpress",
"weightKg": "85"
},
"note": "REPRESENTATIVE -- hand-built to match, never a real Bedrock response."
},
{
"caseId": "4473",
"rawText": "Team,\n\nPlease process this shipment: 200kg, moving from Santiago back to\nLima this time, carrier is RutaSur as usual. Shipment ref CL-4473.\n\nRegards,\nOps\n",
"expectedFields": {
"shipmentId": "4473",
"originCountry": "Chile",
"destinationCountry": "Peru",
"carrier": "RutaSur",
"weightKg": "200"
},
"representativeModelResponse": {
"shipmentId": "4473",
"originCountry": "Chile",
"destinationCountry": "Peru",
"carrier": "RutaSur"
},
"note": "REPRESENTATIVE -- deliberately incomplete (missing weightKg), standing in for a plausible extraction gap on a genuinely ambiguous input."
},
{
"caseId": "4475",
"rawText": "Good morning,\n\nWe have a new shipment ready for pickup: 60kg of electronics parts,\norigin Quito, destination Lima, carried by RutaSur. Please confirm\nonce it is scheduled. Our internal reference is EQ-4475.\n\nBest,\nPartner Logistics Desk\n",
"expectedFields": {
"shipmentId": "4475",
"originCountry": "Ecuador",
"destinationCountry": "Peru",
"carrier": "RutaSur",
"weightKg": "60"
},
"representativeModelResponse": {
"shipmentId": "4475",
"originCountry": "Ecuador",
"destinationCountry": "Peru",
"carrier": "RutaSur",
"weightKg": "60"
},
"note": "REPRESENTATIVE -- hand-built to match, never a real Bedrock response. Extends the fixed batch beyond 4471/4472/4473."
},
{
"caseId": "4476",
"rawText": "Hi,\n\nOne more for this week: 45kg general cargo, from Cali to Guayaquil,\nAndesExpress again. Reference AC-4476.\n\nThanks,\nLogistics Team\n",
"expectedFields": {
"shipmentId": "4476",
"originCountry": "Colombia",
"destinationCountry": "Ecuador",
"carrier": "AndesExpress",
"weightKg": "45"
},
"representativeModelResponse": {
"shipmentId": "4476",
"originCountry": "Colombia",
"destinationCountry": "Ecuador",
"carrier": "AndesExpress",
"weightKg": "45",
"confidenceScore": "high"
},
"note": "REPRESENTATIVE -- deliberately adds an invented field (confidenceScore) never requested, standing in for a plausible over-generation failure mode."
}
]
Cinco casos: los tres envíos ya conocidos (4471, 4472, 4473) más dos nuevos (4475, 4476), exactamente como el diseño de este módulo prometió. Fíjate en el campo note de cada uno: la palabra "REPRESENTATIVE" aparece en las cinco, sin excepción — la etiqueta no vive en la prosa de esta lección, vive dentro del propio archivo de datos, para que nadie que abra sample_manifests.json en el futuro, sin haber leído esta lección, pueda confundir representativeModelResponse con una respuesta real. Y fíjate en los casos 4473 y 4476: uno le falta un campo, el otro tiene uno de más — a propósito, para que el arnés tenga algo real que rechazar, no solo casos que siempre pasan.
Paso 2 — El arnés, completo
evals/manifest_extraction_smoke_test.py:
#!/usr/bin/env python3
"""manifest_extraction_smoke_test.py -- the structural smoke test harness for
extract-shipment-manifest-fields (Module 7, lesson 6).
What this script IS: a real, executed comparison of SHAPE -- does a candidate
response have the five ShipmentFields keys, non-empty, weightKg numeric, no
extra keys? It reuses validate_shipment_fields() from
guardrails/post_invoke_checks.py (Module 4, lesson 6) verbatim, unmodified --
the same schema check that already guards the real write path to Shipments.
What this script IS NOT: a semantic quality eval. It never asks "did the
model understand this manifest correctly?" -- only "does the candidate have
the right shape?". Module 7, lesson 5 draws this line in prose; this script
draws it in code. Semantic quality evaluation is AI Engineering's job (Module
1, lesson 4), not this guide's.
Every "representativeModelResponse" in evals/fixtures/sample_manifests.json is
a hand-built dict, labeled as such in its own "note" field -- never the output
of a real Bedrock invocation (Module 7, lesson 7 explains exactly why none was
invoked). The comparison logic below is 100% real and executed; the data it
compares is representative.
Never uses random or datetime.now(). Same fixtures file, same result, every
run. Run with:
python3 evals/manifest_extraction_smoke_test.py
pytest evals/test_manifest_extraction_smoke_test.py -v
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "guardrails"))
from post_invoke_checks import validate_shipment_fields # noqa: E402
FIXTURES_PATH = Path(__file__).resolve().parent / "fixtures" / "sample_manifests.json"
def load_fixtures(path: Path = FIXTURES_PATH) -> list[dict]:
return json.loads(path.read_text())
def run_smoke_test(fixtures: list[dict]) -> list[dict]:
"""For each fixture, validate representativeModelResponse against
ShipmentFields. Returns one result dict per fixture -- never compares
against expectedFields directly (that would be a semantic check: "is
this the RIGHT extraction for this text?", out of scope here). This
harness only asks: "is the candidate's SHAPE writable to Shipments?"."""
results = []
for fixture in fixtures:
candidate = fixture["representativeModelResponse"]
validation = validate_shipment_fields(candidate)
results.append(
{
"caseId": fixture["caseId"],
"isValid": validation.is_valid,
"errors": validation.errors,
}
)
return results
def format_summary(results: list[dict]) -> str:
lines = []
passed = sum(1 for r in results if r["isValid"])
total = len(results)
for r in results:
status = "PASS" if r["isValid"] else "FAIL"
lines.append(f"[{r['caseId']}] {status}")
for err in r["errors"]:
lines.append(f" - {err}")
lines.append("")
lines.append(f"Smoke test: {passed}/{total} fixtures passed structural validation")
return "\n".join(lines)
def main() -> int:
fixtures = load_fixtures()
results = run_smoke_test(fixtures)
print(format_summary(results))
return 0 if all(r["isValid"] for r in results) else 1
if __name__ == "__main__":
raise SystemExit(main())
Fíjate en el comentario de run_smoke_test(): dice, explícitamente, que nunca compara contra expectedFields. Es una decisión de diseño deliberada, no un descuido — comparar representativeModelResponse contra expectedFields campo por campo sería, exactamente, el primer paso hacia una evaluación semántica ("¿la respuesta coincide con lo que debería haber extraído?"), la línea que la lección 5 ya trazó con cuidado. Este arnés valida contra ShipmentFields —el esquema, la forma— nunca contra expectedFields —el contenido correcto—. El campo expectedFields existe en el archivo de fixtures como documentación de qué extracción sería correcta, útil para un lector humano, pero el código de este arnés nunca lo lee.
Paso 3 — Corriendo el arnés, de verdad
python3 evals/manifest_extraction_smoke_test.py
Qué esperar (literal — la comparación de estructura corrió de verdad; los datos que compara son representativos, etiquetados dentro del propio archivo):
[4471] PASS
[4472] PASS
[4473] FAIL
- missing required field(s): weightKg
[4475] PASS
[4476] FAIL
- unexpected field(s) not in ShipmentFields: confidenceScore
Smoke test: 3/5 fixtures passed structural validation
Tres de cinco. Fíjate en los dos casos que fallan, y en que cada uno falla por una razón distinta — 4473 por un campo faltante, 4476 por un campo inesperado, los dos tipos de fallo que post_invoke_checks.py ya distinguió con precisión en el Módulo 4, lección 6. Ningún caso falla por un motivo inventado para esta lección; ambos reusan, sin cambios, la lógica de validate_shipment_fields() que ya corrió dieciséis veces en ese módulo.
echo $?
1
Código de salida 1 — el mismo mecanismo que un step de CI usaría para detener un pipeline: si algún día este arnés corriera automáticamente en ci.yml contra respuestas reales, un resultado como este (3/5, no 5/5) bloquearía el merge, exactamente igual que cualquier otro check de este ecosistema.
Paso 4 — El suite de pytest, seis casos fijos
evals/test_manifest_extraction_smoke_test.py:
"""pytest suite for manifest_extraction_smoke_test.py -- exercises the
harness itself against the fixed fixtures file. No random, no
datetime.now(). Run with:
pytest evals/test_manifest_extraction_smoke_test.py -v
"""
from manifest_extraction_smoke_test import load_fixtures, run_smoke_test
def test_five_fixtures_loaded():
fixtures = load_fixtures()
assert len(fixtures) == 5
def test_every_fixture_has_a_representative_label():
fixtures = load_fixtures()
for fixture in fixtures:
assert "REPRESENTATIVE" in fixture["note"]
def test_three_of_five_fixtures_pass_structural_validation():
fixtures = load_fixtures()
results = run_smoke_test(fixtures)
passed = [r for r in results if r["isValid"]]
assert len(passed) == 3
def test_4473_fails_on_missing_weight_kg():
fixtures = load_fixtures()
results = run_smoke_test(fixtures)
result_4473 = next(r for r in results if r["caseId"] == "4473")
assert result_4473["isValid"] is False
assert "missing required field(s): weightKg" in result_4473["errors"][0]
def test_4476_fails_on_unexpected_field():
fixtures = load_fixtures()
results = run_smoke_test(fixtures)
result_4476 = next(r for r in results if r["caseId"] == "4476")
assert result_4476["isValid"] is False
assert "confidenceScore" in result_4476["errors"][0]
def test_4471_4472_4475_pass():
fixtures = load_fixtures()
results = run_smoke_test(fixtures)
for case_id in ("4471", "4472", "4475"):
result = next(r for r in results if r["caseId"] == case_id)
assert result["isValid"] is True
cd evals/
pytest test_manifest_extraction_smoke_test.py -v -p no:randomly
Qué esperar (literal — corrido de verdad):
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: evals/
collected 6 items
test_manifest_extraction_smoke_test.py::test_five_fixtures_loaded PASSED [ 16%]
test_manifest_extraction_smoke_test.py::test_every_fixture_has_a_representative_label PASSED [ 33%]
test_manifest_extraction_smoke_test.py::test_three_of_five_fixtures_pass_structural_validation PASSED [ 50%]
test_manifest_extraction_smoke_test.py::test_4473_fails_on_missing_weight_kg PASSED [ 66%]
test_manifest_extraction_smoke_test.py::test_4476_fails_on_unexpected_field PASSED [ 83%]
test_manifest_extraction_smoke_test.py::test_4471_4472_4475_pass PASSED [100%]
============================== 6 passed in 0.01s ===============================
Seis de seis, incluido test_every_fixture_has_a_representative_label — el test que existe, específicamente, para que ningún caso nuevo que alguien agregue a sample_manifests.json en el futuro pueda colarse sin la etiqueta "REPRESENTATIVE" en su campo note. Es la misma disciplina de honestidad de toda esta guía, ahora aplicada como una aserción de pytest que fallaría automáticamente si alguien lo olvidara.
Paso 5 — La tasa de bloqueo del guardrail, con un número real (sobre este lote)
El SLI 3 de la lección 2 —tasa de bloqueo del guardrail propio— obtiene, aquí, su primer dato concreto:
Tasa de bloqueo = candidatos FAIL / candidatos evaluados
= 2 / 5
= 40%
Con la misma honestidad exacta que el Paso 5 de la lección 4 ya aplicó a la tasa de escalamiento: este 40% es real sobre estos cinco candidatos específicos, construidos a propósito para incluir dos tipos de fallo — no es una medición de qué tan seguido una invocación real de Bedrock produciría una respuesta incompleta. Lo que sí demuestra, con evidencia ejecutada: validate_shipment_fields() distingue correctamente entre candidatos completos e incompletos, sin ambigüedad, cien por ciento de las veces, sobre cualquier candidato que se le presente — la garantía que hace que este SLI sea confiable el día que sí exista tráfico real que evaluar.
Errores comunes
Modificar run_smoke_test() para comparar contra expectedFields en vez de solo validar la forma (de "mejorar" el arnés sin darse cuenta de que cruza la frontera). Qué pasa: alguien, viendo que el archivo de fixtures ya tiene expectedFields, agrega una comparación campo por campo entre representativeModelResponse y expectedFields, pensando que hace el arnés "más completo". Cómo detectarlo: si tu versión de run_smoke_test() alguna vez lee la clave expectedFields del fixture. Cómo corregirlo: relee la lección 5 — comparar contra expectedFields es, exactamente, el primer paso hacia una evaluación semántica, el territorio que esta guía nombra pero no construye. El campo existe en el archivo para que un humano pueda leer qué extracción sería correcta; el código de este arnés, deliberadamente, nunca lo toca.
Interpretar el 3/5 del Paso 3 como una señal sobre la calidad de Nova Lite, el modelo elegido en GENAI-COST-PROFILE.md (de confundir el arnés con una medición del modelo real). Qué pasa: alguien concluye "Nova Lite acierta el 60% de las extracciones", basándose en el resultado de este arnés. Cómo detectarlo: si tu interpretación del 3/5 menciona a Nova Lite, o a cualquier modelo, en algún punto. Cómo corregirlo: ningún dato de este arnés viene de Nova Lite ni de ningún modelo — los cinco representativeModelResponse son diccionarios escritos a mano, dos de ellos deliberadamente rotos para que el arnés tenga algo que rechazar. El 3/5 mide, exclusivamente, que validate_shipment_fields() clasifica correctamente cinco candidatos de prueba construidos a propósito — no dice absolutamente nada sobre qué tan bien Nova Lite, o cualquier otro modelo, extraería campos de un manifiesto real.
Olvidar la etiqueta "REPRESENTATIVE" al agregar un caso nuevo a sample_manifests.json (el error que test_every_fixture_has_a_representative_label existe para atrapar). Qué pasa: alguien agrega un sexto caso al archivo de fixtures, con su propio representativeModelResponse, pero olvida escribir la palabra "REPRESENTATIVE" en el campo note. Cómo detectarlo: el pytest del Paso 4 falla, con precisión, en test_every_fixture_has_a_representative_label — el mensaje de error señala exactamente qué caso no cumple. Cómo corregirlo: este es, precisamente, el motivo de que ese test exista como una aserción de código, no solo como una convención documentada en prosa — un olvido humano se detecta automáticamente, la primera vez que alguien corra el suite completo, en vez de colarse silenciosamente en un archivo de datos que nadie vuelve a revisar línea por línea.
Ejercicios
Ejercicio 1 — Agrega, tú mismo, un sexto caso a sample_manifests.json (envío 4477) con un representativeModelResponse que tenga weightKg como un número JSON nativo (120) en vez de una cadena ("120"). Antes de correr el arnés, predice si ese caso pasaría o fallaría, y por qué.
Ver solución
Fallaría — validate_shipment_fields() (Módulo 4, lección 6) marca weight_not_numeric como verdadero si el valor no es una cadena, incluso si ese valor es un número: _is_numeric_string() empieza con if not isinstance(value, str): return False, así que un entero JSON nativo nunca pasa esa primera verificación. El error reportado sería "weightKg is not a numeric string". Este ejercicio confirma, con un caso propio, exactamente la misma trampa que el Módulo 4, lección 6, Errores comunes ya advirtió: ShipmentFields, tal como esta guía lo define, exige que los cinco campos sean cadenas, sin excepción.
Ejercicio 2 — Explica por qué load_fixtures() no necesita ningún manejo especial de errores para un archivo JSON malformado, a diferencia de post_invoke_checks.py, que sí valida explícitamente con try/except json.JSONDecodeError. ¿Es esto una inconsistencia, o una diferencia de contexto justificada?
Ver solución
Es una diferencia justificada, no una inconsistencia. post_invoke_checks.py recibe su entrada JSON desde la línea de comandos (--json), un canal donde un humano puede cometer un error de tipeo en cualquier momento — validar y reportar ese error con precisión es, precisamente, el trabajo de una herramienta de línea de comandos bien diseñada. load_fixtures(), en cambio, lee un archivo committeado al repositorio, versionado, revisado como cualquier otro código — un JSON malformado ahí sería un error de programación detectado inmediatamente por cualquier test de este suite (test_five_fixtures_loaded() fallaría con una excepción antes de siquiera llegar a su aserción), no un error de entrada de un usuario en tiempo real. Agregar manejo de excepciones ahí sería código defensivo sin ningún caso de uso real que lo justifique — la misma disciplina de "no agregues código para un problema que no existe" que un ingeniero cuidadoso aplica en cualquier proyecto.
Ejercicio 3 — Predice qué pasaría con el resultado del Paso 3 (3/5) si alguien corrigiera el caso 4473, agregando "weightKg": "200" a su representativeModelResponse, sin tocar ningún otro caso. ¿Cambiaría también la tasa de bloqueo del guardrail del Paso 5?
Ver solución
El resultado del Paso 3 pasaría de 3/5 a 4/5 — 4473 pasaría a PASS, porque tendría, ahora, las cinco claves requeridas con valores no vacíos y weightKg numérico. La tasa de bloqueo del Paso 5 cambiaría en consecuencia, de 2/5 (40%) a 1/5 (20%) — los dos números están directamente acoplados, porque ambos se calculan sobre el mismo conjunto de resultados de run_smoke_test(). Este ejercicio confirma algo importante: la tasa de bloqueo del guardrail no es un número fijo de esta guía —depende, enteramente, de qué candidatos específicos entran al arnés—, exactamente la misma honestidad que el Paso 5 de esta lección ya declaró sobre el 40% actual.
Resumen y siguiente paso
Esta lección construyó evals/manifest_extraction_smoke_test.py sobre evals/fixtures/sample_manifests.json — cinco casos, extendiendo 4471/4472/4473 con dos nuevos—, y lo corrió de verdad: 3/5 fixtures pasan la validación estructural, con los dos fallos —4473 (campo faltante) y 4476 (campo inesperado)— reusando, sin ningún cambio, validate_shipment_fields() del Módulo 4. Verificaste el arnés con seis casos de pytest, incluido uno que garantiza que ningún caso futuro pueda colarse sin su etiqueta "REPRESENTATIVE". Calculaste, con datos reales sobre este lote específico, la tasa de bloqueo del guardrail: 40%.
Antes de avanzar deberías poder: explicar por qué run_smoke_test() nunca lee expectedFields; recitar el resultado literal (3/5, con las dos razones de fallo exactas); y explicar por qué el 3/5 de esta lección no dice nada sobre la calidad de ningún modelo real.
La lección 7 documenta, con la misma honestidad exacta que el Módulo 3, lección 6 y el Módulo 4, lección 7 ya aplicaron a apply y al bloqueo del guardrail gestionado, el límite final de este módulo: por qué la latencia real de inferencia, y la calidad semántica que el arnés de esta lección deliberadamente no mide, no se pueden medir en este laboratorio $0 — con las métricas reales de CloudWatch que sí existirían, si una invocación real ocurriera.
Recursos
- Google — SRE Workbook, Implementing SLOs — la fuente del patrón de evaluación por inyección de datos que la lección 5 ya citó, aplicado aquí en código.
- Este mismo curso, Módulo 4, lección 6 (
06-hands-on-the-output-schema-validator.md) — el origen devalidate_shipment_fields(), reusado sin ningún cambio en el Paso 2 de esta lección. - Este mismo curso, Módulo 1, lección 3 — el origen de los envíos
4471/4472/4473, extendidos en el Paso 1 de esta lección con4475/4476. - Este mismo curso, Módulo 7, lección 5 (
05-what-is-a-production-eval-and-why-it-is-not-a-unit-test.md) — la frontera conceptual que esta lección construye en código.