Tecnología

Así funcionan los Agents en el ai-service de Omnilude

AAnonymous
12 min de lectura

Introducción

Este artículo es la continuación de Por qué el ai-service de Omnilude adoptó la estructura Assistant / Thread / Run. En el texto anterior expliqué qué hace cada uno de assistant, thread y run. Esta vez quiero mostrar cómo funciona realmente agent sobre esa base.

Cuando explico esta estructura, primero quiero despejar un malentendido. AiAgent de Omnilude no es una gran caja mágica. Tampoco es una capa que convierta a los assistants en algo completamente distinto. En realidad, está mucho más cerca de lo contrario. AiAgent es una estructura que agrupa varios assistants como nodos y guarda su orden de ejecución como un workflow.

Dicho de forma más simple:

Si AiAssistant es un preset individual de LLM, AiAgent es el flujo de trabajo que conecta esos presets.

En este artículo voy a explicar cómo se implementa ese flujo en el código, de la forma más clara posible.

Un Agent no es otra IA, sino un workflow

Lo primero que hay que mirar es la entidad AiAgent. Tiene nombre, descripción y tipo, pero el núcleo real es el campo workflow. Ese valor no se guarda en una tabla separada, sino como JSONB.

Eso significa que, en Omnilude, un agent se parece menos a «un objeto que llama mejor a un modelo» y más a un grafo de ejecución expresado con nodos y edges.

Por ejemplo, un agent puede verse así:

  • Recibe una entrada en un nodo inicial.
  • Un nodo generate-text usa un assistant para generar texto.
  • Un nodo router decide la siguiente ruta.
  • El nodo final finish organiza el resultado.

Cuando lo ves así, la diferencia entre assistant y agent se vuelve mucho más clara.

  • Un assistant es un preset para una sola inferencia.
  • Un agent es el workflow que define en qué orden y bajo qué condiciones se conectan esas inferencias.

Un Agent no es otra IA, sino un workflow (referencia en texto)

AiAssistant
  - preset para una sola llamada de LLM
  - define qué model/provider/instructions usar

AiAgent
  - workflow que conecta múltiples nodos
  - cada nodo puede usar un assistant o comportarse como una tool
  - la clave no es «una llamada», sino «el orden de ejecución y las conexiones»

Una solicitud no se ejecuta de inmediato

En Omnilude, la ejecución de AiAgent normalmente no termina como una simple llamada síncrona. Aquí entra en juego DTE (Distributed Task Executor).

El flujo general es este:

  1. El cliente o el backoffice solicita la ejecución del agent.
  2. El servidor no lo procesa de inmediato, sino que lo convierte en un job de DTE.
  3. WorkflowTaskHandler toma ese job de la cola.
  4. Luego SingleWorkflowExecutor se encarga de la ejecución real del workflow.
  5. Dentro de ese proceso, BasicWorkflowEngine avanza nodo por nodo.

Esta estructura importa por una razón simple. La ejecución de un agent puede durar más de lo esperado, puede incluir streaming, necesita conservar estado intermedio y, en algunos casos, debe ejecutarse en segundo plano. En lugar de meter todo eso dentro de un request-response normal, tratar la ejecución como un job resulta mucho más estable.

Una solicitud no se ejecuta de inmediato (referencia en texto)

Controller
  -> AiAgentExecutor
  -> DistributedTaskQueue
  -> WorkflowTaskHandler
  -> SingleWorkflowExecutor
  -> BasicWorkflowEngine

Puntos clave
  - la ejecución se maneja como un job
  - el streaming y el procesamiento en segundo plano se vuelven más sencillos
  - las ejecuciones largas pueden administrarse dentro de una sola estructura

Todo empieza en un solo start node

Tener un workflow no significa que el motor recorra una lista de principio a fin. BasicWorkflowEngine primero busca un nodo donde startNode=true y cuyo tipo termina en -input. Después inyecta ahí la entrada inicial y arranca la ejecución desde ese punto.

Esa parte me gusta bastante. Porque no trata al agent como «una línea de comando», sino como un grafo de ejecución con un punto de entrada claro.

Por ejemplo, el nodo text-input prepara la cadena que entregó el usuario y pasa ese valor al siguiente nodo. Luego el nodo generate-text recibe ese valor y dispara una llamada al LLM. Después, el output se propaga al siguiente nodo siguiendo el edge.

En otras palabras, un agent no es una función gigante. Es un flujo compuesto por pequeñas unidades de ejecución conectadas entre sí.

Quien realmente ejecuta los nodos es NodeExecutor

Aquí aparece la capa realmente importante: NodeExecutor. Cada nodo tiene una cadena de tipo, y NodeExecutorFactory busca el ejecutor que corresponde a ese tipo. Al mismo tiempo, NodeExecutorRegistry escanea la anotación @NodeType y registra automáticamente qué ejecutor maneja qué tipo.

Gracias a esta estructura, agregar un nodo nuevo no obliga a rehacer todo el motor. Basta con crear un nodo, declarar su tipo y conectar su provider para que pase a formar parte del runtime.

