iapythonllmragembeddingspgvectorlitellm
Imagina que es tu primer día en una empresa nueva. Te dan acceso a ChatGPT corporativo y lo primero que haces, como cualquiera, es soltarle las dudas típicas de onboarding: “¿cuántos días de vacaciones tengo por año?”, “¿cuál es la política de home office?”, “¿cómo pido un reembolso?”. Y ya sabes lo que va a pasar: el modelo responde algo genérico sobre políticas laborales en el mundo, o peor, inventa un número que suena razonable pero no tiene nada que ver con tu empresa.
El problema no es que el modelo sea deficiente. El problema es que nadie le mostró los documentos internos: ese ChatGPT nunca vio tu wiki, tus PDFs de políticas ni tus guías de onboarding. LLMs como Claude o GPT fueron entrenados con información pública general, y no tiene sentido re-entrenarlos cada vez que alguien actualiza una política, ni es viable para información que cambia con frecuencia.
RAG resuelve exactamente este escenario. Antes de responder, el sistema busca los fragmentos de texto más relevantes en tu propia base de conocimiento y los inyecta en el prompt. El LLM ya no responde con lo que cree recordar del entrenamiento, sino apoyándose en documentos que sí son tuyos.
¿Qué es RAG?
RAG son las siglas de Retrieval-Augmented Generation. Cada palabra importa:
- Retrieval: recuperar los fragmentos de texto más relevantes de una base de conocimiento propia.
- Augmented: aumentar el prompt del LLM con ese contexto recuperado.
- Generation: el LLM genera la respuesta basándose en ese contexto aumentado, no solo en su entrenamiento.
La idea central es simple: en lugar de pedirle al modelo que recuerde información que nunca aprendió, le damos la información en el momento de la pregunta.
El pipeline RAG
El pipeline RAG opera en dos fases completamente separadas. La ingesta corre una vez por documento y construye el índice de búsqueda. El query corre en tiempo real con cada pregunta y usa ese índice para responder con contexto real.
Fase de ingesta (offline)
La ingesta convierte documentos crudos en vectores buscables y los guarda en el Vector Store. Corre una vez por documento y sus resultados persisten hasta la próxima actualización.
Ingesta: de documentos a vectores
Fase de query (online)
Con cada pregunta del usuario, el pipeline usa el índice construido durante la ingesta para recuperar los fragmentos más relevantes y pasárselos al LLM como contexto.
Query: de pregunta a respuesta con contexto
- Pregunta: el usuario escribe su consulta en lenguaje natural.
- Embedding: la pregunta pasa por el mismo modelo de embeddings que se usó en la ingesta. Es clave que sea el mismo: ambos vectores deben vivir en el mismo espacio dimensional para que la comparación tenga sentido.
- Búsqueda: el vector de la pregunta se compara contra el Vector Store con similitud coseno y se recuperan los k fragmentos más cercanos semánticamente.
- Chunks: esos fragmentos traen el contexto que el LLM necesita para responder con precisión.
- Prompt aumentado: la pregunta original se combina con los chunks recuperados en un prompt estructurado: primero el contexto, al final la pregunta.
- LLM: el modelo recibe ese prompt y genera la respuesta apoyándose en el contexto inyectado.
- Respuesta: una respuesta basada en los documentos de la empresa, no en suposiciones generales del entrenamiento.
Embeddings: el idioma de los vectores
Para buscar texto de forma semántica, primero hay que convertirlo a un formato que las computadoras puedan comparar matemáticamente. Ahí entran los embeddings.
embeddings
Representaciones numéricas de texto como vectores en un espacio de alta dimensionalidad. Texto con significado similar produce vectores cercanos en ese espacio, aunque no compartan palabras exactas.
Un modelo de embeddings toma un fragmento de texto y lo convierte en un vector de cientos o miles de números. Lo importante no es qué significan esos números individualmente, sino cómo se relacionan entre sí: textos con significado similar producen vectores que apuntan en la misma dirección dentro de ese espacio de muchas dimensiones.
Por ejemplo, si le das a un modelo de embeddings los textos “política de vacaciones pagas” y “días de descanso remunerados”, vas a obtener dos vectores muy cercanos entre sí, aunque no compartan ninguna palabra. Esa es la potencia de los embeddings: capturan semántica, no solo sintaxis.
Similitud coseno y búsqueda vectorial
Una vez que tienes todo convertido a vectores (documentos y pregunta), el sistema necesita encontrar cuáles documentos son más relevantes. La métrica estándar es la similitud coseno: en lugar de medir la distancia entre dos vectores, mide el ángulo entre ellos. Un ángulo pequeño (vectores que apuntan en la misma dirección) indica alta similitud. La fórmula produce un valor entre -1 y 1, donde 1 es idéntico y 0 es sin relación.
Similitud coseno: vectores similares apuntan en la misma dirección
La búsqueda vectorial básica calcula la similitud coseno entre la pregunta y todos los documentos indexados y devuelve los k más similares. Esto funciona bien para miles de documentos, pero a millones de vectores, comparar uno a uno se vuelve prohibitivamente lento. Ahí entran los algoritmos Approximate Nearest Neighbor (ANN).
FAISS y los índices de búsqueda vectorial
La librería de referencia para este problema es FAISS. Si ya exploraste los Two-Tower Models para sistemas de recomendación, ya la conociste en acción: es la que hace posible el serving vectorial a escala en ese contexto. En RAG el rol es el mismo, encontrar los vectores más similares entre miles o millones de chunks sin que la latencia se dispare.
FAISS
Facebook AI Similarity Search. Librería de Meta para búsqueda eficiente en espacios de alta dimensionalidad. Incluye índices que encuentran vectores aproximadamente cercanos en una fracción del tiempo que tomaría la búsqueda exacta.
La clave de FAISS no es la búsqueda exacta sino la aproximada: en lugar de comparar la query contra todos los vectores del índice, usa estructuras de datos que devuelven los vecinos aproximadamente más cercanos a una fracción del costo. Esa pequeña pérdida de precisión se traduce en órdenes de magnitud menos de latencia, que es justo el trade-off que queremos cuando el índice tiene millones de entradas.
Dentro de FAISS, los dos índices más usados son IVF y HNSW. Cada uno organiza el espacio vectorial de una manera distinta:
IVF e HNSW: dos formas de organizar vectores para búsqueda rápida
IVF (Inverted File Index): divide el espacio vectorial en clusters (celdas de Voronoi). Al buscar, primero identifica los clusters más cercanos a la query y solo compara contra los vectores dentro de esos clusters. Esto reduce drásticamente el número de comparaciones, con una pequeña pérdida de precisión.
HNSW (Hierarchical Navigable Small World): construye un grafo de múltiples capas. Las capas superiores tienen pocos nodos muy conectados (cobertura global, navegación rápida). Las capas inferiores tienen todos los nodos con conexiones locales (precisión alta). La búsqueda empieza en la capa superior y desciende refinando el resultado. Es más preciso que IVF en muchos casos, pero usa más memoria.
FAISS vs pgvector
FAISS vive completamente en memoria, no tiene persistencia nativa y requiere serialización manual para guardar los índices. pgvector es una extensión de PostgreSQL: los vectores viven junto con el resto de tus datos, con transacciones ACID y SQL estándar.
| Característica | FAISS | pgvector |
|---|---|---|
| Dónde vive | En memoria (RAM) | PostgreSQL |
| Velocidad | Extremadamente rápido (ANN optimizado) | Rápido para volúmenes medianos |
| Escala | Millones a miles de millones | Hasta ~1-2M cómodamente |
| Persistencia | Manual (serializar a disco) | Nativa (PostgreSQL) |
| Transacciones | No | Sí (ACID) |
| Infraestructura extra | No | Necesita PostgreSQL |
| Ideal para | Búsqueda masiva, latencia crítica | Apps con PostgreSQL existente |
Para la mayoría de casos reales (knowledge bases de empresas, wikis internas, soporte al cliente), pgvector es suficiente y mucho más simple operacionalmente. FAISS entra cuando hay decenas de millones de documentos y la latencia de búsqueda es crítica.
RAG vs MCP: no son lo mismo
Con el auge de MCP aparece la pregunta inevitable: ¿cuál es la diferencia con RAG? Superficialmente, ambos sirven para darle más información al LLM. Pero funcionan de formas fundamentalmente distintas.
MCP
Model Context Protocol. Protocolo abierto de Anthropic que permite a los LLMs conectarse a herramientas externas (APIs, bases de datos, sistemas de archivos) y decidir cuándo y cómo llamarlas durante una conversación.
En RAG, el pipeline recupera contexto automáticamente antes de que el LLM vea la pregunta. El modelo no sabe que hay retrieval: solo recibe un prompt que ya incluye los fragmentos relevantes. La decisión de qué buscar la toma el sistema (basándose en similitud coseno), no el modelo.
En MCP, el LLM tiene acceso a herramientas y decide él mismo cuándo llamarlas y con qué parámetros. Es un modelo agéntico: tiene agencia sobre qué información buscar, cuándo buscarla y cómo interpretar el resultado.
RAG vs MCP: quién decide qué buscar
Lo más interesante es que son complementarios. Un sistema RAG puede exponerse como una tool MCP. Cuando el LLM lo considera necesario, invoca esa tool para recuperar contexto documental. Eso es una arquitectura agéntica con memoria: el modelo decide cuándo buscar y el RAG sabe cómo encontrarlo.
Ahora manos a la obra
Ahora que entendemos la teoría, vamos a construir un RAG funcional. El escenario: “TechCorp” tiene tres documentos internos (política de vacaciones, guía de onboarding y política de gastos) y queremos hacer Q&A sobre ellos usando LiteLLM y pgvector.
Stack del tutorial
- Embeddings: OpenAI
text-embedding-3-smallvía LiteLLM - Generación: Anthropic
claude-3-haiku-20240307vía LiteLLM - Vector store: PostgreSQL + pgvector (Docker)
- Por qué LiteLLM: Claude no tiene un endpoint de embeddings nativo. LiteLLM permite mezclar proveedores con una sola API.
Setup: Docker Compose
El primer paso es levantar PostgreSQL con la extensión pgvector instalada. La imagen oficial
pgvector/pgvector:pg16 incluye todo preconfigurado.yaml
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: ragdb
POSTGRES_USER: raguser
POSTGRES_PASSWORD: ragpass
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:bash
docker compose up -dInstala las dependencias Python y configura tus API keys:
bash
pip install litellm psycopg2-binary
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...Script de ingesta
El script de ingesta hace cuatro cosas: conecta a PostgreSQL, crea la tabla con la columna de tipo
vector(1536), divide los documentos en chunks, genera el embedding de cada chunk con LiteLLM y los inserta en pgvector.python
import psycopg2
from psycopg2.extras import execute_values
import litellm
# Fictional TechCorp internal documents
DOCUMENTS = [
{
"title": "Vacation Policy",
"content": """TechCorp grants 15 paid vacation days per year to all full-time employees.
Days accrue at 1.25 days per month. Unused days roll over up to 30 days maximum.
Requests must be submitted at least 2 weeks in advance.
Requests during December require 4 weeks notice.
Vacation pay is calculated based on the employee base salary."""
},
{
"title": "Onboarding Guide",
"content": """Welcome to TechCorp! Your first week:
Day 1: IT setup, equipment pickup, and access provisioning.
Day 2: HR orientation and benefits enrollment.
Day 3-4: Team introduction and codebase walkthrough.
Day 5: First task assignment and buddy system pairing.
Tools: Slack for communication, Linear for project tracking,
GitHub for version control, Notion for documentation.
Contact IT helpdesk at it@techcorp.com for technical issues."""
},
{
"title": "Expense Policy",
"content": """TechCorp reimburses pre-approved business expenses.
Meals: up to $50 per person, $150 per team event.
Travel: economy class flights, hotels up to $200 per night.
Equipment: pre-approval required for purchases over $500.
Coworking: up to $30 per day when working remotely.
Submit expense reports within 30 days via Expensify.
Attach all receipts. Expenses over $100 require manager approval.
Reimbursements processed within 2 business weeks."""
}
]
def chunk_text(text: str, chunk_size: int = 250) -> list[str]:
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks, current, length = [], [], 0
for para in paragraphs:
if length + len(para) > chunk_size and current:
chunks.append(" ".join(current))
current, length = [para], len(para)
else:
current.append(para)
length += len(para)
if current:
chunks.append(" ".join(current))
return chunks
def get_embedding(text: str) -> list[float]:
# Claude (Anthropic) has no native embeddings endpoint.
# LiteLLM lets us use OpenAI embeddings here and Claude for generation.
response = litellm.embedding(model="text-embedding-3-small", input=text)
return response.data[0].embedding
def setup_db(conn) -> None:
with conn.cursor() as cur:
cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
cur.execute("""
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536)
);
""")
cur.execute("TRUNCATE documents;")
conn.commit()
def main() -> None:
conn = psycopg2.connect(
host="localhost", dbname="ragdb", user="raguser", password="ragpass"
)
print("Setting up database...")
setup_db(conn)
rows = []
for doc in DOCUMENTS:
chunks = chunk_text(doc["content"])
print(f"Processing '{doc['title']}': {len(chunks)} chunks")
for chunk in chunks:
rows.append((doc["title"], chunk, get_embedding(chunk)))
with conn.cursor() as cur:
execute_values(
cur,
"INSERT INTO documents (title, content, embedding) VALUES %s",
rows,
template="(%s, %s, %s::vector)"
)
conn.commit()
conn.close()
print(f"Done. Inserted {len(rows)} chunks.")
if __name__ == "__main__":
main()bash
python ingest.py
Setting up database...
Processing 'Vacation Policy': 2 chunks
Processing 'Onboarding Guide': 3 chunks
Processing 'Expense Policy': 3 chunks
Done. Inserted 8 chunks.Pipeline de query
El script de query implementa el pipeline completo: genera el embedding de la pregunta, busca los chunks más similares en pgvector con el operador
<=> (distancia coseno), arma el prompt aumentado y llama a claude-haiku para generar la respuesta.python
import psycopg2
import litellm
def get_embedding(text: str) -> list[float]:
response = litellm.embedding(model="text-embedding-3-small", input=text)
return response.data[0].embedding
def retrieve(conn, embedding: list[float], top_k: int = 3) -> list[dict]:
with conn.cursor() as cur:
# <=> is pgvector's cosine distance operator (lower = more similar)
# 1 - distance = cosine similarity
cur.execute("""
SELECT title, content, 1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s;
""", (embedding, embedding, top_k))
rows = cur.fetchall()
return [{"title": r[0], "content": r[1], "similarity": round(r[2], 3)} for r in rows]
def ask(question: str) -> str:
conn = psycopg2.connect(
host="localhost", dbname="ragdb", user="raguser", password="ragpass"
)
embedding = get_embedding(question)
chunks = retrieve(conn, embedding)
conn.close()
context = "\n\n".join(f"[{c['title']}]\n{c['content']}" for c in chunks)
prompt = f"""You are an internal assistant for TechCorp.
Answer ONLY using the context below. If the information is not there, say so clearly.
Context:
{context}
Question: {question}
Answer:"""
response = litellm.completion(
model="anthropic/claude-3-haiku-20240307",
messages=[{"role": "user", "content": prompt}],
temperature=0
)
return response.choices[0].message.content.strip()
if __name__ == "__main__":
questions = [
"How many vacation days do I get per year?",
"What is the meal expense limit per person?",
"What tools does the company use for project tracking?",
]
for q in questions:
print(f"\nQ: {q}")
print(f"A: {ask(q)}")Demo en acción
Ejecuta
python query.py y el sistema responde usando únicamente el contexto de los documentos de TechCorp:bash
Q: ¿Cuántos días de vacaciones tengo por año?
A: According to TechCorp's Vacation Policy, you receive 15 paid vacation days per year.
Q: ¿Cuál es el límite de gastos en restaurantes?
A: The meal expense limit is $50 per person for meals.
Q: ¿Qué herramientas usa la empresa para gestión de proyectos?
A: TechCorp uses Linear for project tracking.El operador
<=> en la query SQL es el operador de distancia coseno de pgvector. Un valor menor significa mayor similitud. La expresión 1 - (embedding <=> query) convierte esa distancia en similitud, donde 1 es idéntico y 0 es sin relación.Limitaciones a tener en cuenta
- El tamaño del chunk importa: chunks muy grandes diluyen la señal semántica; chunks muy pequeños pierden contexto. Experimenta entre 200 y 500 tokens según tu caso.
- RAG no es magia: si el documento no tiene la respuesta, el LLM puede inventarla de todas formas. Usa
temperature=0e instrucciones explícitas como “responde solo con el contexto provisto”. - Escala de pgvector: funciona bien hasta ~1-2M vectores. Para más escala, considera FAISS, Qdrant o Pinecone.
- Mismo modelo de embeddings siempre: usa el mismo modelo para indexar y para consultar. Mezclar modelos produce resultados incoherentes.