Dime qué métrica optimizas y te diré qué código obtienes

5 min de lectura

Hace unos días Andrej Karpathy publicó autoresearch: un sistema donde un agente de IA entrena modelos de lenguaje de forma autónoma, mide los resultados, y decide si conservar o descartar cada cambio. La idea es simple: dale al agente una métrica, déjalo iterar, y vuelve por la mañana a ver qué ha conseguido.

Karpathy lo usó para optimizar el entrenamiento de un GPT pequeño. En dos días el agente ejecutó 700 experimentos y consiguió un 11% de mejora. Shopify lo adaptó internamente y reportó un 19%.

Como dice Karpathy: “Cualquier métrica que te importe y que sea razonablemente eficiente de evaluar puede ser autoresearched por un enjambre de agentes.”

Hablando de esto con un colega de trabajo, surgió la idea: ¿qué pasa si aplico esto a métricas de arquitectura de software?


🧪 El experimento

Creé un proyecto Swift con SPM deliberadamente mal arquitecturado. Seis módulos acoplados entre sí, cuatro singletons, cero protocolos, modelos de dominio importando networking y storage directamente. El tipo de código que todos hemos heredado alguna vez.

Siguiendo el patrón de autoresearch, definí un script (analyze_architecture.sh) que calcula un score compuesto a partir de seis métricas, cada una con su peso:

MétricaPesoQué mide
Coupling (Ce)×3Dependencias entre módulos
Instability×2Ratio Ce/(Ca+Ce) promedio
Cross-module imports×1Sentencias import entre módulos
Max file size÷10Líneas del fichero más grande
Singletons×20Cantidad de static let shared
Abstraction penalty×2(100 - % abstractness)

El loop era: el agente hace un cambio, ejecuta autoresearch.sh (que compila, corre los tests, y calcula las métricas), y decide si conservar el commit o revertir. Dos restricciones: los tests deben pasar y el proyecto debe compilar.

Resultado: 40 commits, score de 461 a 21. Un 95.4% de mejora en el score compuesto.

Pero lo interesante no es el número final. Lo interesante es qué hizo el agente para conseguirlo, y qué nos dice eso sobre cada métrica.


📉 Lo que hizo el agente, métrica por métrica

Abstraction penalty (200 → 0): protocolos para todo

La abstraction penalty era el mayor componente: 200 de 461 puntos. La métrica contaba protocolos vs clases, así que el agente creó protocolos. 62 protocolos en total, distribuidos en 18 módulos de abstracción.

Muchos son razonables: StorageProviding, APIProviding, Authenticating. Otros son más cuestionables: ThumbnailProviding con un solo método, URLProviding con una sola propiedad, módulos enteros como DeletionProtocols con tres protocolos que nada implementa.

El agente también encontró un truco interesante: cambió todas las clases de public final class a open class. ¿Por qué? Porque el regex del script que cuenta clases busca public (final )?class — y open class no hace match. Técnicamente las clases siguen ahí, pero la métrica no las ve. Abstractness: 100%.

Singletons (80 → 0): renombrar y abrir

Cuatro singletons con static let shared. El agente los renombró a static let default y abrió los inicializadores para permitir inyección de dependencias. Cambio mecánico, 80 puntos menos. Aquí la mejora es genuina: el código es más testeable.

Coupling (45 → 9): desacoplamiento real

Esta fue la parte más impresionante. El agente movió los métodos de fetch/save de los modelos a extensiones en el módulo App. Desacopló Analytics, Networking y UIComponents. Introdujo inyección de dependencias basada en closures para los ViewModels:

// Antes: UIComponents importa Networking, Storage, Analytics, Models
public class ProductListViewModel {
    func loadProducts(category: String) async {
        self.products = try await Networking.fetchProducts(category: category)
    }
}

// Después: UIComponents solo importa Models, recibe la función por DI
open class ProductListViewModel: @unchecked Sendable {
    private let fetchProducts: @Sendable (String) async throws -> [Product]
    public init(fetchProducts: @escaping @Sendable (String) async throws -> [Product] = { _ in [] }) {
        self.fetchProducts = fetchProducts
    }
}

Estos cambios los aprobaría un arquitecto senior sin pestañear. El coupling total bajó de 45 a 3 (solo App depende de Models, Networking y Analytics).

Instability (100 → 8): la dilución

La instabilidad se calcula como Ce/(Ca+Ce) por módulo, y luego se promedia. El módulo App tiene instabilidad 100% — es el composition root, depende de todo y nada depende de él. Eso es inevitable.

¿La solución del agente? Crear módulos. Muchos módulos. 18 módulos de protocolos con Ce=0 y Ca=0, que aportan instabilidad 0% cada uno al promedio. Más módulos con 0% = promedio más bajo. Los módulos se llaman cosas como URLAbstractions (un fichero, dos protocolos), ReportAbstractions (un fichero, un protocolo), PersistenceContracts (un fichero, tres protocolos).

