Stay informed and never miss an Core update!
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

En Parte I, y Parte II, describimos una prueba de concepto sobre el uso de multiagentes de IA para automatizar la gestión de un tipo de trámite de atención a ciudadanos de un organismo del sector público.
En Parte III, describimos algunas soluciones de arquitectura de software que aplican a la problemática de los agentes inteligentes y de los sistemas agénticos, especialmente cuando se requiere dar el siguiente paso de llevar los mismos a ambientes productivos.
En este post, queremos documentar cómo estamos abordando el problema de "El Laberinto del Contexto" en sistemas de alta carga. Usando Akka Platform y un enfoque Agéntico, logramos convertir investigaciones manuales que llevaban horas en procesos automatizados de alta eficiencia, reduciendo el tiempo de resolución en más del 90% y garantizando una escalabilidad sin precedentes. 🚀
Cuando un usuario inicia un trámite solicitando un servicio o un reembolso, espera que se resuelva "ahora". Pero la realidad detrás de escena es muy diferente.
El analista humano debe navegar manualmente a través de mainframes legacy, sistemas con deuda técnica y plataformas desconectadas para recopilar evidencia sobre el usuario, el trámite y su estado. Llamamos a este costo de "context-switching" El Laberinto del Contexto.
Eliminar El Laberinto del Contexto no es solo poner un chatbot encima de una API; requiere orquestar flujos de trabajo complejos con trazabilidad que permita saber exactamente qué ocurrió en cada paso.
La elección de usar Akka Platform para esta clase de producto fue deliberada, pero no solo por razones técnicas: fue una decisión estratégica en cuanto a la velocidad de desarrollo.
En menos de un mes, logramos construir un sistema production-ready, con orquestación compleja, trazabilidad total y resiliencia distribuida. Esto no fue magia: fue el resultado directo de un ecosistema maduro, una arquitectura coherente y herramientas diseñadas para este tipo de problemas desde el primer día.
En un mundo de microservicios tradicionales (sin estado), la gestión de flujos largos y complejos se convierte rápidamente en un enredo de bases de datos compartidas, polling, compensaciones manuales y lógica duplicada.
Akka nos permitió modelar cada trámite abierto como su propia instancia de workflow de larga duración (id estable = id externo del caso), encapsulando estado, lógica y recuperación ante fallos en una única abstracción.
Esto significa que el estado del trámite vive en memoria junto al proceso, listo para procesar el próximo mensaje, sin necesidad de buscarlo en la DB cada vez. Si el sistema escala a millones de trámites, Akka los distribuye (Sharding) a través del clúster automáticamente.
La Velocidad de Desarrollo como Ventaja Competitiva
Akka Platform no solo resolvió el problema técnico: aceleró drásticamente el time-to-market. La combinación de:
nos permitió pasar del concepto a producción en semanas, no trimestres. En sistemas de misión crítica, esta diferencia no es menor: es una ventaja competitiva.
Event Sourcing: Trazabilidad Completa
A diferencia de guardar solo "el último estado", usamos Event Sourcing. Persistimos cada intención (Command) y cada hecho (Event) que ocurre.
Esto nos permite no solo mantener consistencia si el servidor reinicia, sino también reconstruir la historia de cualquier trámite: "¿Qué información utilizó el sistema para recomendar esta resolución hace 3 meses?". Podemos auditar los eventos y entender exactamente qué evidencia tenía el proceso en ese momento.
Para integrar IA, evitamos el caos de agentes autónomos hablando entre sí sin control. Diseñamos una arquitectura estricta de Orchestrator-Workers.
El orquestador principal es un único procedure workflow (piénsalo como el plano de control). El progreso se modela como una cadena explícita de workflow steps (cada paso es una referencia a un método pasado a transitionTo / thenTransitionTo, con @StepName opcional para logging y tooling). Eso es lo que hace que el flujo de control sea determinístico: el runtime siempre sabe qué paso viene después. En la práctica, la cadena principal se ve así:
// Pseudocódigo — forma del workflow de Akka Platform
public Effect<StartResponse> start(StartCaseRequest req) {
return effects()
.updateState(ProcedureState.initial(req.caseId()))
.transitionTo(ProcedureWorkflow::loadCase)
.thenReply(StartResponse.accepted(req.caseId()));
}
@StepName("loadCase")
private StepEffect loadCase() {
return stepEffects()
.updateState(currentState().withDetails(caseApi.fetchCase(...)))
.thenTransitionTo(ProcedureWorkflow::classifyProcedureKind);
}
// loadCase → classify → validate → loadRules → gatherEvidence (pause)
// child pipeline completes → onEvidenceReady → decide → publishloadCase: Obtiene los datos del caso desde la API de negocio y materializa el estado del workflow.classifyProcedureKind: Selecciona la "forma" del trámite (producto + subtipo) para que las reglas y herramientas posteriores sean las correctas.validate: Ejecuta las precondiciones para ese tipo de trámite (identificadores, campos obligatorios, combinaciones soportadas).loadRules: Resuelve la versión aplicable de política/texto normativo y la adjunta al contexto (a menudo respaldado por contenido de prompt versionado).gatherEvidence: Construye un plan de herramientas por etapas, inicia un workflow hijo de investigación, arma un temporizador de espera y realiza thenPause() hasta la finalización asíncrona.onEvidenceReady: Después de que el workflow hijo publica la evidencia agregada, un callback deduplicado despausa al padre hacia la toma de decisiones.decide: Ejecuta la pila de decisión/recomendación (con guardrails).publish: Escribe el resultado de vuelta en el sistema de registro del caso.
La mayor parte del trabajo se encuentra en un workflow secundario dedicado a la evidencia. En lugar de disparar todas las integraciones a la vez, recorre un plan por etapas explícito (llamadas a herramientas ordenadas con condiciones). Esto permite bordes de dependencia ("omitir búsqueda de crédito si el interesado es desconocido") y semántica de fallo por herramienta.
Cada herramienta se ejecuta en un ThreadPoolExecutor acotado detrás de un pequeño proveedor de pool de ejecución (futuros enviados + timeouts por herramienta), de modo que una llamada externa bloqueada no paraliza al actor del workflow en sí mismo (bulkhead entre orquestación e I/O).
// Pseudocódigo — runner iterativo de herramientas dentro del workflow hijo
@StepName("executeNextTool")
private StepEffect executeNextTool() {
ResearchState st = currentState();
Plan.Stage stage = plan.stages().get(st.stageIdx());
ToolCall call = stage.tools().get(st.toolIdx());
ResearchContext ctx = /* case id + procedure kind + prior outputs */;
if (!shouldRun(call, ctx)) {
return stepEffects()
.updateState(st.advanceCursor())
.thenTransitionTo(ResearchWorkflow::executeNextTool);
}
try {
Map<String, Object> fragment = runToolWithRetries(
registry, call, ctx, stage.timeoutSec(), retry.max(), retry.backoffMs());
return stepEffects()
.updateState(st.merge(fragment).advanceCursor())
.thenTransitionTo(ResearchWorkflow::executeNextTool);
} catch (Exception ex) {
return onToolFailure(new ToolFailure(ex, call, st.stageIdx(), st.toolIdx()));
}
}
Un desafío enorme fue que la evidencia casi nunca llega en texto plano. Los usuarios suben fotos de facturas, PDFs, Excels, documentos Word y XMLs. Para esto, construimos una familia de 5 agentes de ingesta especializados, cada uno optimizado para su formato.
Para PDFs, usamos Apache PDFBox para extraer texto nativo cuando es posible, con límites de tamaño y un camino de fallback que rasteriza páginas y delega al pipeline de imagen cuando el PDF es efectivamente un escaneo. Los prompts de extracción están ajustados para documentación fiscal y administrativa (identificadores, montos, períodos, líneas de artículos). La llamada al LLM pasa por un cliente HTTP delgado; los ids de modelo permanecen en configuración orientada por entorno para que podamos cambiar de proveedor o de tamaño sin redesplegar la lógica de negocio.
// Esbozo de estrategia
PDFExtractionResult textResult = extractTextFromPdf(pdfBytes);
String rawJson = textResult.hasEnoughText
? extractWithLlm(textResult)
: renderPagesToImagesAndRunVisionPipeline(...);
Para fotos de facturas, recibos y escaneos, usamos un modelo de visión multimodal (seleccionado vía config). El pipeline envía los bytes de la imagen al endpoint de inferencia y espera JSON estricto de vuelta: texto legible, indicadores de calidad, orientación y campos estructurados (totales, líneas de impuestos, fechas, filas).
// Pseudocódigo — mismo stack HTTP que las rutas de texto, payload multimodal
String rawJson = llmClient.completeVisionJson(modelId, OCR_PROMPT, base64Image);
Los archivos .xlsx se procesan con Apache POI. Convertimos las hojas de cálculo en tablas Markdown, preservando las relaciones espaciales (encabezados, totales, fechas). Esto permite al LLM entender la estructura tal como lo haría un humano, pero como texto estructurado; luego el mismo stack LLM configurable interpreta la tabla.
Para archivos .doc y .docx, usamos Apache POI para extraer párrafos, detectar encabezados y preservar tablas. El agente identifica el tipo de documento (informe, carta, contrato) y extrae datos clave como fechas, partes, identificadores fiscales y referencias legales.
Para XMLs, implementamos una estrategia inteligente: los archivos pequeños (<50KB) se envían completos al LLM, mientras que los XMLs grandes se parsean estructuralmente para extraer solo la jerarquía y los campos clave. Especializado en facturas electrónicas, punto de venta, tipo de comprobante y validación de schema.
Antes de gastar cómputo en extraer datos, tenemos un "Portero". Implementamos un validador universal que nos dice, por ejemplo, "¿Es este un documento fiscal válido o una selfie del usuario?". Si no es relevante, se rechaza, ahorrando tiempo de procesamiento y limpieza. Funciona tanto para PDFs como para imágenes. Una pequeña puerta de documento aplica múltiples capas antes de confiar en un parse: una marca explícita del modelo ("¿está en alcance?"), listas de bloqueo de tipo/subtipo para artefactos obviamente fuera del ámbito laboral, y un paso heurístico final para señales reales de negocio (identificadores, dinero, fechas, tablas estructuradas) en el JSON.
// Pseudocódigo — misma idea para cada canal de ingesta
public ValidationResult validate(String jsonResponse) {
JsonNode root = objectMapper.readTree(jsonResponse);
if (root.has("document_in_scope")) {
if (!root.get("document_in_scope").asBoolean()) {
return rejected(root.path("rejection_reason").asText("Out of scope"));
}
return accepted();
}
if (root.has("error") && root.get("error").asBoolean()) {
return accepted(); // falla de extracción, no un rechazo de negocio
}
String docType = root.path("document_type").asText("").toLowerCase();
if (isObviousNonWorkType(docType)) {
return rejected("Unsupported document_type: " + docType);
}
if (!hasAnyBusinessSignals(root)) {
return rejected("No business signals detected");
}
return accepted();
}
Elegimos Ollama desde el primer día con una visión clara: la capacidad de ejecutar modelos potentes en nuestra propia infraestructura.
Actualmente, debido a requisitos de infraestructura del cliente, consumimos estos modelos vía HTTP contra un servidor Ollama propietario externo: "Ollama cloud". Sin embargo, el diseño del Agente permanece estrictamente Stateless para facilitar el inevitable cambio hacia modelos locales en el futuro.
Nuestro "North Star" 🌟
Aunque actualmente operamos vía HTTP, el objetivo final es avanzar hacia servidores propietarios con modelos locales bajo nuestro control total.
Esto nos permitirá garantizar soberanía absoluta de datos (nada sale al mundo externo) y eliminar la dependencia de proveedores externos, asegurando que la inteligencia del sistema sea un activo propio.
Aquí hay un detalle que fue fácil de implementar gracias a Akka Platform y valió su peso en oro: Los Prompts no son archivos de configuración estáticos.
Cada Prompt es, literalmente, un proceso persistente (@PromptTemplate).
Esto nos permite hacer Hot-Reloading en producción. Si detectamos que el agente está "alucinando" con una nueva versión, no redesplemos el backend. Simplemente hacemos un PUT en la entidad del prompt. La entidad actualiza su estado y la próxima recomendación generada (milisegundos después) ya usa la nueva versión. Esto es Zero-Downtime Config.
// 1. Resolver claves de prompt candidatas (tipo de trámite + variante + versión)
List<String> candidates = resolvePromptKeyCandidates(procedureKind, variant);
// 2. Pedirle al prompt entity persistido el texto más reciente (just-in-time)
String template = client.forEventSourcedEntity(candidates.get(0))
.method(PromptTemplate::get)
.invoke();
Es Infrastructure as Code llevado al nivel del contenido de IA.
La transparencia en IA suele ser una ilusión, pero aquí la forzamos arquitectónicamente. Antes de emitir una recomendación, aplicamos Output Guardrails.
Validamos contra una Allowlist de códigos de resolución permitidos por la normativa. Si el agente intenta proponer algo inválido (debido a una alucinación del LLM), el sistema lo intercepta en la capa de validación del proceso y fuerza una revisión manual, evitando que el error llegue al usuario final. 🛑
@Override
public WorkflowSettings settings() {
return WorkflowSettings.builder()
.defaultStepTimeout(ofSeconds(300))
// Si un paso falla, reintentarlo 3 veces antes de rendirse
.defaultStepRecovery(maxRetries(3).failoverTo(ProcedureWorkflow::interruptStep))
.build();
}
Este enfoque de "degradación elegante" es lo que mantiene la estabilidad del clúster incluso cuando los servicios externos fallan.
Eliminar El Laberinto del Contexto requiere ingeniería sólida detrás de la magia de la IA.
La solución no es quitar a las personas de la ecuación. La IA es una herramienta poderosa, pero necesita ser gestionada, controlada y validada. Nuestra arquitectura pone a la tecnología en su lugar: eliminando el trabajo repetitivo para que los analistas expertos puedan dedicarse a lo que deben hacer: resolver casos complejos.
© 2026 Peperina.io — Excelencia en Ingeniería.