Módulo 6: Code Quality Patterns para AI
1. Introducción: Code Quality para AI
Descripción
Tienes código funcional con tests (Módulos 2-3), guardrails (Módulo 4) y logging (Módulo 5). Pero "funcional" no es lo mismo que "mantenible." Este módulo aborda la diferencia: cómo organizar ese código con clean architecture específicamente adaptada a apps LLM, de forma que un equipo pueda extenderlo, debuggearlo y cambiarlo sin reescribir todo.
El estado del código después de los módulos anteriores
# Código funcional pero monolítico — lo que probablemente tienes ahora:
from openai import OpenAI
import json
client = OpenAI(api_key="sk-...") # Acoplado al proveedor
SENTIMENT_PROMPT = "Analyze sentiment of: {text}" # Prompt hardcodeado en el módulo
def analyze(text: str) -> dict:
# God function: hace todo en un lugar
# Guardrail (mezclado con lógica)
if len(text) > 5000:
text = text[:5000]
# Prompt construction (mezclado con llamada)
prompt = SENTIMENT_PROMPT.format(text=text)
# LLM call (negocio acoplado a infraestructura)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.7 # Magic number
)
# Parsing (mezclado con todo)
raw = response.choices[0].message.content
try:
result = json.loads(raw)
except:
result = {"sentiment": "unknown", "score": 0.0}
# Logging (mezclado con lógica)
print(f"Result: {result}")
return result
# ¿Qué está mal con esto?
# 1. Si cambias de OpenAI a Anthropic: reescribir el módulo
# 2. Si quieres A/B testear el prompt: editar el código
# 3. Si quieres mock en tests: patch("openai.chat.completions.create") — frágil
# 4. Si quieres cambiar temperatura por entorno: constante o env var dispersa
# 5. Si quieres reutilizar el parser en otro lugar: está mezclado
Los 4 problemas específicos de AI apps
Problema 1: Prompts hardcodeados
# ❌ El prompt como string en el código:
PROMPT = "Analyze the sentiment of the following text and return a JSON..."
# Problemas:
# - Para A/B testear el prompt, necesitas cambiar el código y redeployar
# - Para traducir el prompt a otro idioma, igual
# - No puedes versionar el prompt separado del código
# - No puedes saber qué versión del prompt produjo qué resultado
# ✅ El prompt como configuración:
# prompts/sentiment/v1.yaml
# template: "Analyze the sentiment of..."
# version: "v1"
# author: "mike"
# last_modified: "2024-01-15"
Problema 2: Tight coupling al proveedor
# ❌ Acoplado directamente a OpenAI:
from openai import OpenAI
client = OpenAI()
def analyze(text: str) -> dict:
response = client.chat.completions.create(...) # API específica de OpenAI
# Si mañana Anthropic tiene mejor precio/calidad:
# → Reescribir TODA la lógica de negocio
# ✅ Desacoplado con Protocol:
class LLMProvider(Protocol):
def complete(self, messages: list, **kwargs) -> str: ...
def analyze(text: str, provider: LLMProvider) -> dict:
response = provider.complete([{"role": "user", "content": text}])
# Cambiar a Anthropic: nueva implementación de LLMProvider, una línea en config
Problema 3: Configuración dispersa
# ❌ Config dispersa en múltiples lugares:
MODEL = "gpt-4o-mini" # En models.py
TEMPERATURE = 0.7 # En utils.py
MAX_TOKENS = int(os.getenv("MT")) # En main.py
RETRY_ATTEMPTS = 3 # En llm_client.py
# Resultado: para cambiar la config de staging, buscar en 4 archivos
# ✅ Config centralizada con pydantic-settings:
class Settings(BaseSettings):
model: str = "gpt-4o-mini"
temperature: float = 0.7
max_tokens: int = 500
retry_attempts: int = 3
class Config:
env_file = ".env"
Problema 4: God functions
# ❌ Una función que hace todo:
def process_request(text: str) -> dict:
# 1. Sanitizar (debería ser guardrails)
# 2. Construir prompt (debería ser capa de prompts)
# 3. Llamar LLM (debería ser infrastructure)
# 4. Parsear respuesta (debería ser output processing)
# 5. Validar resultado (debería ser domain)
# 6. Loguear (debería ser wrapper de infrastructure)
# 7. Retornar (ok)
pass
# Consecuencia: no puedes testear cada parte por separado
# No puedes reutilizar el parser para otro endpoint
# No puedes cambiar el LLM sin riesgo de romper el parser
La solución: 4 capas adaptadas a AI
┌─────────────────────────────────────────────────────────┐
│ PROMPT TEMPLATES (configuración) │
│ Archivos YAML/JSON con templates, versión, metadata │
│ Versionables, editables sin tocar código, A/B testeables│
├─────────────────────────────────────────────────────────┤
│ BUSINESS LOGIC (domain) │
│ Orquestación, reglas de negocio, use cases │
│ Solo conoce interfaces — no conoce OpenAI ni JSON │
├─────────────────────────────────────────────────────────┤
│ LLM INFRASTRUCTURE (infrastructure) │
│ Implementaciones: OpenAIProvider, AnthropicProvider, │
│ MockProvider, FallbackProvider │
├─────────────────────────────────────────────────────────┤
│ OUTPUT PROCESSING (processing) │
│ Parsers, validators, transformers │
│ Reciben strings, retornan tipos Python │
└─────────────────────────────────────────────────────────┘
Regla: las dependencias solo apuntan hacia ADENTRO.
Processing no conoce Infrastructure.
Domain no conoce Infrastructure.
Infrastructure no conoce Domain.
La transformación completa
# ANTES: todo mezclado en una función
# DESPUÉS: cada capa tiene su responsabilidad
# ─── prompts/sentiment/v1.yaml ───────────────────────────────────────────
# template: |
# Analyze the sentiment of the following text.
# Return JSON: {"sentiment": "positive|negative|neutral|mixed", "score": float}
# Text: {text}
# version: "v1"
# ─── src/domain/sentiment_service.py ─────────────────────────────────────
from src.infrastructure.llm_provider import LLMProvider
from src.processing.sentiment_parser import parse_sentiment_output
from src.prompts.loader import load_prompt
def analyze_sentiment(text: str, provider: LLMProvider) -> dict:
"""Business logic pura: orquesta sin conocer detalles de infraestructura."""
prompt = load_prompt("sentiment/v1").render(text=text)
raw_response = provider.complete([
{"role": "system", "content": "You are a sentiment analysis expert."},
{"role": "user", "content": prompt}
])
return parse_sentiment_output(raw_response)
# ─── src/infrastructure/openai_provider.py ───────────────────────────────
class OpenAIProvider:
def __init__(self, client, model: str, temperature: float, max_tokens: int):
self._client = client
self._model = model
self._temperature = temperature
self._max_tokens = max_tokens
def complete(self, messages: list) -> str:
response = self._client.chat.completions.create(
model=self._model,
messages=messages,
temperature=self._temperature,
max_tokens=self._max_tokens
)
return response.choices[0].message.content
# ─── src/processing/sentiment_parser.py ──────────────────────────────────
from pydantic import BaseModel
import json
class SentimentOutput(BaseModel):
sentiment: str
score: float
def parse_sentiment_output(raw: str) -> dict:
"""Parser independiente: toma string, retorna dict validado."""
data = json.loads(raw)
return SentimentOutput(**data).model_dump()
# ─── src/app/main.py ──────────────────────────────────────────────────────
from src.domain.sentiment_service import analyze_sentiment
from src.infrastructure.openai_provider import OpenAIProvider
from src.config import get_settings
settings = get_settings()
def get_provider():
return OpenAIProvider(
client=OpenAI(api_key=settings.openai_api_key.get_secret_value()),
model=settings.model,
temperature=settings.temperature,
max_tokens=settings.max_tokens
)
Por qué este módulo viene AQUÍ en la guía
Timeline de la guía:
M1-3: Tests ────────────────────────────────────────────────────
Escribiste tests para el código. Ahora tienes una red de
seguridad. El refactoring de M6 solo es seguro porque
tienes tests.
M4: Guardrails ──────────────────────────────────────────────────
Añadiste lógica de validación. Ahora tienes código sustancial
que vale la pena organizar.
M5: Logging ─────────────────────────────────────────────────────
Añadiste infraestructura de observabilidad. Ahora el código
tiene tres capas mezcladas: negocio + guardrails + logging.
M6: Code Quality ────────────────────────────────────────────────
Con tests (red de seguridad) y suficiente código que organizar,
ahora el refactoring tiene tanto valor como es seguro hacerlo.
M7: Reliability ─────────────────────────────────────────────────
La clean architecture de M6 hace que añadir retry, circuit
breakers y fallbacks sea plug-and-play.
Prerequisitos del módulo
# Dependencias nuevas para este módulo
pip install pydantic-settings # Configuration management
pip install jinja2 # Prompt templating (opcional)
pip install python-dotenv # .env loading
pip install pyyaml # YAML para archivos de prompts
Roadmap del módulo
| # | Cápsula | Feature central | Resultado |
|---|---|---|---|
| 01 | Introducción | El problema del código AI monolítico | Esta cápsula |
| 02 | Clean architecture | Las 4 capas, estructura de directorios | Arquitectura definida |
| 03 | Separation of concerns | Extraer cada responsabilidad | God functions eliminadas |
| 04 | Config management | pydantic-settings type-safe | Config centralizada |
| 05 | Dependency injection | LLMProvider Protocol | Provider desacoplado |
| 06 | Environment management | dev/staging/prod configs | Config por entorno |
| 07 | Proyecto Refactored AI App | Refactoring completo | App organizada |
| 08 | Resumen y troubleshooting | Cierre y anti-patterns | — |
Ejercicios
Ejercicio 1: Diagnóstico de tu código actual
Lista los 3 principales problemas de code quality en el código que has construido en módulos anteriores. Para cada uno, identifica el pattern que lo resuelve:
Ver guía
Problemas comunes:
- "El prompt está hardcodeado en
main.pydentro de la función" → Solución: externalizar a archivo YAML +load_prompt() - "Importo
from openai import OpenAIdirectamente ensentiment_service.py" → Solución: Protocol LLMProvider + dependency injection - "La configuración está en
.env,config.py, ymain.pymezclada" → Solución: pydantic-settings centralizado
Ejercicio 2: Clasificar código
Para cada fragmento, indica en qué capa de la arquitectura debería estar:
# A)
def complete(self, messages: list) -> str:
return openai.chat.completions.create(...)
# B)
def analyze_sentiment(text: str, provider: LLMProvider) -> dict:
prompt = load_prompt("sentiment").render(text=text)
raw = provider.complete([{"role": "user", "content": prompt}])
return parse_output(raw)
# C)
def parse_output(raw: str) -> dict:
return SentimentOutput.model_validate_json(raw).model_dump()
# D)
# sentiment/v1.yaml
# template: "Analyze sentiment: {text}"
Ver solución
A) Infrastructure — Es la llamada al API, específica a un proveedor. B) Business Logic / Domain — Orquesta los pasos, define el use case. C) Output Processing — Transforma raw output en tipo Python. D) Prompt Templates — Configuración del prompt, no código.
Ejercicio 3: ROI del refactoring
Para cada mejora de code quality, estima cuánto tiempo ahorras en el futuro:
- Externalizar prompts a YAML
- Crear LLMProvider Protocol
- Centralizar config con pydantic-settings
Ver guía
- Prompts externalizados: A/B testing de prompts sin redeployar = horas ahorradas por iteración × todas las iteraciones. Si optimizas prompts una vez por semana: 2h/semana ahorradas.
- LLMProvider Protocol: Cambiar de OpenAI a otro proveedor = 30 minutos (nueva implementación) vs 2-3 días (refactoring todo el código). ROI: enorme si cambias una sola vez.
- pydantic-settings: Debugging de config en producción = 5 minutos (startup error) vs 2 horas (buscar dónde está la config incorrecta). ROI: cada vez que hay un error de config.
Resumen
- El código funcional pero monolítico tiene 4 problemas AI-específicos: prompts hardcodeados, tight coupling, config dispersa, y god functions
- La solución es clean architecture en 4 capas: prompt templates, business logic, infrastructure, output processing
- El timing es correcto: los tests de Phase 1 son la red de seguridad para el refactoring
- El objetivo es pragmatismo, no purismo — cada abstracción debe justificarse con un caso de uso real
Recursos adicionales
- Clean Architecture (Robert C. Martin) — El framework conceptual
- pydantic-settings — Configuration management
- typing.Protocol — Interfaces en Python
- Dependency Injection in Python (Martin Fowler) — El patrón fundamental
- Refactoring (Martin Fowler) — El libro de referencia para refactoring seguro