GraphRAG

Avanzado
LangChain
LangChain
Actualizado: 09/07/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

Knowledge graphs con LLMGraphTransformer

Los knowledge graphs representan información como una red de entidades conectadas por relaciones específicas, permitiendo capturar el contexto semántico que los embeddings vectoriales por sí solos no pueden preservar completamente. En el contexto de RAG, esta estructura nos permite realizar búsquedas que van más allá de la similaridad textual, explorando conexiones conceptuales entre diferentes elementos de nuestros documentos.

LLMGraphTransformer es la herramienta de LangChain que automatiza la extracción de entidades y relaciones desde texto no estructurado utilizando modelos de lenguaje. Esta clase analiza documentos y genera automáticamente los nodos (entidades) y aristas (relaciones) que formarán nuestro grafo de conocimiento.

Configuración básica del transformador

Para comenzar a trabajar con LLMGraphTransformer, necesitamos importar las dependencias necesarias y configurar tanto el modelo de lenguaje como la conexión a la base de datos de grafos:

from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_openai import ChatOpenAI
from langchain_neo4j import Neo4jGraph
from langchain_core.documents import Document

Instalar Neo4j para LangChain

pip install neo4j langchain-neo4j

La configuración inicial requiere establecer la conexión con Neo4j y crear una instancia del transformador:

from langchain_neo4j import Neo4jGraph
from langchain_experimental.graph_transformers import LLMGraphTransformer

from langchain_openai import ChatOpenAI

# Configuración de la base de datos Neo4j
graph = Neo4jGraph(
    url="bolt://localhost:7687",
    username="neo4j",
    password="password"
)

# Configuración del modelo de lenguaje
llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0
)

# Creación del transformador
graph_transformer = LLMGraphTransformer(
    llm=llm,
    allowed_nodes=["Person", "Organization", "Location", "Technology"],
    allowed_relationships=["WORKS_FOR", "LOCATED_IN", "USES", "COLLABORATES_WITH"]
)

Los parámetros allowed_nodes y allowed_relationships son cruciales para mantener la consistencia del grafo. Al especificar tipos permitidos, evitamos la creación de entidades y relaciones redundantes o inconsistentes que podrían surgir de las variaciones en el procesamiento del LLM.

Procesamiento de documentos

El proceso de transformación convierte documentos de texto en estructuras de grafo. Veamos cómo procesar un conjunto de documentos técnicos:

# Documentos de ejemplo sobre tecnología
documents = [
    Document(
        page_content="""
        OpenAI desarrolló GPT-4, un modelo de lenguaje avanzado utilizado 
        por muchas empresas tecnológicas. La empresa tiene su sede en 
        San Francisco y colabora frecuentemente con Microsoft en 
        proyectos de inteligencia artificial.
        """,
        metadata={"source": "tech_news.txt"}
    ),
    Document(
        page_content="""
        Microsoft Azure proporciona servicios de nube que incluyen 
        capacidades de IA. La compañía, ubicada en Redmond, utiliza 
        tecnologías de OpenAI para mejorar sus productos de productividad 
        como Office 365.
        """,
        metadata={"source": "cloud_services.txt"}
    )
]

# Transformación a estructuras de grafo
graph_documents = graph_transformer.convert_to_graph_documents(documents)

El resultado de esta transformación son GraphDocument objects que contienen las entidades extraídas, sus relaciones y los metadatos asociados. Cada documento se procesa independientemente, pero las entidades comunes se pueden unificar posteriormente.

Almacenamiento en Neo4j

Una vez extraídas las estructuras de grafo, necesitamos almacenarlas en Neo4j para poder realizar consultas eficientes:

# Almacenamiento en la base de datos
graph.add_graph_documents(
    graph_documents,
    baseEntityLabel=True,
    include_source=True
)

# Verificación del contenido almacenado
result = graph.query("""
    MATCH (n)-[r]->(m) 
    RETURN n.id as source, type(r) as relationship, m.id as target 
    LIMIT 10
""")

