- Con el aumento reciente en la adopción de agentes de IA, también va en aumento el uso de stacks híbridos basados en Go
- Los agentes se caracterizan por tiempos de ejecución largos, costos altos y una frecuente espera de entrada/salida
- Go ofrece un modelo de concurrencia de alto rendimiento con goroutines ligeras, mecanismo centralizado de cancelación y mensajería basada en canales
- Tiene una biblioteca estándar muy amplia, y con herramientas de profiling como
pprof es fácil rastrear fugas de memoria y de hilos
- Aun así, Go también tiene limitaciones como una carencia de ecosistema de machine learning, un rendimiento máximo que no sobresale tanto y menor soporte de terceros frente a otros lenguajes
¿Qué es un agente?
- Un agente se ejecuta en un bucle repetitivo y se refiere a un proceso que puede decidir por sí mismo el siguiente paso de ejecución
- A diferencia de un workflow con una ruta predefinida, determina si debe terminar según condiciones (por ejemplo, “pasar las pruebas”) o un número máximo de iteraciones
- En servicios reales, los agentes suelen ejecutarse durante mucho tiempo, desde segundos hasta horas, y tienen costos altos por llamadas a LLM, automatización del navegador, etc.
- Como deben procesar la entrada del usuario (o la entrada de otro agente), hay mucho tiempo de espera de I/O
Por qué Go es adecuado para agentes
Concurrencia de alto rendimiento
- Las goroutines de Go pueden ejecutar simultáneamente miles o decenas de miles de hilos ligeros usando solo 2 KB de memoria
- Cada goroutine aprovecha el multicore para procesar en paralelo, y permite operar sin problema agentes con I/O y en espera
- La comunicación basada en canales (Channel) permite implementar sincronización mediante paso de mensajes en lugar de memoria compartida (minimizando el uso de Mutex)
- Es adecuado para que los agentes intercambien mensajes de forma asíncrona y administren su estado
Mecanismo centralizado de cancelación
- Al usar context.Context en Go, la mayoría de las bibliotecas y APIs soportan señales de cancelación, por lo que detener la ejecución es muy sencillo
- Mientras que en Node.js o Python coexisten varios patrones de cancelación, Go permite cancelar de manera segura y recuperar recursos con un enfoque consistente
Biblioteca estándar abundante
- Go tiene una biblioteca estándar muy amplia y cubre casi todas las áreas, como HTTP/web, archivos y network I/O
- Todo I/O asume un comportamiento bloqueante dentro de goroutines, por lo que la lógica de negocio puede escribirse de forma lineal
- En Python, la mezcla de distintos patrones de concurrencia como asyncio, threading y procesos añade complejidad
Herramientas de profiling y diagnóstico
- Con herramientas integradas de Go como pprof, es posible rastrear en tiempo real fugas de memoria y fugas de goroutines (hilos)
- Esto es una ventaja para diagnosticar problemas de fugas en agentes que se ejecutan durante mucho tiempo y en paralelo
Buen soporte de codificación con LLM
- Gracias a su sintaxis simple y su amplia biblioteca estándar, los LLM escriben bien código con estilo propio de Go
- Como depende poco de frameworks, el LLM necesita preocuparse menos por versiones o patrones
Limitaciones de Go
- Su ecosistema y las bibliotecas de terceros son más limitados que en Python o TypeScript
- No es adecuado para la implementación directa de machine learning (por límites de rendimiento y soporte)
- Si se necesita el máximo rendimiento, Rust o C++ son mejores opciones
- Puede resultar algo incómodo para desarrolladores que prefieren un manejo de errores más permisivo
2 comentarios
Más que Java, pero menos que Rust :)
Opinión de Hacker News
Se enfatiza que, en la mayoría de los sistemas de agentes, el mayor factor de latencia termina siendo la llamada al LLM. Las ventajas mencionadas en el artículo no favorecen a un lenguaje en particular; en su mayoría se trata de esperas largas, uso costoso de recursos, entrada de usuarios u otros agentes, y grandes tiempos de espera de I/O. Por estas características, se argumenta que, más que la velocidad o eficiencia de ejecución del servidor, una ventaja más importante es el vasto ecosistema de librerías y soporte de IA que tiene un lenguaje como Python. También se señala que en Python hay que pensar en
asyncioo en bibliotecas de multithreading, pero se cree que en la práctica el desarrollo de agentes no es tan difícil y que es fácil empezar porque alguien ya pasó por la experiencia de crear workflows relacionadosAl construir agentes con Go, se ha visto como gran ventaja que los patrones de concurrencia y manejo de backpressure están bien establecidos. Los agentes suelen incluir transacciones con servicios externos lentos, y para este tipo de trabajo los patrones de concurrencia de Go son muy útiles. Claro, el lenguaje en sí no importa tanto, y parece que JavaScript es el más usado. Aun así, si se trata de generación de código, se siente que hay una buena sinergia entre Go y los LLM
Go se diferencia de Python por su excelente manejo de concurrencia y por lo fácil que es desplegarlo. Como en Go basta con distribuir un binario estático, uno se libra de los problemas de entorno y dependencias de Python
Los agentes cumplen el rol de capa de orquestación, y se piensa que Go, Erlang y Node son particularmente adecuados. No es indispensable tener enormes cantidades de librerías relacionadas con IA, y se enfatiza que las tareas con mucho I/O pueden abstraerse detrás de interfaces de herramientas por dominio, construyendo subsistemas en el lenguaje que haga falta
Go no suele ofrecer grandes ventajas para este tipo de carga de trabajo, y la mayor parte del tiempo se dedica a esperar I/O. El sistema de tipos de Go es limitado, y en Go hay que rodear muchas funciones que en lenguajes modernos ya vienen integradas. TypeScript es un excelente lenguaje de pegamento para IA y, junto con Python, tiene muy buen soporte de librerías. La razón por la que se prefiere TypeScript sobre Python es que su sistema de tipos es mucho más potente y maduro. Python también está mejorando rápido. Se considera que la afirmación de que en Node.js y Python es muy difícil interrumpir trabajos de larga duración no tiene una base sólida. La mayoría de las herramientas ya soportan esta función, y los lenguajes principales son Python y JS
Se han probado frameworks de agentes basados en Elixir y BEAM, y se cree que la combinación de BEAM y SQLite es, a día de hoy, la más ideal para agentes. Permite reemplazar agentes de forma segura sin volver a desplegar la aplicación, y la concurrencia de BEAM tiene margen más que suficiente para este trabajo. También es muy fácil implementar agentes con estado o temporales. En adelante se planea construir agentes base en Python, TypeScript y Rust, y también crear servidores MCP para permitir el desarrollo de agentes complejos según la preferencia de cada lenguaje
Se recomienda el proyecto Extism y el SDK de Elixir. Con esta combinación, se puede crear en Elixir el servicio central, el routing y el message passing, aprovechar las ventajas de BEAM/OTP, e incluso incrustar como plugins agentes en forma de módulos Wasm pequeños y livianos escritos en otros lenguajes
Extism
Elixir SDK
Da curiosidad saber si hay alguna razón para elegir SQLite en lugar de mnesia, el datastore integrado de BEAM
mnesia docs
La mayor parte del tiempo de un agente se va en esperar respuestas del LLM y en llamadas a servicios externos (API, DB). En la práctica, el impacto del rendimiento del runtime del lenguaje es casi nulo. Si hubiera una característica del lenguaje realmente importante para el rendimiento y la escalabilidad de un agente, sería el rendimiento de serialización y deserialización de JSON
Por eso se piensa que es mejor usar un lenguaje como TypeScript, que maneja JSON de forma nativa. El sistema de tipos de TypeScript también es mucho más potente que el de Go
Por experiencia, aparte de las llamadas al LLM, lo más costoso en un agente es resolver conflictos de edición asíncrona (
merge,diff,patch). Ese trabajo también puede delegarse a librerías de bajo nivel, pero es un problema tan difícil de optimizar como la serializaciónEsto recuerda a esta guía para construir agentes de ampcode.com. Gracias a su naturaleza de lenguaje dinámico, en Python es natural usar decoradores para convertir métodos directamente en llamadas de herramientas, generar listas iterando funciones de herramientas o transformarlas rápido a esquemas JSON. En cambio, una estructura donde varios disparadores externos nuevos (por ejemplo, entrada de usuario, correos de Gmail, mensajes de Slack, etc.) provocan la ejecución de un nuevo agente resultó mucho más intuitiva en Go usando canales y un
switchdentro de unforloop. En Python se vuelve más complejo porque hay que crear varias colas y threading por separadoSiguiendo la lógica del artículo, Elixir sería ideal para agentes
El sistema de tipos limitado e insuficiente de Go lo hace inadecuado para casi cualquier aplicación. De hecho, se considera que la mayor desventaja de Go es el propio lenguaje. Más bien, son los aspectos externos al lenguaje los que han hecho que se lo tolere
Después de programar varios años en Go, se coincide en que el sistema de tipos tiene muchos problemas. La gente del mundo LLM parece usar casi exclusivamente Python o JavaScript. Se piensa que todos deberían moverse a lenguajes modernos, pero Go podría ser una opción algo mejor frente al desorden de imports y paquetes de Python/JavaScript
Me gustaría escuchar específicamente cómo las limitaciones del sistema de tipos de Go se vuelven un obstáculo al crear agentes
En realidad, que Go tenga un sistema de tipos estático se debe a que no encontró otra manera de alcanzar sus objetivos de rendimiento. Lo correcto sería verlo como un lenguaje que en la práctica se usa como si fuera de tipado dinámico, y esto es resultado de entender mal el propósito de su diseño. Se puede argumentar que los lenguajes de tipado dinámico son inadecuados en general, pero viendo que Python, Erlang, Elixir y otros lenguajes de tipado dinámico se usan activamente, más bien parece que el tipado dinámico se adapta mejor a este dominio del problema
Se siente que varios valores de retorno no se componen bien, el soporte de errores es mejor que las excepciones pero muy verboso, es fácil equivocarse con los canales y los tipos
enumresultan decepcionantes. Aun así, las interfaces funcionan sorprendentemente bien y el sistema de empaquetado es bastante fluido. Al aprender Rust, se notó que la estructura de archivos es mucho más compleja que en Go. Incluso, como el lenguaje es simple, también es más fácil crear una variedad de linters y herramientas de generación de código. El mantenimiento a largo plazo de código Go genera menos preocupación que Python/JSSería genial que existiera un dialecto de LISP/Scheme que compile a Go y se mantenga bien
En procesos largos y costosos de ejecutar, una desventaja es que si el proceso muere se pierde todo el trabajo. Serializar el estado en una base de datos mientras se espera puede ser más seguro, pero no parece haber un lenguaje que haga esto fácil. Escribir máquinas de estado basadas en checkpoints no es sencillo
Se explica que las máquinas de estado basadas en checkpoints son una función central de plataformas como Hatchet(hatchet.run) y Temporal(temporal.io). Guardan el historial de ejecución de funciones dentro del workflow y, si ocurre una interrupción, reproducen automáticamente ese historial. Se sostiene que un historial de avance por salida es mucho más eficiente que un memory sink. (Fundador de Hatchet)
Ya sean goroutines, threads o cadenas de ejecución de larga duración, al final hay que dividirlas en unidades de trabajo atómicas y la serialización de estado es indispensable. Eso permite cumplir requisitos como recuperación ante fallos, rastreo de errores, reconsulta de resultados y distribución multinodo. El framework Oban(github.com/oban-bg/oban) de Elixir funciona de esta manera, y también se recomienda un artículo de Oban que enfatiza la importancia de persistir trabajos asíncronos. (Autor de Oban)
Se está desarrollando una librería de agentes basada en golang, y surgió la idea de que con suficiente logging siempre sería posible restaurar el estado del agente. Si se conocen el timestamp y la ejecución padre, se puede construir el árbol de ejecuciones hijas o ramificadas. Se gestionan sesiones usando en conjunto mapas y DB, y se pueden reconstruir cuando haga falta. La estructura evita mantener objetos individuales: los objetos stateless se buscan por id en mapas, mientras que acciones previas, pasos y contexto se guardan en un objeto de estado. La consistencia del agente/workflow también se resuelve gestionando resultados con hashes. Por ahora solo se implementaron agentes/herramientas básicas, y la lógica de logging, restauración y cancelación aún no está desarrollada
Temporal es bastante útil para checkpointing de procesos largos y además es neutral respecto al lenguaje
Yo también estuve pensando en colas de trabajo, y estoy considerando crear una cola rudimentaria en Postgres. Sus ventajas serían distribuir carga entre servidores, evitar la pérdida de tareas cuando termina un proceso y obtener mejor visibilidad. A cambio, se siente que la complejidad del código puede aumentar mucho, así que no es fácil diseñar la arquitectura de forma simple
Los ingenieros de IA rechazan JavaScript de manera extrema. La cancelación de TensorFlow for Swift marcó el fin de la diversidad de lenguajes en IA
Creo que el rechazo a JavaScript no es algo exclusivo de los ingenieros de IA. Incluso desde la perspectiva de alguien que lleva más de 30 años programando en JS, coincido
JS es un lenguaje muy malo y haberlo llevado al backend fue un error. TypeScript al final tampoco resuelve los problemas del JS subyacente. Se prefiere evitar usar JS o TS y optar por otras alternativas como Go, Rust, Python, Ruby, Elixir o F#
Me da curiosidad por qué JS sería especialmente bueno para agentes
Se siente que en ML hace falta un mejor modelo de concurrencia. Se intentó hacer ML con Go, pero por la falta de soporte de librerías y la dependencia de llamadas gRPC externas o wrappers, en la práctica es casi imposible. Python tiene limitaciones y C++ es tan verboso que termina afectando mucho la productividad