Google ADK: arquitectura multi-agente, orquestación y routing

Google ADK: arquitectura multi-agente, orquestación y routing

Este es el cuarto post de la serie sobre Google ADK. Los anteriores: introducción, setup, y FunctionTool en profundidad.

Un agente solo con 30 herramientas cargadas es un agente confuso. El LLM tiene que procesar el schema completo de todas las herramientas en cada llamada, las instrucciones se vuelven gigantes para cubrir todos los casos, y cuando algo falla es difícil aislar dónde.

La solución es la misma que aplicamos en el código desde hace décadas: separación de responsabilidades. En ADK eso se llama arquitectura multi-agente.

El patrón Orquestador + Especialistas

La idea es simple: un agente raíz entiende la intención del usuario y delega al especialista correcto. Los especialistas tienen herramientas y conocimiento acotado a su dominio.

Usuario: ¿Está bien comprar NVDA a este precio?
         ↓
financial_advisor (orquestador)
  → Esta pregunta es sobre análisis de una acción específica
  → delega a stock_research_agent
         ↓
stock_research_agent
  → llama analyze_stock_fundamentals(NVDA)
  → llama get_analyst_ratings(NVDA)
  → llama compare_sector_peers(NVDA)
  → formula respuesta con los resultados
         ↓
financial_advisor recibe la respuesta del especialista
  → presenta la respuesta al usuario

El orquestador nunca toca las herramientas de análisis de acciones. El especialista nunca ve las herramientas de presupuesto. Cada uno hace lo suyo.

Cómo se implementa en código

En ADK la composición es explícita con el campo subAgents:

// src/agent.ts
import { LlmAgent } from '@google/adk';
import { budgetAgent } from './agents/budgetAgent.js';
import { investmentAgent } from './agents/investmentAgent.js';
import { stockResearchAgent } from './agents/stockResearchAgent.js';
import { personalAdvisorAgent } from './agents/personalAdvisorAgent.js';
// ...otros agentes

export const rootAgent = new LlmAgent({
  name: 'financial_advisor',
  model: 'gemini-3-flash-preview',
  instruction: `Sos FinancialAdvisorAI...

  Tu equipo de especialistas:
  - budget_savings_agent: presupuesto, ahorro, fondo de emergencia
  - stock_research_agent: análisis fundamental de acciones individuales
  - personal_investment_advisor: plan financiero personalizado completo
  ...`,
  subAgents: [
    budgetAgent,
    investmentAgent,
    portfolioAgent,
    goalPlannerAgent,
    riskAssessmentAgent,
    stockResearchAgent,
    personalAdvisorAgent,
  ],
});

El agente orquestador no tiene tools propias — tiene subAgents. Los sub-agentes no tienen subAgents — tienen tools. Esta distinción no es obligatoria en ADK (podés tener un agente con ambos), pero es el patrón más limpio para sistemas complejos.

El mecanismo de routing — cómo el LLM elige

Acá está la parte que más confunde inicialmente: ¿cómo sabe el LLM cuándo llamar a stock_research_agent vs investment_research_agent?

ADK expone los sub-agentes al LLM de una forma similar a las herramientas: el modelo recibe el nombre y la descripción de cada sub-agente. La decisión de delegación la toma el LLM basándose en:

  1. El campo description del sub-agente — es el campo más crítico para el routing
  2. Las instrucciones del orquestador — donde explicás la tabla de routing
  3. El contexto de la conversación — historial de mensajes

Esto significa que si la descripción de tu sub-agente es vaga, el routing va a ser inconsistente.

Comparación de descripciones:

// ❌ Demasiado vaga — el orquestador no puede discriminar
description: "Agente de inversiones",

// ✅ Específica sobre cuándo usar ESTE agente vs otros
description:
  "Especialista en análisis profundo de acciones individuales: " +
  "análisis fundamental (P/E, PEG, márgenes, deuda), ratings de analistas " +
  "y precio objetivo, calidad del dividendo, señales técnicas, y comparación " +
  "vs peers del sector. Usá este agente para due diligence antes de comprar " +
  "una acción específica. NO es para screening de ETFs ni análisis de mercado general.",

La última oración — "NO es para screening de ETFs" — es deliberada. El LLM tiene que diferenciar este agente de investment_research_agent. Decirle explícitamente qué NO hace reduce la ambigüedad.

La arquitectura completa del proyecto

