Qué hay en el motor de un agente de IA

Vistas: 1
0:00 / 0:00
Qué hay en el motor de un agente de IA

Llevaba tiempo preguntándomelo. Tú escribes un mensaje en tu agente de inteligencia artificial favorito, ya sea OpenCode, OpenCLAW, Hermes, el que tú quieras, le das a Enter y, al cabo de unos segundos, recibes una respuesta. Y parece magia. Pero no lo es. Entre medias hay un mundo, una coreografía compleja de mensajes que viajan, eventos que se disparan, herramientas que se invocan y un modelo de lenguaje que no solo habla, sino que decide actuar. ¿Y qué crees que hice? Pues lo que cualquier persona sensata haría: construir mi propio motor de orquestación de agentes. Le he llamado Anacleto, está escrito en Rust, y lo que he descubierto por el camino es fascinante. No porque sea un experto en inteligencia artificial, que no lo soy, sino porque he tenido que abrir la caja negra para saber exactamente cómo funciona todo lo que hay debajo. Y ese es precisamente el viaje que te propongo hoy, asomarnos juntos a lo que hay en el motor de un agente de IA.

El viaje de un mensaje: de la TUI al LLM

Empecemos por el principio. Tú estás en la terminal, en la interfaz de Anacleto, una TUI hecha con ratatui, un framework de Rust para interfaces en terminal, y escribes tu mensaje. Algo como: oye, investiga qué es el protocolo MCP y resúmelo en un documento. Le das a Enter. ¿Qué pasa?

Pues lo primero que ocurre es que ese mensaje no se va directamente a ningún modelo de lenguaje. Se va a una estructura que llamamos el engine, el orquestador. Piensa en él como el director de orquesta. Él decide qué agente recibe el mensaje, cómo se construye la petición y qué hacer con la respuesta. Y créeme, no hay atajos, cada paso tiene su función.

En Anacleto, el engine recibe ese mensaje y se lo envía al agente. El agente, a su vez, construye lo que en la API de OpenAI se llama el array de mensajes. Un array de objetos que tienen un role y un content. Los roles son cuatro: system, user, assistant y tool. Y cada uno tiene una función específica en esta coreografía.

El primer mensaje que se inyecta es el system prompt, básicamente la constitución del agente: su personalidad, sus reglas, sus límites. En Anacleto, el system prompt se construye a partir del Markdown del agente, que es un archivo con frontmatter YAML más cuerpo Markdown, y se renderiza como plantilla. Puedes usar variables como {model}, {workspace}, {tools} y {subagents} para que el prompt sea dinámico según la configuración.

Luego viene tu mensaje, que se añade con role: user. Y si hay archivos de instrucciones del workspace, como AGENTS.md, CLAUDE.md o CONTEXT.md, también se inyectan como contexto de sistema. Es como si el agente tuviera una mochila con todas las instrucciones que necesita antes siquiera de empezar a trabajar.

Una vez que tenemos el array de mensajes completo, se construye la petición HTTP. Es un POST a https://api.openai.com/v1/chat/completions, o a OpenRouter que es un proxy que unifica varios proveedores, con un cuerpo JSON que contiene el modelo, los mensajes, las herramientas disponibles y una bandera clave: stream: true. Porque queremos que la respuesta llegue en tiempo real, no esperar a que termine. Y ahí es donde la cosa se pone interesante.

Streaming: cuando los bytes viajan en tiempo real

Cuando haces una petición con stream: true, la API no te devuelve un JSON con toda la respuesta. Te devuelve un flujo de eventos usando algo que se llama SSE: Server-Sent Events. Es un protocolo HTTP de los de toda la vida: el servidor abre una conexión y va mandando líneas con un formato muy simple, event: nombre\ndata: {...}\n\n. Cada evento es autónomo, y el cliente, en este caso Anacleto, va leyendo línea a línea.

Imagina que tienes un stream de bytes. Anacleto va leyendo, línea por línea. Cada línea que empieza por data: es un chunk de la respuesta, y ese chunk se parsea como JSON. ¿Y qué contiene ese chunk? Pues un array de choices, y dentro de cada choice, un delta. El delta es el fragmento de la respuesta, y puede ser de varios tipos.

