Ir al contenido

Multi-query: descomposición de preguntas

Una pregunta como:

“Comparame Clean Architecture con Hexagonal en términos de testabilidad.”

cubre tres temas: Clean Architecture, Hexagonal Architecture, y testabilidad. Cuando embedeás esa pregunta, el embedding promedia las direcciones semánticas de los tres. Resultado: un vector que está “en el medio” — no apunta fuerte a ninguno de los tres temas.

El retriever te trae chunks que también están en el medio: chunks ambiguos que tocan los temas pero no son el “core” de ninguno.

La solución: buscar tres veces, una por tema, y unir. Es exactamente lo que hace multi-query.

pregunta original
↓ LLM (decompose)
3 sub-preguntas
↓ retrieve cada una (dense, o hybrid)
3 listas de top-K
↓ unión deduplicada por chunk_id
candidatos únicos
↓ (rerank si --rerank también)
top-K final
↓ generate
respuesta

Costo: 1 LLM call extra para descomponer + N retrieves. Para corpus chicos y queries simples es overkill. Para queries multi-tema es win casi seguro.

packages/01-vercel-ai-sdk/src/query/multi-query.ts — un solo export:

multi-query.ts: API
export async function decomposeQuery(query: string, n: number): Promise<string[]>;

Devuelve [query] (la original) si n <= 1 o si la descomposición falla. Esa es la propiedad importante: fail-soft. Si el decompose tira error o el LLM devuelve algo malparseable, multi-query degrada a single-query — no te rompe la pipeline.

multi-query.ts: prompt
const prompt = `Descomponé la siguiente PREGUNTA en exactamente ${n} sub-preguntas más simples y específicas que cubran distintas facetas o partes de la pregunta original. Cada sub-pregunta debe ser autocontenida y buscable por sí sola.
Respondé EXACTAMENTE con este JSON, sin texto adicional, sin bloques de código:
["sub-pregunta 1", "sub-pregunta 2", ...]
PREGUNTA:
${query}
JSON:`;

Tres detalles:

  • autocontenida y buscable por sí sola es la frase mágica. Sin esto, el LLM tiende a hacer sub-preguntas tipo “¿qué dice el primer punto?” — útil para él, inútil para el retriever.
  • exactamente ${n}: si pedís 3, querés 3. Sin esto, el modelo a veces devuelve 5 o 2.
  • JSON cerrado: simplifica el parser. Si no parsea, fallback a [query].

Cada sub-query devuelve top-K chunks. Algunos chunks pueden aparecer en múltiples sub-queries (típicamente los más relevantes a la pregunta original). El orquestador los dedupica por chunk.id:

query.adv.ts: dedupe
const merged = new Map<string, { chunk: Chunk; score: number }>();
for (const sq of queries) {
const dense = await denseRetrieve(sq, wide);
for (const d of dense) {
const cur = merged.get(d.chunk.id);
if (!cur || cur.score < d.score) {
merged.set(d.chunk.id, { chunk: d.chunk, score: d.score });
}
}
}

Para cada chunk visto, guardamos el mejor score de las distintas sub-queries. Eso premia chunks que son top-1 en alguna sub-pregunta — son los que más probablemente respondan algún aspecto del problema.

Ventana de terminal
pnpm vercel:query:adv "<pregunta>" --multi-query 3

Combinable con todo:

Ventana de terminal
pnpm vercel:query:adv "<pregunta>" --multi-query 3 --hybrid --rerank

Pipeline interno: descompone → 3 retrieves (dense + BM25 + RRF cada una) → merge → rerank a top-4 → generate. Es lento (4 LLM calls + 3 retrieves) pero brutalmente fuerte para preguntas complejas.

Probá el contraste:

Ventana de terminal
# Pregunta multi-tema, single-query
pnpm vercel:query:adv "Comparame Clean Architecture con Hexagonal Architecture en términos de testabilidad y separación de capas"
# Misma pregunta, multi-query
pnpm vercel:query:adv "Comparame Clean Architecture con Hexagonal Architecture en términos de testabilidad y separación de capas" --multi-query 3

En la salida del segundo, mirá las sub-queries que generó el LLM en el bloque [multi-query] decomposed into 3:. Algo así:

[multi-query] decomposed into 3:
1. ¿Qué establece Clean Architecture sobre testabilidad y separación de capas?
2. ¿Qué establece Hexagonal Architecture sobre testabilidad y separación de capas?
3. ¿Cuáles son las diferencias clave entre Clean Architecture y Hexagonal Architecture en cuanto a estructura de capas?

Cada una busca un aspecto distinto. La unión te trae chunks de ambos archivos y de los puntos de contraste — algo que el embedding promedio no podía traer.

  • Preguntas single-tema. “¿Qué es la regla de dependencia?” no necesita descomposición.
  • Latencia crítica. Multi-query agrega 1 LLM call al inicio + N veces el costo de retrieve. Si tu SLA es < 500ms total, no entra.
  • Corpus chico y temáticamente homogéneo. Si todos los chunks hablan de variantes del mismo tema, las sub-queries van a traer los mismos chunks deduplicados — multi-query no aporta.

Multi-query asume que la pregunta es completa y bien planteada. Pero las preguntas reales de usuarios son sucias: pronombres, referencias a contexto previo, telegráficas. El siguiente capítulo: query rewriting — limpiar la pregunta antes de buscar.