Google ADK: setup del proyecto y tu primer agente funcional

Google ADK: setup del proyecto y tu primer agente funcional

Este es el segundo post de la serie sobre Google ADK. Si no leíste el primero, ahí está la introducción al framework y al proyecto que vamos a armar.

En este post arrancamos con el código. Vamos a montar el proyecto desde cero, entender cada decisión de configuración, y tener un agente funcional corriendo en minutos.

Prerequisites

  • Node.js 18+ (los ESM nativos del ADK lo requieren)
  • Una API key de Gemini — podés conseguirla gratis en aistudio.google.com
  • npm o pnpm

Estructura del proyecto

La idea de la estructura del proyecto es tener bien separados las responsabilidades:

src/
├── agent.ts          # Punto de entrada — exporta rootAgent
├── agents/           # Un archivo por agente especializado
├── tools/            # Herramientas agrupadas por dominio
└── data/             # Capa de datos desacoplada de la lógica de agentes

Esta separación no es arbitraria. Las herramientas son independientes de los agentes que las usan — podés reutilizar analyzeBudgetTool en dos agentes distintos si tiene sentido. La capa de datos (marketData.ts) es intercambiable: hoy tiene datos mock, mañana se conecta a una API real sin tocar ningún agente.

package.json — las decisiones importantes

{
  "type": "module",
  "scripts": {
    "dev": "npx adk web",
    "start": "npx adk run src/agent.ts"
  },
  "dependencies": {
    "@google/adk": "^0.4.0",
    "dotenv": "^17.3.1",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@google/adk-devtools": "^0.4.0"
  }
}

Tres decisiones que vale la pena mencionar:

"type": "module" — ADK está construido sobre ESM. Si intentás usarlo con CommonJS vas a tener problemas con los imports dinámicos. Todo el proyecto es ESM, los imports llevan .js al final (aunque el archivo sea .ts).

@google/adk-devtools — Este paquete provee el servidor web de desarrollo (adk web). En producción no lo vas a necesitar.

zod — No es opcional. ADK usa los schemas de Zod para generar el JSON Schema que le pasa al LLM describiendo los parámetros de cada herramienta. Sin Zod, no hay tool calling.

tsconfig.json — los flags que importan

{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "esModuleInterop": true,
    "strict": true
  }
}

module: "nodenext" y moduleResolution: "nodenext" son el par que habilita ESM con resolución al estilo Node.js moderno. Con strict: true obtenés el máximo de type checking — importante cuando los retornos de las herramientas los consume el LLM dado que lo mejor es que el compilador te avise si retornás algo inesperado.

Variables de entorno

# .env
GOOGLE_GENAI_API_KEY=tu_api_key_aquí
GOOGLE_GENAI_USE_VERTEXAI=FALSE

Con GOOGLE_GENAI_USE_VERTEXAI=FALSE usás la API de Gemini directamente (ideal para desarrollo y proyectos pequeños). Cuando necesitás mayor control, SLAs, o regiones específicas, cambias a Vertex AI — solo cambia la variable, el código no cambia. Más sobre esto en los siguientes posts cuando hablamos de como pasar este proyecto a un ambiente productivo.

El entrypoint del agente importa dotenv al principio:

// src/agent.ts
import 'dotenv/config';

Tu primera herramienta

Empecemos con algo concreto: una herramienta que calcula la tasa de ahorro de una persona. Esto es de src/tools/budgetTools.ts:

import { FunctionTool } from '@google/adk';
import { z } from 'zod';

export const analyzeBudgetTool = new FunctionTool({
  name: 'analyze_budget',
  description:
    'Analiza ingresos y gastos mensuales. Retorna breakdown por categoría, ' +
    'tasa de ahorro, y comparación con la regla 50/30/20.',
  parameters: z.object({
    monthlyIncome: z.number().describe('Ingresos mensuales en USD'),
    expenses: z
      .array(
        z.object({
          category: z
            .string()
            .describe('Categoría del gasto (ej. Vivienda, Comida)'),
          amount: z.number().describe('Monto mensual en USD'),
        })
      )
      .describe('Lista de gastos mensuales'),
  }),
  execute: ({ monthlyIncome, expenses }) => {
    const totalExpenses = expenses.reduce((sum, e) => sum + e.amount, 0);
    const savings = monthlyIncome - totalExpenses;
    const savingsRate = (savings / monthlyIncome) * 100;

    return {
      status: 'success',
      summary: {
        monthlyIncome,
        totalExpenses,
        currentMonthlySavings: savings,
        savingsRate: savingsRate.toFixed(1) + '%',
      },
      // ... más métricas
    };
  },
});

Tres cosas clave acá:

El name tiene que ser único dentro del agente. Usá snake_case — es lo que el LLM ve cuando decide invocar la herramienta.

El description es la parte más crítica. No es para vos, es para el LLM. Tiene que describir exactamente qué hace la herramienta y qué tipo de resultado retorna, para que el modelo sepa cuándo usarla y qué esperar.

