Módulo 7: Subagents para Debugging, Regenerar vs Editar
Cuándo Regenerar Código
Cuándo Regenerar Código
Descripción de la cápsula
"¿Debería regenerar esto o arreglarlo?" — es la pregunta que te haces varias veces al día cuando trabajas con AI coding tools. Y la respuesta incorrecta tiene costo real: regenerar cuando deberías editar desperdicia contexto, customizaciones, y tiempo. Editar cuando deberías regenerar produce parches frágiles sobre código fundamentalmente incorrecto.
Esta cápsula cubre un lado del espectro: las señales claras de que regenerar es la mejor opción. No "regenerar cuando el código es malo" — eso no es útil. Señales concretas: "regenerar cuando el approach algorítmico es incorrecto: usó un loop O(n²) donde necesitas un dict lookup O(1)."
Pero regenerar no es gratis. Pierdes customizaciones, pierdes contexto que acumulaste, y el nuevo código puede traer problemas diferentes. Entender el costo de regenerar es tan importante como saber cuándo hacerlo.
Las 5 Señales de que Regenerar es Mejor
Señal 1: El approach algorítmico es fundamentalmente incorrecto
Esta es la señal más clara. El código no tiene un bug puntual — tiene el approach equivocado. Editarlo no lo arregla; necesitas un approach diferente.
Ejemplo: Búsqueda ineficiente
Claude Code generó esto para buscar un usuario por email en una lista:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
email: str
name: str
users_db: list[User] = []
@app.get("/users/search")
async def search_user(email: str):
for user in users_db:
if user.email == email:
return user
raise HTTPException(status_code=404, detail="User not found")
¿Por qué editar no lo arregla?
El approach es búsqueda lineal O(n). Con 10 usuarios funciona. Con 100,000 usuarios, cada búsqueda recorre la lista completa. No puedes optimizar un loop lineal en una lista — necesitas cambiar la estructura de datos.
# ❌ Editar: agregar un break o un return temprano no resuelve O(n)
# ❌ Editar: agregar caché sobre la lista sigue siendo un parche
# ✅ Regenerar con approach correcto:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
email: str
name: str
users_by_email: dict[str, User] = {}
@app.get("/users/search")
async def search_user(email: str):
user = users_by_email.get(email)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
La señal concreta: Si necesitas cambiar la estructura de datos fundamental (list → dict, array → tree, loop → query), regenera.
Señal 2: La arquitectura es incorrecta
El código funciona pero usa el patrón arquitectónico equivocado. Parchear la arquitectura solo acumula deuda técnica.
Ejemplo: Síncrono donde necesitas asíncrono
Claude Code generó un endpoint que hace 3 llamadas externas secuencialmente:
import requests
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/dashboard")
def get_dashboard(user_id: int):
user_response = requests.get(f"http://user-service/users/{user_id}")
user = user_response.json()
tasks_response = requests.get(f"http://task-service/tasks?user_id={user_id}")
tasks = tasks_response.json()
notifications_response = requests.get(
f"http://notification-service/notifications?user_id={user_id}"
)
notifications = notifications_response.json()
return {
"user": user,
"tasks": tasks,
"notifications": notifications,
}
¿Por qué editar no lo arregla?
El código usa requests (síncrono/bloqueante) en un endpoint de FastAPI (que es async). Las 3 llamadas se ejecutan secuencialmente — si cada una tarda 200ms, el endpoint tarda 600ms. Pero el problema real es que bloquea el event loop de FastAPI.
No puedes "editar" requests.get para que sea async. Necesitas cambiar la librería, el patrón de concurrencia, y la estructura de las llamadas.
# ✅ Regenerar con arquitectura correcta:
import httpx
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/dashboard")
async def get_dashboard(user_id: int):
async with httpx.AsyncClient() as client:
user_task = client.get(f"http://user-service/users/{user_id}")
tasks_task = client.get(f"http://task-service/tasks?user_id={user_id}")
notif_task = client.get(
f"http://notification-service/notifications?user_id={user_id}"
)
user_resp, tasks_resp, notif_resp = await asyncio.gather(
user_task, tasks_task, notif_task
)
return {
"user": user_resp.json(),
"tasks": tasks_resp.json(),
"notifications": notif_resp.json(),
}
La señal concreta: Si necesitas cambiar de síncrono a asíncrono, de monolítico a modular, de polling a websockets, o de in-memory a database — regenera la función o el archivo.
Señal 3: Más del 50% del código necesita cambiar
Si vas a editar más de la mitad de las líneas, estás reescribiendo de todas formas. Pero lo estás haciendo de la forma más lenta: editando línea por línea en vez de generar el código correcto de una vez.
Ejemplo: CRUD con validación incorrecta
Claude Code generó un endpoint de creación de tareas con múltiples problemas:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
app = FastAPI()
class Task(BaseModel):
title: str
description: str
priority: str
due_date: str
assigned_to: str
tasks = []
next_id = 1
@app.post("/tasks")
def create_task(task: Task):
global next_id
new_task = {
"id": next_id,
"title": task.title,
"description": task.description,
"priority": task.priority,
"due_date": task.due_date,
"assigned_to": task.assigned_to,
"status": "open",
"created_at": str(datetime.now()),
}
tasks.append(new_task)
next_id += 1
return new_task
Problemas que requieren edición:
- ❌
descriptionno debería ser required (no todas las tareas la tienen) - ❌
prioritydebería ser un Enum, no un string libre - ❌
due_datedebería serdatetime, nostr - ❌
assigned_todebería serOptional[int](user_id), nostr - ❌ Usa
globaly lista en memoria en vez de base de datos - ❌ No hay autenticación
- ❌ No hay validación de que priority sea un valor válido
- ❌ No hay response model
- ❌
created_ates un string en vez de un datetime
Conteo: 9 problemas en un código de ~25 líneas. Editar cada uno sería más lento que regenerar con un prompt mejor.
La señal concreta: Cuenta los cambios necesarios. Si necesitas modificar más del 50% de las líneas, regenera con un prompt que especifique todos los requisitos.
Señal 4: El código no cumple los requisitos
A veces Claude Code genera código que funciona perfectamente... para un requisito diferente al que pediste. La lógica de negocio es incorrecta. No es un bug — es la feature equivocada.
Ejemplo: Cálculo de descuento incorrecto
Pediste: "Descuento del 10% para compras mayores a $100, 20% para compras mayores a $500."
Claude Code generó:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Order(BaseModel):
items: list[dict]
total: float
@app.post("/api/orders/calculate-discount")
async def calculate_discount(order: Order):
if order.total > 500:
discount = order.total * 0.20
elif order.total > 100:
discount = order.total * 0.10
else:
discount = 0
return {
"original_total": order.total,
"discount": discount,
"final_total": order.total - discount,
}
Parece correcto, pero el requisito real es más complejo:
Requisito real:
- Descuento del 10% sobre el monto que excede $100 (no sobre el total)
- Descuento adicional del 20% sobre el monto que excede $500
- Los descuentos son progresivos (como brackets de impuestos)
Ejemplo: compra de $700
- Primeros $100: sin descuento
- $100 a $500 ($400): 10% = $40
- $500 a $700 ($200): 20% = $40
- Total descuento: $80
- Final: $620
Lo que Claude Code calculó:
- $700 * 20% = $140 de descuento
- Final: $560
- INCORRECTO: $60 de diferencia
Editar la lógica condicional para implementar brackets progresivos requiere reescribir toda la función de cálculo. Es más claro regenerar con el requisito bien explicado.
La señal concreta: Si la lógica de negocio es fundamentalmente diferente a lo que necesitas, regenera explicando el requisito correcto con ejemplos de input/output.
Señal 5: La deuda técnica sería masiva si se parcha
A veces el código funciona y podrías editarlo, pero cada edición agrega complejidad a un diseño que ya es frágil. El resultado sería un Frankenstein de parches.
Ejemplo: Validación dispersa
Claude Code generó validación inline en cada endpoint en vez de centralizarla:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class TaskInput(BaseModel):
title: str
priority: str
status: str
@app.post("/tasks")
async def create_task(task: TaskInput):
if len(task.title) < 3:
raise HTTPException(400, "Title too short")
if len(task.title) > 200:
raise HTTPException(400, "Title too long")
if task.priority not in ["low", "medium", "high", "critical"]:
raise HTTPException(400, "Invalid priority")
if task.status not in ["pending", "in_progress", "completed", "cancelled"]:
raise HTTPException(400, "Invalid status")
# ... crear task
@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, task: TaskInput):
if len(task.title) < 3:
raise HTTPException(400, "Title too short")
if len(task.title) > 200:
raise HTTPException(400, "Title too long")
if task.priority not in ["low", "medium", "high", "critical"]:
raise HTTPException(400, "Invalid priority")
if task.status not in ["pending", "in_progress", "completed", "cancelled"]:
raise HTTPException(400, "Invalid status")
# ... actualizar task
@app.post("/tasks/bulk")
async def bulk_create(tasks: list[TaskInput]):
for task in tasks:
if len(task.title) < 3:
raise HTTPException(400, "Title too short")
if len(task.title) > 200:
raise HTTPException(400, "Title too long")
if task.priority not in ["low", "medium", "high", "critical"]:
raise HTTPException(400, "Invalid priority")
if task.status not in ["pending", "in_progress", "completed", "cancelled"]:
raise HTTPException(400, "Invalid status")
# ... crear tasks
El problema: La misma validación se repite 3 veces (y crecerá con cada endpoint). Editar los 3 endpoints para agregar una nueva regla de validación significa tocar 3 lugares. Si falta una, tienes inconsistencia.
¿Podrías editarlo? Sí — moverías la validación a Pydantic validators. Pero tocarías 4 archivos (el model y 3 endpoints) y cada edición es propensa a errores.
¿Es mejor regenerar? Sí — regenerar el schema con Pydantic validators es más limpio:
from enum import Enum
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class Status(str, Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class TaskCreate(BaseModel):
title: str = Field(min_length=3, max_length=200)
priority: Priority
status: Status = Status.PENDING
class TaskUpdate(BaseModel):
title: str | None = Field(default=None, min_length=3, max_length=200)
priority: Priority | None = None
status: Status | None = None
@app.post("/tasks")
async def create_task(task: TaskCreate):
# Validación automática por Pydantic
pass
@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, task: TaskUpdate):
# Validación automática por Pydantic
pass
@app.post("/tasks/bulk")
async def bulk_create(tasks: list[TaskCreate]):
# Validación automática por Pydantic
pass
La señal concreta: Si editar requiere tocar el mismo patrón en 3+ lugares, y hay un diseño más limpio que centraliza la lógica — regenera con el diseño correcto desde el inicio.
El Costo de Regenerar: Lo que Pierdes
Regenerar no es gratis. Antes de decidir, considera lo que pagas:
Costo 1: Pierdes customizaciones
Si editaste el código después de la generación inicial — agregaste un header, ajustaste un formato, cambiaste un mensaje de error — todo eso se pierde al regenerar.
Escenario:
1. Claude Code genera endpoint de tareas
2. Tú editas: agregas logging, cambias formato de fecha,
ajustas el mensaje de error a español
3. Descubres que el approach es incorrecto
4. Regeneras el endpoint
5. El nuevo código: no tiene tu logging, usa formato
de fecha americano, mensajes en inglés
→ Necesitas re-aplicar tus customizaciones
→ Si no las recuerdas todas, pierdes algo
Mitigación: Antes de regenerar, anota las customizaciones que quieres preservar. Inclúyelas en el prompt de regeneración.
Costo 2: Pierdes contexto acumulado
Cada interacción con Claude Code construye contexto. Si llevas 20 minutos editando un archivo, Claude Code entiende tu intención, tu estilo, y tus preferencias. Regenerar desde cero pierde ese contexto.
Con contexto acumulado:
"Agrega validación de email"
→ Claude Code sabe que usas Pydantic, sabe tu estilo de error,
sabe que prefieres validaciones en el schema
Sin contexto (después de regenerar):
"Agrega validación de email"
→ Claude Code podría usar regex inline, podría no seguir
el mismo estilo de error, podría poner la validación
en el endpoint en vez del schema
Mitigación: Al regenerar, incluye en el prompt el contexto que necesitas: "Usa Pydantic validators, mensajes de error en español, el mismo formato que el resto del proyecto."
Costo 3: Puedes obtener problemas nuevos
El nuevo código no es una versión mejorada del anterior — es código completamente nuevo que puede tener sus propios bugs.
Código original:
├── Bug: validación faltante en campo priority
├── Correcto: auth, respuesta, format de fecha
└── Correcto: error handling
Código regenerado:
├── Correcto: validación de priority (el fix que buscabas)
├── NUEVO Bug: no hace auth check
├── NUEVO Bug: fecha en formato incorrecto
└── Correcto: error handling
Resolviste un problema y creaste dos. Esto ocurre porque Claude Code no tiene contexto de lo que ya estaba correcto — genera código desde cero basado solo en tu prompt.
Mitigación: Después de regenerar, haz un code review del nuevo código comparándolo con el anterior. Verifica que lo que ya funcionaba sigue funcionando.
Costo 4: Tiempo de regeneración + revisión
Regenerar parece rápido: escribes un prompt, Claude Code genera, listo. Pero el tiempo real incluye:
Tiempo total de regenerar:
├── Escribir un prompt mejor: 3-5 min
├── Esperar la generación: 1-2 min
├── Revisar el código nuevo: 5-10 min
├── Re-aplicar customizaciones: 3-5 min
├── Testear: 5-10 min
└── Total: 17-32 min
vs Tiempo de editar (si el fix es puntual):
├── Identificar el cambio: 2-3 min
├── Hacer la edición: 1-2 min
├── Testear: 3-5 min
└── Total: 6-10 min
Si el fix es puntual, editar es 3x más rápido. Si el fix requiere cambios masivos, regenerar es más eficiente. La decisión correcta depende de la escala del cambio.
Cómo Regenerar Efectivamente
Si decides regenerar, hazlo bien. Un prompt mejor produce mejor código.
Regla 1: Incluye qué estaba mal la primera vez
❌ Prompt vago:
"Genera un endpoint para crear tareas"
✅ Prompt informado por el error anterior:
"Genera un endpoint POST /tasks para crear tareas.
La versión anterior tenía estos problemas:
1. Usaba validación inline en vez de Pydantic Field constraints
2. Priority era string libre en vez de Enum
3. No tenía autenticación
4. Usaba lista en memoria en vez de DB
Requisitos:
- Pydantic model con Field(min_length=3, max_length=200) para title
- Priority como Enum: low, medium, high, critical
- Status con default 'pending'
- Depends(get_current_user) para auth
- SQLAlchemy para persistencia
- Response model explícito"
El prompt incluye lo que aprendiste del primer intento. Eso evita que Claude Code repita los mismos errores.
Regla 2: Da ejemplos de input/output
"El cálculo de descuento debe ser progresivo (como tax brackets):
- Primeros $100: sin descuento
- $100 a $500: 10% sobre el excedente
- Más de $500: 20% sobre el excedente de $500
Ejemplo: compra de $700
- Primeros $100 → $0 descuento
- $100-$500 ($400) → $40 descuento (10%)
- $500-$700 ($200) → $40 descuento (20%)
- Total descuento: $80
- Final: $620"
Los ejemplos concretos eliminan ambigüedad y te dan un test case para verificar el código generado.
Regla 3: Especifica lo que debe preservarse
"Regenera la función calculate_discount.
PRESERVAR:
- El response format: {original_total, discount, final_total}
- El endpoint path: POST /api/orders/calculate-discount
- Los type hints y el modelo Pydantic Order
- Los mensajes de error en español
CAMBIAR:
- La lógica de cálculo de descuento (ver requisitos arriba)
- Agregar validación de que total > 0"
Esto reduce los costos de regeneración — Claude Code sabe qué mantener y qué cambiar.
Regla 4: Especifica el contexto del proyecto
"Este endpoint es parte de una API FastAPI con:
- SQLAlchemy async (usa AsyncSession, no Session)
- Pydantic v2 (usa model_config en vez de class Config)
- Autenticación con JWT (ya existe get_current_user)
- Logging con structlog
- Los imports están en la convención del proyecto:
from app.models import Task
from app.schemas.task import TaskCreate, TaskResponse
from app.dependencies.auth import get_current_user
from app.database import get_db"
Contexto del proyecto evita que Claude Code genere código que no encaja con tu stack.
Regenerar Parcial: No Todo o Nada
Un error común es pensar que regenerar significa "borrar todo y empezar de cero." En la práctica, a menudo la mejor opción es regenerar solo la parte problemática.
El espectro de regeneración
Regenerar archivo completo ← → Regenerar función ← → Editar
Cuándo regenerar archivo completo:
├── El archivo tiene < 100 líneas
├── El 80%+ necesita cambiar
└── La estructura del archivo es incorrecta
Cuándo regenerar una función:
├── La función tiene el approach incorrecto
├── El resto del archivo está bien
└── La función es independiente (pocos side effects)
Cuándo editar:
├── Solo 1-5 líneas necesitan cambiar
├── El fix es claro y puntual
└── El approach es correcto
Ejemplo: Regenerar solo una función
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Task, User
from app.schemas.task import TaskCreate, TaskResponse, TaskStats
from app.dependencies.auth import get_current_user
app = FastAPI()
# ✅ Esta función está bien — NO regenerar
@app.post("/tasks", response_model=TaskResponse)
async def create_task(
task: TaskCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
new_task = Task(**task.model_dump(), owner_id=current_user.id)
db.add(new_task)
db.commit()
db.refresh(new_task)
return new_task
# ❌ Esta función tiene el approach incorrecto — REGENERAR
@app.get("/tasks/stats", response_model=TaskStats)
async def get_task_stats(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
all_tasks = db.query(Task).filter(Task.owner_id == current_user.id).all()
total = len(all_tasks)
completed = len([t for t in all_tasks if t.status == "completed"])
pending = len([t for t in all_tasks if t.status == "pending"])
in_progress = len([t for t in all_tasks if t.status == "in_progress"])
avg_completion_time = 0
for task in all_tasks:
if task.completed_at and task.created_at:
avg_completion_time += (task.completed_at - task.created_at).total_seconds()
if completed > 0:
avg_completion_time /= completed
return TaskStats(
total=total,
completed=completed,
pending=pending,
in_progress=in_progress,
avg_completion_seconds=avg_completion_time,
)
La función get_task_stats carga TODAS las tareas en memoria y hace cálculos en Python. Con 50,000 tareas, esto es un desastre de performance. Necesitas SQL aggregations:
Prompt para regenerar SOLO get_task_stats:
"Regenera solo la función get_task_stats. El approach actual
carga todas las tareas en memoria y calcula en Python.
Necesito que use SQL aggregations (COUNT, AVG) directamente
en la base de datos.
La función debe:
- Usar db.query con func.count y func.avg de SQLAlchemy
- Filtrar por owner_id del current_user
- Calcular avg_completion_time con SQL, no Python
- Mantener el mismo response model TaskStats
- Mantener los mismos Depends (get_current_user, get_db)
- Una sola query eficiente en vez de cargar todas las filas"
Resultado: regeneras una función de 20 líneas, mantienes el endpoint create_task intacto, y preservas la estructura del archivo.
Conexión con Proyecto
En el proyecto integrador (Módulo 8)
Vas a encontrar código donde regenerar es la respuesta correcta. La clave es identificar la señal:
Proyecto integrador — posibles decisiones de regenerar:
1. Una función de búsqueda que usa loop en Python
en vez de SQL WHERE → Señal 1 (approach algorítmico)
2. Llamadas síncronas a servicios externos
en endpoints async → Señal 2 (arquitectura incorrecta)
3. Un endpoint con 8+ problemas de validación,
naming, y estructura → Señal 3 (>50% necesita cambiar)
4. Un cálculo que implementa la lógica de negocio
de forma incorrecta → Señal 4 (no cumple requisitos)
5. Validación duplicada en 4 endpoints diferentes
→ Señal 5 (deuda técnica masiva si se parcha)
Para cada decisión de regenerar, documentarás:
- ✅ Qué señal identificaste
- ✅ Qué se perdería al regenerar (customizaciones, contexto)
- ✅ Qué incluiste en el prompt para mitigar las pérdidas
- ✅ Que revisaste el código nuevo para verificar que no introdujo problemas
Troubleshooting
Problema 1: "Regeneré y el nuevo código tiene problemas diferentes"
Causa: El prompt no incluyó suficiente contexto o no especificó lo que debía preservarse. Solución: Antes de regenerar, anota todo lo que está correcto en el código actual. Inclúyelo en el prompt: "PRESERVAR: el formato de response, la autenticación, los mensajes en español. CAMBIAR: la lógica de cálculo." Después de regenerar, haz code review comparando viejo vs nuevo.
Problema 2: "No sé si el approach es incorrecto o solo tiene bugs"
Causa: A veces es difícil distinguir entre un approach correcto con bugs y un approach fundamentalmente incorrecto. Solución: Pregunta: "Si arreglo todos los bugs, ¿el código va a funcionar correctamente a escala?" Si la respuesta es sí, edita. Si la respuesta es "funcionará pero será lento / inseguro / imposible de mantener," regenera.
Problema 3: "Regeneré con mejor prompt pero Claude Code generó algo muy diferente a lo que esperaba"
Causa: Los LLMs no son deterministas. El mismo prompt puede producir resultados diferentes. Solución: Sé más específico en el prompt. Incluye: la firma exacta de la función, los imports que debe usar, el response format, y un ejemplo de input/output. Cuanto más específico, menos variación.
Problema 4: "No estoy seguro si es 50% o 30% lo que necesita cambiar"
Causa: La regla del 50% es una heurística, no un número mágico. Solución: La pregunta real no es el porcentaje exacto — es "¿será más rápido editar o regenerar?" Si tienes que pensar mucho sobre cuántos cambios necesitas, probablemente son suficientes para regenerar. Si puedes listar los cambios en 30 segundos, edita.
Ejercicios
Ejercicio 1: Identificar la señal (Medio)
Para cada código, identifica cuál de las 5 señales aplica y justifica por qué regenerar es mejor que editar.
Código A:
from fastapi import FastAPI
from datetime import datetime
app = FastAPI()
@app.get("/api/events")
async def get_upcoming_events():
events = get_all_events_from_db()
upcoming = []
now = datetime.now()
for event in events:
event_date = datetime.strptime(event["date"], "%Y-%m-%d")
if event_date > now:
upcoming.append(event)
upcoming.sort(key=lambda e: e["date"])
return upcoming[:10]
Código B:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
username: str
email: str
age: int
@app.post("/users")
async def create_user(user: UserCreate):
if not user.username:
raise HTTPException(400, "Username required")
if len(user.username) < 2:
raise HTTPException(400, "Username too short")
if "@" not in user.email:
raise HTTPException(400, "Invalid email")
if user.age < 0:
raise HTTPException(400, "Invalid age")
if user.age > 150:
raise HTTPException(400, "Invalid age")
# ... save to db
return {"id": 1, **user.model_dump()}
Ver solución
Código A — Señal 1: Approach algorítmico incorrecto
El código carga TODOS los eventos de la base de datos, los filtra en Python, los ordena en Python, y toma los primeros 10. Con 100,000 eventos, esto carga 100,000 registros cuando solo necesitas 10.
Regenerar con: SELECT * FROM events WHERE date > NOW() ORDER BY date LIMIT 10 — una sola query SQL que hace todo en la base de datos.
Editar no resuelve: podrías optimizar el sort o el filtro en Python, pero el problema fundamental es cargar todos los registros.
Código B — Señal 5: Deuda técnica masiva si se parcha
Toda la validación es inline en el endpoint. Si agregas más endpoints que crean o actualizan usuarios, tendrás que duplicar las mismas validaciones. La solución correcta es usar Pydantic Field constraints y validators.
Regenerar el schema con:
class UserCreate(BaseModel):
username: str = Field(min_length=2)
email: EmailStr
age: int = Field(ge=0, le=150)
Editar "funcionaría" pero perpetúa el anti-patrón de validación dispersa.
Ejercicio 2: Calcular el costo de regenerar (Medio)
Este código fue generado por Claude Code y luego editaste manualmente 3 cosas (marcadas con comentarios):
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Product
from app.schemas import ProductCreate, ProductResponse
from app.dependencies.auth import get_current_user
import logging # TU EDICIÓN: agregaste logging
logger = logging.getLogger(__name__) # TU EDICIÓN
app = FastAPI()
@app.post("/products", response_model=ProductResponse)
async def create_product(
product: ProductCreate,
current_user = Depends(get_current_user),
db: Session = Depends(get_db),
):
logger.info(f"Creando producto: {product.name} por user {current_user.id}") # TU EDICIÓN
if product.price < 0:
raise HTTPException(400, "El precio no puede ser negativo")
new_product = Product(**product.model_dump())
db.add(new_product)
db.commit()
db.refresh(new_product)
return new_product
El problema: La validación price < 0 debería ser price <= 0 (un producto no puede costar $0), pero además te das cuenta de que necesitas agregar validación de stock, categoría existente, y nombre único. ¿Regeneras o editas? Calcula el costo de cada opción.
Ver solución
Opción A: Editar
Cambios necesarios:
1. Cambiar < 0 por <= 0 (1 carácter)
2. Agregar validación de stock >= 0 (2 líneas)
3. Agregar verificación de categoría existente (3-4 líneas + query)
4. Agregar verificación de nombre único (3-4 líneas + query)
Costo:
├── Tiempo: ~10 minutos
├── Customizaciones preservadas: ✅ logging (3 líneas)
├── Riesgo: bajo (cambios puntuales)
└── Mantenibilidad: media (validación inline, crece con el tiempo)
Opción B: Regenerar la función
Lo que se pierde:
├── 3 líneas de logging que tú agregaste
└── El mensaje de error en español
Costo:
├── Tiempo: ~15-20 minutos (prompt + review + re-agregar logging)
├── Customizaciones perdidas: logging, mensajes en español
├── Riesgo: medio (código nuevo puede tener bugs nuevos)
└── Mantenibilidad: alta SI mueves validación a Pydantic
Decisión correcta: Editar
El code base tiene pocas validaciones que agregar (3-4), las customizaciones son valiosas (logging), y el approach general es correcto. Editar es más rápido y preserva tu trabajo.
PERO: si decidieras que la validación debería estar en el schema Pydantic (no en el endpoint), entonces regenerar el schema tiene sentido — es un cambio de diseño, no solo agregar validaciones.
Ejercicio 3: Escribir prompt de regeneración (Difícil)
Tienes este código que necesita regenerarse (Señal 2 — arquitectura incorrecta). Escribe el prompt de regeneración que produciría el mejor resultado:
import time
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/health/full")
def full_health_check():
results = {}
try:
import psycopg2
conn = psycopg2.connect("postgresql://localhost/mydb")
conn.close()
results["database"] = "healthy"
except Exception:
results["database"] = "unhealthy"
try:
import redis
r = redis.Redis()
r.ping()
results["cache"] = "healthy"
except Exception:
results["cache"] = "unhealthy"
try:
import requests
resp = requests.get("http://external-api/status", timeout=5)
results["external_api"] = "healthy" if resp.status_code == 200 else "unhealthy"
except Exception:
results["external_api"] = "unhealthy"
return {"status": "healthy" if all(v == "healthy" for v in results.values()) else "degraded", "checks": results}
Ver solución
Regenera el endpoint GET /api/health/full con estas correcciones:
PROBLEMAS DEL CÓDIGO ACTUAL:
1. Es síncrono (def) cuando debería ser async — bloquea el event loop
2. Crea conexiones nuevas a DB y Redis en cada request
en vez de usar los pools del proyecto
3. Usa psycopg2 (sync) en vez del engine de SQLAlchemy async existente
4. Las 3 verificaciones se ejecutan secuencialmente (total: hasta 15s)
cuando podrían ejecutarse en paralelo con asyncio.gather
5. Usa requests (sync) en vez de httpx (async)
6. No tiene timeout individual por check
REQUISITOS:
- Endpoint async que use asyncio.gather para checks paralelos
- DB check usando la sesión de SQLAlchemy existente (from app.database import get_db)
- Redis check usando el cliente Redis existente (from app.cache import redis_client)
- External API check usando httpx.AsyncClient con timeout de 3 segundos
- Timeout individual de 5 segundos por check (si tarda más, reportar "timeout")
- Response format: {"status": "healthy"|"degraded"|"unhealthy",
"checks": {"database": {...}, "cache": {...}, "external_api": {...}},
"response_time_ms": <total>}
PRESERVAR:
- El path del endpoint: GET /api/health/full
- La lógica de status: healthy si todo OK, degraded si algo falla
- Los mismos 3 checks: database, cache, external_api
CONTEXTO:
- Proyecto FastAPI async con SQLAlchemy async y Redis
- Python 3.11+
Este prompt incluye: qué estaba mal, qué necesitas, qué preservar, y el contexto del proyecto. Es 10x más probable que produzca código correcto que un prompt vago.
Ejercicio 4: Regenerar parcial vs total (Difícil)
Este archivo tiene dos funciones. Una está bien y otra no. Decide qué regenerar y qué mantener:
from fastapi import FastAPI, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.database import get_db
from app.models import Task, Category
from app.schemas import TaskResponse, CategoryStats
from app.dependencies.auth import get_current_user
from typing import Optional
app = FastAPI()
@app.get("/tasks", response_model=list[TaskResponse])
async def list_tasks(
status: Optional[str] = Query(None),
priority: Optional[str] = Query(None),
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
current_user = Depends(get_current_user),
db: Session = Depends(get_db),
):
query = db.query(Task).filter(Task.owner_id == current_user.id)
if status:
query = query.filter(Task.status == status)
if priority:
query = query.filter(Task.priority == priority)
offset = (page - 1) * size
tasks = query.order_by(Task.created_at.desc()).offset(offset).limit(size).all()
return tasks
@app.get("/categories/stats", response_model=list[CategoryStats])
async def category_stats(
current_user = Depends(get_current_user),
db: Session = Depends(get_db),
):
categories = db.query(Category).all()
stats = []
for cat in categories:
tasks = db.query(Task).filter(
Task.category_id == cat.id,
Task.owner_id == current_user.id,
).all()
completed = sum(1 for t in tasks if t.status == "completed")
total = len(tasks)
stats.append(CategoryStats(
category_name=cat.name,
total_tasks=total,
completed_tasks=completed,
completion_rate=completed / total if total > 0 else 0,
))
return stats
Ver solución
list_tasks — MANTENER (está bien)
- ✅ Usa paginación con OFFSET/LIMIT
- ✅ Filtros opcionales bien implementados
- ✅ Query construida con SQLAlchemy correctamente
- ✅ Ordena por created_at descendiente
- ✅ Limita el tamaño de página (max 100)
category_stats — REGENERAR (approach incorrecto)
- ❌ Carga TODAS las categorías en memoria
- ❌ Para CADA categoría, hace una query separada (N+1)
- ❌ Carga TODAS las tareas de cada categoría para contar en Python
- ❌ Con 50 categorías y 1000 tareas por categoría = 50 queries + 50,000 objetos en memoria
Prompt de regeneración:
Regenera SOLO la función category_stats.
La función list_tasks está correcta — NO la toques.
Problema: la función actual tiene N+1 queries y
carga datos en memoria. Necesito UNA query SQL que
haga GROUP BY con COUNT y agregaciones.
La query debería:
- JOIN categories con tasks
- Filtrar por owner_id del current_user
- GROUP BY category
- COUNT total y COUNT completed en SQL
- Calcular completion_rate en SQL o Python (pero con
los datos ya agregados, no fila por fila)
Mantener: response_model list[CategoryStats],
los mismos Depends, el mismo endpoint path.
Decisión: regenerar parcial. Solo la función problemática. El archivo y la otra función se mantienen intactos.
Resumen
- Hay 5 señales claras de que regenerar es mejor que editar:
- El approach algorítmico es fundamentalmente incorrecto (O(n²) → O(1))
- La arquitectura es incorrecta (sync → async, monolítico → modular)
- Más del 50% del código necesita cambiar
- El código no cumple los requisitos de negocio
- La deuda técnica sería masiva si se parcha (validación duplicada × 5 endpoints)
- Regenerar tiene 4 costos que debes considerar: pierdes customizaciones, pierdes contexto, puedes obtener problemas nuevos, y toma más tiempo del que parece
- Para regenerar efectivamente: incluye qué estaba mal, da ejemplos de I/O, especifica qué preservar, y da contexto del proyecto
- Regenerar no es todo o nada — a veces regeneras una función pero mantienes el resto del archivo
- Después de regenerar, siempre haz code review del nuevo código comparándolo con el anterior
Recursos Adicionales
- Joel Spolsky — Things You Should Never Do, Part I - El artículo clásico sobre los peligros de reescribir código desde cero
- Martin Fowler — Refactoring vs Rewriting - Cuándo refactorizar incrementalmente vs reescribir
- Big Ball of Mud — Brian Foote, Joseph Yoder - Cómo el código se degrada cuando se parcha sin criterio
- Anthropic — Claude Code Best Practices - Mejores prácticas para prompts de generación de código
- Pydantic V2 Documentation — Validators - Cómo centralizar validación en schemas (vs inline)
Siguiente cápsula: Cuándo Editar Manualmente — el otro lado del espectro.
Debugging & Code Review with Claude Code — Módulo 7, Cápsula 03 Claude Code Agentic Development Path — Guía #6 de 11