Embeddings

Primeiro, a palavra. Um embedding é uma lista de números que representa um pedaço de texto e captura seu significado, de modo que textos com significados similares se tornem números similares. Este capítulo é sobre como transformar palavras em algo que um computador pode comparar, e é o que permite que a busca encontre significado em vez de palavras-chave.
Texto como vetores
Como um computador sabe que "cachorro" e "filhote" estão relacionados, enquanto "cachorro" e "grampeador" não estão? As palavras não compartilham letras que importem.
O truque é transformar texto em números que capturam seu significado, para que significados similares terminem como números similares. Essa lista de números é um embedding, também chamado de vetor. Um modelo lê o texto e produz o vetor.
Você não interpreta os números por si só. O que importa é a relação entre eles.
Uma imagem mental aproximada: imagine cada pedaço de texto colocado como um ponto no espaço, onde coisas que significam coisas similares ficam perto uma da outra. "Cachorro" e "filhote" caem no mesmo bairro. "Grampeador" está do outro lado da cidade. Embeddings reais usam centenas ou milhares de dimensões em vez de dois ou três que você consegue visualizar, mas a intuição continua: proximidade significa similaridade.
De onde vêm esses números, e por que se alinham com o significado? Um modelo de embedding é um primo próximo do modelo de previsão de Como LLMs funcionam, treinado em uma quantidade enorme de texto. Para ficar bom em sua tarefa, teve que aprender quais palavras e ideias aparecem em contextos similares, a observação antiga de que você conhece uma palavra pela companhia que ela mantém.
"Cachorro" e "filhote" aparecem nos mesmos tipos de frases, então o modelo aprendeu a representá-los de forma similar. "Grampeador" mantém companhia diferente, então fica em outro lugar. Os centenas de números são direções aprendidas de significado, cada uma capturando algum eixo de como o texto varia, nenhum deles rotulado de uma forma que um humano leria. Você nunca decodifica os números, você apenas mede o quão perto dois vetores estão.
Você quer busca que encontre significado em vez de ortografia, então "pegar meu dinheiro de volta" encontra uma linha sobre reembolsos. O movimento é transformar cada pedaço de texto em um vetor, uma lista de números de comprimento fixo onde significados similares ficam próximos. Um modelo de embedding lê o texto e emite esse vetor, e o ponto inteiro é que a distância entre vetores representa a similaridade de significado.
O vetor tem um comprimento definido, sua dimensão, frequentemente alguns centos a alguns milhares de números. Cada texto que você incorpora com o mesmo modelo sai com o mesmo comprimento, o que permite comparar qualquer dois deles. Essas dimensões são direções aprendidas de significado, não categorias rotuladas por humanos, então você não lê a dimensão 47 como "formalidade". Você trata o vetor como um único ponto opaco e mede distâncias.
Antes de incorporar qualquer coisa real, há uma etapa que decide se a coisa inteira funciona: chunking. Raramente você incorpora um documento inteiro como um vetor, porque um vetor para um manual de 20 páginas confunde cada tópico nele em um único ponto médio que não combina bem com nada. Em vez disso, você divide o documento em chunks, passagens de aproximadamente um parágrafo a uma página, e incorpora cada chunk por conta própria. Incorpore chunks, não documentos inteiros, para que cada vetor carregue uma ideia focada.
O tamanho do chunk é uma compensação real que você define deliberadamente. Chunks muito grandes fazem a média de várias ideias juntas e recuperam frouxamente. Chunks muito pequenos perdem o contexto circundante que tornou uma frase significativa. Um ponto de partida comum é alguns centos de tokens por chunk com uma pequena sobreposição entre vizinhos, para que uma frase dividida em um limite ainda apareça inteira em um deles, então você ajusta com base no que sua recuperação realmente retorna.
Busca que encontra significado repousa sobre uma ideia: mapear texto em um espaço vetorial (um espaço de coordenadas de dimensão fixa onde cada ponto é uma lista de números) de modo que similaridade semântica se torne proximidade geométrica. Um modelo de embedding é o mapa: ele lê texto e retorna um vetor.
Tudo que você constrói em cima, indexação, recuperação, ranking, é geometria sobre esses vetores. Acerte o mapeamento e nada downstream o recupera.
A primeira alavanca de design é o que você incorpora, e isso é chunking: dividir documentos de origem em passagens antes de incorporar em vez de incorporar arquivos inteiros. A razão é que um embedding é uma compressão com perda de seu input em um único ponto, então quanto mais ideias distintas um chunk contém, mais elas fazem a média em um centroide vago que não recupera nada nitidamente. Dimensione certo o chunk para a unidade que um usuário perguntará sobre, normalmente uma passagem de alguns centos de tokens, com uma pequena sobreposição para que um fato que fica em um limite sobreviva em pelo menos um chunk.
A estratégia de chunking é onde a qualidade de recuperação é ganha ou perdida, mais do que a escolha do modelo:
- Janelas de tamanho fixo são baratas, mas cortam frases no meio do pensamento.
- Divisão ciente de estrutura (em títulos, parágrafos ou blocos de código) mantém cada chunk coerente e tende a recuperar melhor, ao custo de um pipeline mais envolvido.
Seja qual for você escolher, armazene a origem e posição do chunk junto com seu vetor, porque no tempo de recuperação você precisa apontar de volta para onde um fato veio, e um vetor simples não pode contar isso a você.
Uma propriedade a mais para internalizar: dimensões de embedding não são eixos interpretáveis, então você não pode debugar recuperação lendo um vetor. Você debuga embeddings inspecionando o que eles recuperam, nunca lendo os números. Quando uma query puxa o chunk errado, o fix está no chunking, no modelo, ou na query, examinados através dos resultados recuperados, não através das coordenadas em si.
Gerando um embedding
Você pede a um modelo de embedding o vetor, muito como uma chamada de modelo normal mas com um endpoint diferente e um modelo diferente.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small" # ponto de troca para o modelo de embedding
def embed(text):
response = client.embeddings.create(model=EMBED_MODEL, input=text)
return response.data[0].embedding # uma lista de números
vector = embed("um cachorro pequeno e brincalhão")
print(len(vector)) # por exemplo 1536 númerosO resultado é uma lista de números. Uma regra importa acima de tudo: sempre compare embeddings feitos pelo mesmo modelo. Vetores de modelos diferentes vivem em espaços diferentes e não são comparáveis, como medições em unidades diferentes. Misturar modelos dá pontuações sem sentido, toda vez.
E como toda chamada de modelo, você é cobrado por tokens, então incorporar uma grande coleção de documentos tem um custo que vale a pena estimar com antecedência antes de rodá-lo em milhares de documentos.
A chamada em si é um endpoint diferente com a mesma forma que uma chamada de chat: você envia texto, você recebe um vetor de volta.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small"
def embed_batch(texts):
# envie muitos textos em uma request, receba um vetor por texto de volta
response = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [item.embedding for item in response.data]
chunks = ["um cachorro pequeno e brincalhão", "um cachorro jovem", "um suprimento de escritório"]
vectors = embed_batch(chunks) # um vetor por chunk, mesma ordem na entradaA forma varia por provedor, mas a ideia de requisição-e-resposta é a mesma em todo lugar. Dois pontos práticos além da chamada básica. Primeiro, incorpore em lotes: a maioria dos endpoints leva uma lista de textos em uma única requisição e retorna um vetor cada, o que é mais rápido e barato do que uma chamada por chunk porque você paga menos sobrecarga de requisição e atinge menos limites de taxa. Quando você indexa um grande corpus, batching é o que mantém o custo e o tempo de parede sanos.
Segundo, a regra do mesmo-modelo tem um parceiro: modelo e dimensão são uma escolha, não um padrão. Modelos de embedding maiores com mais dimensões capturam distinções mais finas mas custam mais para computar, armazenar e comparar. Escolha o menor modelo e dimensão que ainda separa seus dados.
Alguns modelos até deixam você solicitar um vetor mais curto para trocar um pouco de precisão por armazenamento mais barato e busca mais rápida. Seja qual for você escolher se torna um contrato: mudar o modelo depois e cada vetor armazenado tem que ser regenerado.
A superfície da API é pequena. Você envia texto, você recebe um vetor float, e as decisões interessantes sentam ao redor daquela chamada em vez de nela.
from openai import OpenAI
client = OpenAI()
EMBED_MODEL = "text-embedding-3-small"
def embed_batch(texts):
# uma requisição, muitos vetores de volta, em ordem de entrada
response = client.embeddings.create(model=EMBED_MODEL, input=texts)
return [item.embedding for item in response.data]A forma de requisição e resposta varia por provedor, mas cada endpoint de embedding é esse mesmo contrato de lote-em, vetores-para-fora. Batching não é uma gentileza em escala, é a diferença entre um trabalho de índice que termina e um que estrangula a tarde inteira: cada requisição carrega sobrecarga fixa e conta contra um limite de taxa, então amortize ambos enviando lotes grandes, então recue e tente novamente nas falhas parciais que um grande trabalho vai acertar.
Modelo e dimensão são um comprometimento arquitetural com uma cauda longa. Vetores de dimensão maior armazenam mais distinção mas custam mais em cada eixo que importa em produção: armazenamento por vetor, memória no índice, e tempo por comparação, todos escalam com dimensão. Alguns modelos suportam embeddings Matryoshka (vetores que você pode truncar para um comprimento menor e ainda usar), o que permite que você mantenha vetores completos para precisão e sirva um prefixo truncado para busca de primeira passagem barata. Vale a pena quando seu corpus é grande o suficiente que armazenamento e latência de query, não qualidade de modelo, são a restrição de ligação.
O comprometimento que morde depois é versionamento. Um embedding armazenado está amarrado a uma versão exata de modelo para sempre.
Vetores de duas versões de modelo não são comparáveis, então você não pode incorporar uma nova query com um modelo mais novo e compará-la contra um índice construído no antigo. O resultado é silencioso: pontuações parecem plausíveis e rankings estão errados. Trate a versão do modelo como parte do esquema do seu índice, armazene-a junto aos vetores, e planeje re-incorporação como uma migração, coberta na discussão de freshness mais adiante neste capítulo.
Medindo similaridade
Para descobrir como duas peças de texto estão relacionadas, você mede o quão perto seus vetores estão. A medida padrão é similaridade de cosseno, que retorna um número de -1 a 1: mais perto de 1 significa mais similar, perto de 0 significa não relacionado. Você não precisa da matemática por trás disso para usá-la. Pense nela como comparar a direção que dois vetores apontam.
Direção é o que carrega o significado aqui, por isso o cosseno compara ângulo em vez de distância bruta. Dois textos sobre reembolsos apontam o mesmo caminho se um é uma frase curta ou um parágrafo longo.
import math
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
return dot / (mag_a * mag_b)
dog = embed("um cachorro pequeno e brincalhão")
puppy = embed("um cachorro jovem")
stapler = embed("um suprimento de escritório para prender papel")
print(cosine_similarity(dog, puppy)) # mais alta, os dois estão perto em significado
print(cosine_similarity(dog, stapler)) # mais baixa, os dois não estão relacionadosOs números confirmam a intuição: cachorro e filhote pontuam mais alto, cachorro e grampeador pontuam mais baixo. Comparar um vetor de query contra vetores armazenados é o coração de busca semântica. Essa única função é o movimento que o resto do capítulo constrói em cima.
Você tem vetores, agora precisa de um número que diga o quão perto dois deles estão. O padrão é similaridade de cosseno: ela mede o ângulo entre dois vetores, ignorando seu comprimento, e retorna um valor de -1 a 1 onde mais alto significa mais parecido. Ângulo, não distância, é o que você quer, porque uma passagem longa e uma frase curta sobre o mesmo tópico devem pontuar como similar mesmo que seus vetores tenham magnitudes diferentes.
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
# se vetores são normalizados para comprimento 1, similaridade de cosseno iguala seu produto de ponto
score = dot(query_vector, doc_vector) # nenhuma divisão necessáriaAqui está o movimento que compensa em escala: normalize seus vetores para comprimento 1 quando você os armazena. Uma vez que cada vetor tem o mesmo comprimento, a divisão em similaridade de cosseno desaparece e a pontuação se torna um produto de ponto simples, que é uma multiplicação e adição por dimensão e muito mais rápido para rodar em um grande conjunto. Normalize uma vez no armazenamento, então compare com um produto de ponto. Muitos modelos de embedding retornam vetores já normalizados, mas não assuma isso, verifique.
O outro deslocamento do exemplo de brinquedo: você raramente quer o único resultado mais próximo. Você quer recuperação top-k, os k resultados de pontuação mais alta, normalmente três a dez. Um vetor pode ser uma quase-falha enquanto o segundo e terceiro juntos mantêm a resposta, e retornar um pequeno conjunto dá ao próximo estágio espaço para trabalhar. Retornar apenas o melhor joga fora contexto que você vai querer ter mantido.
Você precisa de uma métrica de similaridade, e similaridade de cosseno (o cosseno do ângulo entre dois vetores, variando -1 a 1) é a padrão porque ignora magnitude, então o comprimento do documento não domina tópico. Essa escolha tem uma consequência de performance que vale a pena projetar em torno de: cosseno sobre vetores brutos custa um produto de ponto mais duas computações de magnitude por comparação, e você paga isso por cada vetor no índice em cada query.
import numpy as np
# normalize no tempo de escrita, uma vez por vetor
def normalize(v):
v = np.asarray(v, dtype=np.float32)
return v / np.linalg.norm(v)
# no tempo de query, cosseno sobre vetores normalizados é um único produto de matriz-vetor
def search(query_vec, matrix):
return matrix @ query_vec # uma pontuação por linha, vetorizadaA otimização é normalizar cada vetor para comprimento unitário no tempo de escrita, o que faz similaridade de cosseno idêntica a um produto de ponto e permite você marcar o corpus inteiro como uma multiplicação de matriz-vetor em vez de um loop Python. Normalize no tempo de escrita e similaridade é um produto de ponto sobre o índice inteiro de uma vez. Este é o baseline de força bruta: exato, direto, e linear no tamanho do corpus, o que é bom até aproximadamente alguns centos de milhares de vetores e cai acima disso, a dica para mover para um índice aproximado coberto depois.
Duas realidades de produção que o brinquedo de métrica esconde completamente. Primeiro, você quase nunca quer o argmax, você quer top-k com um limiar de pontuação, porque a brecha entre o primeiro e segundo resultado diz como você se sente confiante sobre a correspondência, e um resultado superior que ainda pontua baixo não significa nada no seu corpus que realmente respondeu a query. Retorne isso como "nenhuma boa correspondência" em vez de entregar a linha menos-ruim. Segundo, similaridade de cosseno é relativa, não absoluta: uma pontuação de 0.4 é significativa apenas contra a distribuição de pontuações seu corpus e modelo produzem, então calibre limiares empiricamente por índice em vez de copiar um número mágico de um post de blog.
Busca semântica
Junte os pedaços e você pode buscar por significado em vez de palavras-chave. Incorpore seus documentos uma vez e armazene os vetores. Quando uma query chegar, incorpore-a também, então retorne os documentos cujos vetores estão mais próximos.
docs = [
"Redefina sua senha na página de configurações de conta.",
"Nossa política de reembolso permite devoluções dentro de 30 dias.",
"Entre em contato com o suporte por email para dúvidas de faturamento.",
]
# incorpore cada documento uma vez, com antecedência
index = [{"text": text, "vector": embed(text)} for text in docs]
def search(query):
query_vector = embed(query)
scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
return sorted(scored, key=lambda x: x["score"], reverse=True)[0] # a correspondência mais próxima
print(search("como faço para pegar meu dinheiro de volta?"))
# retorna a linha de política de reembolso, mesmo que não compartilhe palavras-chave com a queryA query "como faço para pegar meu dinheiro de volta?" encontra a linha de reembolso apesar de não compartilhar palavras importantes com ela. Busca semântica corresponde sobre significado, não ortografia. Busca por palavras-chave teria perdido completamente, porque a query e o documento não têm palavras importantes em comum.
A forma é sempre a mesma: incorpore uma vez e mantenha os vetores, incorpore a query no tempo de requisição, marque, ordene, retorne a mais próxima. Esse é o loop de busca semântica, e você o vai reutilizar pelo resto deste handbook.
A versão de brinquedo armazena vetores em uma lista Python e marca cada um em cada query. Isso funciona para uma demonstração e quebra no momento em que seu corpus cresce, porque uma varredura linear roda a comparação completa sobre cada vetor para cada query, e você também está reconstruindo o índice em memória a cada reinicialização.
O fix é um banco de dados vetorial: uma loja construída para manter vetores e responder "encontre os k mais próximos para este vetor de query" rapidamente e do disco. Em vez de sua própria lista e loop, você escreve vetores uma vez e o banco de dados gerencia a busca, persistência, e atualizando entradas individuais.
# forma varia por DB vetorial, mas o padrão é o mesmo em todo lugar
index.upsert([
{"id": "doc-1", "vector": embed(chunk), "metadata": {"source": "faq.md", "section": "reembolsos"}}
for chunk in chunks
])
results = index.query(vector=embed(user_question), top_k=5) # os 5 mais próximos, não apenas 1Duas coisas o banco de dados dá você que a lista não dá:
- Ele retorna resultados top-k em uma chamada, os k mais próximos em vez de um único melhor, o que é o que você realmente alimenta o próximo estágio.
- Ele armazena metadados ao lado de cada vetor, o arquivo de origem, seção, e timestamp, então um resultado pode apontar de volta para onde veio e você pode filtrar por isso.
Armazene vetores em um DB vetorial, não em uma lista Python, no momento em que você tem dados reais. A lista é para aprender o mecanismo, não para rodá-lo.
O loop de referência, incorporar uma vez, armazenar, incorporar a query, marcar, ordenar, retornar, está correto e não sobrevive contato com escala. Uma varredura de força bruta é linear no tamanho do corpus por query, então em um milhão de vetores e qualquer taxa de query real você está gastando um segundo de CPU por busca que você não tem. Tudo em busca de produção é sobre não comparar a query contra cada vetor.
Isso é o que busca de vizinho mais próximo aproximado compra você (ANN: algoritmos que encontram os vetores mais próximos rápido aceitando uma pequena chance de perder um verdadeiro mais próximo). Duas famílias dominam:
- HNSW (Hierarchical Navigable Small World) constrói um gráfico de vetores em camadas e o caminha de grosseiro para fino, dando latência de query baixa ao custo de alta memória.
- IVF (arquivo invertido) agrupa vetores e busca apenas os clusters mais próximos, usando menos memória mas precisando de seus centros de cluster retreinados conforme dados mudam.
Acima de alguns centos de milhares de vetores, ANN não é opcional, é o índice. A compensação que você está ajustando é recall (a fração de verdadeiros vizinhos mais próximos que o índice realmente retorna) contra latência e memória: empurre recall para 1.0 e queries desaceleram, relaxe isso e você silenciosamente perde chunks relevantes.
O modo de falha que o loop de brinquedo esconde completamente é freshness. Um índice é um snapshot, então quando um documento de origem muda, seu vetor armazenado agora está obsoleto e continua sendo recuperado como se estivesse atual. Você precisa de um pipeline de re-incorporação com chave na mudança de documento: detecte a edição, re-chunque e re-incorpore esse documento, e upsert os novos vetores enquanto deleta os antigos, idealmente rastreado por um hash de conteúdo para que você re-incorpore apenas o que realmente mudou em vez do corpus inteiro em cada edição. Vetores obsoletos são a forma silenciosa de um sistema de recuperação começar a mentir.
A última lacuna é correspondência exata. Busca vetorial é construída para perdê-la: uma query para ordem A-10293 ou um código de erro incorpora para uma região fuzzy e pode não trazer o único documento que contém essa string. Busca híbrida roda busca por palavras-chave (que acerta tokens exatos) e busca vetorial (que acerta significado) juntas e mescla os rankings, para que identificadores, nomes, e termos raros não sejam perdidos para embaçamento semântico. Chegue a ela sempre que seu corpus contiver coisas que usuários vão digitar exatamente.
O que embeddings não são para
Embeddings são uma ferramenta de similaridade, não uma ferramenta de pensamento. Alguns limites para manter em mente para que você chegue à ferramenta correta:
- Eles não fazem correspondência exata. Para encontrar um número de ordem exato ou um código de erro, busca por palavra-chave ou banco de dados simples é melhor.
- Eles não raciocinam. Eles dizem a você que duas coisas estão relacionadas, não por quê, e não se uma logicamente segue da outra.
- Eles ficam obsoletos. Se seus documentos mudarem, você deve re-incorporá-los, ou sua busca apontará para conteúdo antigo.
Embeddings encontram o que está relacionado, eles não julgam o que é verdade ou atual. Cada uma dessas lacunas tem uma ferramenta melhor, e saber qual usar é metade da habilidade.
A armadilha com uma ferramenta que funciona tão bem é chegar a ela em todo lugar. Três limites marcam onde parar e escolher outra coisa.
Correspondência exata é a primeira. Embeddings mapeiam um ID de ordem ou um código de erro para o mesmo espaço semântico fuzzy que tudo mais, então A-10293 fica perto de outras strings parecidas com ordem, não exatamente em si mesmo. Para identificadores, códigos, e qualquer valor que um usuário digita literalmente, busca por palavra-chave ou banco de dados é a ferramenta certa, e busca híbrida (busca por palavra-chave e vetorial executadas juntas) é como você obtém ambas de uma vez.
Raciocínio é o segundo. Similaridade não é inferência: dois chunks pontuando alto significa que eles tratam de coisas relacionadas, não que um responde a uma pergunta ou logicamente segue do outro. O raciocínio acontece no modelo que você alimenta os chunks recuperados para, não na recuperação. Recuperação encontra candidatos, o modelo faz o pensamento.
Freshness é o terceiro e o que morde silenciosamente. Seu índice é um snapshot de quando quer que você o incorporou, então documentos editados continuam retornando sua redação antiga até que você re-incorpore. Trate re-incorporação na mudança como parte do sistema, não uma coisa que você faz à mão quando alguém percebe que as respostas ficaram erradas.
Saber onde embeddings param é o que mantém um sistema de recuperação confiável, e três fronteiras importam em produção.
Correspondência exata é um ponto cego estrutural, não um problema de ajuste. Embeddings comprimem input em direção a bairros semânticos, então identificadores, códigos, SKUs, e nomes próprios raros ficam manchados em direção a strings parecidas em vez de pousar em si mesmos. Busca vetorial pura vai ranking uma quase-falha acima do hit exato. O fix é recuperação híbrida que funde um índice léxico (palavra-chave ou BM25, um ranking de frequência de termo clássico) com o índice vetorial, para que tokens exatos sejam combinados exatamente enquanto significado é combinado semanticamente, e você não apostar a procura de ordem em geometria.
Drift é a fronteira que corrói silenciosamente. Embedding drift é o descasamento que você obtém quando a versão do modelo por trás do seu índice muda: vetores de duas versões ocupam espaços diferentes e não são comparáveis, então uma query incorporada com um modelo novo contra um índice construído no antigo retorna nonsense que parece plausível. Fixe a versão do modelo em seu esquema de índice, e trate qualquer upgrade de modelo como uma migração completa de re-incorporação, não uma queda de suplente, porque não há erro jogado, apenas resultados silenciosamente piores.
Avaliação é a fronteira que as pessoas pulam até que recuperação já está falhando. Você não consegue olhar para se recuperação é boa, você a mede: mantenha de lado um conjunto de queries com chunks conhecidos-relevantes e marque recall@k (o chunk certo pousou em top k) e precisão (quanto do que você retornou era relevante). Se você não está medindo recall@k, você não sabe se a recuperação funciona.
Esse conjunto de eval também é o que diz a você se uma nova estratégia de chunking, modelo, ou ponderação híbrida realmente ajudou, em vez de trocar um conjunto de falhas por outro que você não percebeu ainda. Qualidade de recuperação é a fundação que RAG fica de pé, então ganha um teste real, não uma verificação de vibe.
Na prática
O fluxo completo vive na função search acima: incorpore um pequeno conjunto de entradas de FAQ uma vez, então responda queries por significado. O padrão é o mesmo em qualquer tamanho, apenas o armazenamento muda conforme você cresce.
No próximo capítulo você pega as correspondências mais próximas e as alimenta para um modelo, para que ele responda do seu documentos em vez de da memória. Essa técnica é RAG, e é construída diretamente sobre o que você escreveu aqui.
O mecanismo está resolvido: chunque, incorpore em lotes, armazene em um DB vetorial, recupere top-k por similaridade. O que muda conforme você vai para produção é o encanamento ao redor, a re-incorporação na mudança, os metadados que você filtra, a versão do modelo que você fixa.
Os chunks recuperados não são o fim. No próximo capítulo você os alimenta para um modelo como contexto para que ele responda do seu documentos em vez de seu treinamento congelado. Isso é RAG, e a qualidade de suas respostas é limitada pela recuperação que você constrói aqui.
Tudo aqui, chunking, normalização, indexação ANN, recuperação híbrida, freshness, e avaliação recall@k, é a camada de recuperação, e é a parte de um sistema de IA que silenciosamente determina se as respostas são boas. O modelo em cima só consegue raciocinar sobre o que recuperação a entrega, então uma resposta confiante errada é geralmente uma falha de recuperação se disfarçando de um costume de geração.
Essa é a ponte para o próximo capítulo. RAG é essa camada de recuperação alimentando contexto fundamentado em um modelo, e cada modo de falha aqui, vetores obsoletos, correspondências exatas perdidas, recall baixo, surge lá como uma resposta errada. Construa a recuperação para ser medida e fresca, e RAG tem algo sólido em que se apoiar.

