
Cómo construir un sistema RAG con Claude (embeddings Voyage + pgvector, 2026)
Claude no tiene modelo de embeddings propio, así que un sistema RAG es: divide tus documentos en fragmentos, genera sus embeddings con Voyage (voyage-4, input_type="document"), guarda los vectores en una BD como pgvector; al consultar, genera el embedding de la pregunta (input_type="query"), recupera los top-k fragmentos más cercanos y pásalos a la Messages API de Claude para una respuesta fundamentada y con citas.
La respuesta corta: chunk, embed con Voyage, guarda, recupera, genera con Claude
Claude no tiene un modelo de embeddings propio, así que un sistema RAG con Claude es un pipeline de dos proveedores: divides tus documentos en fragmentos (chunks), generas sus embeddings con Voyage (voyage-4, input_type="document") y guardas los vectores en una base como pgvector. Al consultar, generas el embedding de la pregunta (input_type="query"), recuperas los top-k fragmentos más cercanos, y se los pasas a la Messages API de Claude para una respuesta fundamentada y con citas. El resto de este artículo es cómo construir cada pieza, con código que sí corre.
Si aún no tienes claro si RAG es lo que necesitas, empieza por RAG vs fine-tuning. Aquí asumimos que ya decidiste que sí.
Qué es RAG en realidad (y por qué Claude necesita Voyage)
RAG (Retrieval-Augmented Generation) significa que, antes de pedirle a Claude que responda, le inyectas los fragmentos de tus propios documentos que son relevantes a la pregunta. Claude no responde de memoria: responde a partir del contexto que tú le diste. Eso reduce alucinaciones y te deja citar la fuente.
La pieza que Claude no trae de fábrica es la búsqueda semántica. Para encontrar “los fragmentos relevantes” necesitas embeddings: vectores numéricos donde textos con significado parecido quedan cerca en el espacio. La documentación de Anthropic es explícita: no ofrece un modelo de embeddings propio y recomienda a Voyage AI (sugiere además evaluar otros proveedores). La arquitectura real es: Voyage para embeddings, una base vectorial para guardarlos, y Claude para generar.
El pipeline RAG de 8 pasos, de un vistazo
Todo sistema RAG que va a producción se reduce a estos ocho pasos. Los primeros tres son offline (indexas tu corpus una vez); los últimos cinco corren en cada consulta.
Vamos paso por paso.
Paso 1: Elige un modelo de embeddings (Voyage voyage-4, -lite, -code-3)
Voyage tiene una familia; elige por lo que vas a indexar. Todos los modelos de texto de última generación comparten 32,000 tokens de contexto y dimensión 1024 por defecto (también puedes truncar a 256, 512 o 2048).
voyage-4: el punto de equilibrio entre calidad y costo. Es tu opción por defecto.voyage-4-lite: optimizado para latencia y costo, cuando el volumen manda.voyage-4-large: la mejor calidad de recuperación general y multilingüe.voyage-code-3: si vas a indexar código fuente.
Para texto en español, la calidad multilingüe de la familia voyage-4 funciona bien sin trucos. En este tutorial usaremos voyage-4.
Paso 2: Divide tus documentos (256-512 tokens + solapamiento)
Antes de embeber, divides cada documento en fragmentos. Esto no es un spec de Anthropic ni de Voyage; es práctica de industria y la presento como guía. Un buen punto de partida para documentación es 256-512 tokens por chunk con ~10-15% de solapamiento entre chunks consecutivos. El solapamiento evita que una idea se corte justo en la frontera entre dos fragmentos.
Un fragmento demasiado grande diluye el vector; uno demasiado pequeño pierde contexto. Si tus documentos tienen estructura clara (encabezados, secciones), el “chunking semántico” —cortar donde cambia el significado, no cada N tokens ciegamente— casi siempre recupera mejor.
Un detalle que la gente salta: limpia los datos antes de embeber. Si traen datos personales, oculta el PII mexicano (CURP, RFC, CLABE) antes de mandarlo al LLM. Lo que embebes es lo que después Claude puede llegar a citar.
Paso 3: Genera los embeddings con Voyage (input_type=“document”)
Instala el paquete y exporta tu llave:
pip install -U voyageai
export VOYAGE_API_KEY="tu_llave_secreta"
Ahora embebe tus chunks. El punto crítico —el que separa un RAG que funciona de uno mediocre— es input_type="document":
import voyageai
vo = voyageai.Client() # usa VOYAGE_API_KEY del entorno
chunks = [
"El plan Pro incluye 50,000 llamadas a la API por mes.",
"Los reembolsos se procesan en un plazo de 5 a 7 días hábiles.",
# ... tus fragmentos
]
doc_embds = vo.embed(chunks, model="voyage-4", input_type="document").embeddings
# doc_embds: lista de vectores de 1024 floats, uno por chunk
El input_type es obligatorio para RAG. No lo omitas ni lo pongas en None. Voyage antepone un prompt distinto a documentos y a preguntas, y eso mejora la calidad de recuperación de forma medible. Embeber el corpus con document y la pregunta con query es lo que hace que los dos vectores se “encuentren” en el espacio.
Paso 4: Guarda los vectores en pgvector (SQL, y la BD es intercambiable)
Los vectores viven en una base vectorial. Uso pgvector aquí porque probablemente ya tienes Postgres, pero la base es intercambiable: la interfaz siempre es “guarda vector + metadata, dame los top-k más cercanos”. Si estás decidiendo entre opciones, compáralas en bases de datos vectoriales: pgvector, Pinecone, Qdrant.
Con pgvector, creas una columna vector con la dimensión del modelo (1024 para voyage-4):
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE chunks (
id bigserial PRIMARY KEY,
content text NOT NULL,
source text,
embedding vector(1024)
);
-- Inserta un chunk con su embedding (vector como literal de texto)
INSERT INTO chunks (content, source, embedding)
VALUES ('El plan Pro incluye 50,000 llamadas...', 'precios.md', '[0.013, -0.021, ...]');
En Python normalmente insertas en lote con el driver de tu preferencia (psycopg, por ejemplo), formateando cada vector como [0.013,-0.021,...]. Guarda siempre el texto original junto al vector: lo vas a necesitar para el paso de generación.
- Divide en chunks256-512 tokens, ~10-15% de solapamiento
- Embed con Voyagevoyage-4, input_type document, 1024 dims
- Inserta en pgvectorcolumna vector(1024) + texto + metadata
- Crea un índiceHNSW o IVFFlat para escalar la búsqueda
Paso 5: Embebe la pregunta y recupera los top-k (input_type=“query”)
En cada consulta, embebe la pregunta del usuario con input_type="query" y busca los vectores más cercanos:
query = "¿Cuántas llamadas a la API trae el plan Pro?"
query_embd = vo.embed([query], model="voyage-4", input_type="query").embeddings[0]
Los embeddings de Voyage están normalizados a longitud 1, así que el producto punto equivale a la similitud coseno (y es más rápido de calcular); coseno y distancia euclidiana dan el mismo ranking. En pgvector, el operador <=> es distancia coseno, así que ordenas ascendente y tomas los primeros k:
SELECT content, source, 1 - (embedding <=> :query_embd) AS score
FROM chunks
ORDER BY embedding <=> :query_embd -- menor distancia = más cercano
LIMIT 20;
Si prefieres hacerlo en memoria para un corpus pequeño, es un np.dot y ordenar:
import numpy as np
# doc_embds: matriz (n_chunks x 1024) recuperada de tu almacen
sims = np.dot(np.array(doc_embds), query_embd) # dot = coseno (vectores normalizados)
top_k = np.argsort(sims)[::-1][:20] # índices de los 20 más cercanos
Un truco que se paga solo: la recuperación híbrida (embeddings densos + BM25 por palabras clave) suele mejorar el recall, sobre todo con nombres propios, códigos de producto o siglas.
Paso 6 (opcional): Reordena a los mejores 5-10 con rerank-2.5
Recuperar 20 candidatos por vector es barato pero ruidoso. Un reranker vuelve a puntuar esos 20 mirando la pregunta y cada documento juntos, y te deja los 5-10 que de verdad importan. Voyage lo expone con rerank-2.5 (o rerank-2.5-lite para latencia y costo), y se llama con rerank():
docs = [c["content"] for c in candidatos] # los ~20 del paso anterior
reranked = vo.rerank(query, docs, model="rerank-2.5", top_k=8)
mejores = [r.document for r in reranked.results] # 8 chunks, ya ordenados
Menos chunks y mejor ordenados significa un prompt más corto, menos tokens y menos ruido. Para la mayoría de aplicaciones, rerankear vale la pena.
Pasos 7-8: Genera una respuesta fundamentada y con citas usando la Messages API de Claude
Ya tienes tus mejores fragmentos. El último paso es inyectarlos como contexto en la Messages API de Claude y pedir una respuesta fundamentada. La regla de oro: instruye a Claude a responder solo con base en el contexto y a citar la fuente; si el contexto no alcanza, que lo diga. Ese es el mecanismo central para un chatbot de IA sin alucinaciones.
import anthropic
client = anthropic.Anthropic()
contexto = "\n\n".join(
f"[Fuente: {c['source']}]\n{c['content']}" for c in mejores
)
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=(
"Responde única y exclusivamente con base en el CONTEXTO. "
"Cita la fuente entre corchetes. Si el contexto no contiene "
"la respuesta, dilo con claridad; no inventes."
),
messages=[
{
"role": "user",
"content": f"CONTEXTO:\n{contexto}\n\nPREGUNTA: {query}",
}
],
)
print(resp.content[0].text)
Claude responde a partir de los fragmentos que recuperaste, no de su memoria, y cita precios.md. Ese es el RAG completo.
Hazlo barato y confiable: prompt caching + fundamentación
Dos ajustes convierten un demo en algo que aguanta producción.
Prompt caching. Si tu bloque de sistema o parte del contexto se repite entre consultas, cachéalo para no volver a pagarlo a precio completo cada vez. Es un cambio pequeño con impacto grande en la factura; lo detallo en prompt caching de Claude para bajar costos de tokens.
Fundamentación estricta. El system prompt de arriba no es decoración: sin la instrucción de ceñirse al contexto y admitir cuando no sabe, Claude tiende a rellenar huecos con conocimiento general. En RAG eso es precisamente lo que no quieres.
Ejemplo completo y ejecutable, de principio a fin
Juntando todo, un RAG mínimo en memoria (sin base vectorial, para probar la idea) se ve así:
import numpy as np
import voyageai
import anthropic
vo = voyageai.Client()
claude = anthropic.Anthropic()
# 1-2. Corpus -> embeddings (offline)
chunks = [
"El plan Pro incluye 50,000 llamadas a la API por mes.",
"Los reembolsos se procesan en 5 a 7 días hábiles.",
"El soporte prioritario está disponible solo en el plan Enterprise.",
]
sources = ["precios.md", "reembolsos.md", "soporte.md"]
doc_embds = np.array(
vo.embed(chunks, model="voyage-4", input_type="document").embeddings
)
# 4-5. Pregunta -> embedding -> top-k
query = "¿Cuántas llamadas trae el plan Pro?"
q = vo.embed([query], model="voyage-4", input_type="query").embeddings[0]
top = np.argsort(np.dot(doc_embds, q))[::-1][:3]
# 7-8. Contexto -> Claude
contexto = "\n\n".join(f"[{sources[i]}] {chunks[i]}" for i in top)
resp = claude.messages.create(
model="claude-sonnet-5",
max_tokens=512,
system="Responde solo con base en el CONTEXTO y cita la fuente. Si no está, dilo.",
messages=[{"role": "user", "content": f"CONTEXTO:\n{contexto}\n\nPREGUNTA: {query}"}],
)
print(resp.content[0].text)
Cambia el bloque en memoria por pgvector cuando tu corpus deje de caber cómodo en RAM. La forma del pipeline no cambia. Este mismo esquema es el que alimenta un agente de IA en WhatsApp para tu negocio: la fuente de datos cambia, el RAG no.
Qué sigue: qué base vectorial usar, y cuándo RAG no es la respuesta
Ya tienes un RAG que corre. Dos decisiones te quedan.
Qué almacén usar. pgvector es genial si ya tienes Postgres y tu corpus es moderado; a mayor escala o con filtros de metadata pesados, quizá quieras Qdrant o Pinecone. La comparación completa está en bases de datos vectoriales: pgvector, Pinecone, Qdrant.
Cuándo RAG no es la respuesta. Si tus datos viven en APIs o bases que cambian en tiempo real —no en documentos estáticos—, muchas veces conviene más conectar el agente a tus datos vía MCP en lugar de indexar todo. Y si lo que necesitas es cambiar el estilo o el formato de las respuestas (no inyectar conocimiento), eso es fine-tuning, no RAG: revisa RAG vs fine-tuning.
RAG es lo correcto
- Conocimiento en documentos relativamente estables
- Necesitas citar la fuente
- Quieres reducir alucinaciones sobre tus datos
- El corpus no cabe en el prompt
Considera otra cosa
- Datos en tiempo real via API: usa MCP
- Cambiar estilo o formato: fine-tuning
- Corpus chico que cabe entero: solo mete todo al prompt
- Consultas exactas por ID: una query SQL normal
Para los detalles finos —cuantización, dimensiones Matryoshka, voyage-context-4 para chunks contextualizados— la documentación de Voyage es la referencia. Empieza con el pipeline de arriba, mídelo con tus propias preguntas, y afina desde ahí.