View in English
Callbacks y guardrails para agentes confiables
Oct 16, 2025
Updated: Jun 25, 2026

Callbacks y guardrails para agentes confiables

Notebook de la lección: Lesson_4.ipynb

Los agentes son potentes y, a la vez, no deterministas. En esta lección añadimos control programático para que tu agente se comporte de forma predecible: afinamos instrucciones y usamos callbacks como guardrails que filtran dominios, enriquecen respuestas y dejan trazabilidad. Al final tendrás un agente listo para producción que combina todo lo previo con controles efectivos.

Versiones que usamos aquí. Esta lección asume google-adk 1.0 o superior y la familia de modelos Gemini 2.5 (gemini-2.5-flash). Dos detalles importan para que el código corra tal cual: pasar una lista a before_tool_callback y after_tool_callback requiere una versión reciente de ADK, y google_search como herramienta integrada requiere un modelo Gemini 2 o superior. Si vienes de instalaciones más antiguas, actualiza con pip install -U google-adk.

Panorama general

  • Reutilizar tools previas: get_financial_context y save_news_to_markdown.
  • Entender los puntos de extensión: before/after agent, tool y model.
  • Implementar dos callbacks: filtrado de fuentes (before tool) y enriquecimiento de respuesta (after tool).
  • Actualizar instrucciones para que el agente sea consciente de los callbacks.
  • Probar en adk web, validar logs de proceso y cerrar servicios.

4.1 Preparación del entorno

A diferencia de las lecciones de voz, este agente es síncrono y orquesta herramientas de texto, así que no necesitamos un modelo Live. Usaremos un modelo Flash estándar, que es la opción recomendada para flujos de texto y llamadas a tools.

adk create app5 --model gemini-2.5-flash --api_key $GOOGLE_API_KEY

Live solo cuando hace falta. Los modelos *-live-* están optimizados para streaming bidireccional de audio. En las lecciones de voz tenían sentido; aquí, para un agente de texto que llama herramientas, gemini-2.5-flash es más adecuado y económico. Reserva el modelo Live para cuando realmente uses la Live API.

Credenciales: define un .env en app5/ (AI Studio: GOOGLE_API_KEY; Vertex AI: GOOGLE_GENAI_USE_VERTEXAI=TRUE, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION). Si usas load_env() en notebooks, trátalo como helper; al ejecutar con adk web, ADK leerá el .env por ti.

4.2 Reutiliza herramientas de lecciones anteriores

Copiamos get_financial_context (lecciones 2 y 3) y save_news_to_markdown (lección 3) a app5/agent.py.

import pathlib
from typing import Dict

def save_news_to_markdown(filename: str, content: str) -> Dict[str, str]:
    if not filename.endswith(".md"):
        filename += ".md"
    file_path = pathlib.Path.cwd() / filename
    file_path.write_text(content, encoding="utf-8")
    return {"status": "success", "message": f"Saved to {file_path.resolve()}"}

4.3 Callbacks en ADK

ADK ofrece hooks en varios puntos del ciclo de vida:

  • before_agent_callback / after_agent_callback
  • before_tool_callback / after_tool_callback
  • before_model_callback / after_model_callback

### Callback 1: filtrado de fuentes (before tool)

Bloquea búsquedas hacia dominios no deseados y responde con errores descriptivos.

BLOCKED_DOMAINS = [
    "wikipedia.org",
    "reddit.com",
    "youtube.com",
    "medium.com",
    "investopedia.com",
    "quora.com",
]

def filter_news_sources_callback(tool, args, tool_context):
    if tool.name == "google_search":
        query = args.get("query", "").lower()
        for domain in BLOCKED_DOMAINS:
            if f"site:{domain}" in query or domain.split(".")[0] in query:
                return {
                    "error": "blocked_source",
                    "reason": f"Searches targeting {domain} are not allowed. Use professional news sources.",
                }
    return None

### Callback 2: enriquecimiento de respuesta (after tool)

Extrae dominios de los resultados, mantiene un process_log en tool_context.state y lo inyecta en la salida. Fíjate en los imports al inicio del bloque: necesitamos re y urlparse, que faltaban en la versión original.

import re
from urllib.parse import urlparse

from google.adk.tools import ToolContext

def initialize_process_log(tool_context: ToolContext):
    if "process_log" not in tool_context.state:
        tool_context.state["process_log"] = []

def inject_process_log_after_search(tool, args, tool_context, tool_response):
    if tool.name != "google_search":
        return tool_response

    # Normaliza la respuesta (str o dict)
    if isinstance(tool_response, dict):
        raw = tool_response.get("search_results") or tool_response.get("results") or ""
    else:
        raw = tool_response

    if isinstance(raw, str) and raw:
        urls = re.findall(r"https?://[^\s/]+", raw)
        unique_domains = sorted(list({urlparse(url).netloc for url in urls}))
        if unique_domains:
            sourcing_log = f"Action: Sourced news from: {', '.join(unique_domains)}."
            tool_context.state["process_log"] = [sourcing_log] + tool_context.state.get("process_log", [])

    # Devuelve la estructura enriquecida
    return {
        "search_results": raw if isinstance(raw, str) else str(raw),
        "process_log": tool_context.state.get("process_log", []),
    }

Para que initialize_process_log no quede como código muerto, lo conectamos en before_agent_callback. Así el estado queda creado antes de la primera búsqueda y el after tool callback solo lo enriquece.

4.4 Modifica el agente para usar callbacks

Aquí aparece un detalle importante de diseño. ADK restringe combinar una herramienta integrada como google_search con tools personalizadas en el mismo LlmAgent. Si las pones todas juntas en tools=[...], lo más probable es que obtengas un error al construir el agente.

