Hace dos meses, mi equipo (somos tres ingenieros) necesitaba automatizar un pipeline bastante específico: tomar un repositorio de GitHub, analizarlo, generar documentación técnica, y proponer mejoras de código con justificación. Nada demasiado raro, pero lo suficientemente complejo como para que un solo agente no lo resolviera bien.
Probé los tres frameworks durante dos semanas. No de manera superficial — monté prototipos reales, los puse a hacer trabajo que importaba, y los rompí de formas que no esperaba. Esto es lo que encontré.
AutoGen 0.5: Conversaciones entre agentes, pero prepárate para la curva
AutoGen viene de Microsoft Research y su propuesta es clara: agentes que se hablan entre sí como si fueran personas en un chat. El modelo de conversación es genuinamente diferente a los otros dos frameworks y, en algunos casos, esa diferencia importa mucho.
El problema que tuve es que llegué a AutoGen 0.5.x después de haber leído tutoriales escritos para la versión 0.2. Son casi frameworks distintos. La API asíncrona nueva es más limpia, pero si vienes de documentación antigua (y hay mucha por ahí), vas a perder tiempo. Yo perdí medio día buscando por qué ConversableAgent ya no aceptaba los parámetros que recordaba. Nadie te avisa de eso en la página principal.
Dicho eso, cuando AutoGen funciona bien, funciona muy bien. El patrón de “dos agentes que se revisan mutuamente el código” es donde brilla de verdad. Tuve un experimento donde un agente generaba código Python y otro lo criticaba — sin intervención mía — y el resultado final después de tres iteraciones fue notablemente mejor que lo que hubiera conseguido con un solo agente.
# AutoGen 0.5.x — dos agentes en conversación con condición de parada explícita
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def main():
# Usamos Claude aquí, pero funciona con cualquier proveedor OpenAI-compatible
model_client = OpenAIChatCompletionClient(model="claude-opus-4-6")
developer = AssistantAgent(
name="developer",
model_client=model_client,
system_message=(
"Escribes código Python limpio y eficiente. "
"Cuando estés satisfecho con la solución final, escribe LISTO."
),
)
reviewer = AssistantAgent(
name="reviewer",
model_client=model_client,
system_message=(
"Revisas código Python. Señalas problemas concretos "
"y sugieres mejoras específicas. Sé directo, no diplomático."
),
)
# Sin esta condición, los agentes pueden circular indefinidamente
termination = TextMentionTermination("LISTO")
team = RoundRobinGroupChat(
[developer, reviewer],
termination_condition=termination
)
result = await team.run(task="Escribe una función para parsear logs de Nginx")
print(result.messages[-1].content)
asyncio.run(main())
Lo que no me gustó: el debugging es doloroso. Cuando los agentes entran en un bucle o la conversación no termina como esperabas, rastrear qué pasó requiere revisar logs conversacionales largos. No hay una herramienta visual de primera mano que te ayude a entender el estado del sistema en un momento dado. Y el modelo de termination_condition requiere más atención de la que parece a primera vista — aprendí esto de la peor manera cuando un pipeline corrió durante diez minutos más de lo necesario en producción.
Mi takeaway con AutoGen: úsalo si tu caso de uso es naturalmente conversacional entre agentes. Para flujos de trabajo más estructurados con ramas y condiciones, vas a pelear con el framework.
CrewAI 0.86: Fácil de empezar, honesto sobre sus límites
CrewAI es el más accesible de los tres. La metáfora de “tripulación con roles” hace que sea muy fácil mapear tu problema al código. Tienes agentes que son “investigadores”, “escritores”, “revisores” — y eso se traduce directamente a clases con role, goal, y backstory.
Monté mi pipeline de documentación en CrewAI en unas tres horas. Eso es rápido. El código quedó legible, mi compañera que no conocía el framework entendió qué hacía con una lectura rápida. Eso tiene valor real cuando trabajas en equipo.
# CrewAI 0.86.x — pipeline secuencial con contexto entre tareas
from crewai import Agent, Task, Crew, Process
from crewai_tools import GithubSearchTool
github_tool = GithubSearchTool()
analyzer = Agent(
role="Analizador de repositorios",
goal="Entender la estructura y propósito del código",
backstory=(
"Llevas años revisando codebases complejos y sabes "
"identificar patrones arquitectónicos rápidamente."
),
tools=[github_tool],
verbose=True
)
writer = Agent(
role="Escritor técnico",
goal="Generar documentación clara para desarrolladores",
backstory=(
"Te especializas en hacer comprensible lo complejo. "
"Tu documentación la leen y la entienden."
),
verbose=True
)
analyze_task = Task(
description=(
"Analiza el repositorio {repo_url} y extrae: "
"estructura principal, dependencias clave, y flujo de datos."
),
expected_output="Informe estructurado con hallazgos del análisis",
agent=analyzer
)
document_task = Task(
description=(
"Con el análisis anterior, genera un README técnico "
"completo con ejemplos de uso reales."
),
expected_output="README.md completo y usable",
agent=writer,
context=[analyze_task] # recibe el output del análisis — esto es lo que lo conecta
)
crew = Crew(
agents=[analyzer, writer],
tasks=[analyze_task, document_task],
process=Process.sequential
)
result = crew.kickoff(inputs={"repo_url": "https://github.com/mi-org/mi-repo"})
Honestamente, CrewAI me decepcionó cuando intenté hacer algo más complejo. Necesitaba un flujo donde, dependiendo del resultado del análisis, se ejecutaran distintas tareas de seguimiento. El Process.hierarchical existe, pero el control que te da sobre las decisiones de enrutamiento es limitado. Terminé haciendo workarounds que se sentían como ir contra la corriente del framework.
También noté que el manejo de estado entre tareas sigue siendo más global de lo que me gustaría. Si necesitas que el estado de tu pipeline evolucione de forma no lineal, estás en territorio incómodo.
Si tu flujo es lineal o jerárquico y relativamente predecible, CrewAI es probablemente la mejor elección en términos de velocidad de desarrollo. Para lógica condicional compleja, busca otra herramienta.
LangGraph 1.2: El control que extrañabas sin saber que lo necesitabas
LangGraph tarda más en entrar. No voy a fingir que no. La abstracción de “grafo de estados” requiere que pienses diferente sobre cómo fluye la información en tu sistema, y eso tiene una curva de aprendizaje real.
Pero — y esto es importante — esa curva se paga.
Cuando tu pipeline tiene ramas condicionales, estados que necesitan persistir entre llamadas, o partes del flujo que pueden ejecutarse en paralelo, LangGraph te da control preciso sobre todo eso. No estás adivinando qué hace el framework detrás de las cortinas.
Una cosa que no esperaba que me importara tanto: el checkpointing. LangGraph puede guardar el estado del grafo en cada nodo. Esto significa que si tu pipeline falla a mitad (y fallará, en algún momento), puedes retomarlo desde el último punto estable en lugar de empezar de cero. Para pipelines largos con muchas llamadas a LLMs, esto es invaluable tanto en costos como en tiempo. En mi caso concreto, recuperé un análisis que había fallado en el paso de generación de documentación sin repagar el análisis completo del repositorio.
# LangGraph 1.2.x — grafo con routing condicional y checkpointing
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
class RepoState(TypedDict):
repo_url: str
analysis: str
complexity: Literal["simple", "complex"]
documentation: str
improvement_proposals: list[str]
def analyze_repo(state: RepoState) -> RepoState:
analysis = run_analysis(state["repo_url"]) # tu llamada al LLM aquí
# La decisión de routing vive en el estado, no en lógica oculta del framework
complexity = "complex" if len(analysis) > 2000 else "simple"
return {"analysis": analysis, "complexity": complexity}
def route_by_complexity(state: RepoState) -> str:
"""El routing condicional es explícito — puedes leerlo y seguirlo"""
return state["complexity"]
def generate_basic_docs(state: RepoState) -> RepoState:
docs = generate_readme(state["analysis"], depth="basic")
return {"documentation": docs}
def generate_full_docs_with_proposals(state: RepoState) -> RepoState:
docs = generate_readme(state["analysis"], depth="full")
proposals = generate_improvements(state["analysis"])
return {"documentation": docs, "improvement_proposals": proposals}
builder = StateGraph(RepoState)
builder.add_node("analyze", analyze_repo)
builder.add_node("basic_docs", generate_basic_docs)
builder.add_node("full_docs", generate_full_docs_with_proposals)
builder.set_entry_point("analyze")
builder.add_conditional_edges(
"analyze",
route_by_complexity,
{"simple": "basic_docs", "complex": "full_docs"}
)
builder.add_edge("basic_docs", END)
builder.add_edge("full_docs", END)
# MemorySaver es para desarrollo; en producción usarías PostgresSaver o similar
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "repo-analysis-001"}}
result = graph.invoke({"repo_url": "https://github.com/mi-org/mi-repo"}, config)
El error más grande que cometí fue intentar modelar todo como un grafo desde el primer día. Empecé con doce nodos para algo que podría haber sido cuatro. LangGraph Studio (la UI de visualización) me ayudó a darme cuenta de que estaba sobrediseñando — cuando ves el grafo dibujado, las conexiones innecesarias se hacen obvias de inmediato.
No estoy 100% seguro de que LangGraph escale de manera sencilla más allá de 20-25 nodos. Los grafos grandes se vuelven difíciles de razonar incluso con la visualización. Para pipelines de esa magnitud, posiblemente tiene sentido modularizarlos en sub-grafos, aunque eso añade complejidad propia. Tu kilometraje puede variar.
Lo que yo usaría hoy — y por qué no me conformo con “depende”
Después de dos semanas probando los tres, tengo una opinión concreta. Sé que “depende de tu caso de uso” es la respuesta segura. También sé que no ayuda a nadie que está eligiendo ahora mismo.
Si estás construyendo un prototipo para validar una idea en dos días, usa CrewAI. La velocidad de desarrollo es real, el código es legible, y es más que suficiente para demostrar un concepto a alguien que necesita ver algo funcionando rápido.
Si tu caso de uso central es que agentes se revisen entre sí de forma iterativa — feedback loops de código, research con validación cruzada — AutoGen tiene un modelo mental que encaja mejor con eso. El coste es tiempo invertido en entender la nueva API 0.5.x y aceptar que el debugging será más conversacional que visual.
Para cualquier cosa que vaya a producción con lógica condicional no trivial, usa LangGraph. El control sobre el estado, las ramas condicionales explícitas, y especialmente el checkpointing me convencieron. Sí, la curva inicial es más pronunciada. Pero el día que necesites depurar por qué tu pipeline tomó la rama equivocada, vas a agradecer tener un grafo explícito en lugar de comportamiento implícito.
En mi equipo, terminamos con LangGraph para el pipeline de documentación y no hemos mirado atrás. El tiempo que tardamos en aprender el framework lo recuperamos en la primera semana de producción, cuando el checkpointing nos salvó de reprocesar análisis costosos después de un timeout de red.
Una cosa más: los tres frameworks están cambiando a un ritmo que hace que cualquier comparativa tenga fecha de caducidad. Lo que hoy es una limitación de CrewAI puede no serlo en seis meses — lo aprendí de la peor manera cuando migré código de AutoGen 0.2 a 0.5 y casi nada era compatible. Revisa los changelogs antes de comprometerte con algo en proyectos largos.