Phase 11 - Lesson 04
Embeddings e Representações Vetoriais
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
O texto é discreto. A matemática é contínua. Toda vez que você pede a um LLM para encontrar documentos "semelhantes", comparar significados ou pesquisar além de palavras-chave, você está confiando em uma ponte entre esses dois mundos. Essa ponte é um embedding. Se você não entende embeddings, você não entende a IA moderna. Você apenas a usa.
Tipo: Build Linguagens: Python Pré-requisitos: Fase 11, Lição 01 (Engenharia de Prompt) Tempo: ~75 minutos Relacionado: A Fase 5 · 22 (Aprofundamento em Modelos de Embedding) cobre denso vs esparso vs multi-vetor, truncamento Matryoshka e seleção de modelo por eixo. Esta lição foca no pipeline de produção (bancos de dados vetoriais, HNSW, matemática de similaridade). Leia a Fase 5 · 22 antes de escolher um modelo.
Objetivos de Aprendizagem
- Gerar embeddings de texto usando provedores de API e modelos de código aberto, e calcular a similaridade de cosseno entre eles
- Explicar por que os embeddings resolvem o problema de incompatibilidade de vocabulário que a busca por palavras-chave não consegue lidar
- Construir um índice de busca semântica que recupera documentos pelo significado em vez de correspondência exata de palavras-chave
- Avaliar a qualidade do embedding usando benchmarks de recuperação (precisão@k, recall) e escolher o modelo de embedding correto para sua tarefa
O Problema
Você tem 10.000 chamados de suporte. Um cliente escreve "meu pagamento não foi aprovado". Você precisa encontrar chamados anteriores semelhantes. A busca por palavras-chave encontra chamados contendo "pagamento" e "não foi aprovado". Ela deixa de fora "falha na transação", "cobrança recusada" e "erro de faturamento". Esses chamados descrevem exatamente o mesmo problema com palavras completamente diferentes.
Este é o problema da incompatibilidade de vocabulário (vocabulary mismatch). A linguagem humana tem dezenas de maneiras de dizer a mesma coisa. A busca por palavras-chave trata cada palavra como um símbolo independente sem significado. Ela não pode saber que "recusada" e "não foi aprovado" se referem ao mesmo conceito.
Você precisa de uma representação de texto onde o significado, e não a grafia, determine a similaridade. Você precisa de uma maneira de colocar "meu pagamento não foi aprovado" e "a transação foi recusada" próximos em algum espaço matemático, enquanto afasta "meu pagamento chegou a tempo", apesar de compartilharem a palavra "pagamento".
Essa representação é um embedding.
O Conceito
O que é um Embedding?
Um embedding é um vetor denso de números de ponto flutuante que representa o significado de um texto. A palavra "denso" importa — cada dimensão carrega informação, ao contrário de representações esparsas (bag-of-words, TF-IDF) onde a maioria das dimensões é zero.
"The cat sat on the mat" torna-se algo como [0.023, -0.041, 0.087, ..., 0.012] — uma lista de 768 a 3072 números, dependendo do modelo. Esses números codificam o significado. Você nunca os inspeciona diretamente. Você os compara.
A Revolução do Word2Vec
Em 2013, Tomas Mikolov e seus colegas no Google publicaram o Word2Vec. O insight principal: treinar uma rede neural para prever uma palavra a partir de suas vizinhas (ou as vizinhas a partir de uma palavra), e os pesos da camada oculta se tornam representações vetoriais significativas.
O resultado famoso:
king - man + woman = queen
A aritmética vetorial em embeddings de palavras captura relações semânticas. A direção de "man" para "woman" é aproximadamente a mesma direção de "king" para "queen". Este foi o momento em que a área percebeu que a geometria poderia codificar o significado.
O Word2Vec produzia vetores de 300 dimensões. Cada palavra recebia um único vetor, independentemente do contexto. "Bank" em "river bank" (margem do rio) e "bank account" (conta bancária) tinha o mesmo embedding. Essa limitação impulsionou a próxima década de pesquisa.
De Palavras para Sentenças
Os embeddings de palavras representam tokens individuais. Sistemas em produção precisam incorporar sentenças inteiras, parágrafos ou documentos. Quatro abordagens surgiram:
Média (Averaging): tira a média de todos os vetores de palavras na sentença. Barato, com perdas, mas surpreendentemente decente para textos curtos. Perde totalmente a ordem das palavras — "dog bites man" (cão morde homem) e "man bites dog" (homem morde cão) obtêm embeddings idênticos.
Token CLS: modelos baseados em transformer (BERT, 2018) geram um embedding de token especial [CLS] que representa toda a entrada. Melhor do que a média, mas o token [CLS] foi treinado para previsão da próxima sentença, não para similaridade.
Aprendizado contrastivo (Contrastive learning): treina o modelo explicitamente para aproximar pares semelhantes e afastar pares diferentes. O Sentence-BERT (Reimers & Gurevych, 2019) usou essa abordagem e se tornou a base dos modelos de embedding modernos. Dados "How do I reset my password?" e "I need to change my password", o modelo aprende que estes devem ter vetores quase idênticos.
Embeddings ajustados por instruções (Instruction-tuned embeddings): a abordagem mais recente. Modelos como E5 e GTE aceitam um prefixo de tarefa ("search_query:", "search_document:") que diz ao modelo que tipo de embedding produzir. Isso permite que um único modelo atenda a múltiplas tarefas.
graph LR
subgraph "2013: Word2Vec"
W1["king"] --> V1["[0.2, -0.1, ...]"]
W2["queen"] --> V2["[0.3, -0.2, ...]"]
end
subgraph "2019: Sentence-BERT"
S1["Como faço para redefinir minha senha?"] --> E1["[0.04, 0.12, ...]"]
S2["Preciso alterar minha senha"] --> E2["[0.05, 0.11, ...]"]
end
subgraph "2024: Ajustado por Instruções"
I1["search_query: redefinição de senha"] --> T1["[0.08, 0.09, ...]"]
I2["search_document: Para redefinir sua senha, clique..."] --> T2["[0.07, 0.10, ...]"]
end
Modelos de Embedding Modernos
O mercado se consolidou em algumas opções de nível de produção (pontuações MTEB do início de 2026, MTEB v2):
| Modelo | Provedor | Dimensões | MTEB | Contexto | Custo / 1M tokens |
|---|---|---|---|---|---|
| Gemini Embedding 2 | 3072 (Matryoshka) | 67.7 (recuperação) | 8192 | $0.15 | |
| embed-v4 | Cohere | 1024 (Matryoshka) | 65.2 | 128K | $0.12 |
| voyage-4 | Voyage AI | 1024/2048 (Matryoshka) | 66.8 | 32K | $0.12 |
| text-embedding-3-large | OpenAI | 3072 (Matryoshka) | 64.6 | 8192 | $0.13 |
| text-embedding-3-small | OpenAI | 1536 (Matryoshka) | 62.3 | 8192 | $0.02 |
| BGE-M3 | BAAI | 1024 (dense+sparse+ColBERT) | 63.0 multilíngue | 8192 | Pesos abertos |
| Qwen3-Embedding | Alibaba | 4096 (Matryoshka) | 66.9 | 32K | Pesos abertos |
| Nomic-embed-v2 | Nomic | 768 (Matryoshka) | 63.1 | 8192 | Pesos abertos |
O MTEB (Massive Text Embedding Benchmark) v2 cobre mais de 100 tarefas em recuperação, classificação, agrupamento (clustering), reordenamento (reranking) e sumarização. Quanto maior, melhor. Em 2026, modelos de pesos abertos (Qwen3-Embedding, BGE-M3) igualam ou superam os modelos proprietários hospedados na maioria dos eixos. O Gemini Embedding 2 lidera em recuperação pura; Voyage/Cohere lideram em dominios específicos (finanças, direito, código). Sempre faça benchmarking em suas próprias consultas antes de se comprometer.
Métricas de Similaridade
Dados dois vetores de embedding, existem três maneiras de medir o quão semelhantes eles são:
Similaridade de cosseno (Cosine similarity): o cosseno do ângulo entre dois vetores. Varia de -1 (oposto) a 1 (direção idêntica). Ignora a magnitude — uma frase de 10 palavras e um documento de 500 palavras podem obter uma pontuação de 1.0 se apontarem para a mesma direção. Este é o padrão para 90% dos casos de uso.
cosine_sim(a, b) = dot(a, b) / (||a|| * ||b||)
Produto escalar (Dot product): o produto interno bruto de dois vetores. Idêntico à similaridade de cosseno quando os vetores são normalizados (comprimento unitário). Mais rápido de calcular. Os embeddings da OpenAI são normalizados, portanto, o produto escalar e o cosseno fornecem a mesma classificação.
dot(a, b) = sum(a_i * b_i)
Distância Euclidiana (L2): distância em linha reta no espaço vetorial. Menor = mais semelhante. Sensível a diferenças de magnitude. Use quando a posição absoluta no espaço importar, não apenas a direção.
L2(a, b) = sqrt(sum((a_i - b_i)^2))
Quando usar cada uma:
| Métrica | Usar quando | Evitar quando |
|---|---|---|
| Similaridade de cosseno | Comparando textos de comprimentos diferentes; a maioria das tarefas de recuperação | A magnitude carrega informação |
| Produto escalar | Os embeddings já estão normalizados; velocidade máxima | Os vetores têm magnitudes variadas |
| Distância Euclidiana | Agrupamento (clustering); problemas de vizinhos mais próximos espaciais | Comparando documentos com comprimentos muito diferentes |
Bancos de Dados Vetoriais e HNSW
Uma busca por similaridade por força bruta compara a consulta com cada vetor armazenado. Para 1 milhão de vetores com 1536 dimensões, isso representa 1,5 bilhão de operações de multiplicação e soma por consulta. Lento demais.
Os bancos de dados vetoriais resolvem isso com algoritmos de Vizinho Mais Próximo Aproximado (ANN - Approximate Nearest Neighbor). O algoritmo dominante é o HNSW (Hierarchical Navigable Small World):
- Constrói um grafo de vetores de várias camadas
- As camadas superiores são esparsas — conexões de longo alcance entre clusters distantes
- As camadas inferiores são densas — conexões refinadas entre vetores próximos
- A busca começa na camada superior, descendo de forma gananciosa (greedy) para refinar os resultados
- Retorna resultados top-k aproximados em tempo O(log n) em vez de O(n)
O HNSW troca uma pequena perda de precisão (geralmente 95-99% de recall) por ganhos massivos de velocidade. Para 10 milhões de vetores, a força bruta leva segundos. O HNSW leva milissegundos.
graph TD
subgraph "Camadas HNSW"
L2["Camada 2 (esparsa)"] -->|"saltos longos"| L1["Camada 1 (média)"]
L1 -->|"saltos mais curtos"| L0["Camada 0 (densa, todos os vetores)"]
end
Q["Vetor de consulta"] -->|"entrar no topo"| L2
L0 -->|"vizinhos mais próximos"| R["Resultados top-k"]
Opções de produção:
| Banco de dados | Tipo | Melhor para | Escala máxima |
|---|---|---|---|
| Pinecone | SaaS Gerenciado | Produção zero-ops | Bilhões |
| Weaviate | Código aberto | Auto-hospedado, busca híbrida | 100M+ |
| Qdrant | Código aberto | Alto desempenho, filtragem | 100M+ |
| ChromaDB | Embarcado | Prototipagem, desenvolvimento local | 1M |
| pgvector | Extensão do Postgres | Já utiliza Postgres | 10M |
| FAISS | Biblioteca | Em processo, pesquisa | 1B+ |
Estratégias de Divisão em Blocos (Chunking)
Os documentos são longos demais para serem incorporados como vetores únicos. Um PDF de 50 páginas cobre dezenas de tópicos — seu embedding se torna uma média de tudo, não se parecendo com nada específico. Você divide os documentos em blocos (chunks) e incorpora cada um deles.
Divisão de tamanho fixo (Fixed-size chunking): divide a cada N tokens com uma sobreposição de M tokens. Simples e previsível. Funciona bem quando os documentos não têm uma estrutura clara. Um bloco de 512 tokens com sobreposição de 50 tokens: o bloco 1 contém os tokens 0-511, o bloco 2 contém os tokens 462-973.
Divisão baseada em sentenças (Sentence-based chunking): divide nos limites das sentenças, agrupando-as até atingir o limite de tokens. Cada bloco contém pelo menos uma sentença completa. Melhor que o tamanho fixo porque você nunca corta um pensamento ao meio.
Divisão recursiva (Recursive chunking): tenta dividir primeiro no maior limite (cabeçalhos de seção). Se ainda for muito grande, tenta os limites de parágrafo. Depois, limites de sentenças. Por fim, limites de caracteres. Este é o funcionamento do RecursiveCharacterTextSplitter do LangChain e funciona bem para corpora de formatos mistos.
Divisão semântica (Semantic chunking): incorpora cada sentença e depois agrupa sentenças consecutivas cujos embeddings são semelhantes. Quando a similaridade do embedding cai abaixo de um limiar, inicia-se um novo bloco. Caro (requer incorporar cada sentença individualmente), mas produz os blocos mais coerentes.
| Estratégia | Complexidade | Qualidade | Melhor para |
|---|---|---|---|
| Tamanho fixo | Baixa | Razoável | Texto não estruturado, logs |
| Baseada em sentenças | Baixa | Boa | Artigos, e-mails |
| Recursiva | Média | Boa | Markdown, HTML, documentos mistos |
| Semântica | Alta | Excelente | Qualidade de recuperação crítica |
O ponto ideal para a maioria dos sistemas: blocos de 256 a 512 tokens com sobreposição de 50 tokens.
Bi-Encoders vs Cross-Encoders
Um bi-encoder incorpora a consulta e os documentos de forma independente e, em seguida, compara os vetores. Rápido — você incorpora a consulta uma única vez e a compara com os embeddings de documentos pré-calculados. É isso que você usa para recuperação.
Um cross-encoder recebe a consulta e um documento como uma única entrada e gera uma pontuação de relevância. Lento — ele processa cada par consulta-documento por meio do modelo completo. No entanto, é muito mais preciso porque consegue prestar atenção (attend) aos tokens da consulta e do documento simultaneamente.
O padrão de produção: o bi-encoder recupera os top-100 candidatos, e o cross-encoder os reordena para os top-10. Este é o pipeline de recuperar e depois reordenar (retrieve-then-rerank).
graph LR
Q["Consulta"] --> BE["Bi-Encoder: incorporar consulta"]
BE --> VS["Busca vetorial: top 100"]
VS --> CE["Cross-Encoder: reordenar"]
CE --> R["Resultados top 10"]
Modelos de reordenamento: Cohere Rerank 3.5 (