El patrón recomendado: aislar `google_search` en un sub agente. Envolvemos google_search en su propio LlmAgent y lo exponemos al agente principal mediante AgentTool. Así el coordinador sigue teniendo sus tools personalizadas y delega la búsqueda al sub agente. Los callbacks de búsqueda viven en el sub agente, que es quien realmente invoca google_search.

AgentTool han cambiado entre versiones de ADK. Antes de publicar, confirma contra la documentación actual de ADK si tu versión ya permite mezclarlas directamente; de ser así, podrías simplificar y prescindir del sub agente.

from google.adk.agents import LlmAgent
from google.adk.tools import google_search
from google.adk.tools.agent_tool import AgentTool

# Sub agente dedicado a la búsqueda: aquí viven los callbacks de google_search.
search_agent = LlmAgent(
    name="news_search_agent",
    model="gemini-2.5-flash",
    tools=[google_search],
    instruction="Busca noticias recientes de IA usando google_search y devuelve los resultados.",
    before_tool_callback=[filter_news_sources_callback],
    after_tool_callback=[inject_process_log_after_search],
)

root_agent = LlmAgent(
    name="ai_news_research_coordinator",
    model="gemini-2.5-flash",
    tools=[
        AgentTool(agent=search_agent),
        get_financial_context,
        save_news_to_markdown,
    ],
    before_agent_callback=[lambda ctx: initialize_process_log(ctx)],
    instruction="""
    Tu propósito exclusivo es preparar un reporte de noticias de IA con contexto financiero.

    Plan de ejecución:
    1) Buscar 5 noticias recientes a través del sub agente de búsqueda.
    2) Extraer tickers y llamar `get_financial_context`.
    3) Formatear todo en un Markdown siguiendo el esquema requerido.
    4) Guardar en `ai_research_report.md` con `save_news_to_markdown`.

    Comprensión de callbacks y salidas modificadas:
    - La búsqueda puede retornar { search_results: str, process_log: list[str] }.
    - Usa `process_log` para incluir fuentes y acciones en el reporte final.

    Regla operativa crucial: no muestres contenido intermedio. Solo confirma el inicio y la entrega final.
    """,
)

Fíjate en que pasamos los callbacks como lista. Esa forma de lista es una característica reciente de ADK; si tu instalación es antigua y solo acepta un único callable, actualiza ADK o pasa la función directamente sin la lista.

4.5 Pruebas de punta a punta

Levanta la UI desde la carpeta padre con adk web y selecciona "app5", o apunta directo a la carpeta:

adk web --port 8000 app5

En Windows, usa --no-reload si es necesario. Detén el servidor con Ctrl-C.

Prueba con un mensaje como "Encuentra las últimas noticias de IA". El agente confirma el inicio, trabaja en silencio y termina con un mensaje final que confirma ai_research_report.md. Cierra los procesos al terminar.

4.6 Visualiza el reporte

Verifica que el Markdown incluya titulares, tickers, métrica financiera y un process_log con los dominios fuente.

from IPython.display import Markdown, display

with open("ai_research_report.md", "r", encoding="utf-8") as f:
    display(Markdown(f.read()))

Buenas prácticas y próximos pasos

  • Documenta tus políticas: qué dominios se bloquean y por qué, y prueba escenarios límite.
  • Incluye el process_log en los artefactos finales para tener trazabilidad.
  • Centraliza los callbacks comunes (observabilidad, validación de entrada y salida, mitigación de prompt injection). Cuando una política debe aplicarse de forma global a todos los agentes, evalúa moverla a un Plugin de ADK en lugar de repetir callbacks.
  • Si apuntas a producción, evalúa mover el almacenamiento del reporte a un bucket o a una base de datos.

Nota sobre Google Search. La herramienta google_search funciona con modelos Gemini 2 o superiores. Si el modelo devuelve "Search suggestions", muéstralas en tu UI, ya que es parte de la política de Grounding. Más información en la documentación de built in tools de ADK.

>

google_search se mueve rápido. Confirma para Gemini 2.5 en la documentación oficial antes de publicar.

Tu agente ahora combina investigación silenciosa con guardrails programáticos y trazabilidad. A partir de aquí puedes escalar a sistemas multi agente y respuestas aún más estructuradas.

Ejercicios propuestos

  1. Añade un tercer dominio a BLOCKED_DOMAINS y comprueba en adk web que el filtro devuelve el error descriptivo cuando la consulta lo apunta.
  2. Convierte initialize_process_log para que registre también la hora de inicio del proceso, y verifica que aparece en el process_log final.
  3. Mueve filter_news_sources_callback a un Plugin global de ADK y compara la experiencia frente a registrarlo callback por callback.

Resumen en 3 puntos

  1. Los callbacks before/after de ADK son tus guardrails programáticos: filtran fuentes, enriquecen respuestas y dejan trazabilidad sin tocar la lógica del modelo.
  2. Para combinar google_search con tools personalizadas, aísla la búsqueda en un sub agente y exponlo con AgentTool; usa gemini-2.5-flash para este flujo de texto y deja los modelos Live para voz.
  3. Pasar los callbacks como lista y mantener un process_log en tool_context.state te da observabilidad lista para producción; centraliza políticas globales en Plugins.

Recursos

Este contenido se basa en el curso "Building Live Voice Agents with Google's ADK!" de DeepLearning.AI (disponible en learn.deeplearning.ai). Este blog busca acercar el material de ADK al español.

Espero que este post te sea de utilidad y lo puedas aplicar a algún proyecto que tengas en mente. Déjame un comentario si te sirvió, si quieres añadir alguna opinión o si tienes alguna duda, y recuerda que si te gustó también puedes compartirlo usando los links a las redes sociales aquí abajo. Buena suerte construyendo agentes confiables.

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias