← Todo el material
Curso Intermedio 23 de agosto de 2026

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

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 — objetos Mock / MagicMock que 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 LLMSíUnit test con entradas fijas
Routing / decisiones de flujoSí (dada la respuesta)Unit test + LLM mockeado
Manejo de errores y fallbacksSíUnit test forzando respuestas raras
Calidad de la respuesta del LLMNoEval (módulo 7), no unit test
¿Sigue instrucciones? ¿Alucina?NoEval (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

DeployTestingpytestAgentesMocks

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.