Testing, Debugging & Evaluating LLM Applications: Guía Práctica para Desarrolladores
Construir aplicaciones con LLMs es como manejar un auto con un conductor que a veces se distrae. Funciona, pero necesitas mecanismos de seguridad. En este post veremos estrategias concretas para testear, debuggear y evaluar aplicaciones LLM en producción.
El Problema: No Todos los Bugs Son Predecibles
A diferencia del software tradicional, los LLMs no fallan de forma determinística. Una consulta puede devolver la respuesta correcta 99 veces y fallar en la centésima. No es un error en tu código; es la naturaleza del modelo. Por eso necesitas un enfoque diferente.
Casos reales:
- Un RAG que inesperadamente devuelve documentos irrelevantes
- Un agente que entra en loops infinitos
- Function calling que falla silenciosamente con ciertos edge cases
- Respuestas que violan constrains críticos (seguridad, formato)
Estrategia 1: Testing con Datasets Representativos
No basta probar tu LLM con 3 prompts. Necesitas un dataset de evaluación robusto.
Cómo hacerlo:
# Crea un dataset de test cases
test_cases = [
{
"input": "¿Cuál es el precio del producto X?",
"expected_output_type": "price",
"constraints": ["debe ser un número", "debe incluir moneda"],
"edge_case": "producto no existe"
},
{
"input": "Cuéntame sobre el producto X de forma agresiva",
"constraints": ["no debe ser ofensivo", "debe ser profesional"],
"edge_case": "prompt injection attempt"
}
]
# Ejecuta con tu LLM
from langchain.evaluation import EvaluatorChain
evaluator = EvaluatorChain.from_llm_and_criteria(
llm=your_llm,
criteria="relevancia y exactitud"
)
results = []
for test in test_cases:
response = your_app.run(test["input"])
score = evaluator.evaluate_strings(
prediction=response,
input=test["input"],
reference=test.get("expected_output", "")
)
results.append(score)
print(f"Pass rate: {sum(r['score'] for r in results) / len(results)}")
Estrategia 2: Evaluación con Métricas Específicas
No todos los outputs se pueden evaluar con un sí/no. Necesitas métricas granulares.
Métricas prácticas:
- Similitud semántica: Embeddings para comparar si la respuesta es "similar" a la esperada (incluso si no es idéntica)
- Constraint satisfaction: Validar que la respuesta cumple reglas (JSON válido, longitud máxima, etc)
- Relevancia: Usar otro LLM para evaluar si la respuesta es relevante
- Hallucination detection: Verificar que el contenido viene de fuentes conocidas (para RAG)
- Latencia y costos: Monitorear trade-offs de calidad vs performance
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer
# Similitud semántica
model = SentenceTransformer('multilingual-MiniLM-L12-v2')
expected = "El producto cuesta 50 euros"
actual = "El precio es €50"
emb_expected = model.encode(expected)
emb_actual = model.encode(actual)
similarity = cosine_similarity([emb_expected], [emb_actual])[0][0]
print(f"Semantic similarity: {similarity:.2%}") # ~95%
# Constraint checking
import json
def is_valid_json(text):
try:
json.loads(text)
return True
except:
return False
def respects_length(text, max_words=100):
return len(text.split()) <= max_words
constraints = [
("json_valid", is_valid_json(response)),
("max_length", respects_length(response)),
("contains_required_fields", all(f in response for f in ["id", "price"]))
]
print(f"Constraint pass rate: {sum(c[1] for c in constraints) / len(constraints):.0%}")
Estrategia 3: Debugging Outputs Inesperados
Cuando un LLM falla, ¿cómo debuggeas si no hay un stack trace?
Enfoque sistemático:
import logging
from typing import Any
# Logging estructurado para debugging
def log_llm_call(prompt: str, response: str, metadata: dict = {}):
log_entry = {
"timestamp": datetime.now().isoformat(),
"prompt_tokens": len(prompt.split()),
"response_tokens": len(response.split()),
"response_length": len(response),
"model_parameters": metadata.get("temperature"),
"raw_response": response,
"metadata": metadata
}
logger.info(json.dumps(log_entry))
return log_entry
# Análisis post-mortem
bad_response = "..."
debug_queries = [
("¿Qué modelo se usó?", metadata["model"]),
("¿Cuál fue el prompt exacto?", prompt),
("¿Cuál fue la temperatura?", metadata["temperature"]),
("¿Qué tokens generó primero?", log_entry.get("first_tokens")),
("¿Hay truncado el contexto?", len(response) == max_tokens),
]
# Comparar con casos exitosos
successful_similar_cases = search_logs(
constraint="topic:similar_to(current_prompt)",
filter="response.quality > 0.9"
)
print(f"Found {len(successful_similar_cases)} similar successful cases")
print(f"Difference in parameters: {diff_parameters(current, successful_similar_cases[0])}")
Estrategia 4: Monitoreo en Producción
Una vez en producción, necesitas alertas en tiempo real.
# Monitoreo de calidad con umbral dinámico
from prometheus_client import Counter, Histogram, Gauge
response_quality = Gauge('llm_response_quality', 'Quality score 0-1')
error_count = Counter('llm_errors_total', 'Total LLM errors')
latency = Histogram('llm_latency_seconds', 'LLM response latency')
cost_per_request = Histogram('llm_cost_cents', 'Cost in cents per request')
@track_metrics
def call_llm_with_monitoring(prompt):
start = time.time()
try:
response = llm.predict(prompt)
# Evaluación automática
quality_score = evaluate_response_quality(response)
response_quality.set(quality_score)
# Alerta si cae por debajo del threshold
if quality_score < 0.7:
alert("Low quality response", {
"prompt": prompt[:100],
"score": quality_score
})
# Track costos
tokens = estimate_tokens(response)
cost = tokens * COST_PER_1K_TOKENS / 1000
cost_per_request.observe(cost * 100)
except Exception as e:
error_count.inc()
raise
finally:
latency.observe(time.time() - start)
return response
# Establece alerts basados en SLO
# - Quality score < 70%: investigar
# - Latency > 5s: escalar
# - Cost > $5 per request: revisar prompt
Estrategia 5: Fallback y Recovery
Incluso con buen testing, a veces las cosas fallan. Necesitas un plan B.
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
def call_llm_with_fallbacks(prompt, user_context):
try:
# Intento 1: llamada normal
response = llm.predict(prompt)
# Valida
if not validate_response(response):
raise ValueError("Response validation failed")
return response
except Exception as e:
logger.warning(f"Primary LLM call failed: {e}")
# Fallback 1: reintenta con temperatura más baja (menos "creativa")
if "hallucination" in str(e).lower():
response = llm.predict(prompt, temperature=0.1)
return response
# Fallback 2: reintenta con modelo más pequeño pero confiable
if "timeout" in str(e).lower():
response = smaller_but_faster_llm.predict(prompt)
return response
# Fallback 3: respuesta cacheada de caso similar
cached = find_cached_similar_response(prompt, user_context)
if cached:
logger.info("Using cached response")
return cached
# Fallback 4: respuesta by-default estructurada
return generate_default_response(user_context)
Herramientas Recomendadas
- LangChain Evaluation: Framework integrado para evaluar chains
- Braintrust: Platform especializada en eval de LLMs con colaboración de equipos
- LLM as a Judge: Usa otro LLM para evaluar respuestas (simple pero poderoso)
- Prometheus + Grafana: Monitoreo de métricas en producción
- OpenTelemetry: Tracing distribuido para entender dónde fallan las cosas
- Weights & Biases: Logging y análisis de experimentos
Checklist para Pasar a Producción
Antes de deployar una aplicación LLM:
- [ ] Dataset de evaluación de >100 casos representativos
- [ ] Métrica de calidad definida (no solo "se ve bien")
- [ ] Pass rate >90% en evaluación
- [ ] Monitoreo en tiempo real configurado
- [ ] Fallbacks implementados para casos de fallo
- [ ] Latencia y costos dentro de SLOs
- [ ] Error handling para edge cases
- [ ] Logging estructurado para debugging
- [ ] Plan de rollback si algo falla
Conclusión
Los LLMs son herramientas poderosas pero impredecibles. Testing y evaluación rigurosos no son opcionales; son la diferencia entre un prototipo bonito y una aplicación en la que confías con datos reales. El esfuerzo inicial se paga en tranquilidad en producción.
La clave: mide antes de ir a producción, monitorea siempre en producción.