for record in result:
    print(f"{record['source']} -> {record['relationship']} -> {record['target']}")

El parámetro baseEntityLabel asegura que todas las entidades tengan una etiqueta base común, facilitando consultas generales. La opción include_source mantiene la trazabilidad hacia los documentos originales.

Personalización del proceso de extracción

Para casos específicos, podemos personalizar el comportamiento del transformador mediante prompts específicos y configuraciones avanzadas:

# Configuración avanzada con prompt personalizado
custom_transformer = LLMGraphTransformer(
    llm=llm,
    allowed_nodes=["Person", "Company", "Product", "Technology", "Location"],
    allowed_relationships=[
        "DEVELOPS", "USES", "COMPETES_WITH", "PARTNERS_WITH", 
        "LOCATED_IN", "WORKS_FOR"
    ],
    prompt="""
    Extrae entidades y relaciones del siguiente texto, enfocándote en:
    - Personas y sus roles profesionales
    - Empresas y sus productos principales
    - Tecnologías y sus aplicaciones
    - Ubicaciones geográficas relevantes
    
    Texto: {text}
    """
)

Esta personalización permite adaptar la extracción a dominios específicos, mejorando la precisión y relevancia de las entidades y relaciones identificadas.

Manejo de entidades complejas

Cuando trabajamos con documentos que contienen referencias ambiguas o entidades con múltiples menciones, es importante configurar estrategias de resolución:

# Procesamiento con resolución de entidades
def process_with_entity_resolution(documents):
    # Primera pasada: extracción básica
    graph_docs = graph_transformer.convert_to_graph_documents(documents)
    
    # Almacenamiento inicial
    graph.add_graph_documents(graph_docs)
    
    # Consulta para identificar posibles duplicados
    duplicates_query = """
    MATCH (n1), (n2)
    WHERE n1.id <> n2.id 
    AND apoc.text.levenshteinSimilarity(n1.id, n2.id) > 0.8
    RETURN n1.id as entity1, n2.id as entity2, 
           apoc.text.levenshteinSimilarity(n1.id, n2.id) as similarity
    """
    
    potential_duplicates = graph.query(duplicates_query)
    
    # Procesamiento manual o automático de duplicados
    for duplicate in potential_duplicates:
        if duplicate['similarity'] > 0.9:
            print(f"Posible duplicado: {duplicate['entity1']} - {duplicate['entity2']}")

Esta aproximación nos permite identificar y resolver entidades duplicadas que podrían fragmentar nuestro grafo de conocimiento, manteniendo la coherencia de las relaciones.

El LLMGraphTransformer proporciona una base sólida para construir grafos de conocimiento automatizados, pero su efectividad depende en gran medida de la calidad de los prompts utilizados y la configuración específica para cada dominio de aplicación.

Retrieval navegando relaciones entre entidades

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Una vez construido nuestro knowledge graph, podemos aprovechar las relaciones entre entidades para realizar búsquedas más sofisticadas que van más allá de la similaridad semántica tradicional. Este enfoque permite descubrir conexiones implícitas y realizar razonamiento contextual sobre la información almacenada.

Configuración del retriever basado en grafos

Para implementar retrieval navegacional, necesitamos configurar un sistema que pueda traversar las relaciones del grafo mientras mantiene la capacidad de búsqueda semántica:

from langchain_neo4j import Neo4jVector
from langchain_openai import OpenAIEmbeddings
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# Configuración del vector store híbrido
vector_store = Neo4jVector.from_existing_graph(
    embedding=OpenAIEmbeddings(),
    graph=graph,
    node_label="Document",
    text_node_properties=["text"],
    embedding_node_property="embedding"
)

# Retriever base para búsqueda semántica
base_retriever = vector_store.as_retriever(search_kwargs={"k": 5})

Esta configuración híbrida combina la búsqueda vectorial tradicional con la capacidad de navegación por el grafo, permitiendo que cada consulta aproveche ambas modalidades de búsqueda.

Implementación de consultas relacionales

