Cómo el cursor indexa realmente su código base

Si combina sus entornos de desarrollo (IDE) con agentes de codificación, probablemente haya visto sugerencias y ediciones de código que son sorprendentemente precisas y relevantes.

Este nivel de calidad y precisión proviene de que los agentes se basan en un conocimiento profundo de su código base.

Tome Cursor como ejemplo. En la pestaña Índice y documentos, puede ver una sección que muestra que Cursor ya ha “ingerido” e indexado el código base de su proyecto:

Sección Indexación y documentos en la pestaña Configuración del cursor | Imagen del autor

Entonces, ¿cómo construimos una comprensión integral de una base de código en primer lugar?

En esencia, la respuesta es la generación de recuperación aumentada (RAG), un concepto con el que muchos lectores quizás ya estén familiarizados. Como la mayoría de los sistemas basados ​​en RAG, estas herramientas se basan en la búsqueda semántica como capacidad clave.

En lugar de organizar el conocimiento únicamente mediante texto sin formato, el código base se indexa y recupera según el significado.

Esto permite que las consultas en lenguaje natural obtengan los códigos más relevantes, que los agentes codificadores pueden usar para razonar, modificar y generar respuestas de manera más efectiva.

En este artículo, exploramos la canalización RAG en Cursor que permite a los agentes de codificación hacer su trabajo utilizando el conocimiento contextual del código base.

Contenido

(1) Exploración del canal RAG de Codebase
(2) Mantener actualizado el índice de base de código
(3) Resumiendo

(1) Exploración del canal RAG de Codebase

Exploremos los pasos en el proceso RAG de Cursor para indexar y contextualizar bases de código:

Paso 1: fragmentación

En la mayoría de los canales de RAG, primero tenemos que gestionar la carga de datos, el preprocesamiento de texto y el análisis de documentos de múltiples fuentes.

Sin embargo, cuando se trabaja con un código base, gran parte de este esfuerzo se puede evitar. El código fuente ya está bien estructurado y organizado limpiamente dentro de un repositorio de proyecto, lo que nos permite omitir el análisis de documentos habitual y pasar directamente a la fragmentación.

En este contexto, el objetivo de la fragmentación es dividir el código en unidades significativas y semánticamente coherentes (por ejemplo, funciones, clases y bloques de código lógico) en lugar de dividir el texto del código de forma arbitraria.

La fragmentación del código semántico garantiza que cada fragmento capture la esencia de una sección de código particular, lo que lleva a una recuperación más precisa y una generación útil en sentido posterior.

Para hacer esto más concreto, veamos cómo funciona la fragmentación de código. Considere el siguiente ejemplo de secuencia de comandos de Python (no se preocupe por lo que hace el código; aquí la atención se centra en su estructura):

Después de aplicar la fragmentación del código, el script se divide claramente en cuatro fragmentos estructuralmente significativos y coherentes:

Como puede ver, los fragmentos son significativos y contextualmente relevantes porque respetan la semántica del código. En otras palabras, la fragmentación evita dividir el código en medio de un bloque lógico a menos que lo requieran restricciones de tamaño.

En la práctica, significa que las divisiones de fragmentos tienden a crearse entre funciones en lugar de dentro de ellas, y entre declaraciones en lugar de en la línea media.

Para el ejemplo anterior, utilicé Chonkie, un marco liviano de código abierto diseñado específicamente para la fragmentación de código. Proporciona una forma sencilla y práctica de implementar la fragmentación de código, entre muchas otras técnicas de fragmentación disponibles.

[Optional Reading] Bajo el capó de la fragmentación de código

La fragmentación del código anterior no es accidental ni se logra dividiendo ingenuamente el código mediante recuentos de caracteres o expresiones regulares.

Comienza con la comprensión de la sintaxis del código. El proceso normalmente comienza utilizando un analizador de código fuente (como tree-sitter) para convertir el código sin formato en un árbol de sintaxis abstracta (AST).

Un árbol de sintaxis abstracta es esencialmente una representación de código en forma de árbol que captura su estructura, y no el texto real. En lugar de ver el código como una cadena, el sistema ahora lo ve como unidades lógicas de código como funciones, clases, métodos y bloques.

Considere la siguiente línea de código Python:

x = a + b

En lugar de tratarlo como texto plano, el código se convierte en una estructura conceptual como esta:

Asignación ├── Variable(x) └── BinaryExpression(+) ├── Variable(a) └── Variable(b)

Esta comprensión estructural es lo que permite una fragmentación eficaz del código.

Cada construcción de código significativa, como una función, bloque o declaración, se representa como un nodo en el árbol de sintaxis.

