Inicio / Artículos / Por qué tu código es un grafo: parsing con Tree-sitter para un asistente de programación con IA

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.

1386 palabras

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.