Volver al blog

De un prompt one-shot a un sistema de IA trazable

Imagen destacada: De un prompt one-shot a un sistema de IA trazable

Durante las últimas semanas he estado muy centrado en entender qué significa pasar de una integración basada en un único prompt a un sistema de IA que pueda evolucionar, observarse y mantenerse.

LiteraryTrip empezó con un MVP sencillo: una Supabase Function recibía una petición y ejecutaba un prompt para generar una ruta literaria. Era suficiente para validar la idea, pero no para entender el sistema cuando algo fallaba.

Construir la siguiente versión no consistió solo en cambiar el prompt. Consistió en separar responsabilidades, modelar estados, introducir validaciones y hacer visible cada etapa de la generación.

Este artículo resume ese recorrido: desde el primer flujo one-shot hasta una arquitectura basada en workers, LangChain, LangGraph, tracing y publicación autoritativa.

El MVP: perfecto para validar la idea

La primera versión tenía un flujo deliberadamente simple:

petición → Supabase Function → prompt → modelo → respuesta generada

La función recibía los datos de la obra, construía el prompt y pedía al modelo una ruta literaria estructurada.

Ese diseño tenía una ventaja enorme: permitía aprender rápido. Antes de diseñar una plataforma completa, podíamos comprobar si el producto tenía sentido.

El problema apareció después, cuando la pregunta dejó de ser “¿podemos generar una ruta?” y pasó a ser:

  • ¿Qué ocurrió exactamente durante esta generación?
  • ¿Qué modelo y versión de prompt participaron?
  • ¿Falló el modelo, la validación o la evidencia?
  • ¿La respuesta fue reparada o se generó de nuevo?
  • ¿Por qué se publicó una ruta y otra quedó descartada?
  • ¿Podemos reconstruir el proceso sin guardar contenido sensible?

Un prompt puede producir una respuesta. No puede, por sí solo, explicar de forma fiable el ciclo de vida completo de esa respuesta.

La primera decisión: separar generación y sistema

El cambio más importante fue dejar de tratar la generación como una única operación.

La generación pasó a formar parte de un pipeline con responsabilidades explícitas:

enqueue
  → resolución de identidad canónica
  → claim del job
  → selección del runtime
  → orquestación
  → proyección
  → publicación autoritativa

La Edge Function de entrada ya no tiene que resolverlo todo. Valida la petición, controla el acceso y crea un job con el contexto necesario.

Después, un worker reclama ese job y ejecuta el pipeline adecuado según el contrato y las capacidades persistidas en el propio job.

Esto permite mantener compatibilidad con el runtime legacy sin hacer fallback silencioso desde el runtime nuevo.

La separación quedó organizada alrededor de varias fronteras:

Capa Responsabilidad
enqueue-generation Autenticación, validación inicial, cuota y creación del job
process-generation-jobs Claim, reintentos, estados terminales y ejecución del worker
Orquestador literario Ejecutar el proceso de generación como datos
Proyección Convertir el resultado en el payload publicable
Publicación Persistir de forma autoritativa e idempotente

Una decisión especialmente importante fue que el orquestador no publicara directamente en la base de datos.

El pipeline capaz devuelve datos. El worker conserva la responsabilidad de persistir, publicar y cerrar el ciclo operativo.

LangGraph: convertir la generación en un proceso

LangGraph permitió expresar la generación como un grafo de estados, en lugar de esconder toda la lógica dentro de una llamada al modelo.

El flujo principal quedó así:

prepare_input
      ↓
generate
      ↓
validate_proposal
   ┌──┴───────────────┐
válido              inválido
   ↓                    ↓
evidence        repair_proposal
   ↓                    ↓
classify_evidence ←─────┘
   ↓
assess_publishability

Si la validación agota el presupuesto de reparación, el flujo registra quality_exhausted y termina como un resultado no publicable.

¿Qué hace realmente generate?

generate es el nodo que realiza la llamada al modelo, pero no contiene directamente la implementación del proveedor.

Recibe el estado actual, ejecuta un generador inyectado, aplica un deadline y registra la operación mediante el tracer.

Conceptualmente:

proposal = await trace("generate", request, () =>
  withDeadline(signal => generator(request, signal), 30_000)
)

Esto hace que el nodo sea testeable y que el orquestador no dependa de un proveedor concreto.

El modelo no es el único actor

Los nodos no representan necesariamente modelos diferentes ni agentes autónomos.

