Combinación de recuperación vectorial y de grafos
Recuperación híbrida: similitud vectorial + recorrido de rutas del grafo para obtener un contexto más completo.
Combinación de recuperación vectorial y de grafos es una lección gratuita de AI Agents en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de AI Agents, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de AI Agents incluye 4 lecciones en total.
¿Por qué usar recuperación híbrida?
La búsqueda vectorial encuentra contenido semánticamente similar, pero no detecta las relaciones estructuradas. El recorrido del grafo captura las relaciones, pero tiene dificultades con la similitud semántica. La recuperación híbrida combina ambos enfoques para proporcionar un contexto más completo.
Resumen de la búsqueda vectorial
La búsqueda vectorial convierte las consultas y los documentos en embeddings (vectores densos) y, a continuación, encuentra los documentos con una alta similitud coseno. Responde a la pregunta ¿De qué documentos tratan el mismo tema?
import openai
import numpy as np
client = openai.OpenAI(api_key='sk-...')
def embed(text: str) -> list:
response = client.embeddings.create(
model='text-embedding-3-small',
input=text
)
return response.data[0].embedding
def cosine_similarity(a: list, b: list) -> float:
a_arr = np.array(a)
b_arr = np.array(b)
return float(np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr)))
# Simple in-memory vector store
class SimpleVectorStore:
def __init__(self):
self.documents = []
def add(self, text: str, metadata: dict):
embedding = embed(text)
self.documents.append({'text': text, 'embedding': embedding, 'metadata': metadata})
def search(self, query: str, top_k: int = 5) -> list:
query_emb = embed(query)
scored = [
(cosine_similarity(query_emb, doc['embedding']), doc)
for doc in self.documents
]
scored.sort(key=lambda x: x[0], reverse=True)
return [doc for _, doc in scored[:top_k]]Resumen de la recuperación mediante grafos
La recuperación mediante grafos responde preguntas relacionales: ¿Quién está conectado con X?, ¿Qué empresas conoce esta persona? Utiliza aristas explícitas en lugar de la similitud semántica.
from neo4j import GraphDatabase
driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'))
def get_entity_context(entity_name: str) -> dict:
with driver.session() as session:
# Get node properties
result = session.run(
'MATCH (n {name: $name}) RETURN n, labels(n) AS labels LIMIT 1',
name=entity_name
)
record = result.single()
if not record:
return {}
node_data = dict(record['n'])
node_labels = record['labels']
# Get connected entities
conn_result = session.run(
'MATCH (n {name: $name})-[r]-(connected) '
'RETURN type(r) AS rel_type, connected.name AS connected_name, labels(connected) AS connected_labels '
'LIMIT 20',
name=entity_name
)
connections = [dict(r) for r in conn_result]
return {
'name': entity_name,
'labels': node_labels,
'properties': node_data,
'connections': connections
}Intercalado de resultados
Una estrategia de fusión consiste en intercalar los resultados vectoriales y del grafo: tomar el primer resultado de la búsqueda vectorial, después el primer resultado del grafo, luego el segundo resultado vectorial y así sucesivamente. Esto garantiza que ambas fuentes contribuyan.
def interleave_results(vector_results: list, graph_results: list) -> list:
combined = []
v_idx, g_idx = 0, 0
while v_idx < len(vector_results) or g_idx < len(graph_results):
if v_idx < len(vector_results):
item = vector_results[v_idx]
item['source'] = 'vector'
combined.append(item)
v_idx += 1
if g_idx < len(graph_results):
item = graph_results[g_idx]
item['source'] = 'graph'
combined.append(item)
g_idx += 1
return combined
# Example
vector_docs = [
{'text': 'Alice led the machine learning initiative at Acme', 'score': 0.92},
{'text': 'Machine learning best practices guide', 'score': 0.85},
]
graph_context = [
{'name': 'Alice', 'type': 'Person', 'connections': ['Acme Corp', 'Bob']},
]
fused = interleave_results(vector_docs, graph_context)
for item in fused:
print(f"[{item['source']}]", item.get('text') or item.get('name'))Combinación ponderada
Asigne a cada resultado una puntuación combinada: final_score = alpha * vector_score + (1-alpha) * graph_score. Ajuste alpha según si en su caso de uso es más importante la similitud semántica o el contexto relacional.
def weighted_fusion(vector_results: list, graph_results: list, alpha: float = 0.6) -> list:
'''
alpha: weight for vector results (0.0 = pure graph, 1.0 = pure vector)
'''
all_results = []
# Normalize vector scores (already in 0-1 range for cosine)
for i, res in enumerate(vector_results):
# Positional score: first result gets highest
positional_score = 1.0 - (i / max(len(vector_results), 1))
combined = alpha * res.get('score', positional_score)
all_results.append({
'content': res,
'source': 'vector',
'final_score': combined
})
# Graph results: score by relevance (e.g., connection count)
for i, res in enumerate(graph_results):
positional_score = 1.0 - (i / max(len(graph_results), 1))
combined = (1 - alpha) * positional_score
all_results.append({
'content': res,
'source': 'graph',
'final_score': combined
})
# Sort by final score
all_results.sort(key=lambda x: x['final_score'], reverse=True)
return all_results
print('Weighted fusion function defined (alpha=0.6 favors vector)')Fusión de rangos recíprocos
La fusión de rangos recíprocos (RRF) es un método sólido para combinar listas ordenadas sin necesidad de normalizar las puntuaciones. Cada documento recibe la puntuación sum(1 / (k + rank)) en todas las listas.
def reciprocal_rank_fusion(result_lists: list, k: int = 60) -> list:
'''
result_lists: list of lists, each containing dicts with an 'id' field
k: constant to reduce impact of high rankings (typically 60)
'''
scores = {}
all_items = {}
for result_list in result_lists:
for rank, item in enumerate(result_list):
item_id = item.get('id') or item.get('text', '')[:50]
if item_id not in scores:
scores[item_id] = 0.0
all_items[item_id] = item
scores[item_id] += 1.0 / (k + rank + 1)
sorted_ids = sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
return [
{**all_items[id_], 'rrf_score': scores[id_]}
for id_ in sorted_ids
]
vector_list = [{'id': 'doc1', 'text': 'About Alice'}, {'id': 'doc3', 'text': 'About AI'}]
graph_list = [{'id': 'doc2', 'text': 'Alice connections'}, {'id': 'doc1', 'text': 'About Alice'}]
fused = reciprocal_rank_fusion([vector_list, graph_list])
for item in fused:
print(f"{item['id']}: RRF score {item['rrf_score']:.4f}")Recuperación híbrida anclada en entidades
Un enfoque híbrido eficaz consiste en extraer las entidades de la consulta, utilizar el grafo para obtener contexto sobre ellas y, después, usar ese contexto para mejorar la consulta de búsqueda vectorial.
import spacy
nlp = spacy.load('en_core_web_sm')
def entity_anchored_retrieval(query: str, vector_store, graph_driver) -> dict:
# Step 1: Extract entities from query
doc = nlp(query)
entities = [ent.text for ent in doc.ents if ent.label_ in ['PERSON', 'ORG', 'GPE']]
# Step 2: Get graph context for entities
graph_contexts = {}
for entity in entities:
context = get_entity_context(entity)
if context:
graph_contexts[entity] = context
# Step 3: Enrich query with graph context
enriched_query = query
if graph_contexts:
context_str = ' '.join([
f"{name} works at {', '.join([c['connected_name'] for c in ctx.get('connections', [])[:3]])}"
for name, ctx in graph_contexts.items()
])
enriched_query = f'{query} Context: {context_str}'
# Step 4: Vector search with enriched query
vector_results = vector_store.search(enriched_query, top_k=5)
return {
'entities_found': entities,
'graph_contexts': graph_contexts,
'vector_results': vector_results
}Creación de un paquete de contexto
El último paso de la recuperación consiste en empaquetar todo el contexto (resultados vectoriales y datos del grafo) en una cadena estructurada para el LLM. El LLM utiliza esta información para generar una respuesta completa.
def build_context_package(vector_results: list, graph_contexts: dict, max_tokens: int = 3000) -> str:
sections = []
# Graph entity context section
if graph_contexts:
graph_section = ['## Entity Context from Knowledge Graph']
for entity_name, context in graph_contexts.items():
connections = context.get('connections', [])
conn_summary = ', '.join([
f"{c['connected_name']} ({c['rel_type']})"
for c in connections[:5]
])
graph_section.append(f'**{entity_name}**: connected to {conn_summary}')
sections.append('\n'.join(graph_section))
# Vector search results section
if vector_results:
vector_section = ['## Relevant Documents']
for i, doc in enumerate(vector_results[:5]):
text = doc.get('text', '')[:500] # Truncate long docs
vector_section.append(f'{i+1}. {text}')
sections.append('\n'.join(vector_section))
context_package = '\n\n'.join(sections)
# Rough token estimate (1 token ~ 4 chars)
if len(context_package) > max_tokens * 4:
context_package = context_package[:max_tokens * 4]
return context_package
if __name__ == '__main__':
demo_vector = [{'text': 'Refunds are processed within 5 business days of approval.'}]
demo_graph = {'Acme Corp': {'connections': [{'connected_name': 'Jane Doe', 'rel_type': 'employs'}]}}
print(build_context_package(demo_vector, demo_graph))
Recuperación paralela asíncrona
Ejecute la recuperación vectorial y la del grafo en paralelo mediante asyncio.gather para minimizar la latencia total. Los resultados estarán disponibles simultáneamente.
import asyncio
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
async def async_vector_search(query: str, vector_store) -> list:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(executor, vector_store.search, query, 5)
async def async_graph_lookup(entities: list) -> dict:
loop = asyncio.get_event_loop()
results = {}
for entity in entities:
context = await loop.run_in_executor(executor, get_entity_context, entity)
if context:
results[entity] = context
return results
async def hybrid_retrieval_async(query: str, entities: list, vector_store) -> dict:
# Run vector search and graph lookup in parallel
vector_task = async_vector_search(query, vector_store)
graph_task = async_graph_lookup(entities)
vector_results, graph_contexts = await asyncio.gather(vector_task, graph_task)
return {
'vector': vector_results,
'graph': graph_contexts
}
print('Async parallel retrieval functions defined')Almacenamiento en caché de los resultados de recuperación
Almacene en caché tanto los resultados de la búsqueda vectorial como las consultas del grafo para evitar llamadas repetidas a las API. Utilice un TTL corto (de minutos a horas), ya que las bases de conocimiento cambian lentamente, aunque no de forma instantánea.
import hashlib
import time
class HybridRetrievalCache:
def __init__(self, vector_ttl: int = 300, graph_ttl: int = 600):
self.vector_cache = {}
self.graph_cache = {}
self.vector_ttl = vector_ttl
self.graph_ttl = graph_ttl
def _key(self, value: str) -> str:
return hashlib.md5(value.encode()).hexdigest()[:12]
def get_vector(self, query: str):
k = self._key(query)
entry = self.vector_cache.get(k)
if entry and time.time() - entry['ts'] < self.vector_ttl:
return entry['data']
return None
def set_vector(self, query: str, results: list):
self.vector_cache[self._key(query)] = {'data': results, 'ts': time.time()}
def get_graph(self, entity: str):
k = self._key(entity)
entry = self.graph_cache.get(k)
if entry and time.time() - entry['ts'] < self.graph_ttl:
return entry['data']
return None
def set_graph(self, entity: str, context: dict):
self.graph_cache[self._key(entity)] = {'data': context, 'ts': time.time()}
cache = HybridRetrievalCache()
print('Hybrid retrieval cache initialized')Elección de los pesos de recuperación
Ajuste el parámetro alpha (peso de los vectores frente al grafo) según el tipo de consulta:
- Preguntas de consulta factual (¿Quién fundó OpenAI?) → mayor peso del grafo
- Preguntas de similitud semántica (Buscar documentos sobre la seguridad de la IA) → mayor peso vectorial
- Preguntas mixtas → peso equilibrado (alpha=0.5)
def auto_tune_alpha(query: str) -> float:
query_lower = query.lower()
# High graph weight for relational questions
relational_keywords = [
'who', 'founded', 'works at', 'connected to',
'related to', 'partner', 'owns', 'acquired'
]
# High vector weight for content questions
content_keywords = [
'explain', 'describe', 'what is', 'how does',
'tell me about', 'documents about', 'find information'
]
relational_count = sum(1 for kw in relational_keywords if kw in query_lower)
content_count = sum(1 for kw in content_keywords if kw in query_lower)
if relational_count > content_count:
return 0.3 # Graph-heavy
elif content_count > relational_count:
return 0.7 # Vector-heavy
else:
return 0.5 # Balanced
queries = [
'Who founded Tesla?',
'Explain transformer architecture',
'What companies is Elon Musk connected to?'
]
for q in queries:
print(f'alpha={auto_tune_alpha(q):.1f} for: {q}')Comprobación de conocimientos: recuperación híbrida
Compruebe su comprensión de la combinación de la recuperación vectorial y la recuperación mediante grafos.
Resumen de la recuperación híbrida
Una recuperación híbrida eficaz combina: búsqueda vectorial para la similitud semántica, recorrido del grafo para el contexto relacional, extracción de entidades para anclar las consultas al grafo, estrategias de fusión (intercalado, ponderación y RRF) para combinar los resultados y ejecución paralela asíncrona para minimizar la latencia. El resultado es un contexto más completo para las respuestas del LLM.
Preguntas frecuentes
¿La lección «Combinación de recuperación vectorial y de grafos» es gratis?
Sí — el texto completo de «Combinación de recuperación vectorial y de grafos» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de AI Agents, actualiza a CoddyKit PRO. El curso de AI Agents incluye 4 lecciones en total.
¿Qué aprenderé en «Combinación de recuperación vectorial y de grafos»?
Recuperación híbrida: similitud vectorial + recorrido de rutas del grafo para obtener un contexto más completo. Practicas AI Agents con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar AI Agents?
No se requiere experiencia previa. AI Agents en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.
¿Cuánto tiempo toma la lección «Combinación de recuperación vectorial y de grafos»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de AI Agents?
Sí. Cada lección de AI Agents incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Extracción de entidades para grafos de conocimiento
- Consultas de Neo4j desde herramientas de agentes
- Combinación de recuperación vectorial y de grafos
- Creación de un agente aumentado con conocimiento