~ / tutoriais /pgvector-postgres-memoria-do-seu-agente $ _

pgvector no Postgres: onde guardar a memória do seu agente

Lucas Souza Lucas Souza 9 min de leitura Tutoriais
pgvector no Postgres: onde guardar a memória do seu agente

Você abriu a documentação de mais um serviço gerenciado de memória para o seu agente. Plano por mês, SDK próprio, mais um vendor no seu diagrama de arquitetura. E aí bate a pergunta honesta: o Postgres que já roda o seu Laravel não dá conta disso?

Quase sempre dá. A memória de um agente — as conversas passadas, os documentos que ele consulta, o histórico que ele precisa "lembrar" — no fundo é busca por similaridade em cima de embeddings. E o Postgres faz isso nativamente com uma extensão chamada pgvector. Mesmo banco, mesma transação, mesma query que você já sabe debugar.

Neste post você vai instalar o pgvector, guardar e buscar vetores, plugar isso num model Eloquent e — o mais importante — entender quando o Postgres resolve e quando faz sentido pagar por um serviço gerenciado. Sem hype. Engenharia.

TL;DR

  • O que é: extensão open-source que adiciona o tipo vector e busca por similaridade ao PostgreSQL.
  • Stack: PostgreSQL 12+, extensão pgvector 0.8+, Laravel/PHP via pgvector/pgvector-php.
  • Custo/Acesso: gratuito e open-source. Já vem disponível em RDS, Azure Database for PostgreSQL, Cloud SQL, Supabase e Neon.
  • Repositório: pgvector/pgvector e pgvector/pgvector-php.

O contexto — por que memória de agente é, no fundo, busca vetorial

Quando a gente fala em "dar memória" para um agente, parece coisa mágica. Não é. É arquitetura.

Você pega um pedaço de texto — uma mensagem antiga, um trecho de documento, um fato sobre o usuário — e transforma em um vetor de números via embedding. Esse vetor é uma representação matemática do significado daquele texto. Textos parecidos viram vetores próximos no espaço. Aí, quando o agente precisa "lembrar" de algo relevante para a conversa atual, você embeda a pergunta, procura os vetores mais próximos e injeta esses trechos no contexto do modelo. Isso é RAG. Isso é memória de agente. É o mesmo mecanismo.

O ponto: guardar e buscar esses vetores não exige um banco vetorial dedicado. O pgvector adiciona um tipo vector ao Postgres e operadores de distância que rodam dentro de uma query SQL normal — você combina similaridade semântica com WHERE, JOIN e transação no mesmo lugar onde já moram os dados da sua aplicação (pgvector).

E não é brinquedo de protótipo. Benchmarks mostram busca abaixo de 20ms em 1 milhão de vetores com índice HNSW, mantendo recall acima de 95% (Northflank). Para a esmagadora maioria dos agentes que você vai colocar em produção, isso sobra.

Pré-requisitos

  • PostgreSQL 12 ou superior (RDS, Supabase, Neon e Azure já suportam pgvector).
  • Permissão para rodar CREATE EXTENSION no banco.
  • Laravel 10+ com a conexão pgsql configurada.
  • Um provider de embeddings (OpenAI, Cohere, Gemini, Voyage — tanto faz; o que importa é a dimensão do vetor).

Mão na massa — pgvector do zero ao Eloquent

Passo 1: ativar a extensão e criar a tabela

Conecta no banco e liga a extensão. Uma linha:

CREATE EXTENSION IF NOT EXISTS vector;

Agora a tabela de memória. O número entre parênteses é a dimensão do embedding — tem que bater exatamente com o que o seu modelo produz (o text-embedding-3-small da OpenAI, por exemplo, gera 1536):

CREATE TABLE agent_memories (
  id          bigserial PRIMARY KEY,
  user_id     bigint NOT NULL,
  content     text NOT NULL,
  embedding   vector(1536),
  created_at  timestamptz DEFAULT now()
);

O pgvector aguenta até 16.000 dimensões, e cada vetor ocupa 4 * dimensões + 8 bytes em disco (pgvector). Guarde esse número — ele volta na hora de decidir entre Postgres e serviço gerenciado.

Passo 2: inserir e buscar por similaridade

Inserir é só passar o array como texto:

INSERT INTO agent_memories (user_id, content, embedding)
VALUES (42, 'cliente prefere contato por email', '[0.012, -0.034, ...]');

A busca é o coração de tudo. Você ordena pela distância entre o vetor da pergunta e os vetores guardados:

SELECT content
FROM agent_memories
WHERE user_id = 42
ORDER BY embedding <-> '[0.011, -0.030, ...]'
LIMIT 5;

Aquele <-> é o operador de distância L2 (euclidiana). O pgvector também tem <=> para distância de cosseno e <#> para inner product negativo — para embeddings normalizados de LLM, cosseno costuma ser a escolha (pgvector). Repare uma coisa que banco vetorial nenhum te dá tão fácil: o WHERE user_id = 42 e a busca semântica acontecem na mesma query, na mesma transação. Memória isolada por usuário, sem gambiarra.

Passo 3: criar índice (e escolher o certo)

Sem índice, o Postgres faz scan exato — perfeito até uns milhares de linhas, lento quando a tabela cresce. Aí você cria um índice aproximado. São dois:

-- HNSW: mais rápido na busca, melhor recall, insert barato
CREATE INDEX ON agent_memories USING hnsw (embedding vector_cosine_ops);