El más común es el content delta, el texto que el modelo va generando token a token. Cuando le preguntas algo a Anacleto y ves cómo el texto aparece carácter a carácter en la pantalla, lo que estás viendo es exactamente eso: cada chunk SSE que llega se envía a la TUI como un evento AgentStreamChunk, la TUI lo va acumulando en current_stream y lo muestra en tiempo real. Es hipnótico, la verdad.

Pero hay más. Si estás usando un modelo de OpenAI o OpenRouter con capacidades de razonamiento, como los modelos o1, o3 o o4, te puede llegar un campo reasoning en el delta. Son los tokens de razonamiento, el modelo pensando en voz alta antes de responderte. En Anacleto, estos se emiten como eventos AgentThinkingChunk y se muestran en un panel aparte en la TUI, si tienes activado /thinking. Ver cómo el modelo se debate, cómo considera opciones, cómo llega a conclusiones antes de escribir la respuesta final es fascinante, de verdad.

Y luego está el caso más interesante: los tool_calls parciales. Porque cuando el modelo decide usar una herramienta, los tool_calls no llegan enteros. Llegan en fragmentos, igual que el texto. Cada fragmento tiene un index que indica a qué tool_call pertenece. Y los campos id, type, function.name y function.arguments pueden llegar en eventos separados. Es como si el modelo estuviera escribiendo la llamada a la función con un teclado y nosotros viéramos las teclas una a una.

Anacleto tiene un acumulador: tool_calls_acc. Es un vector indexado por el index del tool_call. Cuando llega un fragmento, mira el índice, se asegura de que hay espacio en el vector, y va rellenando los campos. Si llega un id, lo pone. Si llega un name, lo añade. Si llegan arguments, los va concatenando con push_str. Y cuando el stream termina con un finish_reason igual a tool_calls, Anacleto saca los tool_calls completos del acumulador y los emite.

Los finish_reasons

Por cierto, los finish_reason son cuatro: stop cuando el modelo ha completado la respuesta, tool_calls cuando ha decidido usar una herramienta, length si se ha alcanzado el límite de tokens, y content_filter si el filtro de contenido ha bloqueado algo. Cada uno indica al motor qué hacer a continuación. Si es stop, mostramos la respuesta y hemos terminado. Si es tool_calls, entramos en el bucle de herramientas. Si es length, la respuesta está truncada y hay que gestionarlo. Y si es content_filter, algo ha ido mal con la moderación.

Y luego, al final del stream, llega el último chunk. Un chunk especial con choices: [] y un campo usage que contiene los tokens usados: prompt_tokens, completion_tokens y total_tokens. Anacleto lo captura, calcula el coste en dólares multiplicando por los precios por millón de tokens, y lo muestra en la barra de estado. Como ya lo dijo alguien, no hay magia, solo bytes que viajan.

Tool calls: cuando el LLM decide actuar

Y aquí llegamos al momento aha. El momento en el que te das cuenta de que un modelo de lenguaje no solo habla, sino que también puede actuar.

Porque el modelo puede devolver un mensaje que no contiene texto, sino que contiene tool_calls. Un array de objetos con id, type: "function" y function: { name, arguments }. El name es el nombre de la herramienta que quiere usar, y arguments es un string JSON con los parámetros. En programación tradicional le dirías al modelo ejecuta este comando si se cumple esta condición. Aquí le proporcionas un conjunto de herramientas y él decide, basándose en el contexto y en su entrenamiento, cuál es la más adecuada para cada situación.

En Anacleto, cuando el stream termina y detectamos que hay tool_calls, entramos en lo que yo llamo el tool loop. Es un bucle que funciona así:

  1. El LLM devuelve un mensaje con tool_calls
  2. Anacleto ejecuta cada tool call
  3. El resultado de cada tool se añade a la conversación como un mensaje con role: tool
  4. Se vuelve a llamar al LLM con toda la conversación actualizada
  5. El LLM puede seguir llamando herramientas o dar la respuesta final

Y este bucle se repite hasta que el LLM decide que ya está, o hasta que se alcanza el límite de pasos, que en Anacleto se configura con max_steps en el agente, con un valor por defecto de 90. Es como un diálogo donde el asistente puede decir espera, necesito buscar esto y vuelve a preguntar.

Ahora bien, ¿qué herramientas puede usar el LLM? Anacleto tiene un montón de herramientas integradas. Herramientas para leer archivos, para hacer búsquedas con grep y glob, para hacer peticiones web, para buscar en internet, para modificar código con parches, para preguntar al usuario. Pero hay tres tipos de herramientas que son especialmente interesantes y que merecen una explicación detallada.