financial_advisor (raíz — solo routing, sin tools propias)
│
├── budget_savings_agent (4 tools)
│   ├── analyze_budget
│   ├── find_savings_opportunities
│   ├── project_savings_growth
│   └── calculate_emergency_fund
│
├── investment_research_agent (5 tools)
│   ├── get_investment_details
│   ├── screen_investments
│   ├── compare_investments
│   ├── get_market_overview
│   └── get_index_fund_recommendations
│
├── portfolio_manager_agent (3 tools)
│   ├── analyze_portfolio
│   ├── rebalance_portfolio
│   └── analyze_portfolio_performance
│
├── goal_planner_agent (4 tools)
│   ├── plan_retirement
│   ├── plan_home_purchase
│   ├── calculate_financial_independence
│   └── plan_education_fund
│
├── risk_assessment_agent (3 tools)
│   ├── assess_risk_profile
│   ├── stress_test_portfolio
│   └── analyze_debt_vs_invest
│
├── stock_research_agent (5 tools)            ← análisis de acciones específicas
│   ├── analyze_stock_fundamentals
│   ├── get_analyst_ratings
│   ├── analyze_dividend_quality
│   ├── get_technical_signals
│   └── compare_sector_peers
│
└── personal_investment_advisor (5 tools)     ← plan personalizado holístico
    ├── build_financial_profile
    ├── generate_personalized_portfolio
    ├── optimize_tax_strategy
    ├── analyze_net_worth
    └── plan_life_event

29 herramientas distribuidas en 7 agentes especializados. Sin esta distribución, el contexto de cada llamada sería enorme y el LLM tendría que razonar sobre herramientas irrelevantes para la mayoría de las consultas.

Flujos multi-dominio

Algunos queries atraviesan múltiples agentes. El orquestador puede hacer delegaciones secuenciales:

Query: "Soy 34 años, gano $95k, tengo $40k ahorrados. ¿Cuál es mi situación y qué debería invertir?"

El orquestador puede:

  1. Delegar a personal_investment_advisorbuild_financial_profile para la evaluación general
  2. Luego delegar a personal_investment_advisorgenerate_personalized_portfolio para el portfolio
  3. Opcionalmente, delegar a risk_assessment_agentassess_risk_profile si necesita más contexto

Todo dentro de una sola conversación. El LLM del orquestador mantiene el contexto entre las delegaciones.

SequentialAgent — pipelines ordenados

El SequentialAgent ejecuta sus sub-agentes uno tras otro, esperando que cada uno termine antes de iniciar el siguiente. El estado de sesión es compartido, así que cada agente puede leer lo que el anterior depositó.

En el proyecto, completePlanAgent encadena tres especialistas para un análisis holístico completo:

// src/agents/completePlanAgent.ts
import { SequentialAgent } from '@google/adk';
import { personalAdvisorAgent } from './personalAdvisorAgent.js';
import { riskAssessmentAgent } from './riskAssessmentAgent.js';
import { goalPlannerAgent } from './goalPlannerAgent.js';

export const completePlanAgent = new SequentialAgent({
  name: 'complete_financial_plan',
  description:
    'Análisis financiero completo en secuencia: ' +
    '(1) construye el perfil financiero y portfolio personalizado, ' +
    '(2) evalúa tolerancia al riesgo y hace stress testing, ' +
    '(3) mapea objetivos concretos (retiro, vivienda, FIRE, educación). ' +
    'Usá cuando el usuario quiere un plan COMPLETO y aportó toda su situación financiera.',
  subAgents: [personalAdvisorAgent, riskAssessmentAgent, goalPlannerAgent],
});

Cada sub-agente puede tener un outputKey para guardar su respuesta en el estado de sesión:

// riskAssessmentAgent.ts — guarda la evaluación de riesgo para que el goalPlannerAgent la lea
export const riskAssessmentAgent = new LlmAgent({
  name: 'risk_assessment_agent',
  model: config.model,
  outputKey: 'user_risk_profile', // ← guardado en session.state["user_risk_profile"]
  // ...
});

Cuándo usar SequentialAgent vs delegaciones explícitas del orquestador:

SituaciónUsar
Los pasos tienen dependencia de datos (paso 2 necesita el output del paso 1)SequentialAgent
El orden es siempre el mismo y todos los pasos siempre se ejecutanSequentialAgent
El orquestador decide dinámicamente qué pasos ejecutar según el contextoDelegaciones del orquestador
Algunos pasos son opcionales o el orden varíaDelegaciones del orquestador

ParallelAgent — ejecución concurrente

El ParallelAgent ejecuta sus sub-agentes simultáneamente en contextos aislados. Cuando todos terminan, los resultados se fusionan. El tiempo total es el del agente más lento, no la suma de todos.

Usalo cuando las tareas son verdaderamente independientes — ningún agente necesita el output del otro para correr:

// src/agents/parallelAnalysisAgent.ts
import { ParallelAgent } from '@google/adk';
import { stockResearchAgent } from './stockResearchAgent.js';
import { riskAssessmentAgent } from './riskAssessmentAgent.js';

export const parallelAnalysisAgent = new ParallelAgent({
  name: 'stock_and_risk_parallel',
  description:
    'Análisis profundo de una acción Y evaluación de riesgo del usuario en paralelo. ' +
    'Usá cuando el usuario quiere saber tanto si una acción es buena como si encaja ' +
    'con su perfil de riesgo, en la misma consulta.',
  subAgents: [stockResearchAgent, riskAssessmentAgent],
});

Advertencia importante: los sub-agentes en un ParallelAgent corren en contextos de sesión aislados. No pueden leer los tool outputs intermedios del otro agente durante la ejecución. Si el agente B necesita los resultados del agente A, usá SequentialAgent.