-- IVFFlat: usa menos memória, exige escolher o número de listas
CREATE INDEX ON agent_memories USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

A regra prática: comece com HNSW. Ele ganha em velocidade de busca e recall, e o insert é uma operação de custo constante no grafo. O preço é memória — o grafo HNSW pode ocupar de 2 a 5 vezes o tamanho dos vetores crus, enquanto o IVFFlat fica perto de 1x (BigDataBoutique). Se RAM for o seu gargalo, IVFFlat. Caso contrário, HNSW e segue o jogo.

Passo 4: plugar no Eloquent

Aqui o Laravel brilha. O pacote pgvector/pgvector-php dá um cast e um trait que escondem o SQL:

composer require pgvector/pgvector

Na migration, o tipo vector vira um método de coluna:

Schema::create('agent_memories', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id');
    $table->text('content');
    $table->vector('embedding', 1536);
    $table->timestamps();
});

No model, o cast Vector e o trait HasNeighbors:

use Pgvector\Laravel\Vector;
use Pgvector\Laravel\HasNeighbors;

class AgentMemory extends Model
{
    use HasNeighbors;

    protected $fillable = ['user_id', 'content', 'embedding'];
    protected $casts = ['embedding' => Vector::class];
}

E a busca vira PHP que você lê e debuga:

use Pgvector\Laravel\Distance;

$memory = AgentMemory::find($id);

$relevantes = $memory->nearestNeighbors('embedding', Distance::Cosine)
    ->where('user_id', $userId)
    ->take(5)
    ->get();

Os métodos de distância cobrem Distance::L2, Distance::Cosine, Distance::InnerProduct, além de L1, Hamming e Jaccard (pgvector-php). E se você usa o Laravel AI SDK mais novo, dá pra gerar o embedding com Str::of($text)->toEmbeddings() e buscar com whereVectorSimilarTo, fechando o ciclo sem sair do framework (Laravel News).

Limitações e pontos de atenção

O pgvector não é bala de prata. Onde você vai se queimar:

  • Índice consome RAM. HNSW grande exige memória. Se você tem dezenas de milhões de vetores em dimensão alta, faça a conta de 4 * dimensões + 8 bytes por linha antes de prometer prazo. Para dimensões acima de 2000, halfvec corta cerca de 50% do consumo de armazenamento com qualidade de busca parecida (dbi-services).
  • Filtro + ANN pode "subfiltrar". Quando você combina índice aproximado com WHERE restritivo, às vezes voltam menos resultados do que o LIMIT pede. O pgvector 0.8.0 resolveu isso com iterative scans (hnsw.iterative_scan) e melhorou a escolha de índice na presença de filtros (PostgreSQL). Use 0.8+.
  • Memória de agente é mais que busca vetorial. Resumo de conversas longas, decaimento por tempo, deduplicação de fatos, extração de entidades — isso é lógica de aplicação que você escreve. O pgvector te dá o armazenamento e a recuperação. Os outros 20% são seus.

FAQ rápido

Preciso trocar de banco para usar pgvector? Não. É uma extensão do Postgres que você já tem. CREATE EXTENSION vector e pronto. Em RDS, Supabase, Neon e Azure já vem disponível, é só habilitar.

HNSW ou IVFFlat? Comece com HNSW: melhor recall e busca mais rápida, insert barato. Migre para IVFFlat só se a memória do índice virar gargalo, já que ele ocupa perto de 1x o tamanho dos vetores contra 2-5x do HNSW.

Aguenta produção de verdade? Sim. Há benchmarks com busca abaixo de 20ms em 1 milhão de vetores e recall acima de 95% com HNSW. O limite prático aparece na casa das dezenas de milhões de vetores em dimensão alta — aí a conversa muda.

Qual dimensão uso na coluna? A que o seu modelo de embedding gera. text-embedding-3-small da OpenAI = 1536. A coluna vector(n) precisa bater com esse número, senão o insert falha.

Conclusão — quando o Postgres resolve e quando não

Volta para a pergunta do começo. Antes de assinar o Mem0 ou qualquer serviço gerenciado de memória, faça três perguntas honestas.

Volume. Está na casa de milhares a poucos milhões de vetores? Postgres resolve com folga. Dezenas de milhões em dimensão alta, com latência agressiva e sharding? Aí um serviço dedicado começa a pagar o próprio preço.

Compliance. Se o dado da memória do agente não pode sair da sua infraestrutura, manter tudo no seu Postgres é mais simples do que auditar mais um processador de dados. Um vendor a menos no contrato.

Time. Serviço gerenciado vende conveniência: resumo automático, decaimento, deduplicação prontos. Se o seu time é pequeno e esses 20% custam caro pra construir, pagar pode fazer sentido. Mas comece sabendo que os 80% — guardar e recuperar por similaridade — o seu banco já faz.

Na prática, a decisão raramente é técnica. É de maturidade de produto. E entender onde a fronteira está — o que o seu Postgres resolve e o que justifica um serviço externo — é exatamente o tipo de decisão de arquitetura que separa quem usa IA de quem constrói produto com IA. É isso que a gente coloca na mesa, com código rodando, no Workshop Arquitetando Soluções de IA.

Agora que você tem memória vetorial no seu banco, o próximo passo é montar o pipeline de RAG do zero em cima dela: chunking, embedding em lote e reranking dos resultados.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando