Vector database ainda é necessário com context windows cada vez maiores?
1. Visão Geral
Cada geração de modelo de linguagem grande (LLM) tem uma context window maior. Por isso, muitos times começam a questionar: se conseguir colocar um repositório inteiro de documentos no prompt, ainda precisa montar um RAG com vector database?
Este artigo não responde com um “sim” ou “não” absoluto. Você verá onde as duas abordagens diferem. O artigo contém código Java que exemplifica como estimar o orçamento de tokens, um vector store mínimo para entender o mecanismo de retrieval, e um seletor de estratégia simples. Depois de ler, você pode decidir por conta própria para seu sistema.
2. Duas formas de colocar conhecimento no LLM
O LLM não conhece os dados internos da sua empresa, por exemplo, política de reembolso, documentação de API ou histórico de Pedidos de Clientes. Existem duas formas principais de colocar esses dados na resposta.
2.1. Retrieval com vector database
O processo típico de RAG (Retrieval-Augmented Generation) consiste em quatro etapas:
- Dividir o documento em pequenos trechos (chunks).
- Usar um modelo de embedding para transformar cada chunk em um vetor numérico.
- Armazenar os vetores em um vector database, com índice para encontrar os vizinhos mais próximos.
- Quando o usuário pergunta, incorporar (embed) a pergunta, obter os top-K chunks mais próximos e colocá-los no prompt.
O prompt final contém apenas alguns trechos relevantes, não o repositório completo de documentos.
2.2. Long context: “colocar tudo no prompt”
Essa abordagem é muito mais simples: concatenar todo o documento no prompt e deixar o modelo encontrar a informação por si só. Você não precisa de chunking, não precisa de embedding, não precisa de infraestrutura adicional.
Com um repositório de documentos pequeno, essa abordagem geralmente dá bons resultados. O modelo vê todo o contexto, então pode responder até perguntas que precisam sintetizar múltiplas partes, algo que o retrieval de top-K pode perder.
3. Por que essa pergunta está recebendo atenção
Há várias razões pelas quais o debate está esquentando:
- Context window aumentando rapidamente. Repositórios de documentos que antes tinham que ser divididos agora cabem em um único prompt.
- Prompt caching. Muitos provedores permitem fazer cache da parte inicial do prompt que é fixa. Como resultado, o custo e a latência de enviar novamente o mesmo bloco de documentos diminuem significativamente.
- RAG é difícil fazer certo. Chunking incorreto, embedding não adequado para o idioma, top-K perdendo trechos importantes são erros muito comuns. Qualquer equipe que já debugou um pipeline RAG quer remover uma camada de complexidade.
Long context também tem suas próprias limitações. A qualidade da recuperação de informações em um prompt muito longo pode ser irregular: informações no meio de um grande bloco de texto às vezes são perdidas. Custo e latência ainda aumentam com o número de tokens de entrada. Além disso, há problemas que a context window não resolve, como permissões granulares e dados em constante mudança. As seções a seguir abordam cada ponto.
4. Começando pelos números: estimando o orçamento de tokens
Antes de escolher uma arquitetura, você precisa saber quanto seu repositório de documentos “pesa” em tokens. O exemplo abaixo usa apenas Java padrão.
import java.util.List;
public class TokenBudget {
// Estimativa grosseira: o tokenizer real produz resultados diferentes, especialmente para português
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(
"Política de reembolso: o cliente recebe reembolso se o pedido tiver um defeito.",
"A taxa de envio é calculada com base no peso e na distância de entrega.",
"A conta do cliente é bloqueada após várias tentativas de login incorretas."
);
long total = estimateCorpus(policies);
long budget = 8_000; // parte da context que você dedica a documentos
System.out.printf("Corpus ~%d tokens, budget %d, fits=%b%n",
total, budget, total <= budget);
}
}
Explicação de cada parte:
CHARS_PER_TOKENé uma heurística muito grosseira. Português com acentos costuma consumir mais tokens que o inglês. Quando você precisa de números exatos, use o tokenizer do modelo específico que está usando.budgetnão é a context window inteira. Você precisa deixar espaço para o system prompt, histórico da conversa e a resposta.- O resultado para os três trechos curtos:
Corpus ~48 tokens, budget 8000, fits=true
Se fits=true e o repositório de documentos muda raramente, long context é um candidato sério. Se o repositório é muitas vezes maior que o orçamento, você ainda precisa de retrieval, não importa o quão longa seja a context window.
5. Um vector store mínimo para entender o mecanismo
Para ver o que o retrieval realmente faz, escrevemos um vector store em memória. O embedding aqui é do tipo “hashing bag-of-words”, apenas para ilustração. Um sistema real chamaria um 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;
}
}
Pontos principais:
embedsepara palavras por caracteres que não são letras (\\P{L}+), preservando caracteres com acentos em português. Cada palavra é feita hash em uma dimensão do vetor.- O vetor é normalizado para ter comprimento 1. Nesse caso, o produto escalar (dot) é a similaridade de cosseno.
searchpercorre todas as entradas. Essa é uma busca exaustiva com complexidade O(n). Um vector database real usa um índice aproximado como HNSW para manter a velocidade quando há muitos vetores.
Testando:
var store = new InMemoryVectorStore(new HashingEmbedder(1024));
store.add(new Document("refund", "Política de reembolso: o cliente recebe reembolso se o pedido tiver um defeito."));
store.add(new Document("shipping", "A taxa de envio é calculada com base no peso e na distância de entrega."));
store.add(new Document("account", "A conta do cliente é bloqueada após várias tentativas de login incorretas."));
store.search("cliente quer reembolso", 2)
.forEach(r -> System.out.printf("%s -> %.3f%n", r.document().id(), r.score()));
refund -> 0.6xx
shipping -> 0.1xx
Os números específicos dependem da função de hash. A ordem é estável: refund aparece primeiro porque compartilha muitas palavras com a pergunta. Apenas esse trecho é colocado no prompt, não todos os três.
Nota: Embeddings reais entendem semântica (“devolver dinheiro” é perto de “reembolso”), enquanto hashing bag-of-words não. Não use o exemplo acima para produção.
Em produção, se você já estiver usando PostgreSQL, a extensão pgvector geralmente é um primeiro passo razoável antes de considerar um vector database dedicado:
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 é passado pela aplicação
SELECT id, content
FROM policy_chunk
WHERE tenant_id = :tenant_id
ORDER BY embedding <=> :query_embedding
LIMIT 5;
O número de dimensões 768 deve corresponder ao modelo de embedding que você está usando. O operador <=> calcula a distância de cosseno. A cláusula WHERE tenant_id = ... nos leva a uma razão importante pela qual o retrieval ainda é necessário, discutido na seção 7.
6. Combinando as duas abordagens: escolhendo estratégia por dados
Na prática, a resposta geralmente é “depende do repositório de documentos”. Podemos transformar essa decisão em código para que toda a equipe concorde com os critérios.
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) {
return ContextStrategy.RETRIEVAL; // prompt cache é pouco útil quando dados mudam constantemente
}
return ContextStrategy.FULL_CONTEXT;
}
}
Lógica em ordem de prioridade:
- Se cada usuário só pode ver uma parte dos dados, você precisa filtrar antes de colocar no prompt. O retrieval com filtro faz isso naturalmente.
- Se o repositório é maior que o orçamento de tokens, não há outra escolha além do retrieval.
- Se os dados mudam constantemente e já consomem uma parte significativa do orçamento, o prompt caching é menos eficaz, então o retrieval geralmente é mais barato.
- Para os casos restantes: use long context pela simplicidade.
O limiar de / 2 é apenas um exemplo. Você deve medir o custo e a qualidade em seus próprios dados e depois ajustar.
7. Testando o comportamento principal
Dependência de teste, se o projeto usar Spring Boot parent então a versão já está gerenciada:
<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", "Política de reembolso: o cliente recebe reembolso se o pedido tiver um defeito."));
store.add(new Document("shipping", "A taxa de envio é calculada com base no peso e na distância de entrega."));
var results = store.search("cliente quer reembolso", 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);
}
}
O terceiro teste é o mais importante do ponto de vista comercial. Mesmo que o repositório de documentos seja pequeno, a exigência de controle de acesso ainda o obriga a usar retrieval. Essa é uma restrição que uma context window longa não consegue contornar.
8. Quando ainda precisa de vector database, quando não
Você ainda precisa de retrieval (vector database ou vector index em um DB existente) quando:
- O repositório de documentos é muitas vezes maior que a context window, por exemplo, todos os tickets de suporte ou toda a wiki interna.
- Há controle de acesso por usuário ou tenant. Você não pode deixar o modelo “ver” dados que o usuário não tem permissão para visualizar, mesmo que apenas no prompt.
- Os dados são atualizados continuamente e precisam refletir quase imediatamente.
- Volume de consultas alto. Enviar um prompt gigante para cada solicitação faz com que o custo e a latência aumentem com o número de tokens.
- Você precisa fornecer citações claras da fonte, ou seja, saber em qual chunk a resposta se baseia.
Você pode prescindir do vector database quando:
- O repositório de documentos é pequeno, estável e cabe no orçamento de tokens, por exemplo, o conjunto de políticas de um produto ou documentação de um módulo.
- A pergunta geralmente precisa sintetizar todo o documento, como “resumir todos os pontos de diferença entre dois contratos”. Top-K retrieval faz isso mal.
- Você está fazendo um protótipo e quer validar o valor antes de investir em infraestrutura.
Também não é necessário usar vector search quando a consulta é de natureza exata, como código de Pedido, SKU ou nome de função. Full-text search ou consultas SQL geralmente funcionam melhor. Muitos sistemas usam busca híbrida, combinando palavras-chave e vetores.
9. Erros comuns
- Considerar a context window como o orçamento de documentos. Você esquece de deixar espaço para o system prompt, histórico de chat e saída, então a solicitação é cortada ou resulta em erro no ambiente real.
- Colocar tudo no prompt sem avaliar a qualidade. Quanto mais longo o prompt, mais fácil o modelo perde detalhes no meio. Construa um conjunto de perguntas de teste com respostas esperadas e meça com seus próprios dados.
- Construir um vector database dedicado muito cedo. Com dezenas de milhares de chunks, um índice vetorial em PostgreSQL geralmente é suficiente, e você evita ter mais um sistema para manter.
- Filtrar permissões após o retrieval. Obter top-K e depois filtrar por permissão pode descartar todos os resultados válidos. Coloque a condição de permissão na própria consulta, como a cláusula
WHERE tenant_idna seção 5. - Mudar o modelo de embedding sem fazer re-indexação. Vetores gerados de dois modelos diferentes não são comparáveis entre si.
Nota: Long context e retrieval não são mutuamente exclusivos. Um padrão comum é um retrieval “generoso”, pegando mais chunks do que antes, e depois aproveitar a context longa para o modelo autosselecinonar. Essa abordagem reduz o risco de perder algo e mantém os custos sob controle.
10. Conclusão
Context windows mais longas mudam o ponto de equilíbrio, mas não eliminam a necessidade de retrieval. Com repositórios de documentos pequenos e estáveis, “colocar tudo no prompt” geralmente é simples e suficientemente bom. Com dados grandes, mudanças constantes, necessidade de controle de acesso ou alto volume, o retrieval ainda é necessário. Porém, não precisa ser um vector database separado: um índice vetorial em um banco de dados existente geralmente é suficiente.
A abordagem prática é medir primeiro, depois decidir. Estime o número de tokens do seu repositório de documentos, identifique os requisitos de controle de acesso e a frequência de atualização, e depois avalie a qualidade em um conjunto de perguntas reais. Quando esses critérios são codificados como ContextStrategySelector, toda a equipe terá uma decisão clara e testável.