Hay cuatro nodos representativos que vale la pena entender en este artículo.

  • TextInputNode: prepara la entrada inicial.
  • GenerateTextNode: usa un assistant para generar el texto real.
  • RouterNode: usa el LLM para decidir la siguiente ruta.
  • FinishNode: reúne el resultado final y termina la ejecución.

Con solo entender estos cuatro, ya se puede captar bastante bien cómo funciona la estructura de agents de Omnilude.

Incluso dentro de un Agent, la inferencia real la hacen los assistants

Aquí es donde este artículo vuelve a conectarse con el anterior.

Aunque exista AiAgent, el agent en sí no llama directamente al modelo. La llamada real al LLM ocurre dentro de un nodo, y en ese momento vuelve a aparecer AiAssistantService.

Por ejemplo, GenerateTextNode lee aiAssistantId desde su configuración. Luego carga el assistant con ese id, combina AiModel, AiProvider y AiApiKey, y crea un RunnableAiAssistant. Solo después de eso ocurre la llamada real al chat model.

La forma correcta de leer esta estructura es así:

  • los assistants son componentes para la inferencia
  • los agents son workflows que deciden en qué orden se ejecutan esos componentes

Cuando entiendes esto, deja de tener sentido ver assistant y agent como conceptos que compiten entre sí. No son alternativas. Son capas.

Incluso dentro de un Agent, la inferencia real la hacen los assistants (referencia en texto)

AiAgent
  - unidad de ejecución superior que contiene un workflow

Node
  - selecciona y llama a un assistant cuando hace falta

RunnableAiAssistant
  - objeto ejecutable real compuesto por assistant + model + provider + key

En la ruta /ai-runs, el agent se monta sobre el contexto de la conversación

Hay otro punto interesante en la ruta /ai-runs. Esta ruta no es simplemente una API que ejecuta de forma directa un agent almacenado. Primero lee los mensajes del thread y, sobre ese contexto, ejecuta un workflow específico de AiAgent.

En la implementación actual, ChatAgentTaskHandler selecciona un tipo de agent como AiAgentType.USING_WEB_SEARCH_TOOL y lo pasa a SingleWorkflowExecutor. Eso significa que, en este punto, un run ya no es solo una llamada simple al LLM. Se convierte en una orden para iniciar un workflow de agent sobre el contexto de una conversación.

Ese es uno de los motivos por los que el ai-service de Omnilude se siente más como una plataforma que como un wrapper ligero de modelos. La misma estructura de inferencia adquiere un significado de producto diferente cuando se monta sobre la interfaz thread/message/run.

Estoy dejando multiagent fuera a propósito en este artículo

Si uno lee el código, también se encuentra con el paquete multiagent. Pero su dirección es un poco distinta de la del workflow de AiAgent que se explica aquí. Se parece más a una capa separada que orquesta varios agents y maneja problemas más grandes, como routing dinámico, reintentos y revisión humana.

Más importante aún: todavía hay partes en progreso, así que no me parece adecuado explicarlo exactamente al mismo nivel que el runtime actual de AiAgent. Por eso lo dejé fuera a propósito. El alcance de este artículo se limita a la estructura para ejecutar un solo workflow de agent combinando assistants.

Reducir así el alcance hace que el texto sea más fácil de seguir y evita que la explicación se vuelva difusa.

Creo que esta estructura es bastante realista

Si lo miras otra vez, el agent dentro del ai-service de Omnilude no es otra IA con un nombre grandilocuente. Tampoco es otro wrapper alrededor de assistants. Si acaso, es algo mucho más práctico.

  • El flujo de ejecución se guarda en AiAgent.workflow
  • La ejecución real se entrega como un job de DTE
  • El motor avanza desde el nodo inicial siguiendo los edges
  • Cada nodo llama a un assistant cuando necesita usar el LLM

Creo que esta estructura funciona porque no exagera su extensibilidad. Assistant, thread, run y agent tienen roles distintos, y el límite de responsabilidad de cada uno es relativamente claro.

Cuantas más funciones de IA se meten en un producto, más importante me parece este tipo de estructura. Lo que perdura no es solo llamar bien a un modelo una vez, sino decidir en qué unidad se guarda algo, a través de qué interfaz se ejecuta y mediante qué flujo se conecta.

Cierre

Si en el artículo anterior assistant / thread / run eran los conceptos básicos que formaban la plataforma de ejecución de Omnilude, en este artículo agent es la capa de composición que se monta encima. AiAgent no es otra IA: es una entidad que guarda un workflow, y su ejecución fluye por DTE -> WorkflowExecutor -> NodeExecutor.

Y la inferencia real sigue estando a cargo de los assistants. Al final, el ai-service de Omnilude no crece poniendo a assistants y agents en oposición, sino construyendo agents a partir de assistants como componentes.

La próxima vez voy a llevar esto un paso más hacia la práctica y mostrar, con ejemplos concretos, cómo diseño el workflow JSON real y la composición de nodos.