Deploy de Agentes — Módulo 6: testing de agentes I — unit tests y mockear el LLM
Sexto módulo del Curso de Deploy de Agentes: el LLM es no-determinista, cuesta plata y latencia, así que no se testea directo. La estrategia: cubrir toda la lógica determinista (tools, parsing, routing) con unit tests y mockear las llamadas al modelo. Ejemplos reales con pytest, monkeypatch y fixtures.
☰ Índice del contenido
- La tesis del módulo
- Por qué el LLM rompe el testing tradicional
- Mockear: reemplazar el LLM por un doble controlado
- Testear una tool: lo más fácil y lo más rentable
- Testear el routing con el LLM mockeado
- La misma idea con monkeypatch
- Qué testear con cada herramienta
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
En el módulo anterior empaquetamos el agente en un contenedor reproducible: si corre en la imagen, corre en producción. Pero “corre” no es lo mismo que “funciona bien”. Antes de construir esa imagen queremos una red de seguridad que grite cuando algo se rompe. Esa red son los tests. Y aquí aparece el problema que hace que testear agentes sea distinto a testear cualquier otro software: en el centro hay un LLM, y un LLM no se porta como una función normal.
La tesis del módulo
No testees contra el LLM: testea alrededor de él. El modelo es no-determinista, cuesta plata y suma latencia. Toda la lógica determinista — tus tools, tu parsing, tu routing — sí se testea con unit tests rápidos y gratis, reemplazando el LLM por un doble controlado. El modelo en sí se mide aparte (módulo 7).
Por qué el LLM rompe el testing tradicional
Un unit test (prueba unitaria: verifica una pieza chica de código de forma aislada) se apoya en una promesa: dada la misma entrada, la misma salida. Con esa promesa puedes escribir assert resultado == esperado y confiar. El LLM rompe esa promesa por tres lados:
- Es no-determinista — significa que la misma pregunta puede devolver respuestas distintas cada vez.
assert respuesta == "texto exacto"fallará tarde o temprano aunque el agente esté perfecto. - Cuesta plata — cada llamada a la API se paga por token. Una suite de 500 tests que llama al modelo de verdad es una factura mensual.
- Suma latencia — cada llamada tarda cientos de milisegundos o segundos. Una suite que debería correr en dos segundos pasa a tardar diez minutos, y nadie la corre.
La conclusión no es “no se puede testear un agente”. Es: separa lo determinista de lo probabilístico y testea cada parte con la herramienta correcta.
Tu agente, por dentro
┌───────────────────────────────────────────┐
│ │
│ parsing ──► routing ──► tools │
│ (determinista, se testea con asserts) │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ LLM │ no-determinista │
│ │ │ (se MOCKEA en unit tests, │
│ └─────────┘ se mide con evals) │
│ │
└───────────────────────────────────────────┘
Casi todo tu agente es código normal de Python que sí cumple la promesa del test. La única pieza rara es la caja del LLM. Si logras aislarla, testeas el resto con tranquilidad.
Mockear: reemplazar el LLM por un doble controlado
Mockear es reemplazar una pieza real (la llamada al LLM) por un doble controlado: un objeto falso que devuelve exactamente la respuesta que tú decidas, sin llamar a ninguna API. En un test le dices “cuando te pregunten, responde esto”, y así pruebas cómo reacciona el resto de tu código a esa respuesta — sin plata, sin latencia, sin sorpresas.
Python trae dos formas de hacerlo de fábrica, ambas parte de pytest + la librería estándar:
monkeypatch— una fixture de pytest que reemplaza temporalmente un atributo o función durante el test y lo restaura solo al terminar.unittest.mock— objetosMock/MagicMockque simulan cualquier interfaz y registran cómo fueron llamados.
Testear una tool: lo más fácil y lo más rentable
Las tools (herramientas: las funciones que el agente puede ejecutar — buscar, calcular, consultar una API — que vimos en tools y el patrón ReAct) son código determinista puro. No tienen LLM adentro. Se testean como cualquier función, y son donde más bugs se esconden.
# src/tools.py — una tool determinista de ejemplo
def aplicar_descuento(precio: float, porcentaje: float) -> float:
"""Aplica un descuento y redondea a 2 decimales."""
if not 0 <= porcentaje <= 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
return round(precio * (1 - porcentaje / 100), 2)
# tests/test_tools.py
import pytest
from src.tools import aplicar_descuento
def test_descuento_normal():
# Caso feliz: 20% sobre 100 = 80
assert aplicar_descuento(100, 20) == 80.0
def test_descuento_cero():
# Borde: 0% no cambia el precio
assert aplicar_descuento(50, 0) == 50.0
def test_descuento_invalido_lanza_error():
# La tool debe rechazar un porcentaje fuera de rango.
# pytest.raises verifica que SÍ se lance la excepción esperada.
with pytest.raises(ValueError):
aplicar_descuento(100, 150)
Ningún LLM involucrado: entradas controladas, salidas exactas, assert. Rápido y gratis. Esto es la mayor parte de la seguridad de tu agente por una fracción del esfuerzo.
Testear el routing con el LLM mockeado
Ahora la parte interesante: una función que decide qué hacer según lo que responde el modelo. Queremos testear nuestra lógica de decisión, no el modelo. Entonces mockeamos el cliente del LLM con una fixture que inyecta respuestas fijas.
# src/router.py
def elegir_ruta(cliente_llm, mensaje: str) -> str:
"""Le pregunta al LLM si el mensaje necesita una tool o es charla.
Devuelve 'tool' o 'chat' según la clasificación."""
respuesta = cliente_llm.clasificar(mensaje) # devuelve "tool" o "chat"
etiqueta = respuesta.strip().lower()
return etiqueta if etiqueta in ("tool", "chat") else "chat" # fallback seguro
# tests/test_router.py
import pytest
from src.router import elegir_ruta
class FakeLLM:
"""Doble controlado: devuelve la respuesta que le pasemos,
sin llamar a ninguna API. Registra qué mensaje recibió."""
def __init__(self, respuesta_fija: str):
self.respuesta_fija = respuesta_fija
self.ultimo_mensaje = None
def clasificar(self, mensaje: str) -> str:
self.ultimo_mensaje = mensaje
return self.respuesta_fija
@pytest.fixture
def llm_dice_tool():
# Fixture: arma el doble y lo entrega a los tests que lo pidan.
return FakeLLM(respuesta_fija="tool")
def test_routing_hacia_tool(llm_dice_tool):
# Si el LLM clasifica "tool", nuestra lógica debe rutear a "tool".
assert elegir_ruta(llm_dice_tool, "convierte 100 USD a CLP") == "tool"
def test_fallback_cuando_llm_devuelve_basura():
# El modelo devolvió algo inesperado: probamos NUESTRO fallback.
llm_raro = FakeLLM(respuesta_fija="no sé qué decir")
assert elegir_ruta(llm_raro, "hola") == "chat"
Fíjate en el segundo test: verificamos qué hace nuestro código cuando el LLM devuelve una respuesta rara. Eso es exactamente lo que no podrías probar de forma confiable llamando al modelo real, y es donde viven los bugs de producción.
La misma idea con monkeypatch
Cuando el cliente del LLM no se inyecta como parámetro sino que está incrustado dentro de una función, monkeypatch lo reemplaza en el momento.
# tests/test_agente.py
from src import agente
def test_agente_usa_respuesta_del_llm(monkeypatch):
# Reemplazamos la función que llama a la API por una que
# devuelve una respuesta fija. monkeypatch la restaura sola al final.
def fake_llamar_llm(prompt: str) -> str:
return "Respuesta simulada del modelo"
monkeypatch.setattr(agente, "llamar_llm", fake_llamar_llm)
salida = agente.responder("cualquier cosa")
assert "Respuesta simulada" in salida
Qué testear con cada herramienta
| Parte del agente | ¿Determinista? | Cómo se testea |
|---|---|---|
| Tools (calcular, buscar, formatear) | Sí | Unit test con assert exacto |
| Parsing de la salida del LLM | Sí | Unit test con entradas fijas |
| Routing / decisiones de flujo | Sí (dada la respuesta) | Unit test + LLM mockeado |
| Manejo de errores y fallbacks | Sí | Unit test forzando respuestas raras |
| Calidad de la respuesta del LLM | No | Eval (módulo 7), no unit test |
| ¿Sigue instrucciones? ¿Alucina? | No | Eval (módulo 7) |
Las dos últimas filas son la frontera. No las fuerces dentro de un assert exacto: ahí no sirve el unit test, sirve el eval, y ese es el tema del próximo módulo.
Ojo: mockear demasiado también engaña. Si mockeas la tool y el LLM y el parsing, tu test verifica que tus mocks concuerdan entre sí, no que el sistema funciona. Regla: mockea solo lo caro y no-determinista (el LLM y las APIs externas). El resto de tu propio código pruébalo de verdad.
Errores comunes
- Testear contra el LLM real “para que sea más realista”. Lento, caro y flaky (falla al azar sin que cambie tu código). El realismo del modelo se mide en los evals, no en la suite unitaria.
assert respuesta == "texto exacto del modelo". Como el LLM es no-determinista, ese test pasa hoy y falla mañana sin que nada se rompa. Ruido puro.- No testear los fallbacks. El bug de producción típico es “el modelo devolvió algo raro y el código explotó”. Mockear respuestas raras es la única forma barata de cubrir eso.
- Sobre-mockear hasta que el test no prueba nada. Si mockeas cada pieza, solo verificas que tus mocks son consistentes. Mockea lo caro; ejercita tu lógica de verdad.
- Olvidar los casos borde de las tools. Cero, negativo, vacío, fuera de rango. Ahí se esconde buena parte de los bugs, y son triviales de testear.
La frase para llevarse
“El LLM no se testea, se aísla. Todo lo determinista alrededor de él sí se testea — y resulta que eso es casi todo tu agente.”
Qué viene
Módulo 7: testing de agentes II — evals como tests que bloquean el deploy. Lo no-determinista — la calidad de la respuesta del modelo — sí hay que medirlo, pero con otra herramienta. Veremos golden datasets, LLM-as-judge y cómo convertir un eval en un gate que frena el deploy si la calidad baja.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.