Un agente de IA que responde preguntas ya no impresiona a nadie. Donde empieza lo interesante es cuando necesitas varios trabajando juntos: uno que clasifica la petición, otro que investiga, un tercero que redacta, y una persona que da el visto bueno antes de publicar nada. Coordinar eso es lo que se conoce como orquestación de agentes, y es exactamente lo que vamos a montar en este tutorial, desde cero y paso a paso, usando Go y la versión 2.0 del kit de Google.
Si nunca has oído hablar de orquestación, tranquilo: empezamos por el concepto, con diagramas incluidos. Si ya tienes el modelo mental, salta directo al tutorial.

Qué es orquestar agentes de IA (y por qué importa)
Un agente de IA es, en esencia, un LLM con instrucciones y herramientas: le entra texto, decide qué hacer y devuelve un resultado. Los servidores MCP se están consolidando como la forma estándar de conectar esas herramientas. Un solo agente funciona bien para tareas acotadas, pero se queda corto en cuanto el problema tiene varias fases o necesita especialización.
Orquestar agentes es definir cómo se coordinan varios de ellos para completar un trabajo: quién va primero, quién recibe la salida de quién, qué decisiones abren un camino u otro, qué tareas corren en paralelo y en qué puntos interviene una persona. Es la misma idea que una orquesta: cada músico toca su parte, pero alguien tiene que llevar el tempo.
Los cuatro patrones básicos que cubre el 95 % de los sistemas reales:
SECUENCIAL ROUTING PARALELO HUMAN-IN-THE-LOOP
A ──▶ B ──▶ C A ──▶ ¿tipo? ──┬─▶ B ┌─▶ B ─┐ A ──▶ [persona] ──▶ B
├─▶ C ├─▶ C ─┼─▶ E
└─▶ D └─▶ D ─┘
El problema es que, si montas esto a mano en Go, acabas con goroutines y canales por todas partes, un switch gigante para decidir qué agente toca ahora y, cuando quieres que una persona apruebe algo a mitad de camino, un sistema casero de “pausa y reanuda” con Redis y buena voluntad. Funciona, pero es código de fontanería que no aporta nada a tu negocio.
Ahí entra la versión 2.0 del Agent Development Kit de Google para Go: trae un motor de workflows basado en grafos como primitiva del propio framework. Modelas el flujo como un grafo de nodos, declaras las aristas, y el scheduler se encarga del paralelismo, el routing y las pausas para human-in-the-loop. Vamos a montar los cuatro patrones del diagrama usando los ejemplos oficiales de workflow.
Paso 1: preparar el entorno
El módulo v2 se importa con su propio path, como manda la convención de Go:
go get google.golang.org/adk/v2
La referencia completa de la API está en pkg.go.dev/google.golang.org/adk/v2. Para los workflows que solo usan nodos de función no necesitas nada más. Para los que llaman a un LLM necesitas credenciales de Gemini: o bien una API key en GOOGLE_API_KEY, o bien Vertex AI con Application Default Credentials (gcloud auth application-default login más GOOGLE_GENAI_USE_VERTEXAI=true, GOOGLE_CLOUD_PROJECT y GOOGLE_CLOUD_LOCATION), tal como indica el README de los ejemplos.
Para seguir el tutorial, clona el repo con los ejemplos:
git clone https://github.com/google/adk-go.git
cd adk-go
Paso 2: tu primer workflow secuencial
El motor vive en el paquete google.golang.org/adk/v2/workflow. Todo workflow es un grafo dirigido: los nodos son unidades de trabajo y las aristas definen quién entrega datos a quién. Según la documentación de los ejemplos, hay cinco tipos de nodo:
FunctionNode: una función de Go cualquiera, tipada entrada/salida. El caballo de batalla.AgentNode: envuelve unLlmAgentpara que participe como un nodo más.ToolNode: envuelve untool.Tool.JoinNode: barrera de fan-in; espera a que terminen todos sus predecesores y entrega unmap[nombreNodo]output.DynamicNode: orquestador imperativo para cuando quieres decidir en Go, en tiempo de ejecución, qué hijos ejecutar.
El ejemplo más simple es basic: dos FunctionNode encadenados con workflow.Chain. El primero pone el texto del usuario en mayúsculas y el segundo le añade un sufijo:
nodeA := workflow.NewFunctionNode("upper", upperFn, nodeConfig)
nodeB := workflow.NewFunctionNode("suffix", suffixFn, nodeConfig)
edges := workflow.Chain(workflow.Start, nodeA, nodeB)
myWorkflow, err := workflowagent.New(workflowagent.Config{
Name: "simple_sequence_workflow",
Description: "Converts string to uppercase and appends a suffix",
Edges: edges,
})
El grafo se empaqueta como un agente normal con workflowagent.New, así que puedes servirlo con el launcher de consola que trae el propio ADK sin escribir ni una línea de servidor. Lo ejecutas así:
go run ./examples/workflow/basic/ console
Tienes un chat en la terminal para probar el flujo. Sin Docker, sin UI, sin nada, y sin API key: basic no toca ningún LLM.
Paso 3: routing, o cómo decidir el camino con un LLM
El patrón secuencial se rompe en cuanto la siguiente fase depende del contenido: un email furioso no se trata igual que una pregunta técnica. Aquí entra el routing, y el ejemplo routing/llm enseña la separación de responsabilidades correcta: el LLM solo clasifica, y el motor hace el routing. Nada de “el modelo devuelve JSON con el siguiente paso y yo lo parseo y rezo”.
Un AgentNode con un LlmAgent clasificador responde con exactamente una palabra (question, exclamation o statement). Después, un nodo emisor convierte esa palabra en rutas:
func routeByClassification(ctx agent.Context, input any, emit func(*session.Event) error) (any, error) {
category := strings.TrimRight(strings.ToLower(strings.TrimSpace(fmt.Sprint(input))), ".")
if category != "question" && category != "exclamation" && category != "statement" {
category = "statement" // fallback defensivo si el LLM se sale del guion
}
ev := session.NewEvent(ctx, ctx.InvocationID())
ev.Routes = []string{category}
if err := emit(ev); err != nil {
return nil, err
}
return nil, nil
}
Y las aristas declaran qué ruta activa cada destino:
edges := workflow.Concat(
workflow.Chain(workflow.Start, classifyNode, routeNode),
[]workflow.Edge{
{From: routeNode, To: question, Route: workflow.StringRoute("question")},
{From: routeNode, To: statement, Route: workflow.StringRoute("statement")},
{From: routeNode, To: exclamation, Route: workflow.StringRoute("exclamation")},
},
)
Hay StringRoute, IntRoute y MultiRoute para routing por valor. El detalle clave: si el LLM se inventa una categoría, tu función normaliza y cae en la rama por defecto. El control lo tienes tú, no el modelo. Un apunte del ejemplo que conviene copiar: registra los agentes LLM envueltos en SubAgents de la config del workflowagent, o el runner soltará un “Event from an unknown agent” en cada turno. Este ejemplo sí necesita las credenciales del paso 1.
Paso 4: paralelismo con fan-out y fan-in
Cuando varias tareas no dependen entre sí, ejecutarlas en serie es tirar tiempo. El ejemplo complex monta una pipeline de investigación: tres agentes investigadores (energías renovables, vehículo eléctrico, captura de carbono) corren en paralelo, un JoinNode espera a los tres, un FunctionNode formatea los resultados y un agente de síntesis redacta el informe final. El cableado son cuatro líneas:
eb := workflow.NewEdgeBuilder()
eb.AddFanOut(workflow.Start, renewableNode, electricVehicleNode, carbonNode)
eb.AddFanIn(gatherNode, renewableNode, electricVehicleNode, carbonNode)
eb.Add(gatherNode, formatNode)
eb.Add(formatNode, synthNode)
El JoinNode (workflow.NewJoinNode("gather")) dispara una sola vez, cuando han terminado todos sus predecesores, y entrega a su sucesor un map[string]any indexado por nombre de nodo. Por eso el ejemplo usa constantes para los nombres de los agentes: son también las claves del mapa. Aquí no hay sync.WaitGroup, ni canal de resultados, ni select. Esa es exactamente la fontanería que el motor te quita.
Esto es distinto a montar un equipo de agentes autónomos con MiniMax Code, donde cada agente es un rol; aquí el workflow es un grafo dirigido con aristas explícitas.
Paso 5: human-in-the-loop, pausar para que una persona decida
Último patrón, y el que más me ha sorprendido, porque suele ser lo primero que te toca implementar a mano en cualquier sistema de agentes serio. En ADK 2.0 pausar un workflow es emitir un evento RequestInput y devolver ErrNodeInterrupted, como hace el ejemplo hitl_simple:
ask := workflow.NewEmittingFunctionNode[any, any]("ask_name",
func(ctx agent.Context, _ any, emit func(*session.Event) error) (any, error) {
if err := emit(workflow.NewRequestInputEvent(ctx, session.RequestInput{
InterruptID: "ask_name-" + uuid.NewString(),
Message: "What's your name?",
})); err != nil {
return nil, err
}
return nil, workflow.ErrNodeInterrupted
},
workflow.NodeConfig{},
)
Pruébalo directamente:
go run ./examples/workflow/hitl_simple/ console
El launcher de consola pinta el prompt, la persona responde, y esa respuesta se entrega al siguiente nodo como su entrada tipada. Fíjate en el InterruptID con un UUID fresco por petición: el comentario del propio ejemplo avisa de que reutilizar IDs puede hacer que la Dev UI trate un prompt posterior como ya respondido. Detalle pequeño que te ahorra una tarde de depuración.
Antes de dejar que un LLM tome decisiones críticas, conviene tener el contexto del sistema bien documentado: un archivo AGENTS.MD reduce alucinaciones y define reglas de comportamiento.
Si tu caso es “un solo nodo que pregunta y se reejecuta con la respuesta” en vez de pasar a otro nodo, el ejemplo hitl_rerun usa ResumeOrRequestInput para ese patrón de reentrada. Y para aprobaciones en mitad de una pipeline (publicar esto sí o no, ejecutar este despliegue sí o no), el patrón es el mismo: un nodo emisor de RequestInput entre el nodo que prepara la acción y el que la ejecuta.
Si vienes de ADK 1.0: los breaking changes que te van a tocar
El README-v2 documenta los breaking changes. Los dos que te van a tocar sí o sí:
session.NewEventahora exige uncontext.Contextcomo primer argumento:session.NewEvent(ctx, ctx.InvocationID()). El ID y el timestamp del evento salen del paqueteplatform, lo que permite a los motores de workflow producir eventos deterministas y reejecutables. Si tenías un helper sin contexto, añade el parámetro y pásalo desde arriba; nada decontext.Background()a mitad de la cadena.ToolContextyCallbackContextse han fusionado en un únicoagent.Context(PR #945). Tus mocks de tests se romperán si implementaban la interfaz antigua. La salida cómoda: embebeagent.StrictContextMocken tu fake, sobreescribe solo lo que el test usa, y deja de parchear mocks cada vez que crece la interfaz. Los métodos no sobreescritos hacen panic con “not implemented”, que es lo que quieres en un test.
De cero a workflow orquestado: recapitulando
Ya tienes el modelo mental completo: orquestar es definir un grafo de agentes y pasos, y los cuatro patrones que necesitas son cadena, routing, paralelo y pausa humana. Con ADK Go 2.0 cada patrón son unas pocas líneas declarativas, y el scheduler se come la fontanería. Si lo que buscas es un agente autónomo que trabaje fuera de un grafo fijo, la configuración óptima de OpenClaw es un buen complemento a este tutorial.
Si tu sistema cabe en una cadena lineal, un Chain con tres FunctionNode y listo, no necesitas más. Donde el motor de grafos empieza a pagar es en cuanto aparece cualquiera de estas tres cosas: decisiones que dependen de la salida de un LLM, trabajo que puede correr en paralelo, o una persona que tiene que aprobar algo antes de continuar. En ese punto, la alternativa es escribir tú el scheduler, y créeme, no quieres mantener un scheduler.
Todo lo que has visto corre en local con un go run, y la mayoría de los ejemplos (7 de los 10 del repo) ni siquiera necesitan API key. El orden que yo seguiría para aprenderlo: basic, luego hitl_simple, después routing/llm y por último complex. En una tarde pasas de no saber qué es la orquestación a tener un workflow multiagente funcionando en tu ordenador.
Preguntas frecuentes
¿Qué es la orquestación de agentes de IA?
Es coordinar varios agentes de IA para completar un trabajo: definir el orden de ejecución, quién recibe la salida de quién, qué decisiones abren caminos distintos, qué corre en paralelo y en qué puntos interviene una persona. Se suele modelar como un grafo de nodos (pasos) y aristas (flujo).
¿Qué necesito para seguir este tutorial?
Go instalado y el repo google/adk-go clonado. Los ejemplos basic, hitl_simple, routing/string, routing/int, hitl_rerun y los de dynamic no necesitan API key. Para routing/llm y complex necesitas credenciales de Gemini (GOOGLE_API_KEY) o Vertex AI.
¿Cómo funciona el human-in-the-loop en ADK Go 2.0?
Un nodo de emisión envía un evento RequestInput y devuelve workflow.ErrNodeInterrupted. El launcher muestra el prompt, la persona responde y la respuesta tipada llega al siguiente nodo como entrada. Así se pueden modelar aprobaciones o preguntas interactivas.



¿Qué te ha parecido?
Déjame tu opinión, pregunta o sugerencia. Los comentarios se sincronizan con GitHub Discussions .