Query rewriting: limpiar la pregunta antes de buscar
Las preguntas reales son sucias
Sección titulada «Las preguntas reales son sucias»En el laboratorio, las preguntas vienen limpias: “¿Qué establece la regla de dependencia en Clean Architecture?”. Bien escritas, autocontenidas, con vocabulario alineado al corpus.
En producción, las preguntas son otra cosa:
- Telegráficas:
"clean arch testabilidad" - Con pronombres:
"¿y eso cómo se aplica?" - Con referencias a contexto previo (chat multi-turn):
"y la diferencia con la otra?" - Con typos:
"que es la rega de dpendencia"
El embedding de cualquiera de esas es ruidoso. La query telegráfica no tiene contexto suficiente. Los pronombres no significan nada para el retriever. Los typos rompen tanto el embedding como BM25.
Solución: un LLM call antes del retrieve que limpia la query.
El pipeline
Sección titulada «El pipeline»pregunta cruda ↓ LLM (rewrite)pregunta limpia y autocontenida ↓ embedding + retrievechunks ↓ generaterespuestaCosto: 1 LLM call extra. Latencia +200-400ms con llama3.2:3b. Casi obligatorio en chat multi-turn, opcional en single-turn.
El archivo
Sección titulada «El archivo»packages/01-vercel-ai-sdk/src/query/rewrite.ts — un solo export, fail-soft:
export async function rewriteQuery( query: string, opts: RewriteOptions = {},): Promise<string>;Si el LLM falla o devuelve algo inutilizable, retorna la query original. Nunca rompe el pipeline.
El prompt
Sección titulada «El prompt»const prompt = `Reformulá la siguiente pregunta para que sea autocontenida y específica. Resolvé pronombres ("eso", "él", "lo") y referencias a contexto previo si las hay. Si la pregunta ya es autocontenida, devolvela igual.
Respondé SOLO con la pregunta reformulada, sin explicación, sin etiquetas, sin comillas.
${historyBlock}PREGUNTA ORIGINAL:${query}
PREGUNTA REFORMULADA:`;Cuatro detalles:
autocontenida y específica: la transformación que querés.resolvé pronombres: explícito porque es el caso más común y el modelo no siempre lo hace por su cuenta.Si la pregunta ya es autocontenida, devolvela igual: importante. Sin esto, el LLM tiende a “mejorar” preguntas que ya estaban bien — agregar palabras de adorno o cambiar el sentido sutilmente.SOLO con la pregunta reformulada, sin explicación: el LLM por defecto agrega “Aquí está la pregunta reformulada:” antes. Eso rompe el pipeline downstream.
El historial es opcional
Sección titulada «El historial es opcional»Para single-turn, llamás:
const cleaned = await rewriteQuery("¿y eso cómo se aplica?");// → "¿Cómo se aplica la regla de dependencia?"// (con suerte, si el LLM adivina bien el referente)Para chat multi-turn, pasás historial:
const cleaned = await rewriteQuery( "¿y la diferencia con la otra?", { history: [ "Usuario: ¿Qué propone Clean Architecture?", "Asistente: Clean Architecture propone separar las dependencias en capas concéntricas...", ]},);// → "¿Cuál es la diferencia entre Clean Architecture y otra arquitectura,// como Hexagonal Architecture?"El historial le da al LLM la pista que necesita para resolver “la otra”.
El parser defensivo
Sección titulada «El parser defensivo»El LLM a veces ignora la instrucción y devuelve algo como:
La pregunta reformulada es: "¿Cuál es la regla de dependencia en Clean Architecture?"El cleanup remueve comillas y prefijos comunes:
const cleaned = data.response.trim().replace(/^["'`]|["'`]$/g, '');return cleaned.length > 0 ? cleaned : query;No es perfecto pero captura los 80% de casos. Si el cleaning resulta en string vacío, fallback a la query original.
El comando
Sección titulada «El comando»pnpm vercel:query:adv "<pregunta>" --rewriteCuando rewrite cambia algo, lo imprime al inicio de la salida:
[rewrite] "que es la rega de dpendencia" → "¿Qué es la regla de dependencia en Clean Architecture?"Si la query ya estaba bien, no imprime nada extra.
Probá una query telegráfica con typo:
pnpm vercel:query:adv "que es regla dependencia clean arch" --rewriteMirá el bloque [rewrite] ... para ver qué hizo el LLM. Después corré la misma sin --rewrite:
pnpm vercel:query:adv "que es regla dependencia clean arch"Compará los chunks recuperados (=== Sources ===). Casi seguro la versión con rewrite trae chunks más específicos a Clean Architecture, y la versión sin rewrite trae chunks más mezclados (porque “regla”, “dependencia”, “clean” sin contexto matchean varios temas).
Combinando con multi-query
Sección titulada «Combinando con multi-query»--rewrite corre antes que --multi-query. Tiene sentido: primero resolvés referencias, después descomponés. En orden inverso, descompondrías una pregunta sucia y heredarías el ruido en cada sub-query.
pnpm vercel:query:adv "y la diferencia con la otra" \ --rewrite --multi-query 3 --hybrid --rerankPipeline:
- Rewrite: “¿Cuál es la diferencia entre Clean Architecture y la otra?” → “¿Cuál es la diferencia entre Clean Architecture y Hexagonal Architecture?” (resuelve “la otra”)
- Multi-query: descompone en 3 sub-preguntas.
- Hybrid retrieve + RRF para cada una.
- Merge + dedupe.
- LLM rerank a top-4.
- Generate.
5 LLM calls + 3 retrieves. Pesado, pero brutalmente fuerte.
Cuándo NO usar rewrite
Sección titulada «Cuándo NO usar rewrite»- Latencia crítica. 200-400ms más por llamada.
- Single-turn con preguntas siempre bien planteadas. (raro en producción real, común en demos)
- Costos. Si pagás por tokens (no es el caso del curso, todo local), un LLM call extra duplica costo de query.
Lo que viene
Sección titulada «Lo que viene»Tenés un retriever fino (hybrid + rerank) y un input limpio (rewrite + multi-query). La pregunta final es: ¿el usuario puede confiar en lo que le respondiste?. Para eso necesitás citation explícita — fuentes inline después de cada afirmación. Eso es el último capítulo del Nivel 3.