← Volver al Blog

GraphRAG vs RAG: 7 pasos para tu primer knowledge graph de código en 30 min

Este es un tutorial de código puro y duro. Si buscabas el cuadro comparativo “¿cuándo GraphRAG y cuándo RAG vectorial?”, eso lo tengo en otro artículo. Aquí montamos un knowledge graph de tu propio código en 30 minutos y medimos cuántos tokens te ahorra frente a RAG vectorial en la misma pregunta.

La palabra “knowledge graph” viene cargada, así que aclaro: el grafo del que hablo es de código. La mayoría de tutoriales que te encontrarás por ahí tiran de artículos, wikis o PDFs; aquí la fuente es tu propio repo y la pregunta que queremos contestar suena tal cual: “si modifico esta función, ¿qué se rompe?”, sin quemar 40k tokens de contexto por el camino.

Los 7 pasos vienen del framework oficial de Neo4j, adaptados para código con Tree-sitter y con los apaños que me tocó hacer la primera vez que lo monté.

Los 7 pasos para construir un knowledge graph de código

Paso 1 — Define el caso de uso (2 min)

El paso más importante y el que casi nadie hace bien. Sin un caso de uso concreto encima de la mesa, el grafo acaba acumulando polvo a los pocos días.

Ejemplo malo: “quiero pasar todo mi código a grafo.”

Ejemplo bueno: “quiero que un ingeniero nuevo entienda en 30 segundos qué servicios se rompen cuando cambio esta función.”

El caso de uso te fija tres cosas de golpe: qué entidades (nodos) hacen falta, qué relaciones (aristas) importan y qué consultas tienen que ir rápidas. En este tutorial nos quedamos con análisis de blast radius: dado un archivo tocado, ¿qué otros archivos y tests se ven afectados?

Paso 2 — Identifica las fuentes (1 min)

Para código, la fuente se cae por su propio peso: el árbol de archivos del repo. Filtramos por lenguaje —Python en este ejemplo— y dejamos fuera node_modules/, .venv/ y archivos generados.

find src/ -name "*.py" -not -path "*/venv/*" > files.txt
wc -l files.txt

Si tu repo pasa de los 3000 archivos Python, ordena por git log reciente y quédate con los últimos 1000. En un grafo así, cubrirlo todo pesa más de lo que ayuda.

Paso 3 — Extrae la estructura con Tree-sitter (5 min)

Tree-sitter parsea código a árbol sintáctico abstracto (AST) sin meter LLMs por el medio. Cubre más de 19 lenguajes, corre en local y va rapidísimo. Cuando escribí esto la versión estable era tree-sitter-python 0.23.6.

# pip install tree-sitter==0.23.6 tree-sitter-python==0.23.6
import tree_sitter_python
from tree_sitter import Language, Parser

PY_LANGUAGE = Language(tree_sitter_python.language())
parser = Parser(PY_LANGUAGE)

def extract_symbols(file_path: str):
    with open(file_path, "rb") as f:
        tree = parser.parse(f.read())
    symbols = []
    for node in tree.root_node.children:
        if node.type == "function_definition":
            name = node.child_by_field_name("name").text.decode()
            symbols.append({"kind": "function", "name": name, "file": file_path})
        elif node.type == "class_definition":
            name = node.child_by_field_name("name").text.decode()
            symbols.append({"kind": "class", "name": name, "file": file_path})
    return symbols

Por cada archivo te devuelve una lista de funciones y clases con su ubicación. Lectura estructurada, sin más historia. La primera vez que ves un AST bien parseado da una sensación curiosa: como si alguien te prestase los planos del edificio en el que llevas meses viviendo.

Paso 4 — Diseña la ontología (5 min)

La ontología es el plano del grafo. Para blast radius de código, con tres nodos y tres aristas nos vale:

Nodos:
  (:File   {path, language, size})
  (:Symbol {name, kind, line})     # función, clase, método
  (:Test   {path, target_symbol})

Aristas:
  (File)   -[:DEFINES]->  (Symbol)
  (Symbol) -[:CALLS]->    (Symbol)
  (Test)   -[:COVERS]->   (Symbol)

Regla que aprendí a base de palos: diseña la ontología desde las consultas, no desde los datos. Si tu pregunta es “¿qué tests cubren esta función?”, te hace falta la arista COVERS. Y si esa pregunta no está sobre la mesa, mejor no meterla.

Paso 5 — Carga los datos a Neo4j (7 min)

Neo4j Community Edition levanta en Docker con un solo comando:

docker run --name neo4j-code \
  -p 7474:7474 -p 7687:7687 \
  -e NEO4J_AUTH=neo4j/testpass \
  -d neo4j:5.24-community

Después, con el driver de Python, insertas los símbolos del Paso 3:

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687",
                              auth=("neo4j", "testpass"))

def load_symbol(tx, sym):
    tx.run("""
        MERGE (f:File {path: $file})
        MERGE (s:Symbol {name: $name, file: $file})
          SET s.kind = $kind, s.line = $line
        MERGE (f)-[:DEFINES]->(s)
    """, file=sym["file"], name=sym["name"],
         kind=sym["kind"], line=sym.get("line", 0))