Representan etapas con responsabilidades distintas:

  • prepare_input prepara y traza el contexto de entrada.
  • generate solicita una propuesta al modelo.
  • validate_proposal comprueba estructura, coordenadas, conteos y calidad.
  • repair_proposal solicita una corrección con instrucciones específicas.
  • evidence enriquece los lugares candidatos.
  • classify_evidence aplica la política de clasificación.
  • assess_publishability decide si el resultado puede publicarse.

La diferencia es fundamental: la llamada al modelo es una pieza del proceso, no el proceso completo.

Reparación acotada, no reintentos infinitos

Cuando una propuesta falla la validación, el sistema no vuelve a intentarlo sin límite.

El nodo repair_proposal recibe el error normalizado y construye instrucciones específicas para corregir la propuesta.

Después vuelve a validate_proposal. El ciclo puede repetirse solo dentro del presupuesto semántico definido por la política del job.

Esto separa dos conceptos que antes podían confundirse:

  • Retry operativo: el worker vuelve a ejecutar un job por un fallo transitorio.
  • Repair semántico: el modelo corrige una propuesta que no cumple el contrato.

No son el mismo problema y no deben tener el mismo contador ni la misma política.

Estado explícito y reducers

El grafo mantiene un estado explícito con campos como:

request
proposal
repairCount
validationError
places
publishability
terminalDecision

Cada actualización del estado tiene un reducer definido. Así, la reparación no pierde la propuesta anterior, el contador no se reinicia accidentalmente y el error de validación sigue disponible para decidir la siguiente transición.

Este detalle parece pequeño, pero es una de las diferencias entre encadenar llamadas y modelar una máquina de estados.

La trazabilidad que se ganó

El refactor añadió trazabilidad en varias dimensiones.

Identidad de la ejecución

La generación ahora puede seguirse mediante una identidad correlacionada:

correlation_id → job_id → trace_id → attempt

El contexto se persiste cuando se crea el job, en lugar de intentar reconstruirlo después desde logs separados.

El worker utiliza el trace_id persistido y mantiene la relación entre el evento de entrada, el procesamiento y el resultado terminal.

Etapas observables

El tracing registra operaciones como:

  • resolución de identidad canónica;
  • búsqueda de caché;
  • carga del prompt;
  • llamada al proveedor;
  • corroboración;
  • política de proyección;
  • publicación;
  • cierre terminal de la generación.

Además, el orquestador registra las etapas del grafo, incluyendo generación, validación, reparación, evidencia y decisión de publicabilidad.

Resultados terminales explícitos

La generación deja de terminar en un simple “éxito” o “error”.

El lifecycle diferencia resultados como:

  • generated;
  • low_quality;
  • failed;
  • paused_resolution;
  • retry_scheduled.

Esto permite que una herramienta operativa distinga un resultado de baja calidad de un fallo técnico o de un job que todavía debe reintentarse.

Runs de modelo y duración

Los intentos del modelo se registran con información operativa como duración, resultado, error, trace e intento.

La observabilidad deja de ser únicamente una colección de mensajes y empieza a funcionar como un registro consultable del comportamiento del sistema.

Langfuse y el ledger local cumplen funciones distintas

La integración con Langfuse aporta una vista externa y temporal de la ejecución.

Permite seguir spans relacionados con una generación sin enviar automáticamente todo el contenido producido por el modelo.

El tracing se encapsula detrás de DeveloperTracer. Así, el orquestador conoce una interfaz de tracing y no queda acoplado a la implementación concreta.

Además del tracing externo, el sistema mantiene un ledger local de LangGraph:

literary_graph_runs
literary_graph_steps

Ese ledger registra la ejecución del grafo y sus pasos con secuencia, nodo, estado, duración y snapshots sanitizados.

La combinación es útil porque resuelve dos necesidades diferentes:

  • Langfuse ayuda a observar la ejecución de forma operativa.
  • El ledger local conserva una representación durable y gobernada del proceso de negocio.

Trazabilidad sin convertirla en una fuga de datos

Observar más no significa almacenar todo.

Los snapshots del ledger pasan por una allowlist de campos. Se conservan conteos, decisiones, códigos de error y versiones de política, pero no el contenido literario completo.

El diseño excluye de la telemetría datos como:

  • identificadores directos del usuario;
  • prompts completos;
  • respuestas completas del modelo;
  • cuerpos de errores HTTP;
  • instrucciones internas de retry;
  • claves de caché;
  • URLs y localizaciones sin necesidad operativa.