Los skills: la unidad fundamental de capacidad

Los skills son la unidad fundamental de capacidad del agente. Son archivos Markdown con frontmatter YAML que contienen el nombre, la descripción y las instrucciones de una habilidad. El LLM decide cuándo invocar un skill según su descripción. Anacleto convierte cada skill en una definición de tool y se la pasa al modelo. Cuando el modelo invoca un skill, Anacleto ejecuta sus instrucciones.

Te pongo un ejemplo. Imagina que tienes un skill de web-research cuya descripción dice: Investiga cualquier tema combinando búsqueda web con SearXNG y fetch de URLs. El LLM, cuando recibe una pregunta sobre un tema que desconoce, mira las herramientas disponibles, ve la descripción del skill y decide: esto es justo lo que necesito. Invoca el skill, y Anacleto se encarga de ejecutar la búsqueda web, leer los resultados y devolverlos al modelo.

Lo que hace especial a los skills en Anacleto no es solo que existan, sino cómo han evolucionado. Al principio eran simples: nombre, descripción e instrucciones. Pero el sistema ha ido creciendo. Primero llegó el skill discovery: Anacleto no solo carga skills de una lista fija, sino que descubre skills en el workspace y en directorios globales. Busca archivos SKILL.md en .agents/skills/ y los carga automáticamente. Luego llegó el lazy loading: no cargas todos los skills al inicio, los cargas cuando se necesitan mediante un mapa hash con nombres en minúsculas como clave y búsqueda O(1). Luego llegó el skill registry: un registro centralizado, thread-safe, con un RwLock para acceso concurrente. Y luego llegó el hook system: ahora los skills pueden declarar hooks en su frontmatter para ejecutar acciones después de ciertos eventos.

Los subagentes: delegación inteligente

El segundo tipo de herramienta especialmente interesante son los subagentes. Un agente puede tener subagentes configurados. Por ejemplo, un agente reviewer que revisa código. El padre expone al hijo como una herramienta. Cuando el LLM decide invocar al subagente, Anacleto lo lanza. El subagente es un agente completo, con su propio system prompt, sus propios skills y su propio modelo. Pero es desechable: no hereda skills, MCPs ni permisos del padre. Hace su tarea y devuelve el resultado, y el padre sigue con lo suyo.

Esto es increíblemente potente. Significa que puedes tener un agente principal que coordina el trabajo y va delegando tareas especializadas a subagentes. Es como tener un equipo de expertos donde cada uno sabe de su área, y el LLM actúa como jefe de proyecto decidiendo a quién llamar para cada tarea. Si necesita investigar algo, llama al subagente researcher. Si necesita revisar código, llama al subagente reviewer. Si necesita redactar documentación, llama al subagente writer. Y cada uno hace su trabajo y devuelve el resultado.

MCP: el protocolo de moda

Y luego está el MCP, el Model Context Protocol. Es JSON-RPC 2.0 para conectar herramientas externas con agentes. Anacleto tiene un cliente MCP que se conecta a servidores externos por stdio o TCP. Puedes tener un servidor MCP que te da el tiempo, otro que busca en tu base de datos, otro que interactúa con tu gestor de contraseñas. Anacleto recolecta las herramientas de todos los servidores MCP y las expone al LLM con un prefijo para evitar colisiones de nombres.

Y todo esto con un sistema de permisos. En Anacleto, los permisos se configuran en el agente. Puedes usar allow by default, todo está permitido excepto lo que deniegues explícitamente, o deny by default, solo está permitido lo que autorices. Y hay un sistema de aprobación humana para operaciones sensibles. Cuando el LLM quiere ejecutar un comando o escribir un archivo, Anacleto puede pedir confirmación al usuario antes de continuar. Esto es importante porque, aunque el LLM decida por sí mismo qué herramientas usar, el usuario tiene el control último.

Extended thinking: cuando el modelo piensa en voz alta

Vale, hablemos de una de las cosas que más me fliparon cuando las descubrí construyendo Anacleto: el extended thinking de Claude. Cuando usas Claude, el modelo de Anthropic, el modelo puede mostrar su proceso de razonamiento interno. Literalmente, el modelo piensa en voz alta antes de darte la respuesta final.

