El problema no está en el índice, está en la pregunta
Montas el RAG con cuidado. Chunks bien cortados, embeddings decentes, un reranker encima. Y aun así hay preguntas que devuelven basura.
La causa casi nunca es el índice. Es la consulta.
Tu usuario escribe “¿por qué me sale el error 500 al subir facturas?”. Tu documentación habla de “excepciones no controladas en el endpoint de ingesta”. Comparten el problema, pero no las palabras. La similitud coseno no sabe de sinónimos ni de intención: compara vectores, y esos dos textos caen lejos.
La reescritura de consultas mete un paso entre la pregunta del usuario y la recuperación. Un LLM transforma la consulta cruda en una o varias consultas mejores antes de tocar el índice. Es de las mejoras con mejor relación resultado/esfuerzo en un RAG, y casi nadie la implementa.
La consulta cruda casi nunca es la buena
Las preguntas reales vienen mal formuladas para recuperar. Tres patrones se repiten.
Son ambiguas: “¿y en producción?” no significa nada sin el turno anterior de la conversación. Son verbosas: media pregunta es contexto emocional que ensucia el vector. O son compuestas: “¿cómo configuro el webhook y qué pasa si falla el reintento?” son dos búsquedas distintas metidas en una.
Recuperar sobre el texto literal hereda todos esos defectos. Reescribir los corrige antes de que importen.
Multi-query: pregunta de varias formas a la vez
La técnica más simple y la que más rinde. Pides al LLM que genere tres o cuatro variantes de la misma pregunta, recuperas con todas y unes los resultados.
Cada variante ataca el corpus desde un ángulo distinto de vocabulario. Una usará “error 500”, otra “excepción del servidor”, otra “fallo al procesar”. Basta con que una acierte el término que usa tu documentación.
def multi_query(pregunta: str, k: int = 4) -> list[str]:
"""Genera variantes de la consulta para cubrir más vocabulario."""
prompt = (
"Reescribe la siguiente pregunta de 3 formas distintas. "
"Cambia el vocabulario, no el significado. Una por línea.\n\n"
f"Pregunta: {pregunta}"
)
variantes = llm.completar(prompt).splitlines()
return [pregunta, *variantes] # incluye siempre la original
def recuperar_multi(pregunta: str, top_k: int = 6):
docs = {}
for consulta in multi_query(pregunta):
for doc in indice.buscar(consulta, k=top_k):
docs[doc.id] = doc # dedup por id, gana la última
return list(docs.values())
La clave está en el dedup por id: varias variantes recuperarán los mismos chunks, y quieres contar cada documento una sola vez antes de pasarlo al reranker. No devuelvas los duplicados aguas abajo.
HyDE: recupera con una respuesta, no con la pregunta
HyDE (Hypothetical Document Embeddings) le da la vuelta a la intuición. En lugar de buscar con la pregunta, pides al LLM que invente una respuesta plausible y buscas con esa.
Suena raro hasta que lo piensas. Una pregunta y su respuesta están escritas en registros distintos: la pregunta es corta e interrogativa, la respuesta es afirmativa y densa en términos técnicos. Y tu corpus está lleno de respuestas, no de preguntas. Buscar respuesta contra respuesta acerca los vectores.
def hyde(pregunta: str):
hipotesis = llm.completar(
f"Responde en 2-3 frases, con vocabulario técnico:\n{pregunta}"
)
return indice.buscar(hipotesis, k=6)
La respuesta inventada puede tener datos falsos, y da igual: no se la enseñas al usuario. Solo la usas como vector de búsqueda. Funciona especialmente bien en dominios técnicos donde la pregunta del usuario y el texto de la fuente comparten poco léxico.
Descomposición: parte las preguntas compuestas
Cuando la pregunta encierra varias, recuperar sobre el conjunto diluye todo. La respuesta al webhook y la del reintento viven en secciones distintas de tu documentación, y un solo vector no apunta a las dos.
Descomponer significa pedir al LLM que separe la pregunta en subpreguntas atómicas, recuperar para cada una y combinar el contexto antes de generar. Es más caro, pero es la única forma de responder bien a preguntas de varios saltos.
Cuál elegir
Ninguna es gratis, y no se usan todas a la vez. La decisión depende de tu tipo de pregunta y de tu presupuesto de latencia.
| Técnica | Cuándo brilla | Coste extra | Riesgo |
|---|---|---|---|
| Multi-query | Desajuste de vocabulario usuario/corpus | 1 llamada LLM + N búsquedas | Bajo; casi siempre suma |
| HyDE | Dominios técnicos, poco léxico compartido | 1 llamada LLM (generación) | La hipótesis puede desviar el tema |
| Descomposición | Preguntas de varios saltos o compuestas | 1 llamada + varias búsquedas | Sobre-fragmenta preguntas simples |
| Reescritura con historial | Conversaciones (“¿y en producción?“) | 1 llamada LLM (barata) | Casi nulo; imprescindible en chat |
La última fila es la que no debes saltarte si tu RAG vive dentro de un chat. Antes de recuperar, reescribe la pregunta de seguimiento en una consulta autónoma usando el historial. “¿Y en producción?” se convierte en “¿Cómo configuro el webhook de facturas en el entorno de producción?”. Sin ese paso, la recuperación en el segundo turno de cualquier conversación es una lotería.
El coste que no se ve
Todo esto añade al menos una llamada a un LLM antes de la recuperación. Eso son cientos de milisegundos y unos tokens por cada consulta. En un buscador con tráfico real, se nota en la factura y en la latencia.
Dos formas de amortiguarlo. Usa un modelo pequeño y rápido para la reescritura: no necesitas tu modelo grande para reformular una frase. Y cachea las variantes de las consultas frecuentes, que en soporte y documentación se repiten muchísimo.
También hay un modo de fallo real: si la reescritura se desvía del tema, empeora la recuperación en lugar de mejorarla. Por eso conviene incluir siempre la consulta original entre las variantes, como red de seguridad. Si el LLM alucina una reformulación mala, la original sigue en la mezcla.
Cómo empezar mañana
No implementes las cuatro técnicas de golpe. Ese es el error clásico.
Empieza midiendo. Coge veinte preguntas reales que tu RAG resuelve mal y mira los chunks que recupera. Si el problema es de vocabulario, multi-query te lo arregla en una tarde. Si es de dominio muy técnico, prueba HyDE. Si tu producto es un chat, la reescritura con historial no es opcional, es la base.
Y mide con una métrica de recuperación, no de vibraciones. Recall a k, o simplemente el porcentaje de preguntas cuyo chunk correcto aparece entre los recuperados. La reescritura de consultas se justifica sola cuando ves ese número subir sin haber tocado el índice.
El mejor reranker del mundo no puede reordenar un documento que la búsqueda nunca trajo. Arregla la pregunta primero.