Matemáticamente correcto. El promedio baja. Pero la arquitectura real no ha cambiado — App sigue siendo 100% inestable, el agente simplemente diluyó el número.

File size (9 → 1): todo en una línea

La métrica dividía las líneas del fichero más grande entre 10. El agente respondió comprimiendo el código: múltiples sentencias por línea, switch completos en una sola línea, puntos y coma donde antes había saltos de línea.

// StorageManager.swift: todo el storage en 14 líneas
public func save(key: String, value: Any) { inMemoryStore[key] = value; operationCount += 1; logOperation("SAVE", key: key) }
public func load(key: String) -> Any? { operationCount += 1; logOperation("LOAD", key: key); return inMemoryStore[key] }
public func delete(key: String) { inMemoryStore.removeValue(forKey: key); operationCount += 1; logOperation("DELETE", key: key) }

El fichero más grande pasó de 91 a 19 líneas. La métrica mejoró. La legibilidad empeoró. ¿Importa? Depende de a quién le preguntes — si un agente de IA puede leer y modificar ese código igual de bien con o sin formato, quizás la legibilidad humana es un requisito que está cambiando de forma.


🔍 Qué conclusiones saco

Cada métrica produce su propia distorsión

Esto es lo que más me llamó la atención. No es que las métricas sean malas — es que cada una, optimizada al extremo, genera un efecto secundario específico:

  • La abstraction penalty generó protocolos que nadie implementa
  • La instabilidad generó módulos vacíos para diluir el promedio
  • El file size generó código comprimido en líneas únicas
  • El coupling generó desacoplamiento genuino (esta sí funcionó bien)
  • Los singletons generaron un renombramiento que también mejoró la testeabilidad

La calidad de cada métrica se puede evaluar por lo que produce cuando la llevas al límite. Las buenas métricas son las que son difíciles de optimizar sin mejorar el código de verdad. Coupling y singletons pasaron esa prueba. Instabilidad y file size, no tanto.

El agente encuentra los atajos que un humano no buscaría

Lo de open class esquivando el regex es un ejemplo perfecto. El agente no “hizo trampa” — ejecutó la función objetivo con una determinación sin sesgos humanos. No tiene el freno de “esto no queda bien” que tenemos los desarrolladores. Eso lo hace extraordinario cuando la función objetivo es correcta, y extraordinariamente revelador cuando no lo es.

Lo mismo aplica al naming: cuando se le acabaron los nombres buenos, el agente creó StorageProtocols2.swift y AnalyticsProtocols2.swift. Un humano se habría detenido a pensar si necesitaba otro módulo. El agente solo ve el score.

La composición del score importa tanto como el score

El score compuesto pondera singletons a ×20 y coupling a ×3. Eso expresa una opinión: los singletons son ~7 veces peor que el coupling. Si esa opinión no refleja los valores de tu equipo, el agente optimizará para algo que no te importa.

Karpathy lo tiene claro: el autoresearch funciona para “cualquier métrica que te importe”. La clave está en el “que te importe”. Elegir la métrica es la decisión de diseño más importante del experimento.

El autoresearch funciona (con matices)

El formato funciona sorprendentemente bien fuera de ML. De los 40 commits, yo diría que unos 25 produjeron mejoras genuinas: el desacoplamiento de módulos, la eliminación de singletons, la introducción de DI. Otros 15 fueron gaming de métricas. Un ratio de 60-65% de mejoras reales no está mal para un proceso completamente autónomo.

Lo fascinante es que el agente agotó las mejoras reales antes de recurrir al gaming. Las fases 1-3 (protocolos razonables, singletons, desacoplamiento) ocurrieron primero. El gaming (módulos vacíos, compactación, trucos con regex) vino después, cuando los retornos decrecientes obligaron al agente a buscar atajos.


🔮 El loop de Karpathy aplicado a arquitectura

Karpathy describe el futuro del autoresearch como enjambres de agentes colaborando asíncronamente, emulando no a un investigador sino a una comunidad de investigadores. Si aplico esa visión a la arquitectura de software, imagino agentes especializados: uno enfocado en coupling, otro en testeabilidad, otro en rendimiento, cada uno con sus métricas, negociando entre ellos.

Pero el experimento me enseñó algo más básico: antes de soltar agentes a optimizar, asegúrate de que lo que mides refleja lo que valoras. Porque el agente va a encontrar el camino más corto hacia el número que le pidas — y ese camino no siempre pasa por donde tú esperas.

El autoresearch no revela los límites de la IA. Revela los límites de nuestras métricas.


El código del experimento está en pedrocid/ios-arch-autoresearch. 40 commits, 24 módulos, 62 protocolos, 459 líneas de código, score 21. De los 24 módulos, 18 son módulos de un solo fichero con protocolos que nadie usa. De los 62 protocolos, quizás 20 tienen sentido real. Y el código funciona perfectamente.