En la API de Anthropic, esto se configura con un campo thinking en la petición. Tiene un type: "enabled" y un budget_tokens, que son los tokens que el modelo puede usar para pensar. En Anacleto, esto se configura con thinking_budget_tokens en la configuración del proveedor.

¿Y cómo se ve esto en la práctica? Pues la respuesta de Anthropic viene con content_blocks de distintos tipos. Puede haber bloques de tipo text, bloques de tipo thinking y bloques de tipo tool_use. Los bloques de thinking contienen el razonamiento del modelo. Y los bloques de tool_use contienen las llamadas a herramientas.

Anacleto, cuando recibe una respuesta de Anthropic, itera sobre los content blocks. Los de tipo text van al contenido principal. Los de tipo thinking van a un campo separado. Y los de tipo tool_use se convierten en tool calls. En la TUI, si tienes /thinking activado, ves el razonamiento del modelo en un panel separado, a menudo en otro color. Es fascinante verlo. Puedes observar cómo el modelo se debate, cómo considera opciones, cómo llega a conclusiones antes de escribir la respuesta final.

Te pongo un ejemplo. Le preguntas a Claude algo complejo, como diseña una arquitectura de microservicios para una aplicación de gestión de proyectos. Y antes de responderte, ves algo como:

Bueno, primero necesito entender los requisitos. Una app de gestión de proyectos necesita usuarios, proyectos, tareas, notificaciones… Voy a dividirlo en servicios: user-service, project-service, task-service, notification-service… Luego necesito una API Gateway, un bus de eventos… Ah, y persistencia, cada servicio con su propia base de datos…

Y luego, cuando ha terminado de pensar, te da la respuesta estructurada. Esto cambia la forma de interactuar con el modelo. No solo ves el resultado, ves el razonamiento. Puedes evaluar si el modelo está yendo por buen camino antes de que termine. Y si ves que se está desviando, puedes cancelar y replantear la pregunta.

En la API de OpenAI, los modelos o1, o3 y o4 también tienen algo similar, pero lo llaman reasoning en lugar de thinking. Y en OpenRouter, que es un proxy que unifica varios proveedores, también pasa los tokens de razonamiento cuando el modelo los soporta. La diferencia clave está en el formato: OpenAI lo envía como un campo reasoning en el delta del stream, mientras que Anthropic lo estructura como content blocks separados. Anacleto tiene que manejar ambos formatos, y te aseguro que no es trivial.

Anacleto: el motor en Rust

Llegados a este punto, te estarás preguntando: ¿y por qué Rust? ¿Por qué no Python o TypeScript como la mayoría de los agentes? Pues porque soy un poco cabezón, la verdad. La mayoría de los agentes que he visto están implementados en Python o en TypeScript, y yo pensé: ostras, lo implemento en Rust que probablemente sea más rápido. Y al final, como de costumbre, me he liado más en el lenguaje de programación que en el objetivo final. 😅

Pero la decisión tiene su lógica. Rust te da un control de memoria que no tienes en otros lenguajes, una concurrencia segura gracias al sistema de ownership y unos tiempos de ejecución que son difíciles de igualar. Para un motor que tiene que procesar streams de datos en tiempo real, gestionar múltiples tool calls concurrentes y mantener el estado de la conversación, Rust es una opción sólida.

Anacleto está estructurado en varios módulos. El módulo src/llm/ contiene los providers: openai.rs para el provider de OpenAI (que también sirve para OpenRouter), anthropic.rs para Anthropic, provider.rs con el trait LlmProvider y la factoría de providers. El módulo src/agent/lifecycle.rs contiene el tool loop, que es el corazón del motor. Y el módulo src/tui/ contiene la interfaz de terminal con ratatui.

La TUI tiene varios paneles. A la izquierda, el panel de chat donde se muestra la conversación. Abajo, el panel de entrada donde escribo los mensajes. A la derecha, un panel de información con pestañas para Skills, MCPs, Subagentes y Agentes. Arriba, una barra de estado que muestra el modelo actual, los tokens consumidos, el coste y el estado del agente.

Y hay comandos slash muy útiles: /skills para listar los skills cargados, /agents para mostrar los agentes disponibles, /models para cambiar de modelo sobre la marcha, /status para ver el estado del engine, /thinking para activar o desactivar la visualización del razonamiento, /debug para mostrar los payloads JSON de las peticiones al LLM, y /mcps para listar y activar o desactivar servidores MCP.