Los .describe() en Zod se convierten en descripciones de campo en el JSON Schema. El LLM los lee para entender qué pasar en cada parámetro, es clave no omitirlos. Es importante también aclarar cosas que quedan fuera del modelo, por ejemplo que los montos son en USD, quizá el modelo requiere otra tool para convertir el monto, o termina pidiendo al usuario que aclare los montos en esa moneda.

Tu primer agente

Con la herramienta lista, el agente se arma en pocas líneas:

// src/agents/budgetAgent.ts
import { LlmAgent } from '@google/adk';
import {
  analyzeBudgetTool,
  findSavingsOpportunitiesTool,
} from '../tools/budgetTools.js';

export const budgetAgent = new LlmAgent({
  name: 'budget_savings_agent',
  model: 'gemini-3-flash-preview',
  description:
    'Especialista en presupuesto personal y optimización del ahorro. ' +
    'Usá este agente para análisis de gastos, encontrar oportunidades de ' +
    'ahorro y proyectar el crecimiento del ahorro en el tiempo.',
  instruction: `Sos un coach experto en finanzas personales especializado en presupuesto.

Tu trabajo es:
1. Analizar el presupuesto del usuario con analyze_budget
2. Identificar dónde puede ahorrar con find_savings_opportunities
3. Siempre usar las herramientas disponibles — nunca estimar a mano

Referencia clave: regla 50/30/20 — 50% necesidades, 30% deseos, 20% ahorro/inversión.
Después de cada análisis, terminá con 2-3 próximos pasos concretos.`,
  tools: [analyzeBudgetTool, findSavingsOpportunitiesTool],
});

El model puede ser cualquier modelo de Gemini. En este caso elegimos gemini-3-flash-preview dado que es rápido, barato, y más que capaz para la mayoría de los casos de uso. Para tareas que requieren razonamiento muy complejo podés escalar a gemini-3-pro-preview.

El agente raíz

El entrypoint del proyecto exporta el agente raíz:

// src/agent.ts
import 'dotenv/config';
import { LlmAgent } from '@google/adk';
import { budgetAgent } from './agents/budgetAgent.js';

export const rootAgent = new LlmAgent({
  name: 'financial_advisor',
  model: 'gemini-3-flash-preview',
  description: 'Asesor financiero personal integral',
  instruction: `Sos FinancialAdvisorAI...`,
  subAgents: [budgetAgent],
});

El nombre rootAgent es la convención que ADK usa para identificar el agente de entrada. Si exportás algo con otro nombre, el CLI no lo va a encontrar.

Ejecutar el proyecto

# Instalar dependencias
npm install

# Copiar y configurar variables de entorno
cp .env.example .env
# Editar .env con tu API key

# Levantar el servidor de desarrollo web
npm run dev
# → http://localhost:8000

El adk web levanta una UI de chat donde podés interactuar con tu agente en tiempo real y ver el trace de las llamadas a herramientas. Mas adelante en los siguientes posts entramos en detalle sobre cómo usarlo para debugging.

Para ejecutar en modo CLI:

npm run run
# Abre un chat interactivo en la terminal

ADK Web en acción

ADK Web en acción

La convención de imports .js

Algo para tener en cuenta con ESM + TypeScript:

// ✅ Correcto — con .js aunque el archivo sea .ts
import { budgetAgent } from './agents/budgetAgent.js';

// ❌ Va a fallar en runtime
import { budgetAgent } from './agents/budgetAgent';

TypeScript con moduleResolution: "nodenext" requiere la extensión real del módulo tal como lo va a resolver Node.js. El compilador es inteligente — sabe que .js apunta al .ts en tiempo de desarrollo. Es contra-intuitivo pero es el estándar moderno.

Selección de modelo por agente

Una ventaja de ADK es que cada agente puede usar un modelo distinto:

// Agente orquestador — necesita buen razonamiento para routing, podriamos usar gemini-3.1-pro-preview
const rootAgent = new LlmAgent({
  model: 'gemini-3.1-pro-preview',
  // ...
});

// Agente de análisis simple — Flash es suficiente y más barato
const budgetAgent = new LlmAgent({
  model: 'gemini-3-flash-preview',
  // ...
});

En el proyecto todos usan gemini-3-flash-preview. Para un sistema en producción con alto volumen, vale la pena evaluar si el agente orquestador gana algo con un modelo más potente para el routing, pero en la práctica Flash maneja bien esta carga.

Lo que viene

En el siguiente post vamos a profundizar en el diseño de herramientas: cómo estructurar los retornos para que el LLM los procese mejor, patrones de manejo de errores, y cómo la descripción de una herramienta es en realidad un API contract con el modelo.


TL;DR del setup:

  1. "type": "module" + moduleResolution: "nodenext" en tsconfig
  2. Imports con .js aunque sean archivos .ts
  3. rootAgent es la convención de exportación que ADK espera
  4. GOOGLE_GENAI_USE_VERTEXAI=FALSE para desarrollo con Gemini API key
  5. npm run devhttp://localhost:8000 para la UI de desarrollo

El código de este post corresponde al commit inicial del proyecto: package.json, tsconfig.json, agent.ts y config.ts.

Comments

Share your thoughts and join the discussion


Related Posts