¿Aún necesitas vector databases con contextos más largos?
1. Introducción
Cada generación de modelo de lenguaje grande (LLM) tiene una ventana de contexto más larga. Por eso muchos equipos comienzan a preguntarse: si podemos pasar todos los documentos al prompt, ¿realmente necesitamos construir un RAG con una base de datos vectorial?
Este artículo no responde “sí” ni “no” de manera absoluta. Verás dónde difieren los dos enfoques. El artículo incluye código Java que ilustra cómo estimar el presupuesto de tokens, un almacén de vectores mínimo para entender el mecanismo de retrieval, y un simple selector de estrategia. Después de leerlo, podrás decidir por tu cuenta qué es lo mejor para tu sistema.
2. Dos formas de incorporar conocimiento en un LLM
Los LLM no conocen los datos internos de tu empresa, como políticas de reembolso, documentación de API o historial de pedidos de clientes. Hay dos formas principales de incorporar esa información en las respuestas.
2.1. Retrieval con vector database
El proceso típico de RAG (Retrieval-Augmented Generation) consta de cuatro pasos:
- Dividir los documentos en fragmentos pequeños (chunks).
- Usar un modelo de embedding para convertir cada chunk en un vector numérico.
- Almacenar los vectores en una base de datos vectorial con índices para encontrar los vecinos más cercanos.
- Cuando el usuario hace una pregunta, embedir la pregunta, recuperar los top-K chunks más cercanos e incluirlos en el prompt.
El prompt final contiene solo unos pocos fragmentos relevantes, no el repositorio completo de documentos.
2.2. Long context: “meter todo en el prompt”
Este enfoque es mucho más simple: concatena todos los documentos en el prompt y deja que el modelo encuentre la información por sí solo. No necesitas chunking, embedding, ni infraestructura adicional.
Con repositorios pequeños, este enfoque suele dar buenos resultados. El modelo ve todo el contexto, por lo que puede responder incluso preguntas que requieren sintetizar múltiples partes, algo que el retrieval con top-K puede perder.
3. Por qué esta pregunta ha cobrado importancia
Hay varias razones por las que este debate se ha intensificado:
- La ventana de contexto crece rápidamente. Los repositorios de documentos que antes había que dividir ahora caben en un único prompt.
- Prompt caching. Muchos proveedores permiten cachear la parte inicial del prompt que es fija. Esto reduce significativamente el costo y la latencia de enviar el mismo bloque de documentos repetidamente.
- RAG es difícil de hacer bien. El chunking incorrecto, embeddings inadecuados para el idioma, y top-K que pierde fragmentos importantes son errores muy comunes. Cualquier equipo que haya depurado un pipeline RAG quiere eliminar una capa de complejidad.
Sin embargo, los contextos largos tienen sus propias limitaciones. La calidad de la recuperación de información dentro de prompts muy largos puede ser inconsistente: la información en medio de un bloque grande de texto a veces se pasa por alto. El costo y la latencia siguen aumentando según el número de tokens de entrada. Además, hay problemas que las ventanas de contexto no resuelven, como control de acceso y datos que cambian constantemente. Las siguientes secciones abordan cada punto.
4. Comenzar con números: estimar el presupuesto de tokens
Antes de elegir una arquitectura, necesitas saber cuántos tokens “pesa” tu repositorio de documentos. El ejemplo a continuación usa solo Java estándar.
import java.util.List;
public class TokenBudget {
// Estimación aproximada: el tokenizer real produce resultados diferentes,
// especialmente para vietnamita
private static final double CHARS_PER_TOKEN = 4.0;
public static long estimateTokens(String text) {
return Math.round(text.length() / CHARS_PER_TOKEN);
}
public static long estimateCorpus(List<String> documents) {
return documents.stream().mapToLong(TokenBudget::estimateTokens).sum();
}
public static void main(String[] args) {
List<String> policies = List.of(
"Chính sách hoàn tiền: khách hàng được hoàn tiền khi đơn hàng bị lỗi.",
"Phí vận chuyển được tính theo khối lượng và khoảng cách giao hàng.",
"Tài khoản Customer bị khoá sau nhiều lần đăng nhập sai."
);
long total = estimateCorpus(policies);
long budget = 8_000; // la porción del contexto que asignas a los documentos
System.out.printf("Corpus ~%d tokens, budget %d, fits=%b%n",
total, budget, total <= budget);
}
}
Explicación por partes:
CHARS_PER_TOKENes una heurística muy aproximada. El vietnamita con diacríticos generalmente consume más tokens que el inglés. Cuando necesites números precisos, deberías usar el tokenizer del modelo específico que estés usando.budgetno es la ventana de contexto completa. Debes reservar espacio para el system prompt, historial de conversación y respuestas.- El resultado para los tres fragmentos cortos:
Corpus ~48 tokens, budget 8000, fits=true
Si fits=true y el repositorio de documentos cambia poco, long context es un candidato serio. Si el repositorio es mucho más grande que tu presupuesto, aún necesitarás retrieval, sin importar cuán larga sea tu ventana de contexto.
5. Un vector store mínimo para entender el mecanismo
Para ver qué hace realmente el retrieval, escribiremos un vector store en memoria. El embedding aquí es de tipo “hashing bag-of-words”, solo para demostración. Un sistema real llamaría a un modelo de embedding.
import java.util.*;
public record Document(String id, String text) {}
public record ScoredDocument(Document document, double score) {}
public class HashingEmbedder {
private final int dimensions;
public HashingEmbedder(int dimensions) { this.dimensions = dimensions; }
public double[] embed(String text) {
double[] vector = new double[dimensions];
for (String token : text.toLowerCase().split("\\P{L}+")) {
if (token.isBlank()) continue;
vector[Math.floorMod(token.hashCode(), dimensions)] += 1.0;
}
double norm = Math.sqrt(Arrays.stream(vector).map(v -> v * v).sum());
if (norm > 0) for (int i = 0; i < dimensions; i++) vector[i] /= norm;
return vector;
}
}
public class InMemoryVectorStore {
private record Entry(Document document, double[] vector) {}
private final HashingEmbedder embedder;
private final List<Entry> entries = new ArrayList<>();
public InMemoryVectorStore(HashingEmbedder embedder) { this.embedder = embedder; }
public void add(Document document) {
entries.add(new Entry(document, embedder.embed(document.text())));
}
public List<ScoredDocument> search(String query, int topK) {
double[] q = embedder.embed(query);
return entries.stream()
.map(e -> new ScoredDocument(e.document(), dot(q, e.vector())))
.sorted(Comparator.comparingDouble(ScoredDocument::score).reversed())
.limit(topK)
.toList();
}
private static double dot(double[] a, double[] b) {
double sum = 0;
for (int i = 0; i < a.length; i++) sum += a[i] * b[i];
return sum;
}
}
Puntos clave:
embeddivide los tokens por caracteres que no son letras (\\P{L}+), preservando los diacríticos vietnamitas. Cada palabra se codifica en una dimensión del vector.- El vector se normaliza a longitud unitaria. Entonces el producto escalar (
dot) es la similitud del coseno. searchescanea todas las entradas. Esta es una búsqueda de fuerza bruta, complejidad O(n). Una base de datos vectorial real usa índices aproximados como HNSW para mantener velocidad con muchos vectores.
Prueba de ejecución:
var store = new InMemoryVectorStore(new HashingEmbedder(1024));
store.add(new Document("refund", "Chính sách hoàn tiền: khách hàng được hoàn tiền khi đơn hàng bị lỗi."));
store.add(new Document("shipping", "Phí vận chuyển được tính theo khối lượng và khoảng cách giao hàng."));
store.add(new Document("account", "Tài khoản Customer bị khoá sau nhiều lần đăng nhập sai."));
store.search("khách hàng muốn hoàn tiền", 2)
.forEach(r -> System.out.printf("%s -> %.3f%n", r.document().id(), r.score()));
refund -> 0.6xx
shipping -> 0.1xx
El número exacto depende de la función de hash. El orden es estable: refund está primero porque comparte más palabras con la pregunta. Solo este fragmento se incluye en el prompt, no los tres.
Nota: El embedding real entiende semántica (“devolver dinero” se parece a “reembolso”), pero hashing bag-of-words no. No uses el ejemplo anterior en producción.
En producción, si ya usas PostgreSQL, la extensión pgvector suele ser un primer paso razonable antes de considerar una base de datos vectorial dedicada:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE policy_chunk (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
content TEXT NOT NULL,
embedding vector(768)
);
CREATE INDEX ON policy_chunk USING hnsw (embedding vector_cosine_ops);
-- :query_embedding suministrado por la aplicación
SELECT id, content
FROM policy_chunk
WHERE tenant_id = :tenant_id
ORDER BY embedding <=> :query_embedding
LIMIT 5;
La dimensión 768 debe coincidir con el modelo de embedding que estés usando. El operador <=> calcula la distancia del coseno. La cláusula WHERE tenant_id = ... señala una razón importante por la que el retrieval sigue siendo necesario, discutida en la sección 7.
6. Combinar ambos enfoques: elegir estrategia según datos
En la práctica, la respuesta generalmente es “depende de tu repositorio de documentos”. Podemos codificar esta decisión para que todo el equipo esté alineado en los criterios.
public enum ContextStrategy { FULL_CONTEXT, RETRIEVAL }
public record CorpusProfile(long totalTokens,
boolean changesFrequently,
boolean needsPerUserAccess) {}
public class ContextStrategySelector {
private final long documentBudgetTokens;
public ContextStrategySelector(long documentBudgetTokens) {
this.documentBudgetTokens = documentBudgetTokens;
}
public ContextStrategy choose(CorpusProfile profile) {
if (profile.needsPerUserAccess()) return ContextStrategy.RETRIEVAL;
if (profile.totalTokens() > documentBudgetTokens) return ContextStrategy.RETRIEVAL;
if (profile.changesFrequently() && profile.totalTokens() > documentBudgetTokens / 2) {
// prompt caching es difícil de aprovechar cuando los datos cambian constantemente
return ContextStrategy.RETRIEVAL;
}
return ContextStrategy.FULL_CONTEXT;
}
}
Lógica en orden de prioridad:
- Si cada usuario solo puede ver una parte de los datos, debes filtrar antes de incluir en el prompt. El retrieval con filtros hace esto naturalmente.
- Si el repositorio es más grande que tu presupuesto de tokens, no hay otra opción que retrieval.
- Si los datos cambian constantemente y ya ocupan una parte significativa del presupuesto, prompt caching es poco efectivo, por lo que retrieval suele ser más económico.
- Para todos los demás casos: usa long context por simplicidad.
El umbral / 2 es solo un ejemplo. Deberías medir el costo y la calidad en tus propios datos, luego ajustar.
7. Pruebas del comportamiento principal
Dependencia de test: si tu proyecto usa Spring Boot parent, las versiones ya están gestionadas:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class RetrievalAndStrategyTest {
@Test
void givenRefundQuestion_whenSearch_thenRefundPolicyRanksFirst() {
var store = new InMemoryVectorStore(new HashingEmbedder(1024));
store.add(new Document("refund", "Chính sách hoàn tiền: khách hàng được hoàn tiền khi đơn hàng bị lỗi."));
store.add(new Document("shipping", "Phí vận chuyển được tính theo khối lượng và khoảng cách giao hàng."));
var results = store.search("khách hàng muốn hoàn tiền", 1);
assertThat(results).hasSize(1);
assertThat(results.getFirst().document().id()).isEqualTo("refund");
}
@Test
void givenSmallStaticCorpus_whenChoose_thenFullContext() {
var selector = new ContextStrategySelector(50_000);
var profile = new CorpusProfile(10_000, false, false);
assertThat(selector.choose(profile)).isEqualTo(ContextStrategy.FULL_CONTEXT);
}
@Test
void givenPerUserAccess_whenChoose_thenRetrievalEvenIfSmall() {
var selector = new ContextStrategySelector(50_000);
var profile = new CorpusProfile(1_000, false, true);
assertThat(selector.choose(profile)).isEqualTo(ContextStrategy.RETRIEVAL);
}
}
El tercer test es el más importante desde el punto de vista de negocio. Incluso si el repositorio es pequeño, el requisito de control de acceso aún te obliga a usar retrieval. Es una restricción que las ventanas de contexto largas no pueden resolver.
8. Cuándo aún necesitas vector database, cuándo no
Aún necesitas retrieval (base de datos vectorial, o índice vectorial en tu DB existente) cuando:
- Tu repositorio de documentos es mucho más grande que la ventana de contexto, por ejemplo, todos los tickets de soporte o toda la wiki interna.
- Hay control de acceso por usuario o tenant. No puedes permitir que el modelo “vea” datos que el usuario no tiene permiso para ver, ni siquiera en el prompt.
- Los datos se actualizan constantemente y necesitan reflejarse casi instantáneamente.
- Alto volumen de consultas. Enviar un prompt gigantesco para cada solicitud hace que el costo y la latencia aumenten según el número de tokens.
- Necesitas citas de fuente claras, es decir, saber en qué chunk se basa cada respuesta.
Puedes prescindir de la base de datos vectorial cuando:
- Tu repositorio es pequeño, estable y cabe en tu presupuesto de tokens, como el conjunto de políticas de un producto o la documentación de un módulo.
- Las preguntas típicamente requieren sintetizar toda la documentación, como “resume todas las diferencias entre dos contratos”. El retrieval top-K es deficiente en esto.
- Estás haciendo un prototipo y quieres validar el valor antes de invertir en infraestructura.
Tampoco necesariamente necesitas vector search para consultas exactas como códigos de pedido, SKU o nombres de funciones. Full-text search o consultas SQL suelen ser mejores. Muchos sistemas usan búsqueda híbrida, combinando palabras clave y vectores.
9. Errores comunes
- Confundir la ventana de contexto con el presupuesto de documentos. Olvidas reservar espacio para el system prompt, historial de chat y salida, causando que las solicitudes se corten o fallen en producción.
- Meter todo en el prompt sin evaluar la calidad. Cuanto más largo sea el prompt, más probable es que el modelo pierda detalles en el medio. Construye un conjunto de pruebas con respuestas de referencia y mide con tus datos.
- Construir una base de datos vectorial dedicada demasiado pronto. Con decenas de miles de chunks, un índice vectorial en PostgreSQL suele ser suficiente, y te ahorras operar un sistema adicional.
- Filtrar por permisos después del retrieval. Recuperar top-K y luego filtrar por permisos puede perder todos los resultados válidos. Incluye la condición de permisos en la propia consulta, como la cláusula
WHERE tenant_iden la sección 5. - Cambiar el modelo de embedding sin re-indexar. Los vectores generados por dos modelos diferentes no son comparables entre sí.
Nota: Long context y retrieval no se excluyen mutuamente. Un patrón común es retrieval “generoso”, recuperar más chunks que antes, luego aprovechar el contexto largo para que el modelo haga su propia selección. Esto reduce el riesgo de pérdida manteniendo el costo bajo control.
10. Conclusión
Las ventanas de contexto más largas cambian el equilibrio, pero no eliminan la necesidad de retrieval. Con repositorios pequeños y estables, “meter todo en el prompt” suele ser simple y suficientemente bueno. Con datos grandes, cambios constantes, requisitos de control de acceso o alto volumen, el retrieval sigue siendo necesario. Sin embargo, no tiene que ser una base de datos vectorial dedicada: un índice vectorial en tu base de datos existente suele ser suficiente.
El enfoque práctico es medir primero, decidir después. Estima los tokens de tu repositorio de documentos, identifica los requisitos de control de acceso y la frecuencia de actualización, luego evalúa la calidad con un conjunto de preguntas reales. Cuando estos criterios se codifican como ContextStrategySelector, todo el equipo tiene una decisión clara y verificable.