El verdadero poder del GraphRAG se manifiesta cuando implementamos consultas que pueden seguir cadenas de relaciones. Veamos cómo crear un retriever que navega por conexiones específicas:

def create_relationship_query(entity_type, relationship, target_type, max_depth=2):
    """Genera consultas Cypher para navegación relacional"""
    return f"""
    MATCH (start:{entity_type})-[:{relationship}*1..{max_depth}]->(target:{target_type})
    WHERE start.id CONTAINS $entity_name
    RETURN DISTINCT target.id as related_entity, 
           target.text as content,
           length((start)-[:{relationship}*]->(target)) as distance
    ORDER BY distance ASC
    LIMIT 10
    """

def relational_retriever(query: str, entity_name: str, relationship_type: str):
    """Retriever que navega relaciones específicas"""
    
    # Búsqueda semántica inicial
    semantic_results = base_retriever.invoke(query)
    
    # Navegación relacional
    cypher_query = create_relationship_query("Person", relationship_type, "Organization")
    
    relational_results = graph.query(
        cypher_query,
        params={"entity_name": entity_name}
    )
    
    # Combinación de resultados
    combined_context = []
    
    # Agregar contexto semántico
    for doc in semantic_results:
        combined_context.append({
            "content": doc.page_content,
            "source": "semantic_search",
            "metadata": doc.metadata
        })
    
    # Agregar contexto relacional
    for result in relational_results:
        combined_context.append({
            "content": result["content"],
            "source": "relationship_traversal",
            "distance": result["distance"],
            "related_entity": result["related_entity"]
        })
    
    return combined_context

Consultas multi-hop avanzadas

Para casos más complejos, podemos implementar navegación multi-hop que sigue múltiples tipos de relaciones en secuencia:

def multi_hop_retriever(start_entity: str, path_pattern: list, query: str):
    """
    Retriever que sigue patrones de relaciones complejas
    path_pattern: [("Person", "WORKS_FOR"), ("Organization", "USES"), ("Technology", "")]
    """
    
    # Construcción dinámica de la consulta Cypher
    match_clauses = []
    where_clauses = []
    return_clauses = []
    
    for i, (node_type, relationship) in enumerate(path_pattern):
        if i == 0:
            match_clauses.append(f"(n{i}:{node_type})")
            where_clauses.append(f"n{i}.id CONTAINS $start_entity")
        else:
            prev_rel = path_pattern[i-1][1]
            match_clauses.append(f"-[:{prev_rel}]->(n{i}:{node_type})")
        
        return_clauses.append(f"n{i}.id as entity_{i}")
        return_clauses.append(f"n{i}.text as content_{i}")
    
    cypher_query = f"""
    MATCH {' '.join(match_clauses)}
    WHERE {' AND '.join(where_clauses)}
    RETURN {', '.join(return_clauses)}
    LIMIT 5
    """
    
    results = graph.query(cypher_query, params={"start_entity": start_entity})
    
    # Procesamiento de resultados multi-hop
    processed_results = []
    for result in results:
        hop_chain = []
        for i in range(len(path_pattern)):
            if f"entity_{i}" in result:
                hop_chain.append({
                    "entity": result[f"entity_{i}"],
                    "content": result[f"content_{i}"],
                    "hop": i
                })
        processed_results.append(hop_chain)
    
    return processed_results

Integración con LCEL para retrieval contextual

Podemos integrar nuestro retriever relacional en una cadena LCEL que combine automáticamente búsqueda semántica y navegación de grafos:

from langchain_core.prompts import ChatPromptTemplate

# Template que aprovecha contexto relacional
contextual_prompt = ChatPromptTemplate.from_messages([
    ("system", """
    Responde usando tanto el contexto semántico como las relaciones entre entidades.
    
    Contexto semántico:
    {semantic_context}
    
    Relaciones relevantes:
    {relational_context}
    
    Considera las conexiones entre entidades para proporcionar una respuesta completa.
    """),
    ("human", "{question}")
])

