Estaba revisando un PR la semana pasada — código generado con agentes en paralelo — cuando vi algo que no cerraba. Dos funciones que se pisaban entre sí. Una escribía un archivo de configuración mientras la otra lo leía. El resultado era un estado intermedio que no era ni una cosa ni la otra. Mi primer instinto fue "la IA alucinó". Mi segundo instinto, después de leer el paper de multi-agentic software development, fue mucho peor: esto no es una alucinación. Esto es una race condition.
Y ahí se me heló la sangre.
Multi-agente sistemas distribuidos desarrollo: el problema que nadie está nombrando bien
El paper — "Multi-Agentic Software Development Is a Distributed Systems Problem" — dice algo que parece obvio una vez que lo leés pero que nadie en la comunidad de IA está diciendo en voz alta: cuando tenés múltiples agentes de IA trabajando sobre el mismo codebase, tenés un sistema distribuido. Con todo lo que eso implica.
No es metáfora. Es literal.
Cuando tenés dos agentes modificando archivos en paralelo, tenés exactamente el mismo problema que tenés con dos procesos compitiendo por un recurso compartido. Los mismos problemas que hacen que los sistemas distribuidos sean difíciles — y los sistemas distribuidos son famosamente difíciles — aparecen acá:
- Condiciones de carrera: dos agentes modifican el mismo archivo simultáneamente
- Deadlocks: el Agente A espera que el Agente B termine un módulo, y el Agente B espera que el Agente A termine una interfaz
- Inconsistencia eventual: el estado del codebase entre agentes es momentáneamente divergente
- Split-brain: dos agentes tienen modelos mentales incompatibles del sistema que están construyendo
Y la diferencia crucial con sistemas distribuidos tradicionales: no tenés el compilador avisándote. No tenés el tipo de Rust que te dice "ey, dos referencias mutables al mismo tiempo, eso no va". No tenés el runtime de Go tirando panic en la goroutine. Tenés código que parece correcto, pasa los tests superficiales, y explota en producción o — peor — nunca explota y simplemente se comporta mal de formas sutiles.
Por qué esto me pegó diferente: el contexto Surelock
Unas semanas atrás escribí sobre Surelock y deadlocks en Rust — bueno, en realidad sobre cómo me quemé intentando entenderlo. La lección de Rust fue que el compilador te obliga a pensar en ownership y borrowing antes de que el programa corra. Es friction intencional. Es el lenguaje diciéndote "antes de seguir, probame que sabés lo que estás haciendo con este recurso compartido".
En multi-agent development no hay esa friction. El modelo de lenguaje no tiene el concepto de "este archivo ya está siendo modificado por otro agente". No tiene una noción de mutex. No tiene transaction semantics. Cada agente tiene un contexto local que puede estar completamente desfasado del estado real del sistema.
Es como si hubieras tomado el problema más difícil de los sistemas distribuidos — la consistencia de estado — y le hubieras sacado todas las herramientas que tenemos para manejarlo.
Los patrones concretos que el paper identifica (y que yo ya había visto sin saber nombrarlos)
Acá es donde se pone interesante. El paper categoriza los fallos en patrones reconocibles:
1. Write-Write Conflict
Dos agentes modifican el mismo archivo. El que commitea último gana. El trabajo del primero se pierde, parcialmente o totalmente.
// Agente A está escribiendo esto:
export interface UserService {
getUser(id: string): Promise<User>;
// Agente A agregó este método nuevo
getUserByEmail(email: string): Promise<User>;
}
// Agente B, en paralelo, está escribiendo esto:
export interface UserService {
getUser(id: string): Promise<User>;
// Agente B agregó ESTE método nuevo
getUsersByRole(role: Role): Promise<User[]>;
}
// Resultado final (el que gana el merge):
export interface UserService {
getUser(id: string): Promise<User>;
// Solo uno de los dos métodos llega. El otro se perdió.
// Y en algún lugar del codebase hay código que llama al que no está.
getUsersByRole(role: Role): Promise<User[]>;
}
2. Read-Write Inconsistency (el que vi yo en el PR)
El Agente A lee la interfaz y toma decisiones basadas en ella. El Agente B modifica esa interfaz mientras A está trabajando. A termina con código que es consistente con una versión del sistema que ya no existe.
3. Context Drift
Con el tiempo, el modelo mental de cada agente sobre el sistema diverge. El Agente A "sabe" que la autenticación usa JWT. El Agente B, que tuvo una conversación diferente, "sabe" que usa sessions. Ninguno está equivocado — ambos tuvieron contexto correcto en su momento. Pero el sistema resultante tiene las dos implementaciones mezcladas.
4. Assumption Collision
Dos agentes hacen asunciones incompatibles sobre comportamiento no especificado. El Agente A asume que los IDs son UUIDs. El Agente B asume que son integers autoincremental. El schema de base de datos tiene las dos cosas y ningún test lo detecta porque cada suite de tests fue escrita por el agente que hizo la asunción.
-- Tabla creada por Agente A
CREATE TABLE usuarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255)
);
-- Tabla creada por Agente B (en otra parte del schema)
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
-- Agente B asumió que usuario_id es integer
usuario_id INTEGER REFERENCES usuarios(id) -- ESTO VA A EXPLOTAR
);
Lo que los sistemas distribuidos reales ya resolvieron (y podríamos aplicar acá)
Acá está la parte que me tiene entusiasmado, porque no estamos inventando soluciones desde cero. Tenemos décadas de ingeniería en sistemas distribuidos que podemos adaptar:
Locks y semáforos a nivel de archivo/módulo: antes de que un agente empiece a trabajar en un módulo, adquiere un lock lógico. Ningún otro agente puede modificar ese módulo hasta que el primero termine y libere el lock. Es exactamente lo que hacemos con mutexes, pero en el nivel de organización del trabajo de agentes.
Transacciones y rollback: si un conjunto de cambios de un agente no puede integrarse de forma consistente, los tirás todos y empezás de nuevo. Igual que una transacción de base de datos que falla por conflicto.
Event sourcing del codebase: en vez de que los agentes lean el estado actual del código, que lean el log de cambios. Cada agente sabe exactamente qué pasó antes de su intervención. Es más caro computacionalmente pero elimina la inconsistencia.
Coordinador central (el patrón más pragmático): un agente orquestador que serializa las decisiones. Los agentes de implementación trabajan en paralelo pero hay uno solo que decide qué entra y en qué orden. Es menos sexy que "IA completamente autónoma" pero es lo que funciona.
Este último punto me recuerda mucho a cómo terminamos resolviendo los problemas de datos en tiempo real en el proyecto de los colectivos: no intentás procesar todo en paralelo sin coordinación. Tenés un punto de serialización. El caos coordinado tiene un árbitro.
Los errores que vas a cometer (y que yo ya cometí)
Error 1: Pensar que el contexto compartido resuelve el problema
"Pero si todos los agentes tienen acceso al mismo repo, ¿no ven el mismo estado?" No. El contexto que cada agente carga en su ventana de contexto es un snapshot. Si el repo cambia mientras el agente trabaja, el agente no lo sabe automáticamente. Es eventual consistency sin los mecanismos de eventual consistency.
Error 2: Confiar en los tests como único validador
Los tests que generan los agentes son consistentes con las asunciones de los agentes. Si el Agente A tiene una asunción incorrecta, va a escribir tests que validan esa asunción incorrecta. Los tests pasan. El sistema está roto. Los tests son necesarios pero no suficientes para detectar este tipo de conflictos.
Error 3: Escalar agentes sin escalar coordinación
Más agentes en paralelo no es linealmente mejor. En sistemas distribuidos esto se llama el problema de coordinación — el overhead de mantener consistencia crece más rápido que el beneficio de la paralelización. Con agentes pasa exactamente lo mismo. Cuatro agentes en paralelo sin coordinación pueden producir más deuda técnica que un agente solo.
Error 4: No versionar las interfaces antes de distribuir el trabajo
Antes de que cualquier agente empiece a implementar, las interfaces tienen que estar definidas y freezadas. Igual que en el diseño de lenguajes: el contrato tiene que existir antes de que los consumidores del contrato empiecen a trabajar. Si el contrato cambia mientras todos están implementando, todo el trabajo previo se convierte potencialmente en deuda.
FAQ: Multi-agente sistemas distribuidos desarrollo
¿Los IDEs con agentes integrados (Cursor, Copilot Workspace) ya resuelven esto?
Parcialmente. Cursor, por ejemplo, tiene contexto del repo completo pero los agentes paralelos siguen sin tener mecanismos de coordinación reales. Es como tener todos los procesos viendo la misma memoria pero sin locks. El problema de write-write conflict y context drift sigue estando presente cuando tenés múltiples sesiones o múltiples agentes corriendo en paralelo.
¿Cuándo vale la pena usar múltiples agentes en paralelo entonces?
Cuando las tareas son genuinamente independientes y tienen interfaces bien definidas entre sí. Si podés dibujar un grafo de dependencias y hay nodos que no se tocan entre sí, esos son candidatos para paralelización segura. Si el grafo es un enredo de dependencias cruzadas, mejor serializar.
¿Esto aplica solo a proyectos grandes o también a proyectos personales?
Aplica en cuanto usás más de un agente trabajando sobre el mismo código. Incluso en proyectos chicos, si tenés una sesión de Claude en el frontend y otra en el backend tocando interfaces compartidas, estás en territorio de sistemas distribuidos. La escala importa para la severidad, no para la existencia del problema.
¿Git resuelve el problema de coordinación entre agentes?
Git resuelve el merge conflict después de que ocurrió. Pero no previene el trabajo desperdiciado — dos agentes que trabajaron horas en soluciones incompatibles. Y no detecta los conflictos semánticos donde el código mergea sin conflictos sintácticos pero el comportamiento resultante es incorrecto. Git es control de versiones, no coordinación de agentes.
¿Hay herramientas específicas para coordinar agentes hoy?
Aunque frameworks como LangGraph o CrewAI tienen primitivas de coordinación, ninguno implementa aún semánticas de sistema distribuido robustas (locks, transacciones, vector clocks). El estado del arte es mayormente "orquestador humano" — alguien que revisa qué hace cada agente y coordina manualmente. Lo cual es válido, pero es importante reconocer que es eso lo que estás haciendo.
¿Esto cambia cómo tengo que diseñar la arquitectura de mi sistema desde el principio?
Sí, y es una de las conclusiones más importantes del paper. Si sabés que vas a usar agentes en el desarrollo, la arquitectura tiene que favorecer módulos con interfaces explícitas y estables, bajo coupling y alta cohesión — exactamente los principios que hacen que los sistemas distribuidos sean manejables. No es coincidencia. Es el mismo problema.
Conclusión: el compilador que no tenemos
Rust me enseñó que la friction temprana es un regalo. El compilador que te dice "no" antes de que el programa corra te salva horas de debugging de condiciones de carrera en runtime. Es incómodo en el momento, es invaluable después.
Con multi-agent development estamos en el momento pre-Rust de los sistemas distribuidos. Tenemos el poder de la paralelización, no tenemos las herramientas de seguridad. Y la consecuencia es exactamente lo que predice la teoría: bugs sutiles, estado inconsistente, trabajo desperdiciado.
Lo que me entusiasma es que el paper está nombrando el problema correctamente. Una vez que sabés que es un problema de sistemas distribuidos, sabés a qué biblioteca de soluciones ir a buscar. No tenés que inventar nada nuevo — tenés que aplicar cuarenta años de ingeniería de sistemas distribuidos a un contexto nuevo.
Lo que me preocupa es que la industria va a tardar en darse cuenta. Vamos a ver montones de proyectos vibe-coded que "funcionan" hasta que no funcionan, y el diagnóstico va a ser "la IA falló" cuando el diagnóstico correcto es "nadie coordinó los agentes como se coordina un sistema distribuido".
Yo ya cambié cómo trabajo con agentes. Defino interfaces antes de distribuir trabajo. Serializo cambios a código compartido. Trato el contexto de cada agente como potencialmente stale. Es más lento que lanzar agentes a lo loco, pero el código que sale del otro lado es código que puedo mantener.
Privacidad y control local del contexto — algo en lo que Apple está apostando fuerte — también va a jugar acá: si el estado del sistema vive en contexto local de cada agente sin sincronización, el problema se multiplica. El modelo de coordinación centralizada y los datos de contexto compartidos y sincronizados son parte de la solución.
El próximo paso para la industria es construir el equivalente del borrow checker para agentes. Hasta entonces, somos Rust antes de 2010: sabemos que los bugs de concurrencia existen, no tenemos el compilador que los previene, y dependemos de que el programador — o el arquitecto — sea cuidadoso.
Soy el arquitecto. Voy a ser cuidadoso.
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)