Testing, Code Review y Documentación en Proyectos de IA
Testing en IA: Un Desafío Diferente
El testing de aplicaciones de IA es fundamentalmente diferente al testing tradicional: los outputs son no determinísticos, las dependencias externas son costosas, y la "correctitud" es a menudo subjetiva.
Estrategia de Testing por Capas
┌─────────────────────────────────────────┐
│ E2E Tests (5%) │
│ Flujo completo: query → response │
├─────────────────────────────────────────┤
│ Integration Tests (20%) │
│ RAG pipeline, agent workflow │
├─────────────────────────────────────────┤
│ Unit Tests (75%) │
│ Chunking, formatting, parsing │
└─────────────────────────────────────────┘
Unit Tests para Componentes de IA
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
# ── Test de Chunking ─────────────────────────────────────
class TestChunking:
def test_chunk_respects_max_size(self):
text = "x " * 1000
chunks = chunk_text(text, max_size=500, overlap=50)
for chunk in chunks:
assert len(chunk) <= 500
def test_chunk_overlap(self):
text = "Sentence one. Sentence two. Sentence three. Sentence four."
chunks = chunk_text(text, max_size=30, overlap=10)
# Verificar solapamiento
for i in range(len(chunks) - 1):
overlap_text = chunks[i][-10:]
assert overlap_text in chunks[i + 1]
def test_empty_text(self):
chunks = chunk_text("", max_size=500)
assert chunks == []
def test_preserves_code_blocks(self):
text = "Intro\n```python\ndef hello():\n print('hi')\n```\nEnd"
chunks = chunk_text(text, max_size=50)
# El bloque de código no debe cortarse a la mitad
for chunk in chunks:
assert chunk.count("```") % 2 == 0 # Siempre pares
# ── Test de Prompt Template ──────────────────────────────
class TestPromptTemplate:
def test_template_includes_context(self):
prompt = build_rag_prompt("¿Qué es RAG?", ["doc1", "doc2"])
assert "doc1" in prompt
assert "doc2" in prompt
assert "¿Qué es RAG?" in prompt
def test_template_limits_context_length(self):
long_docs = ["x" * 10000] * 20
prompt = build_rag_prompt("query", long_docs, max_context_tokens=4000)
assert count_tokens(prompt) <= 5000 # context + prompt overhead
Mocking de LLMs y APIs
# ── Mock de OpenAI ───────────────────────────────────────
class MockLLMResponse:
def __init__(self, content):
self.choices = [MagicMock(message=MagicMock(content=content))]
self.usage = MagicMock(prompt_tokens=100, completion_tokens=50)
@pytest.fixture
def mock_openai():
with patch("openai.AsyncOpenAI") as mock:
client = AsyncMock()
client.chat.completions.create = AsyncMock(
return_value=MockLLMResponse("Mocked response")
)
mock.return_value = client
yield client
@pytest.mark.asyncio
async def test_rag_service_returns_answer(mock_openai, mock_vectorstore):
service = RAGService(llm_client=mock_openai, vectorstore=mock_vectorstore)
result = await service.answer("¿Qué es RAG?")
assert result["answer"] == "Mocked response"
assert result["tokens_used"] == 150
mock_openai.chat.completions.create.assert_called_once()
# ── Mock de Vector Store ─────────────────────────────────
@pytest.fixture
def mock_vectorstore():
store = MagicMock()
store.similarity_search.return_value = [
MagicMock(page_content="RAG es...", metadata={"source": "doc.md"}),
]
return store
Integration Tests para RAG Pipeline
@pytest.mark.integration
class TestRAGPipeline:
"""Tests que usan servicios reales (vector DB local)."""
@pytest.fixture(autouse=True)
def setup(self, tmp_path):
# Usar ChromaDB en memoria para tests
self.vectorstore = Chroma(
embedding_function=FakeEmbeddings(size=384),
persist_directory=str(tmp_path / "chroma"),
)
self.vectorstore.add_texts(
texts=["JWT se configura en auth.config", "Rate limit es 1000/min"],
metadatas=[{"source": "auth.md"}, {"source": "api.md"}],
)
def test_retriever_finds_relevant_docs(self):
results = self.vectorstore.similarity_search("configurar JWT", k=2)
assert len(results) > 0
assert "JWT" in results[0].page_content
def test_full_rag_chain(self):
chain = build_rag_chain(self.vectorstore, FakeLLM())
result = chain.invoke("¿Cómo configuro JWT?")
assert len(result) > 0
def test_no_results_returns_fallback(self):
chain = build_rag_chain(self.vectorstore, FakeLLM())
result = chain.invoke("¿Cuál es la receta de paella?")
assert "no tengo información" in result.lower()
Evaluation Tests (LLM-as-Judge)
@pytest.mark.evaluation
class TestRAGQuality:
"""Tests de calidad que usan LLM como juez."""
EVAL_CASES = [
{
"question": "¿Cómo autenticar con JWT?",
"expected_topics": ["JWT", "token", "configuración"],
"min_faithfulness": 0.8,
},
]
@pytest.mark.parametrize("case", EVAL_CASES)
def test_answer_relevancy(self, rag_service, case):
result = rag_service.answer(case["question"])
answer = result["answer"].lower()
# Al menos 2 de los topics esperados deben mencionarse
matches = sum(1 for t in case["expected_topics"] if t.lower() in answer)
assert matches >= 2, f"Expected topics {case['expected_topics']}, got: {answer[:200]}"
@pytest.mark.parametrize("case", EVAL_CASES)
def test_faithfulness(self, rag_service, judge_llm, case):
result = rag_service.answer(case["question"])
# LLM-as-judge evalúa si la respuesta es fiel al contexto
score = judge_llm.evaluate_faithfulness(
question=case["question"],
answer=result["answer"],
context=result["sources"],
)
assert score >= case["min_faithfulness"]
Code Review para Proyectos de IA
Checklist de Code Review
## Code Review Checklist — AI Systems
### Seguridad
- [ ] ¿Los prompts están protegidos contra injection?
- [ ] ¿Los API keys están en secrets manager (no hardcoded)?
- [ ] ¿Se validan los inputs del usuario antes de pasar al LLM?
- [ ] ¿Las queries SQL generadas por agentes son read-only?
### Calidad
- [ ] ¿Los prompts tienen instrucciones claras y específicas?
- [ ] ¿Hay guardrails para respuestas inapropiadas?
- [ ] ¿Se manejan los edge cases (sin contexto, timeout, rate limit)?
- [ ] ¿La evaluación cubre los principales casos de uso?
### Performance
- [ ] ¿Se cachean embeddings y respuestas frecuentes?
- [ ] ¿Se usa streaming para respuestas largas?
- [ ] ¿El batch size es óptimo para las APIs de embedding?
- [ ] ¿Hay timeouts configurados para llamadas a LLMs?
### Costos
- [ ] ¿Se usa el modelo más económico viable?
- [ ] ¿Hay límites de tokens en los prompts?
- [ ] ¿El contexto recuperado se limita a lo necesario?
- [ ] ¿Hay alertas de costo configuradas?
### Observabilidad
- [ ] ¿Se loggean queries, respuestas y métricas?
- [ ] ¿Hay tracing para el pipeline completo?
- [ ] ¿Las métricas de calidad se monitorean?
Documentación para Proyectos de IA
Documentar Prompts
class PromptRegistry:
"""Registro centralizado de prompts con versionado."""
RAG_SYSTEM_PROMPT = {
"version": "2.1",
"last_updated": "2026-03-15",
"author": "ai-team",
"description": "System prompt para el asistente RAG principal",
"template": """Eres un asistente técnico. Responde SOLO con información
del contexto proporcionado. Si no tienes información, di "No tengo datos suficientes".
Contexto: {context}
Pregunta: {question}""",
"variables": ["context", "question"],
"model": "gpt-4o",
"temperature": 0,
"eval_score": 0.92, # Último score de evaluación
"notes": "v2.1: Añadido 'di No tengo datos' para reducir alucinaciones",
}
ADR: Architecture Decision Records
# ADR-007: Usar Qdrant como Vector Database Principal
## Estado: Aceptado
## Contexto
Necesitamos una vector database para el pipeline RAG que soporte
1M+ documentos con latencia <50ms p99.
## Opciones Consideradas
1. **Pinecone** — Serverless, fácil, $$$
2. **Qdrant** — Self-hosted, rápido, $$
3. **pgvector** — Ya usamos Postgres, $
## Decisión
Qdrant self-hosted en ECS.
## Razones
- Latencia p99 ~20ms (pgvector ~100ms)
- Filtrado avanzado necesario para multi-tenant
- Costo 60% menor que Pinecone a nuestra escala
- pgvector no escala bien más allá de 500K vectores
## Consecuencias
- Necesitamos operar el cluster (backups, upgrades)
- Equipo debe aprender API de Qdrant
Resumen
Testing y documentación en proyectos de IA requieren:
- Testing por capas — unit (determinístico), integration, evaluation (LLM-judge)
- Mocking inteligente — Simular LLMs y vector DBs para tests rápidos
- Code review con checklist — Seguridad, calidad, costos, observabilidad
- Documentación de prompts — Versionado, scores, notas de cambio
- ADRs — Decisiones arquitectónicas documentadas
🧠 Preguntas de Repaso
1. Según la pirámide de testing para proyectos de IA, ¿cuál es la distribución recomendada?
- A) 33% unit, 33% integration, 33% E2E
- B) 75% unit tests (determinísticos), 20% integration tests (RAG pipeline), 5% E2E tests (flujo completo)
- C) 10% unit, 40% integration, 50% E2E
- D) 90% unit tests, 10% E2E
Respuesta: B) — La pirámide es: 75% unit tests (chunking, parsing, formatting — rápidos y determinísticos), 20% integration (RAG pipeline, agent workflow — con mocks de LLM), 5% E2E (flujo completo query → response — lentos y costosos).
2. ¿Por qué se usa FakeEmbeddings(size=384) con ChromaDB en memoria para integration tests?
- A) Para probar con embeddings reales pero más pequeños
- B) Para simular la vector database sin llamar a APIs de embedding costosas, haciendo los tests rápidos y determinísticos
- C) Porque ChromaDB solo acepta dimensiones de 384
- D) Para medir la calidad de los embeddings
Respuesta: B) — Usar FakeEmbeddings con ChromaDB en memoria permite ejecutar integration tests del pipeline RAG sin llamar a APIs externas (OpenAI, Cohere), haciéndolos rápidos, gratuitos, determinísticos y ejecutables en CI/CD.
3. En el code review checklist para IA, ¿qué verificación de seguridad es específica para agentes que generan SQL?
- A) Verificar que el SQL sea eficiente
- B) Asegurar que el SQL generado sea read-only (solo SELECT), bloqueando DROP, DELETE, TRUNCATE y ALTER
- C) Validar que el SQL use índices
- D) Confirmar que el SQL funcione en PostgreSQL y MySQL
Respuesta: B) — Los agentes que generan SQL deben estar restringidos a operaciones read-only (SELECT). Se debe bloquear explícitamente DROP, DELETE, TRUNCATE y ALTER para prevenir que el agente modifique o destruya datos de la base de datos.
4. ¿Qué es un ADR (Architecture Decision Record) y por qué es útil en proyectos de IA?
- A) Un log automático de errores del sistema
- B) Un documento que registra decisiones arquitectónicas con contexto, opciones evaluadas, pros/cons y la decisión final — útil para recordar por qué se eligió una tecnología
- C) Un tipo de test automatizado
- D) Un formato de configuración de deployment
Respuesta: B) — Un ADR documenta decisiones como "¿Por qué Qdrant y no Pinecone?" con contexto, opciones evaluadas, pros/cons, y la decisión final. Es invaluable en proyectos de IA donde las decisiones técnicas (vector DB, modelo, chunking) tienen impacto significativo.