ConstructiCat Logo
CodeBust.
Browse section ▾

Evitación de la refactorización.

Los asistentes de IA apilan código nuevo, casi duplicado, junto al que ya existe en lugar de reestructurarlo, de modo que la duplicación crece y la refactorización —el retrabajo que mantiene sana una base de código— desaparece sin que nadie lo note.

##Signs and Symptoms

Quien revisa ve un diff que es casi enteramente adiciones. El modelo resolvió el prompt añadiendo una función, rama o archivo nuevos en vez de editar la abstracción que debería haber absorbido el cambio. Señales reveladoras:

  • Copiar-pegar-con-retoques: una nueva función es 90% idéntica a una existente, diferenciándose por un literal, un nombre de campo o un if de más.
  • Casi-duplicados en paralelo: formatUserCsv, formatAdminCsv, formatGuestCsv, todos reimplementando a mano el mismo bucle en vez de una sola función parametrizada.
  • Proliferación de ramas en lugar de extracción: un método largo gana otro else if en vez de que el modelo saque una estrategia o tabla de búsqueda.
  • Reinvención: un deepClone/debounce/parser de fechas escrito a mano cuando el repositorio ya importa lodash, date-fns, etc. (OX Security lo llama "Estilo vainilla").
  • Sobreespecificación: una función auxiliar hiperespecífica de un solo uso donde ya existe una genérica dos archivos más allá.
  • "Déjà-vu de fallos": el mismo arreglo hay que aplicarlo en tres copias porque el duplicado nunca se aplicó con DRY.
// Existente en el repositorio:
function priceWithTax(items: Item[]) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * 1.2; // 20% de IVA
}

// Lo que el modelo añade para el nuevo caso "con descuento":
// una segunda copia entera en vez de un parámetro:
function priceWithTaxDiscounted(items: Item[], discount: number) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0); // duplicado
  return subtotal * (1 - discount) * 1.2;                          // lógica de IVA duplicada
}

El olor es estructural, así que es más visible en el cambio completo que en un solo fragmento: mucho verde, poco código movido, y un escáner de duplicación encendiéndose sobre las nuevas líneas.

##Reasons for the Problem

Por qué los modelos lo producen

  • Sesgo aditivo del siguiente token. Un LLM completa el prompt que tiene delante. Emitir un bloque nuevo y autónomo es la continuación localmente más probable y de menor riesgo; editar una abstracción lejana exige mantener el módulo entero en la memoria de trabajo y predecir una edición globalmente coherente, lo cual es más difícil y no es lo que recompensa el prompt inmediato.
  • Contexto del repositorio escaso. Los asistentes de programación rara vez cargan la base de código entera. Si el modelo no puede ver la función auxiliar, la biblioteca o la clase base existentes, no puede reutilizarlas, así que las reconstruye. El informe de OX Security enmarca esto como el efecto "Ejército de júniors": mucho código localmente funcional, sin memoria arquitectónica.
  • Refactorizar es arriesgado y no se recompensa. Reestructurar toca código que al modelo no se le pidió cambiar y puede romper a quienes lo llaman y a los tests. Un asistente adulador, de "tú haz que funcione", minimiza el radio de impacto no tocando el código que ya funciona: justo el comportamiento que OX midió como "Evitación de refactorizaciones" en el 80-90% del código generado por IA, y "Sobreespecificación" (de un solo uso en vez de reutilizable) en otro 80-90%.
  • Obsolescencia de los datos de entrenamiento. El modelo puede no saber que el repositorio adoptó un módulo de utilidades o actualizó una biblioteca después de su fecha de corte, así que reimplementa a mano lo que ya existe.
  • Generar es barato, borrar da miedo. Producir 40 líneas nuevas no le cuesta nada al modelo; convencerlo de borrar y consolidar 40 líneas existentes choca con su instinto de preservar.

Por qué hace daño

  • La duplicación se acumula. El análisis de GitClear de 2025 sobre 211M de líneas modificadas halló que las líneas copiadas/pegadas subieron de ~8,3% (2021) a 12,3% (2024) —el primer año en que la duplicación superó al código "movido" (refactorizado)— mientras que las líneas refactorizadas cayeron de ~25% a menos del 10%, una caída de aproximadamente el 60%. Los bloques de más de 5 líneas duplicadas se multiplicaron ~8× en 2024.
  • Mantenibilidad y corrección. Cada clon es un lugar donde un arreglo futuro puede olvidarse: el "déjà-vu de fallos", donde el mismo defecto reaparece y hay que parchearlo N veces (OX: el 70-80% del código de IA viola así los principios de reutilización).
  • Acumulación de deuda técnica. La refactorización es el retrabajo que mantiene baja la entropía; suprimirla significa que la deuda se crea pero nunca se paga. La base de código crece más rápido de lo que mejora.
  • Carga de revisión. Quien revisa debe ahora comparar a ojo bloques casi idénticos para confirmar que son intencionadamente iguales: el desplazamiento de carga cognitiva que describe el debate sobre "olores de código para agentes de IA" de Stack Overflow; el trabajo pasa de escribir a revisar y consolidar.

##Treatment

Tácticas de prompts y revisión

  • Apunta al modelo hacia lo que existe. "Antes de añadir código, busca en el repositorio una función auxiliar/util/clase base existente y reutilízala; si ninguna encaja, generaliza la más cercana." Pega el módulo relevante en el contexto para que pueda ver de verdad la abstracción.
  • Restringe la forma del diff. "Prefiere editar funciones existentes a añadir nuevas. Si dos vías de código comparten lógica, extrae una función compartida (Extraer función) en lugar de duplicar."
  • Haz que ejecute las herramientas. Exige al asistente que ejecute el escáner de duplicación (jscpd / PMD CPD) y el linter, y que resuelva cualquier nuevo hallazgo de bloque duplicado antes de declararlo terminado: esto convierte un olor invisible en una puerta de control que falla.
  • Pide la refactorización de forma explícita como segundo paso. Generación y consolidación son tareas distintas; haz "que funcione" y luego, por separado, "ahora aplica DRY a esto y elimina la duplicación", algo que los modelos manejan mucho mejor cuando se les pide directamente.
  • Añade una puerta de calidad en CI para que la duplicación no pueda escalar sin que nadie lo note (umbral de líneas duplicadas de Sonar, o jscpd --threshold).

La refactorización real: nombra los movimientos clásicos: Extraer función, Parametrizar función y Subir / Reemplazar condicional por polimorfismo o una tabla de búsqueda para matar el Código duplicado.

// Antes: dos funciones casi idénticas (refactorización evitada)
function priceWithTax(items: Item[]) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * 1.2;
}
function priceWithTaxDiscounted(items: Item[], discount: number) {
  const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
  return subtotal * (1 - discount) * 1.2;
}

// Después: Extraer función + Parametrizar función
const VAT = 1.2;
const subtotalOf = (items: Item[]) =>
  items.reduce((s, i) => s + i.price * i.qty, 0);

function priceWithTax(items: Item[], discount = 0) {
  return subtotalOf(items) * (1 - discount) * VAT;
}

Una única fuente de verdad para el subtotal y el IVA: un cambio futuro en la regla del impuesto es ahora una edición de una línea en lugar de una búsqueda por N sitios.

##Detected by