Qué es el graph engineering: el mapa que tu agente no tiene
Le pedí a un agente que renombrara una función. getUserData → fetchUserProfile. Dos minutos de trabajo.
Hizo grep, encontró siete referencias, las cambió, corrió los tests. Verde. Commit.
Reventó al día siguiente. La función también se invocaba desde un mapa de handlers, handlers[action], con el nombre viajando como string dentro de un JSON de configuración. Grep encontró siete referencias. Había doce.
El agente no falló por falta de contexto ni por usar un modelo flojo. Falló porque grep solo compara cadenas y nadie le dio un mapa de relaciones. De eso va el graph engineering.
Qué es el graph engineering (y el lío que hay con el nombre)
Graph engineering es la práctica de representar tu código y tu documentación como un grafo explícito de relaciones: los nodos son símbolos —archivos, funciones, clases, conceptos— y las aristas son las relaciones reales entre ellos: importa, llama, hereda, contiene, referencia.
En vez de que el agente busque texto y adivine, navega aristas.
Antes de seguir, un aviso honesto: el término no tiene una definición canónica única en 2026. Se usa para dos cosas distintas.
La primera es el grafo de orquestación. Nodos como unidades de ejecución, aristas como flujo de control: LangGraph, org graphs, work graphs. Ahí el "graph engineering" es diseñar cómo se conectan varios agentes. Es la conversación que arrancó Peter Steinberger en julio de 2026 con una pregunta de seis palabras — "Are we still talking loops or did we shift to graphs yet?" — y que es la continuación natural de lo que conté en loop engineering.
La segunda es el grafo de recuperación. Nodos como símbolos de tu código, aristas como dependencias reales. Aquí no se decide qué agente actúa después: se decide qué sabe el agente antes de tocar nada.
Este post va de la segunda. Y no compiten: una es control de flujo, la otra es recuperación. Misma palabra, dos capas del stack.
Si necesitas el atajo: cuando hables de LangGraph o de coordinar varios agentes, es la primera. Cuando hables de qué código ve tu agente antes de editar, es la segunda.
Las tres preguntas que ni grep ni los embeddings responden
Hay tres preguntas sobre tu código que ni la búsqueda por texto ni la búsqueda semántica pueden responder:
- Si cambio esto, ¿qué se rompe? El radio de impacto a uno, dos o tres saltos. Grep te da el primer nivel. El transitivo no lo ve nadie.
- ¿Quién llama a quién? El call graph completo, con su dirección. Grep te dice que dos archivos mencionan
AuthService. No te dice cuál lo consume y cuál lo define. - ¿Qué depende de qué — y qué no depende de nada? Los nodos con grado cero son código muerto, y salen solos. Buscar código muerto con grep es un ejercicio de paciencia.
Las tres son preguntas sobre topología, no sobre contenido. Por eso hacen falta aristas — y por eso las dos herramientas que usas hoy se quedan cortas.
Tu agente tiene dos formas de encontrar código, y las dos tienen el mismo agujero.
Grep busca coincidencia exacta de texto. Es preciso, rápido y determinista. No sabe nada de significado ni de estructura. Si la referencia está construida en runtime, no existe para grep.
Los embeddings buscan parecido semántico. Encuentran la función de autenticación aunque se llame verificarCredenciales. Pero "se parece" no es "está conectado con". Un chunk sobre logging y otro sobre logging viven cerca en el espacio vectorial aunque uno nunca llame al otro. Es la limitación estructural de RAG que ya toqué en RAG vs fine-tuning.
Las dos herramientas responden "¿dónde aparece esto?". Ninguna responde "¿con qué está conectado esto?".
Resumido, con la tercera vía al lado:
| Grep | Embeddings | Grafo de código | |
|---|---|---|---|
| Pregunta que responde | ¿Dónde aparece esta cadena? | ¿Dónde hay algo parecido a esto? | ¿Con qué está conectado esto? |
| Qué necesitas saber antes | El nombre exacto | Una descripción aproximada | Que el símbolo exista |
| Ve el segundo salto | No | No | Sí — affected --depth 2 |
| Ve llamadas indirectas | No | No | Sí, marcadas como INFERRED |
| Nivel de certeza | Binario: aparece o no aparece | Puntuación de similitud | EXTRACTED o INFERRED |
| Coste de mantenerlo | Cero | Reindexar + coste de embeddings | Re-extracción AST, sin LLM |
| Dónde se rompe | La referencia se construye en runtime | Dos cosas se parecen pero no se llaman | Código muy dinámico: DI por string, metaprogramación |
Anatomía del grafo: nodos, aristas y confianza
Un grafo de código tiene tres piezas: nodos (los símbolos: archivos, funciones, clases), aristas (las relaciones entre ellos) y un nivel de confianza por arista.
Lo concreto. Construí un grafo con graphify —CLI open source, parseo AST local con tree-sitter, sin vector store— sobre un proyecto pequeño que tengo por ahí. Pequeño a propósito: quería poder verificar a mano cada arista antes de creerme nada. Salieron 115 nodos y 240 aristas.
Los nodos llevan poco: id, etiqueta, archivo de origen y línea. Lo interesante está en las aristas.
{
"source": "src_chunker",
"target": "src_chunker_needs_chunking",
"relation": "contains",
"confidence": "EXTRACTED",
"source_file": "src/chunker.py",
"source_location": "L10"
}
Los ocho tipos de relación que aparecieron en ese grafo:
| Relación | Qué conecta | ¿La ve grep? |
|---|---|---|
imports / imports_from |
Archivo → módulo o símbolo importado | Sí, si el nombre aparece literal |
contains |
Archivo → función o clase que declara | Parcialmente |
calls |
Función → función que invoca | Solo el primer nivel |
references |
Símbolo usado sin invocarlo | Sí, si el nombre aparece literal |
inherits |
Clase → clase base | Sí |
method |
Clase → método que le pertenece | Sí |
indirect_call |
Llamada resuelta en runtime | No |
Fíjate en la última fila. indirect_call es exactamente la llamada que grep no ve.
Y ahora el campo que más me interesa de todo esto, el que casi nadie menciona: confidence. Cada arista viene marcada como EXTRACTED o INFERRED. En mi grafo: 197 extraídas, 43 inferidas.
EXTRACTED significa que la relación está literalmente en el AST. El parser la leyó, no la dedujo. INFERRED significa que la resolvió el motor uniendo puntos — una llamada cuyo destino tuvo que deducirse.
Eso cambia cómo usas el resultado. Una arista EXTRACTED la das por buena. Una INFERRED es una hipótesis con nombre y apellidos que puedes ir a verificar al archivo y la línea que te da. Ni los embeddings ni grep te dan esa distinción: grep afirma sin matices, y el score de un embedding te dice cuánto se parece algo, nunca de dónde sale la relación. Aquí lo que se etiqueta es la procedencia.
Un explain sobre un nodo devuelve esto:
Node: needs_chunking()
Source: src/chunker.py L10
Degree: 6
Connections (6):
<-- main() [calls] [INFERRED]
<-- transcribe() [calls] [INFERRED]
<-- chunker.py [contains] [EXTRACTED]
--> Path [references] [EXTRACTED]
<-- test_needs_chunking_false_for_small_file() [calls] [INFERRED]
<-- test_needs_chunking_true_for_large_file() [calls] [INFERRED]
Seis líneas. Ahí está el vecindario directo de esa función, con la dirección de cada arista y el nivel de confianza de cada una. Para llegar a lo mismo con grep necesitas varias pasadas y saber de antemano qué buscar.
Pero el vecindario directo no es el radio de impacto. Para eso hay un comando aparte, que es el que responde literalmente a la pregunta 1: un recorrido inverso por las aristas que tú elijas, a la profundidad que tú digas.
graphify affected "needs_chunking" --depth 2 --relation calls
Affected nodes for needs_chunking()
Relations: calls
Depth: 2
- test_needs_chunking_false_for_small_file() [calls] tests/test_chunker.py:L24
- test_needs_chunking_true_for_large_file() [calls] tests/test_chunker.py:L30
- main() [calls] transcribe.py:L17
- transcribe() [calls] watch.py:L36
- test_output_flag_saves_to_specified_path() [calls] tests/test_integration.py:L34
- test_file_not_found_exits_with_code_1() [calls] tests/test_integration.py:L48
- test_unsupported_format_exits_with_code_1() [calls] tests/test_integration.py:L55
- test_api_key_not_in_output() [calls] tests/test_integration.py:L65
- .on_created() [calls] watch.py:L72
Mira la diferencia. De las seis conexiones del explain, solo cuatro eran llamadas entrantes. El affected a dos saltos da nueve, y las cinco nuevas son las interesantes: los cuatro tests de integración y el handler .on_created() del watcher no tocan needs_chunking directamente, llegan a través de main() y transcribe().
Ese es el segundo nivel. El que revienta en producción al día siguiente y el que ninguna búsqueda por texto te va a dar, porque no hay ninguna cadena que buscar: la relación existe en la topología, no en el código fuente de esos archivos.
Aquí está la tesis, y quiero decirla sin vender humo: el grafo no te garantiza encontrar la referencia indirecta. Te da una categoría donde esa relación puede existir y quedar marcada. Grep ni siquiera tiene esa categoría. Esa es toda la diferencia, y es suficiente.
Cómo usar un grafo de código con un agente de coding, en 3 pasos
Tres piezas.
Uno: construyes el grafo y lo dejas en el repo. graphify-out/graph.json más un reporte en markdown. Es un artefacto de tu proyecto, como el lockfile.
uv tool install graphifyy # doble "y" mientras reclaman el nombre en PyPI;
# el comando y el skill siguen siendo graphify
graphify install # registra el skill en tu agente
graphify update . # re-extrae solo lo que cambió, sin LLM
Dos: le das al agente una regla de precedencia. Sin esto no sirve de nada, porque el modelo tira de grep por costumbre. En el CLAUDE.md del proyecto:
- Para preguntas sobre el código, ejecuta primero `graphify query "<pregunta>"`.
Usa `graphify path "<A>" "<B>"` para relaciones, `graphify explain "<X>"`
para un concepto concreto y `graphify affected "<X>"` antes de modificar o
borrar algo. Devuelven un subgrafo acotado, mucho más pequeño que el reporte
completo o la salida cruda de grep.
- Después de modificar código, ejecuta `graphify update .`.
Esa regla es la diferencia entre tener un grafo y usarlo. Es la misma idea de fondo que trabajo en el curso de Construye con IA: el agente no es más listo por tener más herramientas, sino por tener reglas claras de cuándo usar cuál.
Tres: el grafo entra en la ventana como subgrafo, no como volcado. Un explain devuelve seis líneas donde un grep te vuelca cada aparición del término y tú decides después: recuperas menos tokens y mejores, que es el objetivo del context engineering.
Ojo con una cosa: graphify query no devuelve una respuesta en prosa. Devuelve un recorrido BFS con los nodos encontrados. Es una herramienta de recuperación dentro del harness, no un chatbot. Quien interpreta el subgrafo sigue siendo el modelo.
Y un apunte de higiene: el proyecto publica cifras de benchmark en su README. Son autoreportadas. Trátalas como lo que son y mide en tu repo.
Cuándo NO merece la pena montar un grafo de código
No todo proyecto necesita esto. Cuatro casos donde el grafo estorba más de lo que ayuda.
Proyectos pequeños. Si el código entra entero en la ventana, el agente ya tiene el grafo en la cabeza y mejor resuelto. Montar recuperación para veinte archivos es sobreingeniería.
Código muy dinámico. Metaprogramación intensa, inyección de dependencias por string, event buses, decoradores que reescriben comportamiento en runtime. El AST no puede ver lo que solo existe cuando el proceso arranca. El grafo saldrá con más aristas INFERRED que EXTRACTED, o directamente con huecos. Sigue siendo mejor que grep, pero baja mucho el techo.
Y sí: el bug con el que abrí este post vive justo en esta frontera. Un nombre viajando dentro de un JSON no está en ningún AST. Lo que cambia es que el grafo marca ese hueco como INFERRED o lo deja sin arista, y eso es una señal que puedes leer. Grep te devuelve siete referencias con la misma cara de seguridad que si fueran las doce.
Si no puedes mantenerlo actualizado. Un grafo obsoleto es peor que no tener grafo, porque el agente confía en él. Necesitas graphify update en un hook de pre-commit, en CI o con graphify watch. Si esto no está automatizado, no lo montes: en dos semanas tienes un mapa de un territorio que ya no existe.
Si lo que buscas es "qué debería hacer este sistema". El grafo describe el código que existe, no la intención. Para eso el artefacto es la spec — que es, por cierto, otra forma de estructura explícita, y la razón por la que escribí el libro de Spec-Driven Development. El grafo cuenta el presente. La spec define el futuro.
Y un apunte de madurez: graphify va por la 0.9.x. No es 1.0 todavía, y se nota. Herramienta útil, no infraestructura estable.
Cómo empezar con graph engineering hoy
Coge tu repo más feo. El que da miedo tocar.
Construye el grafo, ejecuta un explain sobre la función que más te intimida y mira su grado. Si el número te sorprende, acabas de descubrir por qué ese refactor lleva meses aplazado.
Si quieres ver cómo encaja esto con el resto del stack —agentes, MCP, memoria, specs— lo trabajamos a fondo en Dominicode Labs, con proyectos reales y no con ejemplos de juguete.
Preguntas frecuentes
¿Graph engineering es lo mismo que GraphRAG?
No exactamente. GraphRAG es la implementación de Microsoft que usa un LLM para extraer entidades y relaciones de texto no estructurado, detectar comunidades y resumirlas. Está pensado para corpus documentales.
Graph engineering es el concepto general de estructurar conocimiento como grafo. Aplicado a código, el grafo se extrae del AST de forma determinista, sin LLM y sin coste por token. GraphRAG es una implementación posible, no la única ni la más barata para código.
¿Qué diferencia hay entre graph engineering y loop engineering?
El loop engineering diseña el bucle de ejecución del agente: qué hace, cómo verifica el resultado y cuándo vuelve a intentarlo. El graph engineering, en la acepción de este post, diseña lo que el agente sabe antes de entrar en ese bucle: un mapa de relaciones de tu código en vez de una búsqueda de texto.
No compiten. Un agente con un buen bucle y sin mapa repite el mismo error más rápido. Si tus fallos vienen de contexto estructural incompleto, el grafo rinde antes que otra iteración del loop.
¿Funciona con TypeScript o solo con Python?
Los ejemplos de este post salen de un proyecto en Python, pero la extracción es por AST con tree-sitter y las gramáticas que trae cubren los lenguajes habituales: Python, TypeScript, JavaScript, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala y PHP.
Con TypeScript hay un matiz: cuanto más tira el proyecto de inyección por token, decoradores y factories, más aristas caen en INFERRED. El grafo sigue siendo mejor que grep, pero léelo sabiendo qué parte es hipótesis.
¿Necesito una base de datos de grafos como Neo4j?
Para un repo, no. El grafo de un proyecto normal cabe en un JSON en disco y se recorre con un BFS en memoria. Herramientas como graphify funcionan así, sin servidor y sin dependencias externas.
Neo4j tiene sentido cuando el grafo es un producto en sí mismo, se consulta desde varios servicios o supera lo que quieres cargar en memoria. Para dar contexto estructural a un agente en tu máquina, es infraestructura que no necesitas.
¿El grafo sustituye a los embeddings y a la búsqueda semántica?
No, y montarlo como sustituto es un error. Responden preguntas distintas.
Los embeddings responden "¿dónde hay algo parecido a esto?" y toleran que no sepas los nombres exactos. El grafo responde "¿con qué está conectado esto?" y exige que el símbolo exista. Lo razonable es tener las dos vías y una regla de precedencia: para preguntas de estructura, grafo; para exploración difusa, semántica; para strings literales, grep.
¿Cada cuánto hay que reconstruir el grafo?
En cada cambio de código relevante, y automatizado. La re-extracción incremental de código no necesita LLM, así que el coste es tiempo de CPU, no dinero.
Lo práctico es un hook de pre-commit, un paso en CI o un proceso en watch mientras trabajas. Reconstruirlo a mano cuando te acuerdas es la vía rápida a un grafo obsoleto, y un grafo obsoleto le miente al agente con toda la confianza del mundo.
¿Sirve en monorepos grandes?
Es donde más rinde, precisamente porque el código ya no cabe en la ventana de contexto y grep devuelve ruido. La pega es operativa: la visualización HTML se vuelve pesada por encima de unos miles de nodos, y para eso está la opción de saltarla y quedarte solo con el JSON consultable, que es lo que consume el agente.
Y si tu organización tiene varios repos en vez de uno solo, puedes fusionar sus grafos en uno para cruzar dependencias entre paquetes.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