Ilustración de ejemplo de un árbol de sintaxis abstracta simple | Imagen del autor

En lugar de operar con texto sin formato, la fragmentación funciona directamente en el árbol de sintaxis.

El fragmentador atravesará estos nodos y agrupará los adyacentes hasta alcanzar un límite de token, produciendo fragmentos que son semánticamente coherentes y de tamaño limitado.

A continuación se muestra un ejemplo de un código un poco más complicado y el correspondiente árbol de sintaxis abstracta:

mientras b != 0: si a > b: a := a – b else: b := b – a return

Ejemplo de sintaxis abstracta libre | Imagen utilizada bajo Creative Commons

Paso 2: Generar incrustaciones y metadatos

Una vez que se preparan los fragmentos, se aplica un modelo de incrustación para generar una representación vectorial (también conocida como incrustaciones) para cada fragmento de código.

Estas incorporaciones capturan el significado semántico del código, lo que permite que la recuperación de consultas de los usuarios y los mensajes de generación coincidan con código semánticamente relacionado, incluso cuando las palabras clave exactas no se superponen.

Esto mejora significativamente la calidad de recuperación para tareas como la comprensión, refactorización y depuración del código.

Más allá de generar incrustaciones, otro paso crítico es enriquecer cada fragmento con metadatos relevantes.

Por ejemplo, los metadatos, como la ruta del archivo y el rango de líneas de código correspondiente para cada fragmento, se almacenan junto con su vector de incrustación.

Estos metadatos no solo proporcionan un contexto importante sobre el origen de un fragmento, sino que también permiten el filtrado de palabras clave basado en metadatos durante la recuperación.

Paso 3: mejorar la privacidad de los datos

Como ocurre con cualquier sistema basado en RAG, la privacidad de los datos es una preocupación primordial. Naturalmente, esto plantea la cuestión de si las rutas de los archivos en sí mismas pueden contener información confidencial.

En la práctica, los nombres de archivos y directorios a menudo revelan más de lo esperado, como estructuras internas de proyectos, nombres en clave de productos, identificadores de clientes o límites de propiedad dentro de una base de código.

Como resultado, las rutas de los archivos se tratan como metadatos confidenciales y requieren un manejo cuidadoso.

Para solucionar esto, Cursor aplica ofuscación de ruta de archivo (también conocido como enmascaramiento de ruta) en el lado del cliente antes de transmitir cualquier dato. Cada componente de la ruta, dividido por / y ., se enmascara mediante una clave secreta y un pequeño nonce fijo.

Este enfoque oculta los nombres reales de archivos y carpetas y al mismo tiempo conserva suficiente estructura de directorios para admitir una recuperación y un filtrado eficaces.

Por ejemplo, src/paids/invoice_processor.py se puede transformar en a9f3/x72k/qp1m8d.f4.

Nota: Los usuarios pueden controlar qué partes de su código base se comparten con Cursor utilizando un archivo .cursorigignore. Cursor hace todo lo posible para evitar que el contenido enumerado se transmita o se haga referencia a él en las solicitudes de LLM.

Paso 4: almacenamiento de incrustaciones

Una vez generadas, las incrustaciones de fragmentos (con los metadatos correspondientes) se almacenan en una base de datos vectorial utilizando Turbopuffer, que está optimizado para una búsqueda semántica rápida en millones de fragmentos de código.

Turbopuffer es un motor de búsqueda de alto rendimiento sin servidor que combina búsqueda vectorial y de texto completo y está respaldado por almacenamiento de objetos de bajo costo.

Para acelerar la reindexación, las incrustaciones también se almacenan en caché en AWS y se codifican mediante el hash de cada fragmento, lo que permite reutilizar el código sin cambios en ejecuciones de indexación posteriores.

Desde una perspectiva de privacidad de datos, es importante tener en cuenta que en la nube solo se almacenan incrustaciones y metadatos. Significa que nuestro código fuente original permanece en nuestra máquina local y nunca se almacena en los servidores de Cursor o en Turbopuffer.

Paso 5: Ejecutar la búsqueda semántica

Cuando enviamos una consulta en Cursor, primero se convierte en un vector utilizando el mismo modelo de incrustación para la generación de incrustaciones de fragmentos. Garantiza que tanto las consultas como los fragmentos de código vivan en el mismo espacio semántico.

Desde la perspectiva de la búsqueda semántica, el proceso se desarrolla de la siguiente manera:

