La mayoría de las demos de RAG mueren en producción. Funcionan con el conjunto de 10 preguntas de la demo y se rompen en cuanto los usuarios preguntan algo novedoso. Esta guía presenta la arquitectura y los patrones que resisten: segmentación que respeta los límites semánticos, recuperación híbrida que detecta consultas difusas y literales, una estructura de prompt que evita las alucinaciones y disciplina de costes que mantiene estable la factura al escalar.
Las cinco decisiones importantes
- Estrategia de segmentación — afecta más a la calidad de recuperación que el modelo
- Modelo de embeddings — determina el límite de recuperación
- Estrategia de recuperación — el vector por sí solo no basta
- Estructura del prompt — decide si el modelo alucina
- Modelo de generación + almacenamiento en caché — determina la economía unitaria
1. Segmentación: lo aburrido supera a lo ingenioso
No le des demasiadas vueltas. Usa un divisor recursivo de caracteres con 1000-1500 caracteres por segmento y una superposición de 100-200 caracteres. Intenta dividir primero en los límites de los párrafos, después en las frases y luego en las palabras. Es estable en distintos dominios:
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answerCosas que parecen más inteligentes pero rara vez ayudan: modelos conscientes de las frases, divisores conscientes de Markdown y ventanas deslizantes con embeddings. No son malas; solo son marginalmente mejores a cambio de un esfuerzo enorme. Dedica ese esfuerzo a la recuperación.
2. Modelo de embeddings: text-embedding-3-large es el valor predeterminado
Kunavo no ofrece embeddings; llama directamente a un proveedor de embeddings para este paso. /v1/embeddings es un formato de conexión implementado sin ningún modelo habilitado detrás, por lo que una solicitud falla; GET /v1/models es siempre la autoridad sobre lo que se puede invocar. En la práctica, esto no añade más que una segunda clave al pipeline RAG: la llamada de embeddings y la llamada de generación son solicitudes separadas de todos modos, así que genera los embeddings con OpenAI, Voyage o Cohere y genera con Kunavo.
text-embedding-3-large ofrece el mejor equilibrio entre compatibilidad multilingüe y precisión para la mayoría de los casos de uso en producción, al precio publicado por la propia OpenAI; consúltalo en su página de precios en lugar de aquí, ya que no lo revendemos y no deberíamos citar una cifra. Vuelve a generar los embeddings cuando cambies la estrategia de segmentación o el modelo; no cuando se actualice el contenido (simplemente añade segmentos nuevos).
Excepciones en las que funcionan embeddings más pequeños: recuperación de textos cortos exclusivamente en inglés (preguntas frecuentes, tarjetas de producto). Para esos casos, text-embedding-3-small es 4 veces más barato con una pérdida de calidad insignificante. Para contenido multilingüe o técnico, mantente en large.
3. Recuperación: el vector puro pierde frente a la híbrida
La similitud vectorial es excelente para la coincidencia semántica difusa ("¿cómo cancelo?" → "política de cancelación de suscripciones"). Es terrible con términos literales ("SKU-A92837" u "order #4729"). La solución es híbrida: búsqueda de palabras clave BM25 en paralelo y después fusión de rangos recíprocos:
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
q_embed = embed([question])[0]
vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
keyword_hits = bm25_search(question, k=k * 2)
# Reciprocal rank fusion — simple, robust
scores: dict[str, float] = {}
for rank, hit in enumerate(vector_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
for rank, hit in enumerate(keyword_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
return [chunk_by_id[i] for i in top_ids]Este es el mayor aumento individual de calidad después de elegir un modelo de embeddings decente. Registra recall@5 en una evaluación retenida de 100 preguntas; si pasa del 70% al 90% después de añadir BM25, acabas de eliminar un tercio de los fallos de "no lo sé".
4. Estructura del prompt: tres patrones que evitan las alucinaciones
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""
def answer(question: str) -> dict:
chunks = retrieve(question, k=5)
context = "\n\n---\n\n".join(
f"[doc:{c['id']}] {c['text']}" for c in chunks
)
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": [{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}]},
{"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
],
max_tokens=600,
)
return {
"text": resp.choices[0].message.content,
"sources": [c["id"] for c in chunks],
}Tres elementos innegociables en el prompt del sistema:
- Cita las fuentes — haz que el modelo adjunte
[doc:42]a cada afirmación. Si el ID citado no es real, has detectado una alucinación - Rechaza explícitamente — "si el contexto no responde, di que no tengo esa información". Sin esto, el modelo completa la respuesta con conocimiento del mundo
- Salida concisa — las respuestas breves se correlacionan con respuestas precisas. Un máximo de 600 tokens es un buen valor predeterminado para producción
5. Modelo de generación + almacenamiento en caché: la capa de costes
Claude Sonnet 4.6 es el valor predeterminado. Claude Haiku 4.5 es la alternativa económica si tienes restricciones de coste. Encapsula siempre el prompt del sistema en cache_control; el prompt del sistema es el mismo en cada llamada, así que el almacenamiento en caché reduce esa parte al 10% de la tarifa de entrada.
Coste realista por consulta a escala de producción (5K de contexto, 500 de salida, con el almacenamiento en caché activado):
| Modelo | Coste por consulta | Con 10.000 consultas/día | Calidad frente a Sonnet |
|---|---|---|---|
claude-sonnet-4-6 | ~$0.007 | ~$70/día | Referencia |
claude-haiku-4-5 | ~$0.001 | ~$10/día | ~85%, 4× más barato |
Con 10.000 consultas/día → $70/día en Sonnet, $10/día en Haiku. Elige Sonnet para casos de uso de alto riesgo (cara al cliente, legales, médicos) y Haiku para procesamiento interno o masivo.
Qué se rompe en producción (y cómo detectarlo)
- Deriva de distribución: el corpus de entrenamiento deja de coincidir con las preguntas reales de los usuarios. Muestrea 100 consultas/semana y comprueba manualmente recall@5
- Embeddings obsoletos: los documentos fuente se actualizan, pero los embeddings no. Registra semanalmente el tamaño del índice frente al tamaño de la fuente
- Citas falsas: el modelo inventa IDs de documentos que parecen reales. Valida cada
[doc:N]contra la lista real de IDs recuperados antes de renderizar - Picos de latencia: la base de datos vectorial se desborda al superar un millón de vectores. Usa indexación HNSW y particiona por tenant si es multi-tenant
Hoja de ruta de producción de 4 semanas
- Semana 1: crea un prototipo con 100 documentos, 10 preguntas de prueba y evaluación manual
- Semana 2: escala al corpus completo, crea la recuperación híbrida y escribe un conjunto de evaluación de 100 preguntas
- Semana 3: ajusta la segmentación + el prompt del sistema iterando sobre la evaluación y publícalo para usuarios internos
- Semana 4: monitorización, presupuesto de costes y lanzamiento público con una interfaz para citar fuentes
Preguntas frecuentes
¿Qué tamaño de fragmento debe utilizar un sistema RAG en producción?
Entre 1.000 y 1.500 caracteres por fragmento, con una superposición de 100–200 caracteres, divididos mediante un separador recursivo de caracteres que primero corte en los límites de los párrafos, después en las oraciones y finalmente en las palabras. Los modelos conscientes de las oraciones, los separadores conscientes de Markdown y las ventanas deslizantes son ligeramente mejores con un esfuerzo mucho mayor; ese esfuerzo se aprovecha mejor en la recuperación.
¿Qué modelo de embeddings debe utilizar una canalización RAG?
text-embedding-3-large es el valor predeterminado para contenido multilingüe y técnico, y para la recuperación de textos cortos exclusivamente en inglés, como preguntas frecuentes y fichas de producto, text-embedding-3-small cuesta aproximadamente 4 veces menos con una pérdida de calidad insignificante. Ten en cuenta que Kunavo no sirve embeddings: /v1/embeddings es un formato de comunicación implementado sin ningún modelo habilitado detrás, por lo que el paso de embeddings llama directamente a OpenAI, Voyage o Cohere, mientras que el paso de generación llama a Kunavo. Esto no cuesta nada adicional a una canalización salvo una segunda clave, ya que, de todos modos, son dos solicitudes independientes.
¿La búsqueda vectorial por sí sola es suficiente para la recuperación RAG?
No. La similitud vectorial gestiona bien la coincidencia semántica difusa, pero falla con términos literales como un SKU o un número de pedido. Ejecutar en paralelo una búsqueda de palabras clave BM25 y fusionar los resultados con reciprocal rank fusion suele elevar recall@5 de aproximadamente un 70 % a aproximadamente un 90 %: es la mayor mejora de calidad individual después de elegir un modelo de embeddings decente.
¿Cómo se evita que un sistema RAG alucine?
Tres patrones en el prompt del sistema: exigir una cita [doc:N] en cada afirmación factual, indicar al modelo que diga que no tiene la información cuando el contexto no responda, y mantener las respuestas breves: un máximo de 600 tokens de salida es un buen valor predeterminado para producción. Después, valida cada ID citado contra el conjunto recuperado antes de renderizar; un ID que no sea real es una alucinación detectada.
¿Cuánto cuesta una consulta RAG en producción?
Con 5K tokens de contexto y 500 tokens de salida, con el almacenamiento en caché del prompt activado: aproximadamente $0.007 por consulta en Claude Sonnet 4.6 y aproximadamente $0.001 en Claude Haiku 4.5 (4× más barato, con aproximadamente el 85% de la calidad). Con 10.000 consultas al día, son aproximadamente $70/día en Sonnet y aproximadamente $10/día en Haiku.