Estado actual y aprendizajes

Después de todo este viaje construyendo Anacleto, ¿qué he aprendido? Pues varias cosas, algunas sorprendentes y otras frustrantes.

Lo más sorprendente ha sido ver cómo el LLM decide usar herramientas por sí mismo. Esto no es programación tradicional. Tú no le dices ejecuta este comando. Le dices aquí tienes estas herramientas, úsalas si crees que son necesarias. Y el modelo decide. A veces te sorprende. A veces usa herramientas que no esperabas. Y a veces las usa de formas que no habías previsto. Es como tener un becario que es increíblemente listo pero que de vez en cuando hace cosas que te dejan con la boca abierta, para bien y para mal.

Lo más difícil ha sido manejar todos los casos borde del streaming. ¿Qué pasa si el servidor se cae a mitad de un stream? ¿Qué pasa si un tool_call llega fragmentado en quince eventos diferentes? ¿Qué pasa si el finish_reason es length porque el modelo se ha quedado sin tokens? ¿Qué pasa si el último chunk con usage nunca llega? Cada uno de estos casos ha sido una batalla, y te aseguro que he perdido más de las que he ganado.

Lo más gratificante ha sido ver cómo encajan todas las piezas. Cuando construyes un motor de agentes, no solo estás escribiendo código. Estás diseñando un protocolo de comunicación entre el usuario, el engine, el LLM y las herramientas. Y cuando todo funciona, es una maravilla. Ves el stream, ves los tool calls, ves cómo el modelo razona, y piensas: esto es lo que hay dentro de la caja negra.

Y lo más frustrante ha sido la falta de estandarización entre proveedores. OpenAI tiene un formato de streaming, Anthropic tiene otro completamente diferente. Los tool calls de OpenAI llegan en fragmentos con índice. Los tool calls de Anthropic llegan como content blocks completos. El extended thinking de Anthropic son content blocks de tipo thinking. El reasoning de OpenAI es un campo en el delta. OpenRouter, al ser un proxy, intenta unificarlo, pero no siempre funciona.

Y lo que viene: más proveedores, mejorar la gestión de MCP, implementar el streaming real para Anthropic, que ahora mismo en Anacleto cae a no-streaming porque el formato SSE de Anthropic es diferente, y seguir mejorando el sistema de hooks y plugins.

Y sí, me he encontrado con un problema gordo. A veces el tool loop se vuelve loco: el LLM llama a una herramienta, recibe el resultado y vuelve a llamar a la misma herramienta una y otra vez hasta que se alcanza el límite de max_steps y se agota. Esto en algunos casos me ha sucedido y en otros no, y no tengo muy claro si es un problema de cómo he definido las herramientas, si es un problema del motor o si es cuestión de ajustar mejor los skills. Lo que sí he aprendido es que la calidad de los skills es más importante que la potencia del modelo. Un modelo modesto con skills bien definidos da mejores resultados que un modelo potente con skills genéricos. La clave está en depurar los skills, en ir refinándolos con el uso, en decirle al modelo la próxima vez que hagas esto, tienes que resolverlo de esta manera.

La API de OpenAI no es magia

Y con esto llegamos al final. La API de OpenAI es un protocolo bien definido. Un array de mensajes, un stream de eventos delta, un finish_reason y tool_calls que el motor debe ejecutar. Cuando entiendes cómo funciona, dejas de verlo como una caja negra y empiezas a verlo como lo que es: un sistema de comunicación entre un modelo, un motor y unas herramientas.

Anacleto es código abierto. Lo tienes en github.com/atareao/anacleto. Escrito en Rust, con TUI en ratatui, soporte multi-provider, skills, MCP, permisos, hooks y plugins. Y está en evolución constante. Si te pica la curiosidad, pásate, haz un fork, abre un issue o simplemente mira cómo está hecho por dentro. Porque al final, lo más valioso de construir tu propio agente no es tener un agente más, es entender exactamente lo que pasa cuando le preguntas algo a una máquina y ella te responde.

Y tú, ¿alguna vez te has planteado qué hay dentro de la caja negra? ¿Te animarías a construir tu propio agente? Cuéntamelo en los comentarios, que estoy deseando saberlo.


Más información

Deja una respuesta