Google ADK: instructions como código

Este es el quinto post de la serie sobre Google ADK. Los anteriores: introducción, setup, FunctionTool, y arquitectura multi-agente.
Las instrucciones de un agente son, en la práctica, código. No tienen tipos ni el compilador te avisa cuando rompés algo, pero determinan el comportamiento del sistema tanto como cualquier función TypeScript. Se versionan, se revisan, se testean, y se refactorizan.
En este post vamos a ver cómo estructurarlas, qué hace que unas funcionen y otras fallen, y los patrones que emergieron del proyecto del asesor financiero.
Por qué las instrucciones importan más de lo que parece
Un agente con herramientas perfectas e instrucciones malas va a dar respuestas inconsistentes. Un agente con herramientas mediocres e instrucciones excelentes puede resultar sorprendentemente útil. Las instrucciones son el cerebro; las herramientas son los brazos.
Las instrucciones del sistema cumplen varios roles simultáneos:
- Identidad — quién es el agente, qué expertise tiene
- Comportamiento — cómo razona, qué hace primero, qué prioriza
- Routing (para orquestadores) — cuándo delegar a quién
- Restricciones — qué no debe hacer
- Formato — cómo estructurar las respuestas
Estructura que funciona
Hay una estructura que se repite en los agentes del proyecto y que da buenos resultados:
1. Rol e identidad (1-2 oraciones)
2. Qué hace este agente (lista concreta)
3. Guías de comportamiento / filosofía
4. Reglas de routing o uso de herramientas (para orquestadores)
5. Conceptos clave del dominio
6. Disclaimers y limitacionesNo todos los agentes necesitan todos los bloques. Un especialista con 3 herramientas necesita mucho menos que el orquestador con 7 sub-agentes.
El agente orquestador — routing como feature principal
Las instrucciones del orquestador en src/agent.ts son las más complejas del sistema. El objetivo central es que el LLM tome decisiones de routing consistentes:
instruction: `Sos FinancialAdvisorAI — un asesor financiero personal de clase mundial...
## Tu equipo de especialistas
| Agente | Mejor para |
|--------|------------|
| **budget_savings_agent** | Análisis de presupuesto mensual, optimización de gastos... |
| **stock_research_agent** | Análisis fundamental profundo de acciones individuales... |
| **personal_investment_advisor** | Plan financiero personalizado completo... |
## Reglas de routing
**Usá stock_research_agent cuando:**
- El usuario pregunta por análisis profundo de UNA acción específica
- Quiere ratings de analistas o precio objetivo
- Quiere comparar una acción vs peers del sector
**Usá personal_investment_advisor cuando:**
- El usuario quiere un plan COMPLETO y PERSONALIZADO
- Comparte su situación financiera y quiere consejo holístico
- Pregunta sobre estrategia fiscal o eventos de vida
**Usá investment_research_agent cuando:**
- El usuario quiere FILTRAR inversiones (ETFs, sectores)
- Quiere overview del mercado — NO análisis de una acción específica
...`,
Varias cosas a notar:
La tabla de agentes en formato markdown — el LLM la procesa bien, es legible para humanos, y es fácil de mantener.
Las reglas de routing son explícitas y en primera persona — "Usá X cuando..." en lugar de "X es para...". El LLM entiende mejor las instrucciones directas sobre qué hacer que las descripciones abstractas.
Cada regla de routing tiene negaciones — "investment_research_agent cuando quieras FILTRAR... NO para análisis de una acción específica". Esto reduce la ambigüedad en los casos límite.
El agente especialista — profundidad sobre routing
Los especialistas tienen instrucciones más enfocadas. Mirá stockResearchAgent.ts:
instruction: `Sos un analista senior de renta variable con expertise en análisis
fundamental, valuación, y due diligence de inversiones.
Tu trabajo es ayudar a los usuarios a:
1. Analizar fundamentos antes de comprar acciones
2. Interpretar ratings y precio objetivo de analistas
3. Evaluar la seguridad y calidad de dividendos
4. Leer señales técnicas para timing de entrada
5. Comparar una acción con sus peers del sector
## Framework de research (usarlo en este orden para análisis completo)
1. analyze_stock_fundamentals — valuación, calidad, crecimiento, score general
2. get_analyst_ratings — consenso y precio objetivo de Wall Street
3. compare_sector_peers — validar que es la mejor opción en el sector
4. get_technical_signals — timing de entrada
5. analyze_dividend_quality — solo si es una acción de dividendos
## Cuando el usuario menciona una acción sin una pregunta específica:
Hacé un análisis COMPLETO: llamá analyze_stock_fundamentals, get_analyst_ratings,
y compare_sector_peers. Resumí con una recomendación clara de compra/mantener/vender.
`,
Dos patrones interesantes acá:
El framework de uso de herramientas en orden explícito. En lugar de dejar que el LLM decida cuándo invocar cada herramienta, le damos el flujo preferido. Esto hace el comportamiento más predecible y el output más consistente.
La última sección — comportamiento por defecto. Si el usuario dice "qué me decís de NVDA" sin más contexto, el agente sabe que tiene que hacer un análisis completo. Sin esto, podría simplemente describir la empresa de memoria sin invocar ninguna herramienta.
El campo description del sub-agente — diferente al instruction
Es fácil confundirlos pero tienen roles completamente distintos:
const stockResearchAgent = new LlmAgent({
name: 'stock_research_agent',
// description: lo que el ORQUESTADOR lee para decidir si delegar acá
// Tiene que ser específico sobre cuándo usar ESTE agente vs los demás
description:
'Especialista en análisis profundo de acciones individuales. ' +
'Usá este agente para due diligence antes de comprar una acción: ' +
'fundamentals, ratings de analistas, dividendos, técnicos, peers. ' +
'NO para screening de ETFs ni overview de mercado general.',
// instruction: lo que EL AGENTE lee para saber cómo comportarse
// Lo ve el agente especialista cuando ejecuta, no el orquestador
instruction: `Sos un analista senior de renta variable...`,
});
description → para el LLM del orquestador (routing)
instruction → para el LLM del especialista (comportamiento)
outputSchema — respuestas estructuradas y determinísticas
Cuando necesitás que un agente devuelva JSON con un schema fijo (no texto libre), usá outputSchema con un esquema Zod:
import { z } from 'zod';
const riskOutputSchema = z.object({
profile: z.enum([
'Very Conservative',
'Conservative',
'Moderate',
'Growth',
'Aggressive Growth',
]),
score: z.number().describe('Risk score from 0 to 120'),
stocks: z.number().describe('Recommended % in stocks'),
bonds: z.number().describe('Recommended % in bonds'),
warnings: z.array(z.string()),
});
export const riskJudgeAgent = new LlmAgent({
name: 'risk_judge',
model: config.model,
outputSchema: riskOutputSchema, // ← fuerza JSON estructurado
outputKey: 'risk_assessment', // ← guarda en session.state["risk_assessment"]
// Cuando usás outputSchema, el agente no puede "charlar" ni delegar
disallowTransferToParent: true,
disallowTransferToPeers: true,
description:
'Evalúa el perfil de riesgo del usuario y retorna un JSON estructurado.',
instruction: `Analizá los datos del usuario y retorná un perfil de riesgo estructurado.
Basá tu evaluación en edad, horizonte temporal, estabilidad de ingresos, y reacción ante caídas del mercado.`,
});
Cuándo usar outputSchema:
- El agente es un paso interno en un pipeline (no habla directamente con el usuario)
- Su output es consumido por código o por otro agente, no solo leído por una persona
- Querés garantía de que siempre retorna los campos esperados
Cuándo NO usarlo:
- El agente tiene conversación libre con el usuario (respuestas naturales, follow-up questions)
- La estructura del output varía según el contexto
outputKey — estado persistente entre agentes
outputKey guarda la respuesta del agente en session.state al terminar su ejecución. No requiere outputSchema — guarda el texto o JSON tal como lo produce el agente.
export const riskAssessmentAgent = new LlmAgent({
name: 'risk_assessment_agent',
model: config.model,
outputKey: 'user_risk_profile', // ← session.state["user_risk_profile"] = <respuesta>
// ...
});
Estados con prefijo: ADK permite prefijos de scope en las claves de estado:
app:clave— compartido entre todas las sesiones de la aplicaciónuser:clave— persistente para el mismo usuario entre sesionestemp:clave— temporal, solo para la invocación actual
// Guardar preferencias del usuario de forma persistente entre sesiones
context.state.set('user:preferred_investment_style', 'passive');
// Guardar resultado temporal solo para esta invocación
context.state.set('temp:research_draft', researchOutput);
Instrucciones dinámicas con {key} templating
Las instrucciones de un agente pueden ser funciones en lugar de strings. Esto permite inyectar valores del estado de sesión directamente en el texto de la instrucción:
import { LlmAgent, injectSessionState } from '@google/adk';
import type { ReadonlyContext } from '@google/adk';
export const goalPlannerAgent = new LlmAgent({
name: 'goal_planner_agent',
model: config.model,
instruction: async (context: ReadonlyContext) =>
injectSessionState(
`Sos un planificador de objetivos financieros.
El perfil financiero del usuario es:
{financial_profile}
Su evaluación de riesgo es:
{user_risk_profile}
Con ese contexto, ayudá al usuario a planificar sus objetivos concretos:
retiro, compra de vivienda, FIRE, educación universitaria.
Siempre usá las herramientas disponibles para los cálculos.`,
context
),
// ...
});
ADK reemplaza {financial_profile} y {user_risk_profile} con los valores actuales de session.state en cada invocación. Este patrón es la base de los pipelines secuenciales: los resultados de los agentes previos fluyen a los siguientes a través de outputKey + {key} templating sin ningún código de coordinación explícito.
Errores comunes en instrucciones
Instrucciones contradictorias. Si en un lugar decís "siempre usá las herramientas disponibles" y en otro "no necesitás una herramienta para responder preguntas simples", el LLM va a tener comportamiento inconsistente. Las contradicciones son bugs.
Sobre-especificación del formato. Si escribís "la respuesta tiene que tener exactamente estos 5 puntos en este orden", el LLM va a cumplirlo aunque el resultado sea raro. El LLM es bueno formateando respuestas apropiadas para el contexto — confiale esa decisión.
Instrucciones que deberían ser código. Si tu instrucción dice "calculá el interés compuesto usando la fórmula FV = PV × (1 + r)^n", eso debería ser una herramienta. Las instrucciones son para comportamiento y razonamiento, no para cómputos.
Demasiado largas sin estructura. Bloques de texto sin títulos ni listas son difíciles de procesar para el LLM. Usá markdown: headers, bullets, tablas.
Resumen
| Item | Aplicación |
|---|---|
| Las instrucciones son código | Versionarlas, revisarlas, testearlas |
| Routing explícito > implícito | "Usá X cuando..." con casos específicos y negaciones |
| Frameworks de herramientas | Decirle al LLM el orden preferido de invocación |
| Knowledge encoding | Poner en el system prompt el conocimiento de dominio relevante |
description vs instruction | Routing vs comportamiento — son para LLMs distintos |
| Estructura markdown | Headers, bullets, tablas — más procesable que prosa |
| No computación en instrucciones | Si hay un cálculo → hacelo una herramienta |
outputSchema para agentes de pipeline | Solo cuando el output es consumido por código/otros agentes |
outputKey para estado compartido | Permite que los resultados fluyan entre agentes sin coordinación explícita |
{key} templating para instrucciones dinámicas | Inyecta contexto del estado en las instrucciones de cada agente |
En el próximo post entramos al ciclo de desarrollo: cómo usar adk web para debugging, estrategias de testing para agentes, y los gotchas que te van a ahorrar horas de debugging.
Los patrones de este post se pueden seguir en el estado del repo en este punto de la serie:
src/agent.tspara el orquestador,src/agents/para los especialistas con susinstruction,description,outputKeyyoutputSchema.

