Módulo 1: Genai In Production Vs A Notebook
3. La carga de IA de Andes Cargo: cuando el parser determinista no alcanza
Descripción
process-shipment-manifest, la función Lambda que aws-core-services-guide construyó y que aws-serverless-and-containers-guide conectó al bus de eventos de Andes Cargo, hace exactamente una cosa: lee un archivo de texto subido a andes-cargo-shipment-docs, lo parsea con una regla simple —una línea, un separador =, una clave y un valor—, y si esa regla encuentra los campos que necesita, escribe un registro en Shipments. Funciona perfectamente, siempre, para el formato que espera. El problema que esta guía existe para resolver no es que esa función tenga un error. Es que algunos socios logísticos de Andes Cargo no envían manifiestos en ese formato — envían el cuerpo de un correo, una nota copiada de un sistema propio, texto libre que ningún separador = puede leer. Hoy, esos manifiestos simplemente fallan.
Esta lección hace tres cosas: te muestra el parser real corriendo contra ambos tipos de entrada, para que veas el fallo con tus propios ojos en vez de aceptarlo como afirmación; te muestra la decisión de arquitectura que resuelve el problema sin reescribir la función que ya funciona; y presenta, por su nombre, la pieza nueva que esta guía construye en los módulos siguientes — extract-shipment-manifest-fields — sin construirla todavía.
Conexión con el módulo
La lección 2 te dio la teoría: por qué un modelo que responde bien en el playground no dice nada sobre producción. Esta lección le da a esa teoría un caso concreto y real, el mismo que sostiene el resto de esta guía. La lección 4 traza, inmediatamente después, la frontera exacta entre lo que esta guía construye alrededor de ese caso (infraestructura, guardrails, costo) y lo que no construye (el prompt que hace la extracción en sí — eso es AI Engineering).
El parser heredado, tal como lo dejó aws-core-services-guide
Este es el código real de process-shipment-manifest, sin ninguna modificación — el mismo que corrió en aws-core-services-guide, Módulo 6:
import json
import urllib.parse
import boto3
s3 = boto3.client("s3")
def lambda_handler(event, context):
processed = []
for record in event["Records"]:
bucket_name = record["s3"]["bucket"]["name"]
object_key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
response = s3.get_object(Bucket=bucket_name, Key=object_key)
content = response["Body"].read().decode("utf-8")
manifest = parse_manifest(content)
# write_shipment_record(manifest, object_key) sigue aquí,
# heredado de aws-core-services-guide Módulo 7 -- sin cambios.
processed.append(manifest)
return {"statusCode": 200, "manifestsProcessed": len(processed)}
def parse_manifest(text):
fields = {}
for line in text.strip().splitlines():
if "=" in line:
key, _, value = line.partition("=")
fields[key.strip()] = value.strip()
return fields
parse_manifest es deliberadamente simple: por cada línea del texto, si hay un =, todo lo que está antes es la clave, todo lo que está después es el valor. No sabe qué es un "envío" — solo sabe leer clave=valor. Esa simplicidad es, precisamente, lo que la hace rápida, gratis de ejecutar y 100% predecible.
El mismo parser, contra dos tipos de entrada reales
Corre este script — es exactamente parse_manifest, sin ninguna modificación, contra dos manifiestos: uno con el formato que Andes Cargo siempre esperó, y uno con el formato que un socio logístico real mandaría si describiera el mismo envío en sus propias palabras.
structured_manifest = """shipmentId=4471
originCountry=Peru
destinationCountry=Chile
carrier=AndesExpress
weightKg=120"""
free_text_manifest = """Hi team,
Following up on the shipment we discussed on the call. We're sending
120kg of textile goods from our Lima warehouse to the distribution
center in Santiago. AndesExpress is handling the pickup this Thursday.
Shipment reference on our side is AC-4471.
Let us know if you need anything else.
Regards,
Logistics Team
"""
print(json.dumps(parse_manifest(structured_manifest), indent=2))
print(json.dumps(parse_manifest(free_text_manifest), indent=2))
Qué esperar (literal — este es exactamente el mismo parse_manifest de process-shipment-manifest, corrido para escribir esta lección, sin ningún cambio):
{
"shipmentId": "4471",
"originCountry": "Peru",
"destinationCountry": "Chile",
"carrier": "AndesExpress",
"weightKg": "120"
}
{}
El primer manifiesto — el formato de siempre — produce exactamente los cinco campos que write_shipment_record necesita para escribir en Shipments. El segundo — un correo real, con la misma información de fondo (120kg, Lima a Santiago, AndesExpress, el envío AC-4471) — produce un diccionario vacío. No hay ningún = en el texto del correo, así que parse_manifest nunca entra al bloque if "=" in line, y fields termina exactamente como empezó: sin ningún campo. No es un error del parser — el parser hizo exactamente lo que promete. Es que la pregunta que le hicimos ("¿esto tiene la forma clave=valor?") tiene una respuesta honesta de "no" para este texto, y el parser no tiene ningún plan B para ese "no".
Qué pasa hoy cuando el diccionario llega vacío
Sin ningún cambio de código, el siguiente paso de process-shipment-manifest —heredado de aws-core-services-guide, Módulo 7— intenta leer manifest_data["originCountry"] para escribir el registro en Shipments. Con manifest = {}, esa lectura lanza un KeyError de Python, sin capturar. La invocación de la función Lambda termina en error. ShipmentManifestWorkflow —el workflow de Step Functions que aws-serverless-and-containers-guide Módulo 3 construyó, con Retry/Catch real— reintenta un par de veces, y si el manifiesto sigue sin poder parsearse (que es exactamente lo que va a pasar, porque el texto no cambia entre reintentos), el paso termina marcado como fallido. No hay ninguna vía automática de recuperación. El manifiesto de un socio logístico real, con información de envío perfectamente válida, simplemente no llega a Shipments. Alguien, en algún momento, tiene que notar la falla y procesar ese envío a mano.
La decisión de arquitectura: evolución, no reescritura
La respuesta de esta guía no es reemplazar parse_manifest por un modelo de lenguaje. Es agregarle a process-shipment-manifest una salida honesta para el caso que hoy termina en KeyError sin control: cuando el parseo clave=valor no produce los campos mínimos que necesita, en vez de intentar escribir de todas formas y fallar sin aviso, la función publica un evento nuevo — ManifestParseFailed — en el mismo bus andes-cargo-events que ya usa ShipmentProcessed, con el texto crudo del manifiesto adjunto. Una función nueva, extract-shipment-manifest-fields, escucha específicamente ese evento, e intenta extraer los mismos campos invocando un modelo de Bedrock — y solo si esa extracción tiene éxito, escribe en Shipments exactamente como lo haría el parser determinista.
HOY (sin esta guía) DESPUÉS (con esta guía, M3-M4)
manifiesto texto libre manifiesto texto libre
│ │
▼ ▼
parse_manifest() → {} parse_manifest() → {}
│ │
▼ ▼
KeyError sin control publica ManifestParseFailed
│ │
▼ ▼
Retry/Catch agota intentos extract-shipment-manifest-fields
│ │
▼ ▼
falla, sin recuperación intenta extracción vía Bedrock
automática │
▼
éxito → Shipments (igual que
el camino determinista)
El evento ManifestParseFailed sigue exactamente la misma forma que ShipmentProcessed, para no introducir un patrón nuevo donde el existente ya alcanza:
{
"Source": "andescargo.shipments",
"DetailType": "Manifest Parse Failed",
"Detail": "{\"manifestKey\": \"manifests/year=2026/month=08/shipment-4474-manifest.txt\", \"rawText\": \"Hi team,\\n\\nFollowing up on the shipment...\", \"reason\": \"no key=value pairs found\"}",
"EventBusName": "andes-cargo-events"
}
Fíjate en el nombre del campo reason. No es un detalle cosmético: la razón exacta por la que el parseo falló —"no se encontró ningún par clave=valor"— es información que extract-shipment-manifest-fields no necesita, pero que cualquier humano revisando el bus de eventos sí. Ninguna parte de esta guía construye esa función todavía — el HCL, el handler y los guardrails llegan en los módulos 3 y 4. Esta lección instala la decisión y el nombre; el resto de la guía la construye pieza por pieza.
La lección de arquitectura, dicha en una frase
El camino barato y determinista sigue siendo el default; el LLM es un camino de escalamiento, no el camino principal. process-shipment-manifest sigue siendo la primera y única parada para todo manifiesto bien formado — cero cambios a su costo, su velocidad ni su confiabilidad. extract-shipment-manifest-fields entra en escena únicamente cuando el camino barato ya falló, exactamente el mismo patrón que un mostrador de atención con un formulario estándar y, solo si el formulario no aplica, una persona que lee el caso con calma — más lenta, más cara, usada a propósito con moderación. Es la decisión que el M1.8 de esta lección va a fijar por escrito, como un ADR, y la que el M8.4 va a probar en vivo: un manifiesto bien formado nunca toca Bedrock.
Esta decisión también te da, sin invocar Bedrock ni una sola vez, la métrica de confiabilidad más honesta que esta guía puede ofrecer: la tasa de escalamiento — cuántos de cada cien manifiestos terminan publicando ManifestParseFailed sobre el total procesado. Es un número real, calculable con eventos que sí corren en el laboratorio $0 de esta guía, y el M7 lo convierte en el primer SLI de una carga de IA.
Errores comunes
Pensar que esta lección ya construyó extract-shipment-manifest-fields (de expectativa). Qué pasa: alguien termina esta lección buscando el código del handler que invoca Bedrock. Cómo detectarlo: si buscas, en esta lección, una llamada real a bedrock-runtime invoke-model. Cómo corregirlo: esta lección presenta el nombre, el evento que lo dispara y la decisión de arquitectura que lo sostiene — el handler real, con la llamada a Bedrock aislada detrás de una interfaz simple, se construye en el M3 (infraestructura) y el M4 (guardrails). Es exactamente el mismo patrón de las guías anteriores: la lección de arquitectura primero, la construcción después.
Concluir que process-shipment-manifest "tiene un bug" porque no maneja texto libre (de alcance). Qué pasa: alguien lee que un manifiesto en texto libre produce KeyError y concluye que la función heredada está mal escrita. Cómo detectarlo: si tu reacción es "esto debería haberse manejado desde aws-core-services-guide". Cómo corregirlo: process-shipment-manifest cumplió exactamente su contrato original — parsear manifiestos clave=valor, un formato que, en su momento, era el único que Andes Cargo recibía. El requisito de leer texto libre es nuevo, no un defecto retroactivo; es exactamente el tipo de evolución de producción —agregar un camino nuevo sin romper el que ya funciona— que esta guía entera modela.
Asumir que todo manifiesto en texto libre necesita, ahora, pasar por Bedrock (de lectura del diagrama). Qué pasa: alguien lee el diagrama "antes/después" y concluye que, a partir de ahora, cada manifiesto pasa primero por una verificación de IA. Cómo detectarlo: si tu explicación del flujo nuevo empieza con "primero el modelo revisa el manifiesto". Cómo corregirlo: el orden es el opuesto, y es la decisión de arquitectura central de esta lección — parse_manifest() sigue siendo el primer y único intento para todo manifiesto. extract-shipment-manifest-fields solo se invoca cuando ese primer intento ya falló y publicó ManifestParseFailed. Un sistema que consultara al modelo primero, "por si acaso", sería exactamente el antipatrón que el M1.6 de este mismo módulo nombra con su propio nombre: el LLM como default, en vez de como escalamiento.
Ejercicios
Ejercicio 1 — Corre el parser tú mismo, con un tercer manifiesto. Toma el código de parse_manifest de esta lección y pruébalo con un manifiesto que mezcle ambos formatos: dos líneas clave=valor seguidas de un párrafo de texto libre. Antes de correrlo, predice cuántos campos vas a obtener.
Ver solución
Con una entrada como:
shipmentId=4474
originCountry=Bolivia
Please process this one as priority, the client called twice already.
parse_manifest recorre línea por línea, y solo las dos primeras contienen = — la tercera línea (el párrafo de texto libre) no lo tiene, así que nunca entra al bloque if. El resultado es {"shipmentId": "4474", "originCountry": "Bolivia"}: dos campos, no cero, porque el parser no evalúa el manifiesto completo como una unidad — evalúa cada línea de forma independiente. Esto importa para el M4: un manifiesto parcialmente parseable como este todavía dispararía ManifestParseFailed si destinationCountry, carrier o weightKg faltan, aunque el diccionario no esté completamente vacío.
Ejercicio 2 — Explica por qué el evento se llama ManifestParseFailed y no ManifestNeedsAI. El nombre del evento nuevo describe lo que pasó (el parseo falló), no lo que debería pasar después (usar IA). ¿Por qué esa elección de nombre es la correcta, según la lección de arquitectura de esta lección?
Ver solución
Porque process-shipment-manifest —la función que publica el evento— no sabe, ni debería saber, qué va a pasar después con ese manifiesto. Su única responsabilidad es intentar el parseo determinista y anunciar, con precisión, que ese intento falló. Nombrar el evento por lo que pasó (ManifestParseFailed), en vez de por lo que se espera que pase después (ManifestNeedsAI), mantiene a process-shipment-manifest desacoplada de cómo se resuelve el problema — mañana podría existir un segundo consumidor de ese mismo evento (una cola de revisión manual, por ejemplo) sin que process-shipment-manifest tenga que cambiar una sola línea. Es el mismo principio de diseño event-driven que aws-serverless-and-containers-guide ya enseñó con ShipmentProcessed: el productor anuncia hechos, no instrucciones.
Ejercicio 3 — Calcula, con datos inventados pero coherentes, una tasa de escalamiento. Si Andes Cargo procesa 850 manifiestos en un mes, y 62 de ellos publican ManifestParseFailed, ¿cuál es la tasa de escalamiento? ¿Qué te dice ese número sobre si el default determinista sigue siendo la decisión correcta?
Ver solución
62 / 850 ≈ 7,3%. Ese número, por sí solo —sin invocar Bedrock ni una sola vez—, confirma que la decisión de arquitectura de esta lección sigue siendo la correcta: más del 92% de los manifiestos se resuelven con el camino barato y determinista, y solo una fracción menor escala al camino más caro y lento. Si esa tasa empezara a subir mes a mes —por ejemplo, si un socio logístico grande cambiara su formato de envío—, sería la señal, con evidencia, de que vale la pena invertir en ampliar lo que parse_manifest reconoce directamente, en vez de aceptar un volumen creciente de invocaciones a Bedrock. Es exactamente el tipo de decisión con números, no con intuición, que el M6.7 de esta guía retoma al comparar on-demand contra Provisioned Throughput.
Resumen y siguiente paso
En esta lección viste, con el parser real de process-shipment-manifest corriendo contra dos entradas —una clave=valor, una en texto libre—, exactamente dónde y por qué el camino determinista de Andes Cargo se queda corto: cero campos extraídos de un correo real con información de envío perfectamente válida. Viste la decisión de arquitectura que resuelve el problema sin reescribir la función que ya funciona: un evento nuevo, ManifestParseFailed, publicado en el bus existente, y una función nueva, extract-shipment-manifest-fields, que escala al modelo solo cuando el parser determinista ya lo intentó y falló. Y viste la tesis central de toda esta guía, dicha en una frase: el camino barato sigue siendo el default; el LLM es un camino de escalamiento.
Antes de avanzar deberías poder: explicar, con el resultado literal del parse_manifest de esta lección, por qué un manifiesto en texto libre produce un diccionario vacío, no un error de sintaxis; nombrar el evento nuevo y qué campo lleva la razón exacta del fallo; y explicar, sin dudar, por qué el LLM entra al final del flujo, no al principio.
La lección 4 traza la frontera exacta entre lo que esta guía construye alrededor de extract-shipment-manifest-fields —infraestructura, guardrails, costo, observabilidad— y lo que no construye —el prompt que hace la extracción en sí—, con una cita textual del diseño del propio ecosistema.
Recursos
aws-core-services-guide, Módulo 6 (04-hands-on-deploying-your-first-function.md) y Módulo 7 (07-connecting-lambda-to-dynamodb.md) — el código completo y heredado deprocess-shipment-manifest, sin ningún cambio en esta lección.aws-serverless-and-containers-guide, Módulo 3 (Step Functions) y Módulo 4 (04-custom-events-and-event-patterns.md) —ShipmentManifestWorkflowy el contrato exacto deShipmentProcessed, el patrón queManifestParseFailedsigue.- AWS CLI —
events put-eventsCommand Reference — referencia oficial de la forma de evento (Source/DetailType/Detail) usada en esta lección. - AWS Docs — Amazon EventBridge event patterns — referencia oficial de cómo una regla, en el M3 de esta guía, filtraría específicamente por
ManifestParseFailed.