La observabilidad se convierte así en un contrato de datos, no en un console.log con más información.

Provenance y calidad: generar no equivale a verificar

La modularidad también permitió separar la generación de la procedencia de los datos.

La ruta usa identidad canónica para evitar que el texto introducido por un usuario se convierta directamente en la fuente de verdad.

Los contratos de evidencia incluyen fuente, título, locator, fecha de recuperación, versión, derechos, licencia y nivel de la fuente.

También se distingue entre una afirmación factual verificada y la corroboración de otro modelo.

La corroboración de IA puede reducir el riesgo de alucinación, pero no equivale a una verificación factual.

Esta distinción es esencial cuando el resultado termina alimentando una experiencia de producto y no solo una demo.

Modularidad mediante contratos y adaptadores

El orquestador depende de interfaces, no de implementaciones concretas.

Entre los contratos principales están:

  • RouteProposalGenerator para generar o reparar propuestas;
  • LiteraryEvidenceSource para enriquecer lugares con evidencia;
  • LiteraryOrchestrator para ejecutar la orquestación;
  • puertos de runtime para separar el flujo legacy del flujo LangGraph.

Esto permite sustituir un proveedor, una fuente de evidencia o un tracer sin reescribir el grafo completo.

También permite probar el comportamiento con dependencias controladas, sin tener que invocar un modelo real en cada test.

La modularidad no significa tener muchos archivos. Significa que cada cambio tiene un lugar claro y un contrato que protege al resto del sistema.

Antes y después

Antes Después
Una función concentraba gran parte del flujo Entrada, worker, orquestador, proyección y publicación tienen responsabilidades separadas
El modelo devolvía una respuesta final El grafo gobierna generación, validación, reparación y decisión
Los fallos eran difíciles de reconstruir Cada job tiene correlación, trace, intento y resultado terminal
Los retries y las correcciones podían mezclarse Retry operativo y reparación semántica tienen políticas distintas
La telemetría podía contener demasiado contexto Los datos observables están gobernados por allowlists
La persistencia estaba cerca de la generación El orquestador devuelve datos y la publicación es autoritativa e idempotente

Qué cambió en mi forma de pensar como AI Engineer

Este trabajo me dejó varias ideas importantes.

1. El prompt es una pieza, no una arquitectura

Un prompt puede resolver el primer problema de producto. No resuelve por sí solo identidad, estado, validación, reintentos, privacidad ni publicación.

2. La calidad debe estar fuera del modelo

El modelo propone. El sistema valida, repara dentro de límites y decide si el resultado cumple la política de publicación.

3. La observabilidad se diseña desde el principio

Añadir logs al final ayuda a investigar síntomas. Diseñar identidad correlacionada y estados terminales permite reconstruir la historia completa.

4. La modularidad reduce el coste de aprender

Cuando el proveedor, el evidence source y el tracer están detrás de contratos, es posible experimentar sin convertir cada experimento en una migración completa.

5. La IA necesita ingeniería alrededor

LangChain y LangGraph no sustituyen el diseño del sistema. Ayudan a expresar la ejecución, pero la calidad depende también de contratos, políticas, persistencia, seguridad y pruebas.

Lo que puedo afirmar y lo que todavía no

El refactor demuestra mejoras de estructura, trazabilidad, privacidad, modularidad y control del lifecycle.

No sería correcto afirmar todavía que redujo la latencia, el coste o la tasa de error en producción sin métricas comparables.

Tampoco sería correcto presentar la corroboración de otro modelo como verificación factual.

Para mí, esta cautela forma parte del cambio de mentalidad: una arquitectura puede estar mejor diseñada sin que eso autorice a inventar resultados de producción.

Cierre

LiteraryTrip empezó como una llamada directa a un modelo porque esa era la forma correcta de validar la idea.

Después necesitó convertirse en un sistema porque las preguntas importantes ya no eran solo sobre generación.

Eran preguntas sobre control, trazabilidad, calidad, seguridad y evolución.

El paso de un one-shot prompt a una arquitectura con workers, LangGraph, LangChain, tracing y publicación autoritativa no fue añadir complejidad por añadirla.

Fue hacer explícita la complejidad que el producto ya tenía.

Ese es, para mí, el aprendizaje central de esta transición hacia AI Engineering.

La llamada al modelo produce una propuesta. La arquitectura determina si esa propuesta puede ser entendida, validada y convertida en producto.

#AIEngineering #LangGraph #LangChain #Supabase #Observability