with driver.session() as session:
    for sym in all_symbols:
        session.execute_write(load_symbol, sym)

Las aristas CALLS piden un segundo pase: por cada función miras qué nombres invoca y creas la arista si el destino existe como símbolo. Si te choca ver tanto MERGE, es porque funciona como UPSERT — crea el nodo cuando falta y respeta el que ya esté.

Paso 6 — La consulta que hace todo esto valer la pena (5 min)

Blast radius en Cypher, en una sola consulta:

// ¿Qué se rompe si cambio la función `get_user`?
MATCH path = (target:Symbol {name: "get_user"})<-[:CALLS*1..3]-(caller:Symbol)
RETURN caller.name AS symbol,
       length(path) AS distancia,
       caller.file AS archivo
ORDER BY distancia ASC
LIMIT 20

La respuesta llega en milisegundos, ni te da tiempo a mirar el reloj. Te devuelve las funciones que llaman a get_user en 1, 2 o 3 saltos, sin inventarse rutas que no existen.

Ahora compáralo con RAG vectorial resolviendo lo mismo:

  • RAG vectorial: embebe los últimos 40 archivos modificados, recupera los 10 chunks más parecidos, los mete en un prompt de 40k tokens y le pide al LLM que razone. Tiempo: 8-15s. Coste: 40k tokens × $3/M = $0.12. Precisión: variable, con recall parcial de dependencias transitivas.
  • GraphRAG sobre código: una consulta Cypher directa, 300 tokens de resultado, opcionalmente pasados por un LLM para que te lo explique. Tiempo: 200-400ms. Coste: <1k tokens = $0.003. Precisión: 100% en dependencias directas (es una consulta estructural, no una búsqueda).

En blast radius la diferencia canta. Del orden de 40 veces menos tokens por consulta.

Paso 7 — Operación y expansión (5 min)

El grafo no es un “móntalo y olvídate”. Hay tres tareas de mantenimiento que no te puedes ahorrar:

  • Frescura: un hook de pre-commit (o una GitHub Action) que reindexe los archivos tocados en cada commit. Solo los diffs, sin volver a masticar el repo entero.
  • Calidad: pasar una vez por semana una consulta que detecte nodos huérfanos (símbolos sin DEFINES entrante). Esos huérfanos suelen ser código muerto, o archivos que Tree-sitter no llegó a parsear.
  • Expansión: cuando añades una pregunta nueva (por ejemplo, “¿qué endpoints exponen esta clase?”), a veces necesitas una arista extra (EXPOSES). Reindexa solo lo que cambia.

Si no te apetece llevar el Neo4j tú mismo, Neo4j AuraDB tiene un plan gratuito con 200k nodos, suficiente para repos medianos.

Cuándo GraphRAG gana y cuándo no

Con el grafo ya montado, toca la comparación honesta:

  • Blast radius, dependencias transitivas, “qué llama a qué”: GraphRAG gana por 40× en tokens y por precisión. Es una consulta estructural, sin más.
  • “¿En qué archivo vive la lógica de facturación?”: aquí gana RAG vectorial. Es búsqueda semántica sobre nombres y comentarios, terreno donde el grafo no pinta nada.
  • “Resume qué hace este módulo”: también RAG vectorial + LLM. El grafo no entiende de semántica del texto.

La regla que uso: si la pregunta se puede expresar como un patrón de nodos y aristas, tira de GraphRAG. Si lo que hace falta es semántica del lenguaje natural, RAG vectorial. En un asistente de código de verdad acabas queriendo los dos, con un router sencillo delante que elija el motor según la pregunta.

Un apunte sobre LLMO

Esa misma pregunta del router es la que están resolviendo iniciativas más amplias como llmoframework.com, que empieza a ordenar cómo los buscadores AI recuperan y citan contenido técnico. Un knowledge graph bien diseñado sobre tu código te sirve en dos frentes: acelera el desarrollo hoy y deja preparada la fuente que un futuro asistente va a preferir, con hechos estructurados listos para consultar. Merece la pena invertir el rato ahora en dejar las dependencias explícitas.

En resumen

Lo que llevas montado en 30 minutos:

  1. Un caso de uso concreto (blast radius)
  2. Un archivo de fuentes (los .py de tu repo)
  3. Un extractor con Tree-sitter que no depende de LLM
  4. Una ontología minimalista (3 nodos, 3 aristas)
  5. Neo4j corriendo en Docker con los datos cargados
  6. Una consulta que responde 40× más barato que RAG vectorial en la misma pregunta
  7. Un plan de mantenimiento que no explota

Si te preguntan si GraphRAG “sustituye” a RAG vectorial, la respuesta va con matiz: lo sustituye en el subconjunto de preguntas estructurales, y ese subconjunto en código es enorme. Empieza midiendo cuántas de tus consultas caen ahí. Si superan el 30%, el grafo se amortiza solo en una semana de uso.


ken imoto · WebRTC & Voice AI engineer · kenimoto.dev