May 7, 2026

Agentes de Inteligencia Artificial para modernizar la lógica de negocio de las aplicaciones, Parte IV: Research AI — Optimizando la Resolución de Trámites

Agentes de Inteligencia Artificial para modernizar la lógica de negocio de las aplicaciones, Parte IV: Research AI — Optimizando la Resolución de Trámites

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.

¿Por qué Akka Platform? 🏗️

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:
  • Modelo mental coherente (Actor + Workflow + Event Sourcing)
  • Infraestructura distribuida "batteries-included"
  • Semántica clara para fallos, reintentos y recuperación
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.

Deep Dive: Orchestrator-Workers y Flujos 🎼

Para integrar IA, evitamos el caos de agentes autónomos hablando entre sí sin control. Diseñamos una arquitectura estricta de Orchestrator-Workers.

1. ProcedureWorkflow: El Cerebro Determinístico

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 → publish
  • Paso loadCase: Obtiene los datos del caso desde la API de negocio y materializa el estado del workflow.
  • Paso classifyProcedureKind: Selecciona la "forma" del trámite (producto + subtipo) para que las reglas y herramientas posteriores sean las correctas.
  • Paso validate: Ejecuta las precondiciones para ese tipo de trámite (identificadores, campos obligatorios, combinaciones soportadas).
  • Paso loadRules: Resuelve la versión aplicable de política/texto normativo y la adjunta al contexto (a menudo respaldado por contenido de prompt versionado).
  • Paso 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.
  • Comando onEvidenceReady: Después de que el workflow hijo publica la evidencia agregada, un callback deduplicado despausa al padre hacia la toma de decisiones.
  • Paso decide: Ejecuta la pila de decisión/recomendación (con guardrails).
  • Paso publish: Escribe el resultado de vuelta en el sistema de registro del caso.

2. Pipeline de investigación: ejecución resiliente

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()));
    }
}

Ojos para el Agente: Ingesta Multimodal de Documentos 👁️

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.

Ingesta de PDF: primero texto, visión cuando sea necesario 📄

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(...);

Ingesta de imágenes: OCR multimodal 🖼️

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);

Ingesta de hojas de cálculo 📊

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.

Documentos Word 📝

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.

XML y payloads estructurados 🏷️

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.

Guardrail del Validador Fiscal 🛡️

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();
}

La Capa Cognitiva: ¿Por qué Ollama? 🦙

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.

Prompt Hot-Loading: Ajustando el "Cerebro" en Caliente 🧠

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.

Auditabilidad y Guardrails 👮‍♂️

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. 🛑

Resiliencia 💥

  • Recuperación por paso: Configuramos políticas maxRetries(3) con backoff para cada paso. Si una API de uno de los sistemas externos falla temporalmente, el workflow reintenta solo ese paso sin perder contexto.
  • Temporizador de espera de investigación: Mientras el workflow padre está pausado en un estado de ciclo de vida tipo WAITING_FOR_RESEARCH, un único temporizador resguarda la transferencia hacia el pipeline hijo. Si se activa, el padre registra un timeout en el agregado pero aún puede avanzar hacia decide para que los operadores reciban una recomendación con la evidencia que llegó (posiblemente ninguna). Por separado, las herramientas opcionales en el workflow hijo se degradan a advertencias en lugar de fallar toda la ejecución de investigación.

@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.

Conclusiones 🤹🏼

Eliminar El Laberinto del Contexto requiere ingeniería sólida detrás de la magia de la IA.

  • Akka Platform proporciona la semántica necesaria para manejar estado distribuido de manera confiable, y además nos permitió construir un sistema completo en semanas.
  • Event Sourcing transforma la base de datos de un simple almacén en una fuente de verdad auditable y reproducible.
  • Separar el flujo en Orquestadores (Control) y Workers (Ejecución) nos permite escalar las partes pesadas (I/O, llamadas al LLM) independientemente de la lógica de negocio central.

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.

Herramientas mencionadas 🛠️

© 2026 Peperina.io — Excelencia en Ingeniería.

Francisco Perrotta
Ingeniero de software y arquitecto de backend en Peperina Software con amplia experiencia en sistemas distribuidos