Por qué tu código es un grafo: parsing con Tree-sitter para un asistente de programación con IA
Por qué los asistentes de programación con IA no pueden leer repos enteros, cómo los grafos y los árboles sintácticos capturan la estructura y cómo parsear código con Tree-sitter para crear un índice de símbolos.
El código es un grafo, no un montón de texto
Cualquiera que llega a un proyecto nuevo conoce ese momento. Clonas el repo y te preguntas dónde vive la lógica de reintentos de pago. El reflejo es una de estas dos opciones:
Ctrl + Shift + F
o
grep -r "retry" .
A veces basta, pero muchas veces no. El código de reintentos puede estar detrás de una interfaz, en otro módulo, dentro de un decorador, en una clase base o detrás de un patrón strategy. Encontrarlo exige algo más que coincidencias de texto, porque depende de cómo se relacionan las piezas entre sí.
Ahora pregúntale a Cursor «¿Cómo funcionan los reintentos de pago?» y en un par de segundos resalta varios archivos, explica el flujo de ejecución e incluso propone mejoras. Parece un poco magia. Lo sorprendente es que no está leyendo todo tu repositorio, ni podría hacerlo.
Un modelo mental equivocado muy común
La mayoría de los desarrolladores se imagina al principio la herramienta así:
Repository
│
▼
Large Language Model
│
▼
Answer
Suena razonable, pero no puede funcionar. Piensa en una base de código de tamaño medio:
12,000 Python files
≈ 2 million lines of cod
Ni siquiera los modelos con ventanas de contexto enormes pueden procesar un repositorio entero de forma eficiente en cada prompt, y aunque pudieran, saldría caro. Enviar cada archivo fuente, README, configuración, test y archivo generado supondría una latencia terrible y una factura enorme. Peor aún, casi todos esos tokens serían irrelevantes. Si la pregunta va de reintentos de pago, el modelo no tiene por qué leer código de procesamiento de imágenes.
Tú tampoco lo lees todo
Fíjate en cómo encuentra las cosas una ingeniera con experiencia. Si le preguntan dónde está implementada la autenticación, nadie abre main.py y lee los archivos en orden. Enseguida se forma un mapa mental, algo así:
Request
│
▼
API Router
│
▼
Authentication Middleware
│
▼
JWT Validator
│
▼
User Service
Sigues relaciones, es decir, recorres un grafo. Esa diferencia es enorme. Los ingenieros con experiencia no memorizan millones de líneas. Recuerdan conexiones.
Qué es de verdad un repositorio
Un repositorio no es una bolsa de archivos. Es una red de símbolos conectados. Toma un proyecto pequeño:
src/
├── api.py
├── payment.py
├── retry.py
├── queue.py
└── logger.py
A primera vista son cinco archivos sin relación. Si dibujas las relaciones, la historia cambia:
api.py
│
▼
payment.py
│ │
▼ ▼
retry.py logger.py
│
▼
queue.py
Ese dibujo explica más de lo que jamás explicará el árbol de directorios. Cada llamada a función, import, herencia, implementación de interfaz y dependencia es una arista del grafo. Visto así, los asistentes de programación con IA tienen más sentido: navegan por relaciones y no se limitan a buscar en archivos.
Dónde falla la búsqueda de texto
Mira este pequeño ejemplo:
class PaymentProcessor:
def charge(self, order):
return self.gateway.execute(order)
¿Dónde se hace realmente el pago? No hay forma de saberlo. gateway podría ser Stripe, Razorpay, PayPal, un mock o un doble de pruebas, y buscar «charge» no lo aclara. Añade inyección de dependencias y la cosa empeora:
processor = container.resolve(PaymentProcessor)
Ahora la implementación se elige en tiempo de ejecución y la búsqueda de texto casi no sirve. La ingeniería moderna se apoya mucho en la abstracción y, paradójicamente, la abstracción es justo lo que perjudica a la búsqueda ingenua.
El código tiene estructura
Los lenguajes de programación no son texto plano. Están estructurados. Toma esta función:
def calculate_total(price, tax):
return price + tax
Tú ves caracteres; un compilador ve estructura, más o menos así:
FunctionDefinition
├── Name
│ └── calculate_total
├── Parameters
│ ├── price
│ └── tax
└── Return
└── BinaryExpression(+)
Esa representación es un árbol de sintaxis abstracta, o AST. Conserva el significado y descarta el formato. Estas dos formas de escribirlo:
return price + tax
y
return (
price
+
tax
)
producen en esencia el mismo AST. Precisamente por eso los asistentes de programación modernos empiezan parseando el código fuente en lugar de tratarlo como texto sin más.
Tree-sitter
Muchas herramientas para desarrolladores se apoyan en Tree-sitter. En lugar de recorrer un archivo línea a línea, construye un árbol sintáctico que entiende el lenguaje. Toma este código:
class PaymentService:
def retry_payment(self):
pass
Tree-sitter no ve dos líneas de texto. Ve algo más parecido a esto:
Module
│
├── Class
│ └── PaymentService
│
└── Function
└── retry_payment
Esa estructura compensa más adelante, porque permite responder preguntas como a qué clase pertenece un método, qué funciones son públicas, qué métodos llaman a una función concreta y qué símbolos se exportan. La búsqueda de texto nunca podrá responderlas con fiabilidad.
Parsear un repositorio
Toca construir la primera pieza del asistente. Instala las dependencias:
pip install tree-sitter tree-sitter-python
Después parsea un archivo:
from tree_sitter import Language, Parser
import tree_sitter_python as tspython
parser = Parser()
PY_LANGUAGE = Language(tspython.language())
parser.language = PY_LANGUAGE
code = """
class PaymentService:
def retry_payment(self):
print("Retrying...")
"""
tree = parser.parse(bytes(code, "utf8"))
print(tree.root_node)
La salida tiene este aspecto:
(module
(class_definition
(identifier)
(block
(function_definition))))
No es bonito, pero es útil. Te dice dónde empieza cada clase, dónde termina cada función, dónde están los imports y dónde se definen los decoradores, y nadie ha tenido que escribir un parser a mano.
Extraer todas las funciones
A continuación, recorre el árbol sintáctico y recoge todas las funciones:
from tree_sitter import Parser, Language
import tree_sitter_python as tspython
parser = Parser()
parser.language = Language(tspython.language())
def extract_functions(node, source):
if node.type == "function_definition":
name = node.child_by_field_name("name")
print(source[name.start_byte:name.end_byte].decode())
for child in node.children:
extract_functions(child, source)
source = open("payment.py", "rb").read()
tree = parser.parse(source)
extract_functions(tree.root_node, source)
Salida:
retry_payment
refund
calculate_total
Fíjate en lo que ha pasado. El código no ha buscado la palabra clave def. Le ha pedido al árbol sintáctico cada definición de función, y esa diferencia importa.
Construir un índice
En lugar de guardar archivos completos, crea un índice estructurado con entradas como esta:
{
"symbol": "retry_payment",
"type": "function",
"file": "payment.py",
"class": "PaymentService",
"start_line": 42,
"end_line": 67
}
Hazlo con cada función, clase, interfaz, enum y constante, y un repositorio sin estructura se convierte en conocimiento en el que se puede buscar.
Lo que hay hasta ahora
El pipeline construido hasta aquí queda así:
Git Repository
│
▼
Repository Scanner
│
▼
Tree-sitter Parser
│
▼
Abstract Syntax Tree
│
┌────────────┴────────────┐
▼ ▼
Function Index Class Index
│ │
└────────────┬────────────┘
▼
Repository Graph
Todavía no se parece a un sistema de IA, y es porque no lo es. Es un pipeline de extracción de conocimiento, que es por donde empieza cualquier asistente de programación con IA moderno.
Por qué empezar aquí
Muchos tutoriales saltan directamente a los embeddings y las bases de datos vectoriales. Es como intentar construir Google antes de que exista ninguna página web. Lo bien que un asistente entiende un repositorio antes de que entre en juego el aprendizaje automático determina en gran medida lo bueno que es. Si el índice es malo, no hay prompt engineering que lo salve. Basura entra, basura sale.
Lo que viene
En este punto el asistente sabe dónde está cada función y cada clase, pero no entiende su significado. El siguiente problema es el chunking. La mayoría de los desarrolladores corta los repositorios en bloques arbitrarios de 500 o 1.000 tokens, algo que suena sensato y es una de las vías más rápidas hacia un asistente mediocre. Lo mejor es el chunking semántico: dividir siguiendo funciones, clases, símbolos y límites lógicos, para que la recuperación siga la forma en que los ingenieros con experiencia piensan el código. El resultado es un índice del repositorio hecho de unidades con sentido, listo para embeddings, búsqueda vectorial y una recuperación más inteligente.