Regla de decisión:

¿Necesita el resultado de A para ejecutar B?
  Sí → SequentialAgent
  No → ParallelAgent (si la latencia importa)
        Delegaciones del orquestador (si la decisión es dinámica)

LoopAgent — ciclos de feedback con control de calidad

El LoopAgent ejecuta sus sub-agentes en un ciclo hasta que se cumple una condición de salida. Es el patrón ideal para flujos de investigación + evaluación donde querés iterar hasta alcanzar calidad suficiente.

El ciclo se interrumpe cuando un sub-agente especial emite una señal de escalación (escalate: true). Ese sub-agente es el "árbitro" — revisa el output y decide si el loop terminó o si hay que iterar.

import { LoopAgent, LlmAgent, BaseAgent } from '@google/adk';
import type { InvocationContext } from '@google/adk';

// Agente árbitro — verifica si el research es suficientemente bueno
class ResearchQualityChecker extends BaseAgent {
  async *runAsyncImpl(context: InvocationContext) {
    const feedback = context.session.state.get('research_feedback');
    if (feedback?.status === 'pass') {
      // Research aprobado — señal de salida del loop
      yield { escalate: true };
    }
    // Si no hay señal, el LoopAgent vuelve a iterar desde el principio
  }
}

const researchLoop = new LoopAgent({
  name: 'research_loop',
  maxIterations: 3, // ← guardia de seguridad — evita loops infinitos en producción
  subAgents: [
    researchAgent, // hace el research
    judgeAgent, // evalúa la calidad y guarda feedback en session.state
    new ResearchQualityChecker({ name: 'quality_checker' }), // decide si continuar
  ],
});

Puntos clave del LoopAgent:

  • Siempre definir maxIterations como guardia de seguridad
  • El árbitro/checker guarda su veredicto en session.state para que los otros agentes lo lean
  • La señal de salida es { escalate: true } en el evento del árbitro
  • Útil para: research + validación, generación + revisión, negociación multi-paso

beforeAgentCallback — lógica antes del ciclo de ejecución

Los callbacks de ciclo de vida permiten ejecutar lógica antes (beforeAgentCallback) o después (afterAgentCallback) de que el agente procese cada invocación.

// src/agent.ts — callback de inicialización de sesión
import type { CallbackContext } from '@google/adk';

const initializeSession = (context: CallbackContext): undefined => {
  if (!context.state.has('session_start')) {
    context.state.set('session_start', new Date().toISOString());
    context.state.set('conversation_turn', 0);
  }
  const turn = (context.state.get<number>('conversation_turn') ?? 0) + 1;
  context.state.set('conversation_turn', turn);

  // undefined → el agente continúa procesando normalmente
  return undefined;
};

export const rootAgent = new LlmAgent({
  name: 'financial_advisor',
  model: config.model,
  beforeAgentCallback: initializeSession,
  // ...
});

La arquitectura completa actualizada

financial_advisor (raíz — routing + inicialización de sesión)
│
├── budget_savings_agent (4 tools)
├── investment_research_agent (5 tools)
├── portfolio_manager_agent (3 tools)
├── goal_planner_agent (4 tools)
├── risk_assessment_agent (3 tools, outputKey: user_risk_profile)
├── stock_research_agent (5 tools)
├── personal_investment_advisor (5 tools, outputKey: financial_profile)
│
├── complete_financial_plan [SequentialAgent]    ← plan holístico en secuencia
│   ├── personal_investment_advisor (paso 1)
│   ├── risk_assessment_agent (paso 2)
│   └── goal_planner_agent (paso 3)
│
└── stock_and_risk_parallel [ParallelAgent]      ← análisis concurrente
    ├── stock_research_agent ─────────────────────┐ ejecutan simultáneamente
    └── risk_assessment_agent ────────────────────┘

Los mismos agentes especializados se reutilizan en múltiples contextos: como sub-agentes directos del orquestador (para consultas específicas) y como pasos en pipelines de composición (para flujos complejos). ADK permite esta reutilización sin costo adicional.

Cuándo NO usar multi-agente

El patrón multi-agente agrega complejidad. No siempre es la solución:

  • Si tenés menos de 8-10 herramientas → un solo agente con instrucciones claras es suficiente
  • Si los dominios se superponen mucho → la ambigüedad de routing crea comportamiento inconsistente
  • Si el volumen de tokens ya es bajo → el overhead del routing es innecesario

El punto de inflexión suele estar en 8-12 herramientas o cuando las instrucciones de un solo agente se vuelven tan largas que son difíciles de mantener.


En el próximo post vamos a diseccionar cómo escribir instrucciones que funcionen bien, cómo estructurarlas para que sean mantenibles, y los errores más comunes que hacen que el routing falle.

El código de este post abarca dos commits: add risk/investment/stock tools (los agentes especializados con sus herramientas) y add complete and parallel agents (SequentialAgent, ParallelAgent, LoopAgent y beforeAgentCallback).

Comments

Share your thoughts and join the discussion


Related Posts