Módulo 6: Code Quality Patterns para AI
8. Resumen y Troubleshooting del Módulo 6
Descripción
Este módulo transformó código funcional pero monolítico en una arquitectura mantenible de 4 capas. Esta cápsula consolida todos los patterns aprendidos, los errores más comunes del refactoring, y el checklist completo para verificar que la arquitectura es correcta.
Lo que construiste en este módulo
ANTES (código monolítico funcional):
src/app/main.py (300 líneas)
├── Prompts hardcodeados como strings en el módulo
├── from openai import OpenAI directamente en el endpoint
├── json.loads() inline mezclado con la lógica
├── os.getenv() disperso por el código
├── God function: hace guadrails + LLM + parse + validate + log
└── Tests requieren patch("openai.chat.completions.create")
DESPUÉS (clean architecture de 4 capas):
prompts/sentiment/v1.yaml ← Template YAML versionable
src/config.py ← pydantic-settings type-safe
src/domain/
└── sentiment_service.py ← 20 líneas, solo orquestación
src/infrastructure/
├── llm_provider.py ← Protocol LLMProvider
└── openai_provider.py ← Implementación desacoplada
src/processing/
└── sentiment_parser.py ← Parser testeable independientemente
src/app/
├── main.py ← App factory
├── dependencies.py ← Wiring de DI
└── routers/sentiment.py ← Endpoint limpio con Depends()
Tests:
test_sentiment_service.py ← MockProvider, sin patch()
test_sentiment_parser.py ← Parser en aislamiento
test_config.py ← Validaciones de producción
Mapa de decisiones: ¿dónde va cada cosa?
¿Dónde va este código?
│
├─ ¿Es el texto/contenido que se le dice al LLM?
│ └─ PROMPT TEMPLATES: prompts/*.yaml
│ Ejemplo: "Analiza el sentimiento de: {text}"
│
├─ ¿Es la regla de negocio? ("confidence mínima es 0.3")
│ └─ DOMAIN: src/domain/
│ Ejemplo: analyze_sentiment(), LowConfidenceError
│
├─ ¿Es una llamada a una API externa (OpenAI, Anthropic)?
│ └─ INFRASTRUCTURE: src/infrastructure/
│ Ejemplo: OpenAIProvider.complete(), request HTTP
│
├─ ¿Es transformar texto del LLM en un tipo Python?
│ └─ PROCESSING: src/processing/
│ Ejemplo: parse_sentiment_output(), SentimentOutput
│
├─ ¿Es config del sistema (model, temperature, API key)?
│ └─ CONFIG: src/config.py
│ Ejemplo: Settings.model, Settings.temperature
│
└─ ¿Es el punto de entrada HTTP?
└─ APP: src/app/
Ejemplo: FastAPI endpoints, middleware, Depends()
Los 6 anti-patterns eliminados y cómo detectarlos
Anti-pattern 1: Prompt hardcodeado en código
# ❌ Detectar:
grep -r "f\"Analyze" src/
# Si aparece en un .py que no sea loader.py, es un anti-pattern
# ❌ Código problemático:
PROMPT = f"Analyze sentiment: {text}"
# ✅ Solución:
# prompts/sentiment/v1.yaml
# template: "Analyze sentiment: {text}"
# Código: load_prompt("sentiment/v1").render(text=text)
Anti-pattern 2: Import de OpenAI en domain
# ❌ Detectar:
grep -r "from openai" src/domain/
# Si aparece algo, es un anti-pattern
# ❌ Código problemático:
# src/domain/sentiment_service.py
from openai import OpenAI # ← Domain conoce OpenAI
# ✅ Solución:
# src/domain/sentiment_service.py
from src.infrastructure.llm_provider import LLMProvider # Solo el Protocol
Anti-pattern 3: Config con os.getenv disperso
# ❌ Detectar:
grep -r "os.getenv" src/ --include="*.py"
# Si aparece fuera de config.py, es un anti-pattern
# ❌ Código problemático:
MODEL = os.getenv("MODEL", "gpt-4o-mini") # En models.py
TEMP = float(os.getenv("TEMPERATURE", "0.7")) # En utils.py
# ✅ Solución:
settings = get_settings()
settings.model, settings.temperature
Anti-pattern 4: json.loads() en domain
# ❌ Detectar: parsing de JSON en domain
# src/domain/sentiment_service.py
data = json.loads(response) # ← Processing concern en domain
# ✅ Solución:
result = parse_sentiment_output(response) # Delegar al parser
Anti-pattern 5: patch() de OpenAI en tests de unit
# ❌ Tests frágiles:
@patch("openai.chat.completions.create")
def test_analyze(mock_create):
mock_create.return_value = MagicMock(...) # 10 líneas de setup
# ✅ Tests con DI:
def test_analyze():
mock = MockProvider('{"sentiment": "positive", "score": 0.8, "confidence": 0.9}')
result = analyze_sentiment("Great!", mock)
assert result["sentiment"] == "positive" # 3 líneas totales
Anti-pattern 6: God function sin separación
# ❌ Detectar: una función con múltiples responsabilidades
def analyze(text: str) -> dict:
# guardrail: if len(text) > 5000...
# prompt construction: prompt = f"..."
# LLM call: client.chat.completions.create(...)
# JSON parse: json.loads(raw)
# validation: if score > 1.0: score = 1.0
# logging: print(f"Done: {result}")
return result
# ✅ Solución: cada responsabilidad en su capa
# La función domain solo orquesta
def analyze_sentiment(text: str, provider: LLMProvider) -> dict:
messages = [{"role": "system", ...}, {"role": "user", ...}]
raw = provider.complete(messages)
return parse_sentiment_output(raw) # 3 líneas claras
Los 5 errores más comunes del refactoring
Error 1: Mover código antes de tener tests
Síntoma: Después de mover código, algo falla pero no sabes dónde.
Causa: No tenías tests de la función antes de refactorizar.
Diagnóstico:
- ¿Tienes tests que cubren la función que vas a mover?
- pytest tests/ → ¿cuántos pasan ANTES del cambio?
Fix:
1. PRIMERO: añadir tests de la función actual (en su estado original)
2. LUEGO: mover el código
3. Correr tests → deben seguir pasando
Error 2: Violar la regla de dependencia
# Síntoma: tests del domain requieren API key de OpenAI
# Causa: domain importa de infrastructure
# src/domain/service.py
from src.infrastructure.openai_provider import OpenAIProvider # ← INCORRECTO
# Diagnóstico:
grep -r "from src.infrastructure" src/domain/
# Fix: domain solo debe importar el Protocol
from src.infrastructure.llm_provider import LLMProvider # Solo el Protocol
def analyze(text: str, provider: LLMProvider) -> dict: ... # DI
Error 3: Settings no se cargan con los valores correctos
# Síntoma: los tests leen el .env del proyecto en lugar de la config de test
# Causa: lru_cache guarda la config del primer get_settings()
settings_1 = get_settings() # Lee .env
settings_2 = get_settings() # Retorna el mismo objeto cacheado
# Fix: limpiar el cache antes de cada test
@pytest.fixture(autouse=True)
def clear_settings_cache():
get_settings.cache_clear()
yield
get_settings.cache_clear()
Error 4: load_prompt falla en tests porque no encuentra el archivo
# Síntoma: FileNotFoundError: Prompt not found: .../prompts/sentiment/v1.yaml
# Causa: el path relativo en load_prompt() es relativo al directorio de trabajo,
# que puede ser diferente cuando corres pytest desde diferentes directorios
# Fix: usar Path(__file__) en lugar de paths relativos
PROMPTS_DIR = Path(__file__).parent.parent.parent / "prompts"
# Este path es absoluto y funciona desde cualquier directorio de trabajo
# Fix alternativo: en pytest.ini
[pytest]
testpaths = tests
rootdir = . # Asegura que el directorio raíz es correcto
Error 5: FallbackProvider no funciona porque los errores son del tipo incorrecto
# Síntoma: FallbackProvider no captura el error y no hace fallback
# Causa: el error lanzado no es LLMProviderError sino el error original
# (openai.RateLimitError, httpx.TimeoutException, etc.)
# Fix en OpenAIProvider: siempre envolver en LLMProviderError
try:
response = self._client.chat.completions.create(...)
return response.choices[0].message.content
except Exception as e:
raise LLMProviderError(str(e), original_error=e) # ← SIEMPRE wrap
Checklist de producción del módulo 6
ARQUITECTURA
[ ] src/domain/ no importa de src/infrastructure/ (solo el Protocol)
[ ] src/domain/ no importa de openai, anthropic, ni httpx
[ ] src/processing/ no importa de src/infrastructure/
[ ] Los prompts están en prompts/*.yaml, no hardcodeados en .py
CONFIG
[ ] Toda la config va a través de get_settings()
[ ] No hay os.getenv() fuera de config.py y startup.py
[ ] openai_api_key usa SecretStr
[ ] model_validator rechaza use_mock_llm=True en producción
[ ] model_validator rechaza log_level=DEBUG en producción
[ ] .env.example documentado con todas las variables
DEPENDENCY INJECTION
[ ] Los endpoints usan Depends(get_llm_provider)
[ ] Domain functions reciben LLMProvider como argumento
[ ] get_llm_provider() retorna MockProvider si use_mock_llm=True
[ ] FallbackProvider disponible si se necesita alta disponibilidad
TESTS
[ ] Unit tests del domain usan MockProvider, no patch("openai...")
[ ] Unit tests del parser son independientes del provider
[ ] Tests de config verifican validaciones de producción
[ ] pytest tests/ → todos pasan después del refactoring
VERIFICACIÓN RÁPIDA
[ ] grep -r "from openai" src/domain/ → sin resultados
[ ] grep -r "os.getenv" src/ (excepto config.py) → sin resultados
[ ] grep -r "f\".*{text}" src/domain/ → sin resultados (prompts en YAML)
[ ] pytest tests/ → todos pasan
Vocabulario del módulo
| Término | Definición |
|---|---|
| Clean Architecture | Organización de código en capas con dependencias que apuntan hacia adentro |
| Separation of Concerns | Cada módulo/función tiene una responsabilidad clara |
| God Function | Función que hace demasiadas cosas y es difícil de testear y mantener |
| Protocol | Interface de Python que define una firma sin herencia explícita |
| Dependency Injection | Pasar dependencias como parámetros en lugar de crearlas internamente |
| pydantic-settings | Librería para configuration management type-safe |
| SecretStr | Tipo de Pydantic que enmascara el valor en repr y logs |
| lru_cache | Decorator que cachea el resultado de una función |
| Prompt Template | Archivo de configuración con el texto del prompt y variables |
| App Factory | Función que crea y configura la app FastAPI |
| Fail fast | Detectar y reportar errores de configuración al inicio, no en runtime |
.env.{environment} | Archivo de configuración específico por entorno |
Conexión con el Módulo 7
El Módulo 7 (Reliability Patterns & Production Checklist) añade la última capa técnica: la capacidad del sistema de sobrevivir cuando las cosas fallan.
La clean architecture de este módulo hace que el Módulo 7 sea directo:
- Retry logic: va en
OpenAIProvider.complete()— infrastructure, no domain - Circuit breaker: también en infrastructure o en un nuevo
CircuitBreakerProviderwrapper - Fallback entre modelos:
FallbackProviderya existe, solo hay que configurarlo - Budget enforcement: en
OpenAIProvidero en unBudgetAwareProviderwrapper - Health checks: el startup checks del Módulo 6 son la base de los health checks del Módulo 7
La transición: "Tu código está limpio y organizado → ahora agrégale la capacidad de recuperarse cuando fallan las cosas."
Recursos adicionales del módulo
- Clean Architecture (Robert C. Martin) — El framework conceptual completo
- pydantic-settings Docs — Configuration management
- typing.Protocol — Interfaces en Python
- FastAPI Dependency Injection — Depends() en la práctica
- Refactoring (Fowler) — Cómo refactorizar con seguridad
- 12-Factor App — La filosofía detrás de config management y environment management
- Domain-Driven Design (Evans) — Conceptos de domain layer