Convirtiendo Repos de Codigo en Conocimiento: Generacion Automatica de Documentos de Arquitectura con IA
Necesitaba que mi asistente de IA respondiera preguntas de arquitectura, pero cada herramienta de codigo que probe entendia archivos individuales y fallaba en el panorama general. Asi que construi un sistema que genera automaticamente documentos de arquitectura completos a partir de repositorios de codigo usando extraccion hibrida de AST y sintesis con LLM. Aqui explico como funciona y lo que aprendi.
Hay una brecha en mi organizacion que deje de pretender que no existe. Tengo el codigo. Miles de archivos, cientos de modulos, anos de decisiones acumuladas. Y tengo personas -- nuevas contrataciones, equipos adyacentes, mi yo del futuro -- que necesitan entender lo que ese codigo realmente hace a nivel de sistema. El codigo existe. La comprension no.
La documentacion es la respuesta obvia, pero la documentacion tiene un defecto fatal: se descompone. En el momento en que alguien escribe un documento de arquitectura, el codebase empieza a alejarse de el. En semanas, la seccion de endpoints le faltan tres rutas nuevas. En meses, el diagrama de modelo de datos es ficcion. En un ano, el documento es activamente enganoso. Esto no es un problema de disciplina. Es un problema estructural. La documentacion que requiere mantenimiento manual siempre perdera contra la velocidad de los cambios de codigo.
Asi que hice una pregunta diferente: ¿que pasa si el codigo pudiera documentarse a si mismo? No comentarios inline o referencias API auto-generadas -- esas ya existen. Me refiero a un documento de arquitectura completo que describe lo que hace un repositorio, como esta estructurado, como se ven sus modelos de datos, cuales son sus flujos principales y como se relacionan sus componentes entre si. Generado automaticamente. Actualizado en cada push a main. Indexado para retrieval-augmented generation para que un asistente de IA pueda realmente responder preguntas como "¿como funciona la autenticacion en nuestro backend?"
Construi este sistema. Funciona. Y llegar ahi requirio entender por que todas las herramientas existentes se quedan cortas ante este problema especifico.
Las Herramientas Que Existen Hoy (Y Lo Que Realmente Hacen)
Antes de construir nada, investigue siete herramientas que afirman resolver "comprension de codebase" de varias maneras. El panorama es impresionante, pero ninguna hace lo que yo necesitaba.
El Panorama Competitivo
Aider es lo que mas se acerca a mi objetivo. Construye un repo-map usando Tree-Sitter para parsear codigo fuente en ASTs, luego aplica PageRank en un grafo donde los archivos son nodos y las aristas representan dependencias. El resultado es un mapa rankeado de los simbolos mas importantes en el codebase, optimizado para caber dentro de un presupuesto de tokens. Esto es genuinamente inteligente -- identifica las funciones y clases mas referenciadas para darle al LLM una vista comprimida del repo. Pero produce un mapa de simbolos, no un documento de arquitectura. Te dice que existe, no que significa ni como encaja todo.
CodeRabbit construye un grafo de dependencias usando analisis AST y almacena contexto en LanceDB para retrieval sub-segundo a traves de 50,000+ PRs diarios. Su framework de ingenieria de contexto divide la informacion en capas de Intent, Environment y Conversation. Impresionante para code review -- mantienen una relacion 1:1 de codigo a contexto en sus prompts de LLM. Pero reconstruyen este grafo por review. No hay documento de arquitectura persistente. La comprension es efimera, reconstruida en cada pull request.
Greptile construye un knowledge graph completo de codebases usando extraccion AST, linters y analisis con LLM. Logran comprension cross-file y pueden detectar bugs que abarcan limites de modulos. Este es el enfoque con mas infraestructura -- un grafo en tiempo real, siempre actualizado de todo tu codebase. Pero es una herramienta de code review, no un generador de documentacion. El conocimiento se queda dentro del sistema de Greptile.
Sourcegraph Cody tomo una decision estrategica fascinante: abandonaron completamente los vector embeddings para su busqueda empresarial en favor de busqueda por keywords BM25. El razonamiento fue pragmatico -- mantener embeddings y buscar en bases de datos vectoriales para codebases con 100,000+ repositorios introducia demasiada complejidad y limitaba sus funcionalidades de contexto multi-repositorio. Esta es una leccion importante: los embeddings no son siempre superiores a la busqueda por keywords para codigo, especialmente a escala.
Augment Code procesa codebases completos de mas de 400,000 archivos a traves de su Context Engine, logrando un score de precision de 70.6% en SWE-bench. Entienden patrones arquitectonicos y dependencias cross-service. Pero de nuevo -- la comprension vive dentro de su sistema. No hay documento exportado, ningun artefacto que puedas consultar de forma independiente.
GitHub Copilot Workspace maneja ciclos completos de desarrollo pero trata cada repositorio como un limite de contexto aislado. No puede entender bibliotecas compartidas entre repos, patrones cross-repositorio ni dependencias de servicios en arquitecturas de microservicios.
Sweep AI automatiza reviews y correcciones de PRs usando chunking basado en AST que respeta la estructura sintactica -- un patron que luego fue adoptado por LlamaIndex para RAG de codigo. Bueno para contexto por PR, no para comprension holistica.
La Brecha
Esto es lo que me llamo la atencion despues de revisar las siete: ninguna herramienta mainstream genera un documento de arquitectura persistente y completo a partir de un repositorio. Todas construyen representaciones internas -- grafos, embeddings, mapas de simbolos -- para impulsar sus funcionalidades especificas (code review, completions, busqueda). Pero ninguna produce un artefacto standalone que diga: "esto es lo que hace este repositorio, como esta estructurado, como es su API, cuales son sus modelos de datos y como funcionan sus flujos principales."
Ese artefacto es exactamente lo que un sistema RAG necesita para responder preguntas de arquitectura. Y no existe.
Por Que el Codigo Crudo Es Mal Contexto para RAG
Para entender por que esto importa, necesitas entender lo que pasa cuando indexas codigo de forma ingenua para retrieval RAG.
Mi primer enfoque fue clonar un repositorio, dividir archivos en chunks, hacerles embedding y almacenarlos en una base de datos vectorial. Cuando un usuario hacia una pregunta, el sistema recuperaba los chunks mas similares y los alimentaba al LLM.
Esto suena razonable. Funciona terriblemente.
Un chunk como def validate_token(token: str) -> bool le dice al LLM casi nada. ¿Donde se llama esta funcion? ¿Que tokens valida? ¿Es parte del flujo de autenticacion o del sistema de API keys? ¿Siquiera se usa todavia? El chunk de codigo crudo es ruido descontextualizado. En investigacion sobre RAG para repositorios de codigo a gran escala, uno de los hallazgos principales es que los metodos ingenuos de chunking tienen problemas para delinear con precision segmentos significativos de codigo, y proporcionar segmentos de codigo incompletos a un LLM aumenta las alucinaciones.
Esto es medible. En ContextIA -- el sistema RAG que estaba construyendo -- los chunks de codigo crudo indexados con source_type="git" recibian un boost de retrieval de 0.7 -- el mas bajo despues de mensajes de Slack (0.4) -- precisamente porque producian contexto de baja calidad para las preguntas que mas importaban. Cuando preguntaba "como funciona nuestro sistema de pagos", obtenia firmas de funciones individuales y snippets de configuracion en lugar de una explicacion coherente.
El problema no es la precision de retrieval. El problema es que los chunks de codigo, incluso cuando se recuperan con precision, no contienen la informacion semantica que responde preguntas de arquitectura.
La Solucion: Extraccion Hibrida de AST + Sintesis con LLM
El enfoque que construi combina dos fases: extraccion estructural sin ningun involucramiento de LLM, seguida de sintesis con LLM que convierte esa estructura en un documento de arquitectura completo.
Fase 1: Extraccion Estructural
La primera fase es determinista y barata. Escanea el repositorio clonado y extrae informacion estructurada usando heuristicas y matching de patrones de archivos:
class ArtifactGenerator:
"""Generates SYSTEM_ARTIFACT.md for a repository."""
SKIP_DIRS = {
"node_modules", ".git", "__pycache__", ".venv",
"venv", "dist", "build", ".next", "coverage",
}
def extract_structure(self, repo_path: Path) -> dict:
"""
Structural extraction without LLM:
- tree: filtered directory (max 3 levels, skip irrelevant dirs)
- deps: package.json / requirements.txt / go.mod -> dependency list
- models: files in models/, schemas/, migrations/ -> names
- routes: files in routes/, api/, controllers/ -> names
- config: .env.example, config.*, settings.* -> names
- tests: files in tests/, __tests__/ -> structure
- readme: README.md content if present
"""
structure = {}
structure["tree"] = self._build_filtered_tree(repo_path, max_depth=3)
structure["deps"] = self._extract_dependencies(repo_path)
structure["models"] = self._find_files_in_dirs(
repo_path, ["models", "schemas", "migrations"]
)
structure["routes"] = self._find_files_in_dirs(
repo_path, ["routes", "api", "controllers"]
)
structure["config"] = self._find_config_files(repo_path)
structure["readme"] = self._read_readme(repo_path)
return structure
def select_key_files(self, repo_path: Path, structure: dict) -> list[dict]:
"""
Select key files for LLM context:
- README.md (always)
- Primary config (package.json, requirements.txt, etc.)
- Top model files (up to 5)
- Top route/API files (up to 5)
- Top service files (up to 5)
Returns list[{"path": str, "content": str}], max ~20 files.
"""
key_files = []
# README always first
readme_path = repo_path / "README.md"
if readme_path.exists():
key_files.append(self._read_file(readme_path))
# Config files, models, routes, services...
for category in ["deps", "models", "routes", "services"]:
files = self._prioritize_files(structure.get(category, []))
key_files.extend(files[:5])
return key_files[:20] # Hard cap at 20 filesEsto es intencionalmente simple. Sin parsers AST por lenguaje, sin resolucion compleja de dependencias, sin herramientas externas. El matching de patrones en nombres de directorios y extensiones de archivos es suficiente para identificar los elementos estructurales importantes de la mayoria de los repositorios. Corre en milisegundos y produce un resumen estructurado sobre el que el LLM puede razonar.
Fase 2: Sintesis con LLM
La segunda fase envia la estructura extraida y los contenidos de archivos clave a Claude Opus, que sintetiza un documento de arquitectura completo:
def generate_artifact(
self, repo: str, structure: dict, key_files: list[dict]
) -> str:
"""
Call Opus with the SYSTEM_ARTIFACT template.
Input: extracted structure + key file contents.
Output: complete SYSTEM_ARTIFACT.md (~200-500 lines).
"""
prompt = f"""You are a software architecture analyst. From the structure
and key files of a repository, generate a SYSTEM_ARTIFACT.md that documents
the project architecture.
The document must be:
- Comprehensive but concise (200-500 lines)
- Based ONLY on what you observe in the code (do not invent endpoints
or models that do not exist)
- Useful for a RAG system answering questions about the project
REPOSITORY STRUCTURE:
{json.dumps(structure, indent=2)}
KEY FILES:
{self._format_key_files(key_files)}
Generate the SYSTEM_ARTIFACT.md with these sections:
1. Purpose
2. Tech Stack
3. Directory Structure
4. Data Models
5. API / Endpoints
6. Main Services and Components
7. Principal Flows
8. External Dependencies
9. Configuration
10. Tests
"""
response = self.llm_client.generate(
model="claude-opus-4-6",
prompt=prompt,
max_tokens=8000,
)
return response.contentLa decision de diseno de usar Opus en lugar de un modelo mas barato fue deliberada. Opus entiende flujos complejos, relaciones entre componentes y patrones arquitectonicos significativamente mejor que modelos mas pequenos. A aproximadamente $0.30-0.50 por generacion, el costo es aceptable porque la generacion solo ocurre en merges a main -- no en cada commit.
¿Por que no enviar todo el repositorio al LLM? Costo y calidad. Enviar cada archivo produce peores resultados porque el LLM se abruma con codigo irrelevante (utilidades de test, boilerplate de configuracion, archivos de migracion). El enfoque hibrido -- extraer estructura primero, seleccionar archivos clave, luego sintetizar -- produce documentos mas enfocados y precisos mientras usa menos tokens.
La Estrategia de Indexacion Dual
El artefacto generado se indexa para retrieval RAG junto al codigo crudo, pero con pesos diferentes:
type_boost = {
"notion": 0.8,
"git": 0.7, # raw code chunks
"docs": 1.0,
"slack": 0.4,
"system_artifact": 1.0, # architecture document - same as docs
}.get(src_type, 0.5)El artefacto se indexa como source_type="system_artifact" con un boost de 1.0 -- el mismo peso que la documentacion escrita a mano. El codigo crudo se re-indexa como source_type="git" a 0.7. Ambos viven en la misma coleccion de ChromaDB, ambos son buscables, pero el documento de arquitectura obtiene prioridad en el ranking de retrieval.
Esta indexacion dual significa que cuando alguien pregunta "¿como funciona la autenticacion?", el sistema RAG devuelve primero la explicacion coherente del artefacto sobre el flujo de auth, complementada con chunks de codigo especificos si el usuario profundiza mas. El artefacto proporciona el mapa; el codigo proporciona el territorio.
El constructor de contexto usa deteccion de intencion para ajustar el retrieval:
ARCHITECTURE_KEYWORDS = [
"architecture", "how does", "system", "flow", "endpoint",
"model", "structure", "component", "service", "design",
"stack", "dependency", "integration", "diagram", "overview",
# Spanish equivalents for bilingual teams
"arquitectura", "como funciona", "sistema", "flujo", "modelo",
]
def append_artifact_context(
tenant_id: str, query: str, context: str, sources: list, rag
) -> tuple[str, list]:
"""Add artifact chunks to context, boosted for architecture queries."""
is_arch_query = any(kw in query.lower() for kw in ARCHITECTURE_KEYWORDS)
top_k = 5 if is_arch_query else 2
extra, extra_sources = rag.retrieve_from_source_type(
tenant_id, query, source_type="system_artifact", top_k=top_k
)
if not extra:
return context, sources
header = "\n\n--- System Artifact Context (repository architecture) ---\n\n"
return (context + header + extra).strip(), sources + extra_sourcesLas queries con intencion de arquitectura recuperan 5 chunks del artefacto. Las queries generales recuperan 2. Esto mantiene el contexto del artefacto proporcional a la necesidad real del usuario.
El Pipeline de Automatizacion
Todo el sistema esta automatizado a traves de webhooks de GitHub. Cuando un desarrollador pushea a main, el pipeline se activa:
- GitHub envia un evento push al endpoint del gateway
- El gateway valida HMAC-SHA256, verifica la rama y encuentra el tenant propietario
- Si el commit es nuevo (no procesado previamente), dispara la tarea de generacion
- Un worker de Celery clona el repo, extrae la estructura, genera el artefacto con Opus, lo indexa en ChromaDB y lo persiste en PostgreSQL
- El codigo crudo se re-ingesta en paralelo
@celery_app.task(name="artifact_generation_task", queue="ingest")
def artifact_generation_task(
tenant_id: str,
repo: str,
commit_sha: str | None = None,
github_token: str | None = None,
also_ingest_code: bool = True,
):
"""
Full pipeline:
1. git clone --depth 1
2. extract_structure() - heuristics, no LLM
3. select_key_files() - max ~20 files
4. generate_artifact() - Opus generates SYSTEM_ARTIFACT.md
5. index_artifact() - index in ChromaDB (source_type=system_artifact)
6. Persist in PostgreSQL (github_artifacts table)
7. If also_ingest_code: trigger code re-ingest
8. Cleanup: shutil.rmtree(tmp_dir)
"""El trigger basado en webhooks significa que la documentacion se mantiene actualizada sin ninguna intervencion humana. Cada merge a main produce un documento de arquitectura fresco en minutos. El problema de la descomposicion de documentacion se resuelve estructuralmente -- el artefacto siempre se deriva del estado actual del codigo.
La multi-tenancy se maneja a traves de PATs de GitHub por tenant encriptados con Fernet, almacenados en la base de datos del gateway. Cada tenant configura una allowlist de repositorios que quiere documentar. El secreto del webhook es unico por tenant, asegurando un aislamiento adecuado.
Los Bugs Que Mas Te Ensenan
Dos bugs surgieron post-implementacion que vale la pena compartir porque ilustran el tipo de problemas sutiles de integridad de datos que surgen en sistemas RAG multi-fuente.
Bug 1: El Artefacto Desaparecido
El pipeline de generacion de artefactos tenia un paso que re-ingeria codigo crudo junto al documento de arquitectura. El problema: el ingest_task existente llamaba a delete_by_source_repo(tenant_id, repo) antes de re-indexar codigo -- lo que eliminaba TODOS los chunks para ese repo, incluyendo el artefacto recien generado. El documento de arquitectura vivia en ChromaDB aproximadamente tres segundos antes de ser borrado por la re-ingesta de codigo que corria justo despues.
La correccion fue agregar un parametro source_type a la funcion de eliminacion:
# Before (broken): deletes everything including system_artifact
delete_by_source_repo(tenant_id, repo)
# After (fixed): only deletes raw code chunks
delete_by_source_repo(tenant_id, repo, source_type="git")Un parametro. Tres segundos de vida util del artefacto. Horas de depuracion.
Bug 2: Eliminacion Cross-Repo
Al indexar un nuevo artefacto, el codigo llamaba a delete_by_source_type(tenant_id, "system_artifact") para limpiar la version anterior. Esto eliminaba todos los chunks system_artifact para el tenant completo -- no solo el repo que se estaba actualizando. Un tenant con cinco repositorios conectados perdia cuatro artefactos cada vez que se actualizaba un repositorio.
# Before (broken): nukes all artifacts for the tenant
delete_by_source_type(tenant_id, "system_artifact")
# After (fixed): only deletes artifacts for the specific repo
delete_by_source(tenant_id, f"artifact:{repo}")Ambos bugs comparten una causa raiz: el alcance de eliminacion era demasiado amplio. En un sistema RAG multi-fuente donde diferentes tipos de fuente coexisten en el mismo vector store, cada operacion de eliminacion necesita tener el alcance mas estrecho posible. Esto no es obvio cuando estas construyendo el happy path. Se vuelve dolorosamente obvio cuando los datos empiezan a desaparecer.
Lo Que Aprendi de la Investigacion de la Industria
Varias ideas de investigar herramientas existentes dieron forma a las decisiones de diseno:
La retirada de Sourcegraph de los embeddings fue esclarecedora. Encontraron que mantener embeddings a traves de 100,000+ repositorios era mas problema que la busqueda por keywords BM25. Para mi caso de uso -- generar un documento, no buscar codigo crudo -- los embeddings tienen sentido porque el artefacto es prosa, no codigo. Pero la leccion se mantiene: no uses busqueda vectorial por defecto sin considerar si enfoques mas simples funcionan.
El enfoque de PageRank de Aider fue inspirador pero insuficiente. Rankear simbolos por conteo de referencias identifica lo que es importante en el codigo. Pero importancia no es explicacion. La funcion mas referenciada podria ser una utilidad que es arquitectonicamente irrelevante. El enfoque hibrido -- extraccion estructural para lo que existe, sintesis con LLM para lo que significa -- cierra esa brecha.
La relacion 1:1 de codigo a contexto de CodeRabbit valido el enfoque de indexacion dual. Si la mejor herramienta de code review necesita tanto contexto como codigo en sus prompts, entonces un sistema RAG que responde preguntas de arquitectura necesita aun mas. El documento de arquitectura es ese contexto, pre-sintetizado y listo para retrieval.
La encuesta sobre generacion de codigo con retrieval augmented confirmo que la comprension a nivel de repositorio es un problema abierto. La mayoria de los enfoques tratan los repositorios como colecciones planas de documentos. La comprension real requiere capturar dependencias inter-archivo, patrones arquitectonicos y logica de negocio -- exactamente lo que el SYSTEM_ARTIFACT esta disenado para codificar.
Resultados
Despues de desplegar el sistema en multiples repositorios:
- Las preguntas de arquitectura que antes devolvian snippets de codigo descontextualizados ahora devuelven explicaciones coherentes a nivel de parrafo
- El boost de retrieval del artefacto (1.0 vs 0.7 para codigo crudo) significa que el contexto de arquitectura aparece primero en las respuestas RAG, con detalles de codigo disponibles para seguimiento
- La regeneracion disparada por webhook mantiene la documentacion a un merge de distancia de lo actual
- El costo de generacion promedia $0.30-0.50 por artefacto en Opus, disparado solo en pushes a main
- Todo el pipeline -- clonar, extraer, generar, indexar -- se completa en menos de dos minutos para repositorios con hasta 500 archivos
La metrica mas reveladora es cualitativa: los miembros del equipo dejaron de preguntar "¿donde esta la documentacion de X?" en Slack y empezaron a preguntarle al asistente de IA. La documentacion existe porque el codigo existe. Sin esfuerzo manual requerido.
Conclusiones Aplicables
Si estas construyendo sistemas que necesitan entender repositorios de codigo, esto es lo que llevaria conmigo:
-
No indexes codigo crudo para preguntas de arquitectura. Los chunks de codigo son utiles para "muestrame como funciona la funcion X" pero inutiles para "¿como funciona el sistema de pagos?". Genera un artefacto de nivel superior e indexa eso en su lugar.
-
La extraccion hibrida supera al LLM puro. Extraer estructura con heuristicas deterministas antes de enviar al LLM produce mejores resultados a menor costo que volcar todo el repo en una ventana de contexto. Deja que el LLM sintetice, no que busque.
-
Indexacion dual con diferentes boosts. Manten tanto codigo crudo como documentos sintetizados en tu vector store, pero ponlos a diferentes pesos. Documentos de arquitectura a 1.0, codigo crudo a 0.7. La intencion del usuario determina cual aparece primero.
-
Automatiza via webhooks, no con schedules. La documentacion disparada por cambios reales de codigo siempre esta actualizada. La regeneracion basada en cron desperdicia recursos y puede quedar obsoleta entre ejecuciones.
-
Acota tus eliminaciones obsesivamente. En un sistema RAG multi-fuente, cada operacion de eliminacion debe especificar exactamente lo que esta eliminando. Las eliminaciones amplias destruiran silenciosamente datos que no tenias intencion de remover. Ambos bugs que encontre fueron problemas de alcance de eliminacion.
-
Usa el modelo mas capaz para generacion, no el mas barato. El artefacto es una generacion unica por merge. La diferencia de calidad entre Opus y un modelo mas pequeno en sintesis arquitectonica es significativa. Ahorra dinero en tareas de alta frecuencia y bajo riesgo. Gastalo en las de baja frecuencia y alto riesgo.
-
Estudia lo que otros construyeron antes de construir lo tuyo. El enfoque Tree-Sitter + PageRank de Aider, el movimiento de Sourcegraph de embeddings a BM25, el framework de ingenieria de contexto de CodeRabbit -- cada uno de estos informo una decision de diseno especifica. Las mejores soluciones son recombinaciones de ideas existentes, no invenciones desde cero.
La brecha entre tener codigo y entender codigo es real, y le estaba costando a mi equipo tiempo, calidad de onboarding y velocidad de toma de decisiones. Los documentos de arquitectura generados automaticamente no resuelven el problema completamente -- no capturan el "por que" detras de las decisiones, los trade-offs considerados, las opciones rechazadas. Pero resuelven el "que" y el "como" a un nivel que no existia antes, y lo hacen sin requerir que nadie escriba ni mantenga una sola linea de documentacion.
Eso es un paso adelante significativo.