Cursor compara la incrustación de consultas con las incrustaciones de código en la base de datos vectorial para identificar los fragmentos de código semánticamente más similares. Turbopuffer devuelve estos fragmentos candidatos en orden de clasificación según sus puntuaciones de similitud. Dado que el código fuente sin formato nunca se almacena en la nube o en la base de datos vectorial, los resultados de la búsqueda consisten únicamente en metadatos, específicamente las rutas de archivos enmascaradas y los rangos de líneas de código correspondientes. Al resolver los metadatos de las rutas de archivos descifrados y los rangos de líneas, el cliente local puede recuperar los fragmentos de código reales de la base de código local. Los fragmentos de código recuperados, en su formato de texto original, se proporcionan como contexto junto con la consulta al LLM para generar una respuesta contextual.

Como parte de una estrategia de búsqueda híbrida (semántica + palabra clave), el agente de codificación también puede utilizar herramientas como grep y ripgrep para localizar fragmentos de código basados ​​en coincidencias exactas de cadenas.

OpenCode es un popular marco de agente de codificación de código abierto disponible en terminales, IDE y entornos de escritorio.

A diferencia de Cursor, funciona directamente en el código base mediante búsqueda de texto, coincidencia de archivos y navegación basada en LSP en lugar de búsqueda semántica basada en incrustaciones.

Como resultado, OpenCode proporciona una fuerte conciencia estructural pero carece de las capacidades de recuperación semántica más profundas que se encuentran en Cursor.

Como recordatorio, nuestro código fuente original no se almacena en los servidores de Cursor ni en Turbopuffer.

Sin embargo, al responder una consulta, Cursor aún necesita pasar temporalmente los fragmentos de código original relevantes al agente de codificación para que pueda producir una respuesta precisa.

Esto se debe a que las incrustaciones de fragmentos no se pueden utilizar para reconstruir directamente el código original.

El código de texto sin formato se recupera solo en el momento de la inferencia y solo para los archivos y líneas específicos necesarios. Fuera de este tiempo de ejecución de inferencia de corta duración, el código base no se almacena ni persiste de forma remota.

(2) Mantener actualizado el índice de base de código

Descripción general

Nuestra base de código evoluciona rápidamente a medida que aceptamos las ediciones generadas por el agente o realizamos cambios de código manuales.

Para mantener precisa la recuperación semántica, Cursor sincroniza automáticamente el índice de código mediante comprobaciones periódicas, normalmente cada cinco minutos.

Durante cada sincronización, el sistema detecta cambios de forma segura y actualiza solo los archivos afectados eliminando incrustaciones obsoletas y generando otras nuevas.

Además, los archivos se procesan en lotes para optimizar el rendimiento y minimizar la interrupción de nuestro flujo de trabajo de desarrollo.

Usando árboles Merkle

Entonces, ¿cómo hace Cursor para que esto funcione tan perfectamente? Escanea la carpeta abierta y calcula un árbol Merkle de hashes de archivos, lo que permite al sistema detectar y rastrear de manera eficiente los cambios en todo el código base.

Muy bien, entonces, ¿qué es un árbol Merkle?

Es una estructura de datos que funciona como un sistema de huellas digitales criptográficas, lo que permite realizar un seguimiento eficiente de los cambios en un gran conjunto de archivos.

Cada archivo de código se convierte en una huella digital corta y estas huellas digitales se combinan jerárquicamente en una única huella digital de nivel superior que representa toda la carpeta.

Cuando un archivo cambia, sólo es necesario actualizar su huella digital y una pequeña cantidad de huellas digitales relacionadas.

Ilustración de un árbol Merkle | Imagen utilizada bajo Creative Commons

El árbol Merkle del código base se sincroniza con el servidor Cursor, que comprueba periódicamente si hay discrepancias en las huellas dactilares para identificar qué ha cambiado.

Como resultado, puede identificar qué archivos se modificaron y actualizar solo esos archivos durante la sincronización del índice, manteniendo el proceso rápido y eficiente.

Manejo de diferentes tipos de archivos

Así es como Cursor maneja eficientemente diferentes tipos de archivos como parte del proceso de indexación:

Archivos nuevos: agregados automáticamente al índice Archivos modificados: incrustaciones antiguas eliminadas, nuevas creadas Archivos eliminados: eliminados rápidamente del índice Archivos grandes/complejos: se pueden omitir por motivos de rendimiento

Nota: La indexación del código base del cursor comienza automáticamente cada vez que abre un espacio de trabajo.

(3) Resumiendo

En este artículo, miramos más allá de la generación de LLM para explorar el proceso detrás de herramientas como Cursor que crea el contexto adecuado a través de RAG.

Al fragmentar el código a lo largo de límites significativos, indexarlo de manera eficiente y actualizar continuamente ese contexto a medida que evoluciona la base del código, los agentes de codificación pueden ofrecer sugerencias mucho más relevantes y confiables.