Switch from keyword search to real embeddings and build a minimal MemoryManager that stores and retrieves support tickets semantically — the core of the support-automation agent's memory layer.
A busca por palavras-chave opera com base em sobreposição exata de tokens. Se a consulta contém "cobrança" e um documento contém "cobrança", eles correspondem. Se o documento semanticamente relevante usa "falha de pagamento" e "acesso à conta revogado" em vez disso, não corresponde — mesmo sendo exatamente o que o usuário está procurando.
Embeddings resolvem isso convertendo texto em um vetor de números — um ponto em um espaço de alta dimensionalidade onde o significado determina a proximidade. Documentos sobre problemas de cobrança e documentos sobre falhas de pagamento ficam próximos nesse espaço, mesmo que não compartilhem nenhuma palavra.
Pense nisso como um mapa de uma cidade. Dois endereços podem ser strings completamente diferentes — "Rua Principal, 123" e "a cafeteria na esquina da Principal com Oak" — e ainda assim podem estar a 15 metros de distância. Embeddings criam um mapa de significado onde a proximidade reflete similaridade conceitual, não sobreposição lexical.
A implicação prática para a memória de agentes é significativa. Quando um usuário pergunta "o que aconteceu com meu pagamento na semana passada?", um sistema de recuperação por palavras-chave busca "pagamento" e "semana passada". Um sistema de recuperação semântica codifica a intenção — cobrança de conta, histórico recente, questão financeira — e encontra as memórias mais próximas no espaço de significado, independentemente da formulação exata.
Quando fazer a transição: Migre da busca por palavras-chave para embeddings quando usuários descrevem conceitos em vez de palavras-chave, quando a mesma ideia é expressa de múltiplas formas, ou quando a qualidade da recuperação importa mais do que a velocidade.
Aqui está como ficou a transição na consulta que expôs o problema:
A diferença não era marginal. Mudar para recuperação baseada em embeddings foi a mudança que tornou o agente de suporte realmente útil em produção.
Embeddings não são mágica: Eles codificam padrões estatísticos de dados de treinamento. Jargão específico de domínio ou frases raras podem não ter bons embeddings com um modelo de propósito geral. Avalie a qualidade da recuperação em consultas reais antes de se comprometer com um modelo.
Um modelo de embeddings recebe um trecho de texto e retorna uma lista de comprimento fixo de números de ponto flutuante. Para o modelo Voyage AI voyage-3 — o modelo de embeddings que uso no sistema de automação de suporte — essa lista tem 1024 números. Textos semelhantes produzem vetores que apontam em direções semelhantes nesse espaço; textos diferentes apontam em direções diferentes.
O pacote Python é voyageai, instalado com pip install voyageai. Ele lê VOYAGE_API_KEY do ambiente, seguindo o mesmo padrão do SDK da Anthropic.
O vetor em si não é interpretável por humanos. Você não consegue ler a 47ª dimensão e descobrir qual conceito ela codifica. O que importa é que dois vetores com alta similaridade de cosseno vieram de textos com significado semelhante. A similaridade de cosseno varia de -1 (significado oposto) a 1 (significado idêntico). Na prática, pontuações acima de 0.85 indicam forte alinhamento semântico; pontuações abaixo de 0.6 geralmente são ruído.
Você pode embutir múltiplos textos em uma única chamada em lote — a API aceita uma lista. Isso importa para o throughput: carregar 100 tickets históricos é muito mais eficiente como um lote do que 100 chamadas individuais.
Embedding no momento da consulta: A consulta deve ser embebida com o mesmo modelo dos documentos armazenados. Misturar modelos produz pontuações de similaridade sem sentido. Sempre registre qual modelo gerou seus vetores armazenados.
Ponto de verificação: Antes de continuar, considere isto — se você embutir "conta bloqueada" e "usuário não consegue autenticar" com o mesmo modelo voyage-3, você esperaria que a similaridade de cosseno deles estivesse mais próxima de 0.9 ou 0.2? A resposta molda como você pensa sobre a qualidade da recuperação pelo resto deste módulo.
A resposta é mais próxima de 0.9. Ambas as frases descrevem o mesmo conceito subjacente — uma falha de autenticação — mesmo usando vocabulário diferente. Essa é exatamente a propriedade que torna os embeddings úteis para memória de agentes: o modelo já aprendeu que "bloqueada" e "não consegue autenticar" pertencem à mesma região do espaço de significado. Você herda esse conhecimento ao usar o modelo de embeddings; não precisa ensiná-lo você mesmo.
Armazenar vetores de 1024 dimensões em uma coluna comum do banco de dados e calcular similaridade de cosseno com uma varredura completa da tabela funciona em pequena escala. Com dezenas de milhares de memórias, fica lento demais para recuperação em tempo real.
Um banco de dados vetorial é um banco de dados — ou extensão de banco de dados — projetado exatamente para esse problema. Ele indexa vetores para que "encontrar os 5 vizinhos mais próximos deste vetor de consulta" execute em milissegundos em vez de segundos. Uso o pgvector, uma extensão do PostgreSQL, porque o sistema de automação de suporte já usa PostgreSQL para seus dados relacionais. Adicionar busca vetorial a uma instância Postgres existente requer apenas um CREATE EXTENSION e um CREATE INDEX.
Aqui está o esquema da tabela de memórias:
O tipo de coluna VECTOR(1024) é fornecido pela extensão pgvector. A coluna metadata JSONB contém IDs de tickets, níveis de prioridade, identificadores de origem, ou qualquer outra coisa que deve acompanhar a memória mas não faz parte do conteúdo pesquisável.
O índice é o que torna a recuperação rápida:
ivfflat é um índice de vizinho mais próximo aproximado. vector_cosine_ops instrui a otimizar para distância de cosseno. lists = 100 é um padrão razoável para até algumas centenas de milhares de linhas; ajuste para próximo de sqrt(contagem_de_linhas) à medida que a tabela cresce.
Chroma como alternativa: Se você quiser um banco de dados vetorial dedicado sem executar o PostgreSQL, o Chroma oferece uma API Python simples e funciona bem para desenvolvimento. Para um sistema que já depende do Postgres, o pgvector adiciona menos complexidade operacional.
Com o esquema em vigor, a busca semântica se reduz a uma consulta SQL. A chave é o operador <=> do pgvector, que calcula a distância de cosseno entre dois vetores — e usa o índice ivfflat, portanto não varre todas as linhas.
A expressão 1 - distância converte distância de cosseno (menor = mais próximo) em similaridade de cosseno (maior = mais similar). A cláusula ORDER BY usa o índice ivfflat. Você passa o vetor de consulta embebido como parâmetro do Python — a implementação completa está no método MemoryManager.retrieve() na próxima seção.
Para tornar o antes-e-depois concreto, aqui está a mesma consulta de suporte pelos dois caminhos:
O caminho por palavras-chave buscou documentos contendo "cobrança" e "login" juntos. Encontrou uma página de FAQ e uma página de contato de departamento, depois recorreu a uma resposta genérica. O ticket relevante — registrado em erros de autenticação mas descrevendo uma falha de pagamento — marcou zero correspondências.
O caminho por embeddings converteu a consulta em um vetor voyage-3 e classificou por similaridade de cosseno. Os três primeiros resultados foram: "falha de pagamento causou bloqueio silencioso de conta" (0.91), "expiração de assinatura revoga acesso à API" (0.87), e "cobrança recusada, usuário vê erro de autenticação" (0.84). O agente encontrou o padrão de causa raiz sem nenhuma sobreposição de palavras entre consulta e resultados.
Reflexão: No seu próprio agente, qual texto você embeberia como conteúdo da memória — a mensagem bruta do usuário, um resumo estruturado, ou uma destilação gerada por LLM? Cada escolha muda como a superfície de recuperação se parece. Quais são os trade-offs?
A função de recuperação acima funciona como utilitário, mas um loop de agente em produção precisa de um único objeto que gerencie a conexão com o banco de dados, possua a referência ao modelo de embeddings e exponha uma interface limpa de store / retrieve. A classe MemoryManager faz isso em três segmentos.
Os três segmentos abaixo constroem a classe progressivamente. Leia cada segmento pelo que ele introduz antes de passar para o próximo — a interface molda o método store, e o método store molda o método retrieve.
Segmento 1 — interface e inicialização:
O construtor usa DATABASE_URL do ambiente se nenhuma URL explícita for fornecida. Registrar self.model na instância garante que todas as chamadas de store e retrieve usem o mesmo modelo — uma incompatibilidade aqui corromperia pontuações de similaridade silenciosamente.
Segmento 2 — armazenando uma memória:
O método embebe o texto, insere o vetor junto com o conteúdo bruto e metadados fornecidos pelo chamador, e retorna o ID da nova linha. RETURNING id evita uma segunda ida e volta ao banco de dados.
Segmento 3 — recuperando por similaridade semântica:
Com ambos os métodos implementados, o loop do agente torna-se direto. Armazene um ticket quando ele chegar; recupere as memórias mais próximas quando um usuário enviar uma consulta:
O que isso proporciona: O agente agora encontra os tickets certos independentemente da incompatibilidade de vocabulário entre consultas do usuário e descrições de tickets — o modo de falha que a busca por palavras-chave não conseguia recuperar.
Uma conexão por gerenciador: Este código abre uma única conexão persistente com o banco de dados. Para workers de agente concorrentes, use um pool de conexões (psycopg2.pool.ThreadedConnectionPool ou asyncpg) em vez de compartilhar uma conexão entre threads.
1. Um usuário busca 'acesso à conta negado após pagamento'. O ticket mais relevante diz 'falha de cobrança revogou privilégios de assinatura'. Por que a busca por palavras-chave perde este ticket?
Correct Answer: B
A busca por palavras-chave requer sobreposição de tokens. Nenhuma das palavras 'acesso', 'conta', 'negado' ou 'pagamento' aparece em 'falha de cobrança revogou privilégios de assinatura' — portanto o ticket marca zero de relevância apesar de descrever exatamente a mesma situação.
2. O que o modelo de embeddings voyage-3 retorna para um único texto de entrada?
Correct Answer: C
O voyage-3 retorna um vetor de comprimento fixo — 1024 floats para cada entrada. O vetor codifica o significado como uma coordenada no espaço de alta dimensionalidade, não uma lista de palavras ou resumo.
3. Dois tickets marcam similaridade de cosseno 0.91 com uma consulta. Um terceiro marca 0.43. Qual é a interpretação mais razoável?
Correct Answer: B
A similaridade de cosseno acima de ~0.85 tipicamente indica forte alinhamento semântico; abaixo de ~0.6 é geralmente fraco. 0.91 vs 0.43 é uma lacuna significativa — os dois primeiros tickets estão muito mais próximos em significado da consulta do que o terceiro.
4. Qual é o propósito do índice ivfflat criado com vector_cosine_ops?
Correct Answer: C
ivfflat é um índice de vizinho mais próximo aproximado otimizado para distância de cosseno. Sem ele, toda consulta de recuperação varre todas as linhas da tabela — viável para centenas de memórias, proibitivamente lento para dezenas de milhares.
5. Você armazena todos os tickets usando voyage-3, depois muda para um modelo de embedding diferente para novos tickets. O que dá errado?
Correct Answer: C
Diferentes modelos de embedding produzem vetores em espaços diferentes. A similaridade de cosseno entre um vetor voyage-3 e um vetor de um modelo diferente é calculada numericamente mas semanticamente sem sentido — a recuperação pode retornar tickets completamente errados sem nenhum erro.
6. O método MemoryManager.retrieve() chama self.embedder.embed() na consulta. Quando isso acontece em relação à consulta no banco de dados?
Correct Answer: B
A consulta deve ser embebida antes de poder ser comparada com vetores armazenados. embed() é chamado primeiro para produzir query_vector, e esse vetor é então passado para a cláusula SQL ORDER BY.
7. Por que o MemoryManager armazena o texto de conteúdo bruto junto com o vetor de embedding?
Correct Answer: B
Um vetor de embedding codifica significado como números mas não pode ser convertido de volta ao texto original. O agente precisa da coluna de conteúdo bruto para mostrar aos usuários o que foi recuperado — o vetor é usado apenas para classificação.