def format_relational_context(relational_results):
    """Formatea resultados relacionales para el prompt"""
    formatted = []
    for result in relational_results:
        if result["source"] == "relationship_traversal":
            formatted.append(
                f"- {result['related_entity']} (distancia: {result['distance']}): {result['content'][:200]}..."
            )
    return "\n".join(formatted)

def format_semantic_context(semantic_results):
    """Formatea resultados semánticos para el prompt"""
    return "\n".join([doc["content"][:300] + "..." for doc in semantic_results])

# Cadena LCEL completa
graph_rag_chain = (
    {
        "question": RunnablePassthrough(),
        "semantic_context": lambda x: format_semantic_context(
            relational_retriever(x, "", "")[:3]  # Solo contexto semántico
        ),
        "relational_context": lambda x: format_relational_context(
            relational_retriever(x, "OpenAI", "COLLABORATES_WITH")  # Ejemplo específico
        )
    }
    | contextual_prompt
    | llm
    | StrOutputParser()
)

Optimización de consultas relacionales

Para mejorar el rendimiento de las consultas relacionales, podemos implementar estrategias de caché y optimización:

from functools import lru_cache
import time

class OptimizedGraphRetriever:
    def __init__(self, graph, vector_store, cache_size=128):
        self.graph = graph
        self.vector_store = vector_store
        self.query_cache = {}
        self.cache_size = cache_size
    
    @lru_cache(maxsize=128)
    def _cached_graph_query(self, query_hash, cypher_query, params_str):
        """Caché para consultas Cypher repetidas"""
        params = eval(params_str)  # En producción, usar json.loads
        return self.graph.query(cypher_query, params=params)
    
    def retrieve_with_relationships(self, query: str, entity_focus: str = None):
        """Retrieval optimizado con navegación relacional"""
        
        # Búsqueda semántica base
        semantic_docs = self.vector_store.similarity_search(query, k=3)
        
        # Si hay entidad específica, buscar relaciones
        if entity_focus:
            relationship_query = """
            MATCH (e)-[r]->(related)
            WHERE e.id CONTAINS $entity
            RETURN type(r) as relationship_type, 
                   related.id as related_entity,
                   related.text as related_content
            ORDER BY related.importance DESC
            LIMIT 5
            """
            
            query_hash = hash(f"{relationship_query}_{entity_focus}")
            params_str = str({"entity": entity_focus})
            
            related_entities = self._cached_graph_query(
                query_hash, relationship_query, params_str
            )
            
            # Enriquecimiento del contexto
            enriched_context = []
            for doc in semantic_docs:
                enriched_context.append({
                    "content": doc.page_content,
                    "type": "semantic",
                    "metadata": doc.metadata
                })
            
            for rel in related_entities:
                enriched_context.append({
                    "content": rel["related_content"],
                    "type": "relational",
                    "relationship": rel["relationship_type"],
                    "entity": rel["related_entity"]
                })
            
            return enriched_context
        
        return [{"content": doc.page_content, "type": "semantic"} for doc in semantic_docs]

Esta implementación optimizada combina caché inteligente con estrategias de enriquecimiento contextual, permitiendo que el sistema escale eficientemente mientras mantiene la riqueza del contexto relacional.

El retrieval navegacional transforma la búsqueda de información de un proceso basado únicamente en similaridad a uno que puede razonar sobre conexiones conceptuales, proporcionando respuestas más completas y contextualmente relevantes.

Aprendizajes de esta lección

  • Comprender qué son los knowledge graphs y su ventaja sobre embeddings vectoriales.
  • Aprender a configurar y utilizar LLMGraphTransformer para extraer entidades y relaciones desde texto.
  • Conocer cómo almacenar y gestionar grafos de conocimiento en Neo4j.
  • Implementar retrieval navegacional que combina búsqueda semántica con navegación por relaciones del grafo.
  • Optimizar consultas relacionales y construir cadenas LCEL para retrieval contextual avanzado.

Completa LangChain y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración