Arquitetura de memória de AI agents: checkpoints e vector stores
Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.
Atualização do artigo
Publicado originalmente em 14 de fevereiro de 2026. Revisto e atualizado em 6 de setembro de 2026. A atualização aborda a compactação de contexto, benchmarks de memória e APIs de armazenamento, e clarifica as diferenças entre estado de trabalho, checkpoints e memória de longo prazo.
Um reasoning loop só sobrevive a um pedido se o seu estado for armazenado fora do worker. Sem agent memory, o agent não consegue retomar um plano pausado, recuperar após uma falha ou recordar uma preferência de uma sessão anterior. A Parte 1 abordou o fluxo de controlo. Este artigo identifica o estado de que cada turno posterior precisa e onde esse estado deve residir.
Vou usar o Market Analyst Agent — um pequeno agent LangGraph que obtém dados de mercado e escreve um relatório de analista — para enquadrar a discussão sobre hot checkpoints. As secções independentes sobre cold vectors e Markdown bruto apresentam designs ilustrativos que mostram extensões ainda não implementadas pelo projeto atual. Depois, abordarei quando fazem sentido PostgreSQL, Redis, Qdrant, key-value stores e ficheiros Markdown simples.
Em resumo: para pausar e retomar, use um checkpoint store. Para factos exatos sobre o utilizador, use armazenamento estruturado; adicione vector retrieval apenas quando a pergunta variar na formulação. Use ficheiros quando as pessoas precisarem de inspecionar e editar conhecimento de projeto acumulado. Um checkpoint preserva o estado; o grafo e o harness continuam a decidir o que fazer com ele.
Todos os stores abaixo são lidos pelo harness, o código que conduz o loop em torno do modelo. O harness decide qual do seu conteúdo chega à context window; os stores não o fazem. Este artigo trata de onde esse estado reside antes de o harness o ir buscar. A Parte 3 e a Parte 4 explicam o que o harness faz depois com o prompt.
O que é a memória de um AI agent?
AI agent memory é a camada de estado que permite a um agent preservar o progresso de uma tarefa, recuperar conhecimento anterior e atualizar aquilo que sabe entre execuções. Um design pode usar checkpoints, stores semânticos ou estruturados e documentos legíveis por humanos. Escolha apenas os stores necessários para os requisitos de recuperação e recuperação após falhas do produto.
| Necessidade | Predefinição recomendada | Motivo |
|---|---|---|
| Pausar e retomar uma execução | PostgreSQL checkpoint store | Durável, consultável e fácil de operar com os dados da aplicação |
| Estado transitório de baixa latência | Redis checkpoint store | Retoma rápida e estado de curta duração, com compromissos de persistência |
| Recuperação semântica entre threads | Qdrant ou pgvector | Recupera memórias pelo significado, não apenas por chaves exatas |
| Factos estruturados sobre o utilizador | PostgreSQL ou key-value store | Atualizações determinísticas são melhores do que fuzzy retrieval para preferências e IDs |
| Convenções e procedimentos de projeto | Ficheiros Markdown ou JSON | Legíveis por humanos, comparáveis e fáceis de atualizar pelos agents |
| Memória de relações entre entidades | Knowledge graph | Útil quando as relações importam mais do que os factos individuais |
Não comece pela memória só porque parece inteligente. Comece pela falha visível para o utilizador: perder progresso, esquecer uma preferência, repetir investigação ou não reutilizar uma convenção do projeto.
As falhas que exigem memória
Um agent sem estado consegue responder a uma pergunta isolada, mas esquece o pedido assim que a chamada termina. Este design falha quando o produto precisa de qualquer um dos seguintes comportamentos:
- Pausar e retomar: um utilizador inicia uma tarefa de investigação, fecha o portátil e regressa no dia seguinte. Sem estado guardado num checkpoint, o agent recomeça do zero.
- Coerência em vários turnos: ao longo de uma conversa longa, o agent tem de se lembrar das ferramentas que chamou, dos dados que recolheu e dos passos do plano que concluiu.
- Personalização: um utilizador que regressa espera que o agent conheça a sua tolerância ao risco, o nível de profundidade de análise preferido e as interações anteriores.
- Human-in-the-loop (HITL): o agent reúne as evidências e aguarda que uma pessoa aprove o passo seguinte. O estado de “espera” tem de sobreviver a reinícios do processo.
No Market Analyst Agent da Parte 1, o pedido “Analyze NVDA” produz um plano, cinco tool calls, dados recolhidos e um rascunho de relatório. Quando o utilizador responde “parece bem, mas adiciona uma análise da concorrência”, um checkpoint restaura o plano e a investigação a partir do último passo concluído. Adicionar o passo de análise da concorrência exigiria interpretação e replanning posteriores; o companion não implementa esse comportamento. O checkpoint fornece o estado anterior, enquanto a aplicação tem de decidir como o novo pedido altera o plano.
A memória de longo prazo trata de um caso diferente. Se o utilizador regressar uma semana depois e perguntar “Atualiza a minha análise da NVDA”, o agent poderá ter de recordar uma preferência por avaliações de risco conservadoras e um interesse em ações de semicondutores. Um vector-backed memory store pode recuperar esses factos entre sessões sem voltar a perguntar por eles.
Os exemplos de implementação abaixo usam LangGraph, a biblioteca open-source da LangChain para criar agents como grafos de estado explícitos; os limites de armazenamento que define generalizam-se a qualquer framework. Pense numa conversa contínua do utilizador sobre “Analyze NVDA” como uma thread. Cada vez que o grafo é executado para responder ou continuar é uma run dentro dessa thread. Enquanto uma run está ativa, o contexto do modelo e as variáveis locais do programa constituem a sua working memory; desaparecem quando esse trabalho termina. O LangGraph chama short-term memory ao estado guardado para essa thread e long-term memory aos factos disponíveis para outras threads. Abaixo, “thread” e “conversa” significam o mesmo. A Parte 5 usa “session” para o registo durável de uma run, pelo que este artigo evita esse termo para a conversa.
Uma taxonomia da memória de AI agents
Antes de entrar na implementação, é útil classificar aquilo de que os agents precisam de se lembrar. O framework CoALA — Cognitive Architectures for Language Agents (Sumers, Yao et al., 2023) — é uma taxonomia amplamente citada, baseada na ciência cognitiva. Introduzi o âmbito da memória no meu artigo sobre context engineering; aqui desenvolvo-o em seis categorias:
| Tipo de memória | Âmbito | Duração | Exemplo | Padrão de armazenamento |
|---|---|---|---|---|
| Working | Passo atual | Milissegundos | Argumentos de tool call, resposta atual do LLM | Em processo (Python dict) |
| Short-term | Thread atual | Minutos–horas | Histórico da conversa, progresso do plano, dados recolhidos | Checkpoint store |
| Episodic | Entre threads | Dias–meses | ”Na semana passada, o utilizador perguntou pelos resultados da NVDA” | Vector store / KV store |
| Semantic | Entre threads | Meses–permanente | ”O utilizador prefere investimentos conservadores” | Vector store / KV store |
| Document | Entre threads | Dias–permanente | Notas de projeto, resumos de investigação, padrões aprendidos | File store (Markdown/JSON) |
| Procedural | Todo o sistema | Permanente | ”Ao analisar ações, consultar sempre os filings da SEC” | Config / system prompt |
A working memory contém as observações atuais, os factos recuperados e os resultados intermédios usados pela run ativa. Alguns vivem em variáveis da aplicação; as mensagens e os tool results selecionados formam a entrada do modelo. Essa entrada tem de caber na context window do modelo, enquanto o estado da aplicação pode ser maior e persistir ao longo de vários passos. A memória do processo perde-se numa falha, salvo se for explicitamente guardada. Os outros níveis fornecem informação para este estado de trabalho.
A short-term memory é o checkpoint que o LangGraph escreve após cada unidade de execução do grafo — um super-step, definido na secção seguinte. As memórias episodic e semantic persistem entre threads. A document memory armazena notas de projeto, resumos de investigação e convenções aprendidas em ficheiros que pessoas e agents podem inspecionar. A procedural memory inclui instruções de sistema, definições de ferramentas e procedimentos reutilizáveis que podem ser recuperados para uma tarefa. Os tempos de vida na tabela são ilustrativos; a retenção segue a política da aplicação, e o estado de trabalho pode durar uma run ativa completa.
Para efeitos de implementação, cinco dessas seis categorias condensam-se em três níveis de armazenamento. A short-term memory torna-se hot memory, o checkpoint da thread atual. As memórias episódica e semântica tornam-se cold memory, para recuperação entre threads. A document memory mantém o conhecimento de projeto acumulado legível e diretamente editável. A working memory é agrupada com o nível hot porque os checkpoints podem preservar o estado necessário para reconstruir uma run ativa. Um checkpoint não é o cálculo interno completo do modelo. Os procedimentos podem ser distribuídos com o agent ou armazenados e recuperados a partir de ficheiros ou de outro store. Estes níveis descrevem as escolhas de implementação deste artigo, não tipos de memória mutuamente exclusivos.
A CoALA classifica as memórias working, episodic, semantic e procedural. O inquérito Memory in the Age of AI Agents organiza a memória por forma, função e dinâmica, incluindo documentos, codebases e workflows reutilizáveis. Os ficheiros podem implementar várias dessas categorias. Este artigo nomeia separadamente a document memory para tornar visíveis as suas responsabilidades de armazenamento e manutenção.
O mesmo padrão de armazenamento surge noutros domínios. Um agent de Minecraft (Voyager) armazena competências de jogo reutilizáveis como bibliotecas de código, e web agents induzem workflows de browsing reutilizáveis a partir de runs bem-sucedidas. Voltarei a ambos mais adiante. Ficheiros inspecionáveis e indexed retrieval podem coexistir: o Voyager recupera programas usando embeddings das respetivas descrições.
A memória gerida pelo agent também difere de um pipeline RAG fixo quanto a quem executa a escrita. O agent ou o seu harness seleciona o que armazenar, atualizar e eliminar, escolhendo mais tarde quando o recuperar.
O artigo Generative Agents (Park et al., 2023) mostrou até onde isto pode chegar: agents simulados armazenaram, refletiram sobre e recuperaram as suas próprias memórias. O seu memory stream classificava candidatos por recência, importância e relevância — um design que continua a ser uma referência útil para retrieval em memória de agents.
A compactação mantém uma conversa utilizável
Uma context window maior não elimina a necessidade de escolher o que sobrevive. As APIs atuais podem resumir uma conversa antiga antes de esta preencher a janela. A compactação do lado do servidor do Claude, ainda uma funcionalidade beta em 2026-09-06, devolve um bloco compaction que os pedidos seguintes usam em vez do conteúdo anterior. Pode reduzir o trabalho de summarization do lado do cliente, mas o resumo pode omitir um facto necessário mais tarde.
Mantenha o estado autoritativo da tarefa fora desse resumo: efeitos concluídos, aprovações, referências a fontes e restrições exatas do utilizador. Um checkpoint restaura a execução; a compactação encurta o contexto do modelo; a memória de longo prazo seleciona conhecimento para outra conversa. Teste estes três comportamentos separadamente. Force a compactação a meio de um teste e verifique se a ação seguinte continua a respeitar uma restrição anterior. Não use uma transcrição compactada como único registo do que foi aprovado.
Memória de curto prazo do agent: o checkpoint store
O LangGraph cria checkpoints do estado do grafo nos limites de super-step — um nó ou um lote de nós executados em paralelo. Com o durability="async" predefinido, o passo seguinte pode ser executado enquanto essa escrita termina; o durability="sync" aguarda pela persistência antes de prosseguir, acrescentando latência de escrita. A recuperação após uma falha usa o último checkpoint persistido, que não é necessariamente o passo concluído mais recentemente. Esta é a base para pausar/retomar, debugging com time travel e workflows HITL.
Um checkpoint contém o estado do grafo necessário para retomar: o AgentState da Parte 1 — mensagens, identidade, perfil do utilizador, passos do plano, dados de investigação e modo de execução. Após uma interrupção HITL ou um reinício do processo, o LangGraph restaura o último estado guardado e usa os metadados de scheduling para escolher o nó seguinte. Retoma num limite de nó concluído, não numa linha Python arbitrária. Os detalhes armazenados incluem um ID de checkpoint e um timestamp, uma versão para cada channel (o nome que o LangGraph dá a uma chave de estado) e as versões dos channels que cada nó já observou. O número do passo é metadado desse checkpoint. Um checkpoint também é diferente de um event log append-only ou de um trace; a Parte 5 separa explicitamente estas superfícies de observabilidade do runtime.
Como funciona o checkpointing do LangGraph
O BaseCheckpointSaver do LangGraph é uma interface simples: put() escreve um checkpoint, get_tuple() lê o último para uma thread e list() devolve o histórico. Cada checkpoint é identificado por (thread_id, checkpoint_ns, checkpoint_id), onde thread_id identifica a conversa, checkpoint_ns trata do namespacing de subgrafos e checkpoint_id é uma versão única.
A decisão importante é escolher o backend a colocar por trás desta interface. PostgreSQL e Redis são duas opções comuns em produção.
PostgreSQL vs Redis
| Dimensão | PostgreSQL (langgraph-checkpoint-postgres) | Redis (langgraph-checkpoint-redis) |
|---|---|---|
| Modelo de durabilidade | Transações ACID, WAL e replicação | Persistência configurável: command log append-only (AOF) ou snapshots periódicos (RDB) |
| Histórico de checkpoints | Histórico durável para retoma e debugging | A retenção depende do saver e das definições de eviction |
| Restrição principal | Latência de escrita da base de dados e crescimento das tabelas | Utilização de RAM, eviction e configuração de persistência |
| Adequação operacional | Equipas que já operam bases de dados relacionais | Equipas que já operam Redis com throughput elevado |
| Melhor predefinição para | Retoma durável e debugging reproduzível | Estado de sessão recuperável e sensível à latência |
Benchmarks genéricos de bases de dados não preveem o desempenho de checkpoints. Meça o tamanho do estado serializado, a frequência de escrita, as definições de persistência e a concorrência do seu próprio grafo.
PostgreSQL: a predefinição durável
O PostgreSQL é a predefinição mais segura para a maioria das equipas. Os checkpoints sobrevivem a falhas, obtém semântica transacional completa e o histórico de checkpoints torna simples o debugging com time travel.
Uma versão simplificada da configuração de checkpoints em memory/hot.py. Se um atacante pudesse escrever checkpoints, defina LANGGRAPH_STRICT_MSGPACK=true ou configure allowed_msgpack_modules. Isso limita a desserialização a tipos seguros ou declarados; a predefinição permissiva avisa sobre tipos não registados, mas continua a permiti-los.
import asyncio
from contextlib import asynccontextmanager
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
@asynccontextmanager
async def postgres_checkpointer(connection_string: str):
"""Yield a PostgreSQL-backed checkpoint store.
PostgreSQL gives us ACID guarantees — if a checkpoint write succeeds,
the state is durable even if the process crashes immediately after.
`from_conn_string` is itself an async context manager: it owns the
connection and closes it on exit, so the graph has to run inside it.
"""
async with AsyncPostgresSaver.from_conn_string(connection_string) as checkpointer:
# Create the checkpoint tables if they don't exist.
# This is idempotent — safe to call on every startup.
await checkpointer.setup()
yield checkpointer
async def main(authenticated_user_id: str) -> None:
# The graph lives inside the context manager's scope.
async with postgres_checkpointer(
"postgresql://user:pass@localhost:5432/agent_memory"
) as checkpointer:
graph = create_graph(checkpointer=checkpointer)
# Every invoke/stream call now persists state automatically.
config = {"configurable": {"thread_id": "user-123-session-1"}}
result = await graph.ainvoke(
{"user_id": authenticated_user_id,
"messages": [HumanMessage(content="Analyze NVDA")]}, config
)
# After the server authenticates the approver and validates approval
# of this exact draft, update the companion's approval field.
await graph.aupdate_state(config, {"report_approved": True})
# Continue the static interrupt_before pause; new input starts a new run.
result = await graph.ainvoke(None, config)
# Local fixture identity. A server supplies this only after authentication.
asyncio.run(main(authenticated_user_id="user-123"))
O user_id na entrada do grafo provém do contexto autenticado do servidor; thread_id apenas localiza checkpoints e não estabelece identidade nem autoriza o acesso a uma thread. O AsyncPostgresSaver usa o pacote langgraph-checkpoint-postgres, que cria quatro tabelas: checkpoints (o estado serializado), checkpoint_blobs (dados binários de grande dimensão), checkpoint_writes (escritas pendentes para recuperação após falhas) e checkpoint_migrations (versão do schema). Os writers concorrentes são separados pela chave primária (thread_id, checkpoint_ns, checkpoint_id) e por upserts, não por locking — dois workers na mesma thread não corromperão os dados um do outro, mas também não irão coordenar-se.
Redis: quando a latência é o bottleneck
Quando a latência dos checkpoints é o bottleneck, o Redis é uma opção para estado recuperável. Meça o tamanho do estado serializado, as definições de persistência e a concorrência antes de o escolher em vez do PostgreSQL.
Uma versão simplificada da configuração de checkpoints em memory/hot.py:
import asyncio
from contextlib import asynccontextmanager
from langgraph.checkpoint.redis.aio import AsyncRedisSaver
@asynccontextmanager
async def redis_checkpointer(redis_url: str):
"""Yield a Redis-backed checkpoint store.
Redis keeps checkpoints in memory for low-latency access.
Durability depends on RDB snapshots, AOF fsync policy, and replication.
AOF with appendfsync everysec can still lose about one second of writes
after a crash; enabling AOF alone is not a no-loss guarantee.
"""
async with AsyncRedisSaver.from_conn_string(redis_url) as checkpointer:
# Initialize Redis data structures
await checkpointer.asetup()
yield checkpointer
async def main() -> None:
# Same graph API, different backend.
async with redis_checkpointer("redis://localhost:6379") as checkpointer:
graph = create_graph(checkpointer=checkpointer)
asyncio.run(main())
O AsyncRedisSaver de langgraph-checkpoint-redis armazena cada checkpoint como o seu próprio documento RedisJSON, sob a mesma chave (thread_id, checkpoint_ns, checkpoint_id) que o saver de Postgres. O redesign v0.1.0 colocou os valores dos checkpoints inline e substituiu a recuperação por channel por um path JSON.GET. Essa alteração diz respeito à recuperação de valores, não a todas as operações de persistência; as medições de latência do vendor dependem da sua workload. O Redis 8.0+ inclui RedisJSON e RediSearch por predefinição — não é necessário instalar módulos adicionais.
Escolha a política de persistência e fsync do Redis com base na janela de perda tolerada. O RDB pode perder escritas desde o último snapshot; a política AOF appendfsync everysec habitual pode perder cerca de um segundo. always troca latência de escrita por uma persistência mais forte, enquanto no deixa o flushing ao cuidado do sistema operativo. Teste a recuperação com as definições reais de disco e replicação.
Em deployments limitados pela memória, ShallowRedisSaver armazena apenas o checkpoint mais recente por thread — sem histórico, mas com utilização mínima de RAM. Use-o quando precisar de pausar/retomar, mas não de debugging com time travel.
Quando usar cada um
Use PostgreSQL quando:
- Precisa do histórico completo de checkpoints para debugging com time travel ou retoma reproduzível
- A durabilidade é inegociável (serviços financeiros, saúde)
- Já utiliza PostgreSQL na sua stack
- O agent executa tarefas longas em que perder estado significa horas de recomputação
- Quer um unified data store — PostgreSQL com pgvector pode ser um único backend para checkpoints, memória de longo prazo e pesquisa vetorial
Use Redis quando:
- A latência dos checkpoints é o seu bottleneck (chat em tempo real, UX de streaming)
- Está a criar voice bots ou experiências de streaming em que o acesso aos checkpoints está num caminho crítico de latência medido
- Precisa de scaling horizontal entre muitas threads independentes. Se vários agents alterarem estado partilhado, atribua um owner a esse estado e coordene fora do checkpoint saver.
- Trabalha com sessões de curta duração em que perder um checkpoint é recuperável
- Quer semantic caching para reduzir chamadas redundantes ao LLM (Redis LangCache coloca em cache queries semanticamente semelhantes para evitar chamadas repetidas ao LLM)
Outras opções: langgraph-checkpoint-sqlite funciona para desenvolvimento local e deployments num único processo. Para stacks AWS-native, langgraph-checkpoint-aws fornece um DynamoDBSaver com offloading automático do payload — o saver documentado faz offload acima do limite de 350 KB quando está configurado um bucket S3. Esse limite é uma política de implementação, não o limite de 400 KB por item do DynamoDB. O preço serverless e a ausência de infraestrutura para gerir tornam-no atrativo para deployments com carga variável.
Memória de longo prazo: recordar entre sessões
A hot memory trata da conversa atual. A memória de longo prazo abrange o utilizador que regressa na semana seguinte: armazena factos, preferências e histórico de interações que persistem entre threads.
O LangGraph disponibiliza uma interface Store para memória entre threads através da sua classe BaseStore. Cada item de memória é um par (namespace, key) com um valor JSON e um vector embedding opcional. O namespace codifica normalmente o utilizador ou a organização: ("user", "user-123", "preferences").
Vector storage: recuperação semântica com Qdrant
Quando o agent precisa de recordar factos não estruturados (“O que disse o utilizador sobre o seu horizonte de investimento?”), a pesquisa vetorial fornece recuperação semântica. Em vez de procurar chaves exatas, o agent consulta pelo significado.
Qdrant é uma base de dados vetorial especializada, escrita em Rust, que trata do armazenamento de embeddings, indexação (Hierarchical Navigable Small World, ou HNSW) e pesquisa filtrada. Abordei detalhadamente o HNSW e os seus compromissos no meu artigo sobre ranking de pesquisa. O Qdrant também disponibiliza um MCP server que funciona como camada de memória semântica — útil se o seu framework de agents suportar o Model Context Protocol.
O exemplo seguinte é um design Qdrant ilustrativo e independente. Não é uma versão simplificada do memory/long.py atual. O projeto atual armazena perfis de utilizador com filtragem exata por user_id e um placeholder de zero-vector. A integração real de embeddings continua a ser trabalho futuro. O request handler tem de autenticar o pedido e construir principal a partir da identidade verificada; o cliente nunca o fornece. O filtro do Qdrant define o âmbito do retrieval, não a autorização.
from qdrant_client import QdrantClient
from qdrant_client.models import (
PointStruct, Distance, VectorParams, Filter, FieldCondition, MatchValue,
)
import hashlib
import json
from dataclasses import dataclass
@dataclass(frozen=True)
class AuthenticatedPrincipal:
"""Created by the server after authentication, never from request JSON."""
user_id: str
class UserMemoryStore:
"""Long-term memory backed by Qdrant vector search.
Stores user facts as embedded vectors for semantic retrieval.
Each fact is a short natural-language statement about the user.
"""
def __init__(self, qdrant_url: str, collection_name: str = "user_memory"):
self.client = QdrantClient(url=qdrant_url)
self.collection_name = collection_name
self._ensure_collection()
def _ensure_collection(self):
"""Create the collection if it doesn't exist."""
collections = [c.name for c in self.client.get_collections().collections]
if self.collection_name not in collections:
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=VectorParams(
size=1536, # text-embedding-3-small dimensions
distance=Distance.COSINE,
),
)
def store_fact(
self, principal: AuthenticatedPrincipal, fact: str, embedding: list[float]
):
"""Store a user fact with its embedding."""
identity = json.dumps([principal.user_id, fact], ensure_ascii=False).encode()
point_id = hashlib.sha256(identity).hexdigest()[:32]
self.client.upsert(
collection_name=self.collection_name,
points=[PointStruct(
id=point_id,
vector=embedding,
payload={"user_id": principal.user_id, "fact": fact},
)],
)
def recall(
self,
principal: AuthenticatedPrincipal,
query_embedding: list[float],
top_k: int = 5,
):
"""Retrieve the most relevant facts for a user given a query."""
results = self.client.query_points(
collection_name=self.collection_name,
query=query_embedding,
query_filter=Filter(
must=[FieldCondition(
key="user_id", match=MatchValue(value=principal.user_id)
)]
),
limit=top_k,
)
return [hit.payload["fact"] for hit in results.points]
O ID do ponto faz hash de um array JSON com o ID do utilizador e o facto, para que delimitadores dentro de qualquer um dos valores não possam fundir duas identidades. Por exemplo, o utilizador a:b com o facto c tem de ser diferente do utilizador a com o facto b:c. Os 32 caracteres hexadecimais cabem na representação de point-ID UUID do Qdrant.
O fluxo tem três passos. Neste design ilustrativo, um LLM extrai factos importantes da interação (“o utilizador tem elevada tolerância ao risco”, “o utilizador está interessado em ações de semicondutores”). Esses factos são transformados em embeddings e armazenados no Qdrant. No início da conversa seguinte, o servidor fornece o principal autenticado e o agent consulta o Qdrant com a nova mensagem do utilizador para recuperar contexto relevante. O Market Analyst Agent atual ainda não implementa este fluxo de extração semântica e criação de embeddings.
Scoring do retrieval: para além da similaridade de cosine
A similaridade de cosine bruta é um ponto de partida, mas os sistemas de memória em produção precisam de um retrieval mais rico. O artigo Generative Agents (Park et al., 2023) introduziu uma função de scoring que combina três sinais:
- Recência: Decaimento baseado em regras, para que as memórias recentes obtenham uma pontuação superior. Uma função de decaimento exponencial faz com que um facto de ontem supere um facto equivalente de há seis meses.
- Importância: Significância avaliada por um LLM numa escala de 1 a 10. “A carteira do utilizador caiu 40%” obtém uma pontuação superior a “o utilizador disse olá.”
- Relevância: Similaridade de cosine entre a query e o facto armazenado, calculada através de embeddings.
O artigo normaliza os três sinais para escalas comparáveis antes de os combinar. Faça o mesmo antes de ajustar os pesos; caso contrário, uma pontuação de importância bruta de 1–10 dominaria um sinal de 0–1. A pontuação final de retrieval é uma soma ponderada: score = alpha * recency + beta * importance + gamma * relevance. Isto impede que factos recentes e importantes fiquem escondidos sob factos obsoletos mas semanticamente semelhantes. Para um protótipo de análise financeira, começaria com alpha = 0.3 para recência, beta = 0.2 para importância e gamma = 0.5 para relevância, porque é normalmente a query atual que determina qual dos factos válidos deve entrar no contexto. O artigo Generative Agents usou pesos iguais; estes valores são um ponto de partida proposto, não uma melhoria medida. Ajuste-os contra métricas de recall reservadas e verificações da qualidade da tarefa antes de depender deles.
Alternativas à pesquisa vetorial
A pesquisa vetorial é poderosa, mas nem sempre é a ferramenta certa. Eis quando usar alternativas:
| Abordagem | Melhor para | Principal custo operacional |
|---|---|---|
| Pesquisa vetorial (Qdrant) | Recuperação semântica de factos não estruturados | Ciclo de vida de embeddings e do índice |
| Key-value store (Redis) | Perfis e preferências estruturados de utilizadores | Utilização de memória e política de persistência |
| Document store (ficheiros) | Conhecimento de projeto e notas geridas pelo agent | Concorrência, permissões e pesquisa |
| Full-text search (PostgreSQL GIN index) | Recuperação por palavras-chave no histórico da conversa | Crescimento do índice e tuning de queries |
| Knowledge graph (Neo4j) | Relações entre entidades e queries multi-hop | Modelação do grafo e outro sistema de dados |
| Híbrida (vector + keyword) | Recall quando a intenção da query varia | Dois caminhos de scoring para ajustar e avaliar |
Os key-value stores funcionam bem com dados estruturados. Se a sua memória de longo prazo for um perfil de utilizador — tolerância ao risco, horizonte de investimento, setores preferidos — um hash Redis ou uma coluna JSONB do PostgreSQL é mais simples e rápido do que criar embeddings e consultar vetores. Use pesquisa vetorial quando a memória não for estruturada e a query de retrieval variar na formulação.
O Store integrado do LangGraph fornece uma interface key-value baseada em namespaces, com pesquisa vetorial opcional. A API BaseStore é simples: put(), get(), search() e delete(), com scoping hierárquico por namespace. Estão disponíveis três implementações:
InMemoryStore— para desenvolvimento e testes (os dados perdem-se quando o processo termina)PostgresStore— store persistente de produção com queries SQL completasAsyncRedisStore— memória entre threads com pesquisa vetorial, suporte de TTL e filtragem por metadados
A configuração index ativa a pesquisa vetorial sobre itens armazenados usando um modelo de embeddings configurável. Para muitos casos de utilização, este store integrado é suficiente, sem necessidade de recorrer a uma base de dados vetorial dedicada.
import asyncio
from langgraph.store.memory import InMemoryStore
# Create a store with vector search enabled
store = InMemoryStore(
index={
"dims": 1536,
"embed": my_embedding_function, # e.g., OpenAI text-embedding-3-small
}
)
async def main() -> None:
# Store a user preference (namespace scopes to user).
await store.aput(
namespace=("user", "user-123", "preferences"),
key="risk-profile",
value={"risk_tolerance": "high", "horizon": "long-term"},
)
# Semantic search across the user's memories.
# The namespace prefix is positional here — `search`/`asearch` declare it
# as positional-only `namespace_prefix`, unlike `aput`.
results = await store.asearch(
("user", "user-123"),
query="What is their investment style?",
limit=5,
)
asyncio.run(main())
Escolher uma estratégia de memória de longo prazo
Comece por key-value se a sua memória for estruturada e bem definida (perfis de utilizador, definições, entidades nomeadas). Adicione pesquisa vetorial quando precisar de retrieval semântico sobre factos não estruturados ou quando a formulação da query variar de forma imprevisível.
Os knowledge graphs justificam-se quando as relações entre entidades são importantes, por exemplo: “Que empresas mencionadas pelo utilizador são concorrentes da NVDA?” O projeto recente mais interessante nesta área é o Graphiti (da Zep), que cria um knowledge graph com consciência temporal, acompanhando quando os factos eram verdadeiros, não apenas o que era verdadeiro. As suas relações temporais podem preservar intervalos de validade e valores substituídos; a lógica de extração e atualização continua a determinar se um facto é atual. O artigo da Zep reporta 94,8% de precisão DMR para o sistema Zep avaliado, suportado pelo Graphiti com GPT-4 Turbo, contra 94,4% para contexto completo. O DMR usa conversas de 60 mensagens e uma tarefa limitada de recuperação de factos. Esta pequena diferença não estabelece uma vantagem geral para grafos temporais.
O problema é operacional. Operar uma base de dados de grafos não é trivial e, para a maioria das aplicações de agents, a pesquisa vetorial com filtragem por metadados cobre o mesmo terreno com menos infraestrutura.
Frameworks de memória gerida, como Mem0 e Letta (anteriormente MemGPT), tratam por si do pipeline de extração-consolidação-retrieval. A abordagem do Mem0 é particularmente interessante: um LLM extrai memórias candidatas, um motor de decisão compara cada facto novo com as entradas existentes no vector store e um resolver decide se deve adicionar, atualizar, eliminar ou não fazer nada. Isto mantém o memory store coerente e sem redundâncias. O Letta adota uma perspetiva de sistemas operativos: os agents gerem a sua própria context window usando ferramentas de gestão de memória, movendo autonomamente dados entre “core memory” (no contexto) e “archival memory” (fora do contexto). Ambos merecem ser avaliados se quiser reduzir o time-to-production e não precisar de controlo total sobre o pipeline de memória.
Document memory: o arquivo do agent
Os vector stores e os backends key-value tratam bem da recuperação semântica e das consultas estruturadas. O contexto de projeto acumulado — convenções, notas de investigação e decisões transportadas entre sessões — pertence muitas vezes a ficheiros que as pessoas podem ler, rever e versionar.
Isto é document memory: o agent lê e escreve ficheiros estruturados (Markdown, JSON, YAML) num diretório conhecido. Sem embeddings, sem base de dados, sem infraestrutura. Apenas ficheiros em disco que tanto o agent como o developer podem cat, grep, git diff e editar manualmente.
Numa avaliação realizada por um vendor, a Letta reportou 74,0% de precisão no LoCoMo — um benchmark de question-answering em conversas longas — para um agent GPT-4o mini que usava ficheiros anexados, embeddings automáticos, search_files semântico e regras obrigatórias para search tools. A melhor variante de grafo do Mem0 obteve 68,5%. Trata-se de um vendor, modelo, benchmark e harness específicos. O resultado mostra que uma interface orientada para ficheiros pode funcionar bem nessa configuração; não mostra que Markdown bruto ou pesquisa por palavras-chave sejam suficientes. A vantagem operacional é independente: os developers podem ler, editar e comparar diretamente o conhecimento armazenado.
Context windows maiores também tornam prático ler ficheiros completos para alguns documentos de projeto. O retrieval em chunks continua adequado para corpora grandes, mas um ficheiro curto de convenções ou handoff pode muitas vezes ser carregado diretamente. A escolha depende do tamanho do documento, da precisão do retrieval, do orçamento de contexto e da frequência com que as pessoas precisam de rever ou editar a memória.
Porquê ficheiros?
Para um projeto de agent de longa duração, use um diretório de notas bem organizado quando as pessoas precisarem de um registo que possa ser revisto. Considere um coding agent que trabalha no mesmo projeto durante semanas:
- Aprende que o projeto usa Pydantic v2, não v1
- Descobre que os testes têm de ser executados com
pytest -x --tb=short - Acumula conhecimento sobre a arquitetura da codebase
- Aprende as preferências do developer (“usar sempre
pathlib, nuncaos.path”)
Estes factos poderiam residir num sistema vetorial ou key-value. Os ficheiros são a melhor predefinição neste caso porque o developer precisa de ler, editar, rever e versionar notas relacionadas. Adicione pesquisa por palavras-chave ou semântica apenas quando o corpus de documentos e o padrão de queries o exigirem. Se o agent aprender algo errado, abra o ficheiro e corrija-o.
Claude Code, Cursor e Devin Desktop usam variantes deste padrão. Os exemplos seguintes mostram como cada um armazena e carrega os seus ficheiros.
Implementar um file memory store
A implementação é deliberadamente simples. O agent recebe quatro operações: escrever um documento, ler um documento, listar documentos disponíveis e pesquisar em todos os documentos por palavra-chave.
O exemplo seguinte é um file store ilustrativo independente, baseado em Markdown bruto. Não é uma versão simplificada do memory/document.py atual. O projeto atual usa DocumentMemory, que exige um namespace e uma key e escreve um envelope JSON com content, metadata e created_at. Este exemplo define um design diferente para mostrar os compromissos dos ficheiros Markdown legíveis por humanos:
from pathlib import Path
import json
class FileMemory:
"""Document memory backed by the local filesystem.
Stores agent knowledge as human-readable files organized by topic.
No embeddings, no database — just files that both the agent and
the developer can read, edit, and version-control.
"""
def __init__(self, base_dir: str | Path):
self.base_dir = Path(base_dir).resolve()
self.base_dir.mkdir(parents=True, exist_ok=True)
def _resolve_path(self, path: str) -> Path:
"""Return a path inside base_dir, rejecting escapes and symlinks."""
requested = Path(path)
if requested.is_absolute() or ".." in requested.parts:
raise ValueError("path must be relative to base_dir without traversal")
resolved = (self.base_dir / requested).resolve()
try:
resolved.relative_to(self.base_dir)
except ValueError as error:
raise ValueError("path must stay inside base_dir") from error
return resolved
def write_doc(self, path: str, content: str, metadata: dict | None = None):
"""Write or overwrite a document at the given path.
Paths are relative to base_dir. Directories are created automatically.
Metadata (if provided) is stored as a JSON sidecar file.
"""
full_path = self._resolve_path(path)
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.write_text(content, encoding="utf-8")
if metadata:
meta_path = self._resolve_path(
str(full_path.relative_to(self.base_dir).with_suffix(full_path.suffix + ".meta"))
)
meta_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
def read_doc(self, path: str) -> str | None:
"""Read a document by path. Returns None if not found."""
full_path = self._resolve_path(path)
if full_path.exists():
return full_path.read_text(encoding="utf-8")
return None
def list_docs(self, pattern: str = "**/*") -> list[str]:
"""List documents matching a glob pattern."""
self._resolve_path(pattern)
return [
str(self._resolve_path(str(p.relative_to(self.base_dir))).relative_to(self.base_dir))
for p in self.base_dir.glob(pattern)
if self._resolve_path(str(p.relative_to(self.base_dir))).is_file()
and not p.name.endswith(".meta")
]
def search_docs(self, query: str, pattern: str = "**/*.md") -> list[dict]:
"""Search documents by keyword. Returns matching files with context.
This is intentionally simple — grep-style keyword search.
For semantic search, use a vector store instead.
# ponytail: linear scan of file bytes; add an index when measured
# latency, concurrency, or retrieval quality requires it.
"""
self._resolve_path(pattern)
results = []
for path in self.base_dir.glob(pattern):
path = self._resolve_path(str(path.relative_to(self.base_dir)))
if not path.is_file() or path.name.endswith(".meta"):
continue
content = path.read_text(encoding="utf-8")
if query.lower() in content.lower():
# Return the paragraph containing the match for context
for paragraph in content.split("\n\n"):
if query.lower() in paragraph.lower():
results.append({
"path": str(path.relative_to(self.base_dir)),
"match": paragraph.strip()[:500],
})
return results
O helper de paths é partilhado deliberadamente por reads, writes e resultados de glob: os paths relativos ainda podem sair de um diretório através de .. ou de um symlink existente. Esta classe ilustrativa destina-se a um filesystem de utilizador único de confiança ou controlado. Verifica um path resolvido antes de o usar; num limite multi-tenant hostil, use operações no-follow relativas a descritores, para que uma mutação do filesystem não possa fazer race à verificação. Execute este pequeno teste de regressão depois de copiar a classe:
from tempfile import TemporaryDirectory
with TemporaryDirectory() as root:
memory = FileMemory(root)
memory.write_doc("notes/ok.md", "safe memory")
assert memory.read_doc("notes/ok.md") == "safe memory"
assert memory.list_docs() == ["notes/ok.md"]
assert memory.search_docs("safe")[0]["path"] == "notes/ok.md"
(Path(root) / "escape").symlink_to(Path(root).parent, target_is_directory=True)
for operation in (
lambda: memory.write_doc("../escape.md", "nope"),
lambda: memory.read_doc("/tmp/escape.md"),
lambda: memory.read_doc("escape/outside.md"),
lambda: memory.list_docs("../**/*"),
lambda: memory.search_docs("safe", "../**/*.md"),
):
try:
operation()
except ValueError:
pass
else:
raise AssertionError("FileMemory accepted an escaped path")
Estrutura de pastas
Grande parte do valor da document memory vem da organização do diretório. Eis a estrutura que usaria para um agent de investigação. O Market Analyst Agent usa namespaces sob memory/documents/, mas o seu DocumentMemory atual escreve cada entrada como um envelope JSON com uma string content, em vez de Markdown bruto. A organização em Markdown bruto abaixo pertence ao design FileMemory ilustrativo e independente apresentado acima:
.agent-memory/
README.md # What this directory is, for human readers
PROGRESS.md # Handoff for the next session: what is done, what is next
user-profiles/
user-123.md # Preferences, history, risk profile
user-456.md
research/
NVDA-2026-02.md # Research notes from recent analysis
TSLA-2026-01.md
conventions/
analysis-format.md # How to structure analysis reports
data-sources.md # Preferred data sources and API patterns
learnings/
common-errors.md # Mistakes the agent has learned to avoid
tool-patterns.md # Effective tool call sequences
No design FileMemory ilustrativo, todos os documentos são Markdown e o objetivo de cada documento é evidente a partir do seu path. Pode git diff todo o diretório de memória para ver o que o agent aprendeu numa sessão, git revert uma aprendizagem incorreta ou copiar o diretório para outro projeto. Os envelopes JSON do projeto atual mantêm a estrutura de namespace e key, mas não proporcionam a mesma experiência de diff em Markdown bruto.
Quando usar document memory, vector ou key-value
Os três backends de memória servem padrões de acesso diferentes:
| Dimensão | Vector Store | Key-Value Store | Document Store |
|---|---|---|---|
| Padrão de query | ”Encontrar factos semelhantes a X" | "Obter o valor da key" | "Ler o documento neste path” |
| Melhor para | Recall não estruturado e variável | Consultas estruturadas | Contexto de projeto, notas |
| Legível por humanos | Payloads de texto legíveis | Parcialmente (JSON) | Sim (Markdown) |
| Fácil de depurar | Inspecionar payloads e scores | Fácil (keys exatas) | Inspecionar ficheiros e pesquisar |
| Controlável por versão | Através de exports ou change logs | Possível | Sim (git-native) |
| Infraestrutura de embeddings | Necessária | Não necessária | Não necessária |
| Escala para | Milhões de factos | Milhões de keys | Depende dos bytes e do índice |
| Capacidade de pesquisa | Similaridade semântica | Correspondência exata | Path, palavra-chave, índice opcional |
Use document memory quando:
- O agent acumula conhecimento de projeto ao longo de várias sessões
- Os developers precisam de inspecionar, editar ou substituir aquilo que o agent “sabe”
- O conhecimento está estruturado como documentos (notas, resumos, convenções), e não como factos isolados
- Quer versionar a memória do agent com base em git
- Não ter infraestrutura é um requisito absoluto
Use vector stores quando:
- Precisa de retrieval semântico difuso (“encontrar memórias relacionadas com X”)
- A formulação da query varia de forma imprevisível
- Tem milhares a milhões de factos individuais
Use key-value stores quando:
- Precisa de pesquisas exatas e rápidas para dados estruturados (perfis de utilizador, definições)
- O schema dos dados é bem definido
Os três stores podem coexistir, mas isso não é obrigatório. O Market Analyst Agent atual usa checkpoints PostgreSQL para hot memory, Qdrant para armazenamento exato de perfis de utilizador com vectors placeholder e um document store namespaced baseado em envelopes JSON. As variantes de semantic recall e Markdown bruto deste artigo são extensões ilustrativas.
Exemplos do mundo real
Este padrão já é comum nos AI coding assistants:
- Claude Code lê ficheiros
CLAUDE.mda partir da raiz do projeto e dos diretórios-pai, e mantém um ficheiro de memória por projeto em~/.claude/projects/para aprendizagens entre sessões. O sistema de memória consiste em ficheiros Markdown simples, e os ficheiros ao nível do projeto são commitados juntamente com o código. - Cursor carrega regras do projeto a partir de
.cursor/rulescomo ficheiros.mdc— convenções de código, preferências de frameworks e decisões arquiteturais — com frontmatter a controlar quando cada regra se aplica. - O Cascade agent legado do Devin Desktop lê regras de
.devin/rules/, mantendo.windsurf/rules/e o.windsurfrulesna raiz como fallbacks legados. O Cascade armazena memórias geradas automaticamente localmente por workspace e recupera-as mais tarde; o agent Devin Local predefinido para novos separadores não persiste memórias. - A memory tool da Anthropic para a Claude API é uma ferramenta do lado do cliente que o modelo conduz através de operações sobre ficheiros —
view,create,str_replace,insert,deleteerename— num diretório/memories. A aplicação implementa cada comando, pelo que decide onde os ficheiros residem efetivamente (disco local, S3, base de dados).
As variantes baseadas em ficheiros armazenam o conhecimento do agent como texto legível por humanos com operações explícitas de leitura/escrita, e nenhuma precisa de um pipeline de embeddings. O agent decide o que escrever; quando esse texto vive num diretório local gerido por Git, o developer pode vê-lo e editá-lo num git diff. Quando um handler da memory tool da Anthropic mapeia /memories para S3 ou uma base de dados, a inspeção e o versioning dependem dessa implementação.
Notas declarativas e skills executáveis
O conhecimento baseado em ficheiros também aparece fora dos coding assistants, mas o formato de armazenamento não indica como é usado. O Voyager armazena programas JavaScript reutilizáveis: o agent pode executar esse código. O método Agent Workflow Memory principal adiciona, pelo contrário, web workflows induzidos ao contexto do prompt como orientação para ações posteriores. A sua experiência AWM_AS separada expõe workflows como ações invocáveis. Um procedimento descrito no contexto e um procedimento executável exigem verificações diferentes.
Teste as skills invocáveis executando-as num ambiente controlado e verificando os efeitos. Reveja as notas de projeto e os workflows contextuais quanto aos factos, restrições e orientações de ação que fornecem; depois teste se essas instruções melhoram o comportamento posterior. Qualquer uma das formas pode conduzir a uma ação prejudicial; nenhuma concede permissões adicionais.
A mesma fronteira mantém a memória distinta das skills e das ferramentas. O standard Agent Skills usa ficheiros SKILL.md para indicar a um agent como executar uma classe de trabalho; a memória regista factos aprendidos de um projeto ou de uma run anterior. A Parte 3 traça a fronteira vizinha entre uma skill e uma tool. Escolha um file store para contexto aprendido e inspecionável; escolha uma skill ou tool apenas quando o requisito for um procedimento ou capacidade reutilizável.
Escalar document memory para produção
A implementação baseada em ficheiros acima é adequada para um filesystem controlado de utilizador único. Vários tenants e writers concorrentes exigem controlo explícito de acesso e coordenação de escrita, independentemente do número de documentos.
O raw store acima não tem coordenação de escritas concorrentes, modelo de tenants ou índice de pesquisa. Meça esses requisitos antes de o substituir. Uma base de dados ou object store pode fornecer contratos diferentes de concorrência e acesso; os ficheiros também podem ser indexados.
Três abordagens comuns:
Abordagem A: híbrida com uma camada fina de base de dados
Mantenha os ficheiros para authoring (os developers editam Markdown localmente), mas sirva a partir de uma base de dados em runtime. No deployment, sincronize os ficheiros com rows do PostgreSQL. O agent lê da base de dados, não do disco. Isto proporciona:
- Ergonomia para developers (editar Markdown, fazer commit para git)
- Desempenho de queries em produção (leituras indexadas na base de dados)
- Separação clara entre authoring e serving
Abordagem B: object storage + vector index sidecar
Armazene documentos no S3/GCS como objetos, com uma collection Qdrant que indexa os seus embeddings. O agent consulta o Qdrant para obter IDs de documentos relevantes e depois vai buscar o conteúdo ao object storage. Isto escala horizontalmente e suporta pesquisa semântica, mas acrescenta complexidade: dois sistemas para gerir, um pipeline de embeddings para manter e consistência eventual entre o store e o índice.
Abordagem C: structured document store com PostgreSQL (recomendada)
Armazene documentos como rows JSONB do PostgreSQL com full-text search (GIN index) e embeddings vetoriais opcionais (pgvector). Isto fornece hybrid search (keyword + semantic), transações ACID e um único sistema operacional.
Um exemplo da Abordagem C. O score combinado executa scoring exato sobre um corpus de tenants limitado; não usa um índice approximate nearest-neighbor (ANN). O pgvector exige ordenação direta ascendente por distância com LIMIT para esse caminho de índice. Para um corpus maior, recupere separadamente candidatos limitados por keyword e por vector e depois faça a fusão dos seus ranks. Este é um padrão RLS, não código de aplicação pronto a usar: o seu database role só deve estar disponível para o application server de confiança. O servidor autentica o pedido e constrói principal; não aceita um tenant ID do caller. O PostgreSQL RLS torna esse âmbito aplicável mesmo que uma query posterior omita o seu predicado de tenant.
from typing import Optional
from dataclasses import dataclass
import asyncpg
@dataclass(frozen=True)
class AuthenticatedPrincipal:
"""The verified identity returned by the application's authentication layer."""
tenant_id: str
class ProductionDocumentMemory:
"""Illustrative PostgreSQL document memory with hybrid search and RLS.
Apply this schema and policy as the table owner during deployment:
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
path TEXT NOT NULL,
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536), -- pgvector extension
ts_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, path)
);
CREATE INDEX ON documents USING GIN(ts_vector);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_documents ON documents
USING (tenant_id = current_setting('app.tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.tenant_id', true));
`FORCE` also subjects the table owner to the policy. Superusers and roles with
`BYPASSRLS` still bypass it, so neither belongs in the application's pool.
"""
def __init__(self, pool: asyncpg.Pool):
self.pool = pool
async def write(
self,
principal: AuthenticatedPrincipal,
path: str,
content: str,
metadata: Optional[dict] = None,
embedding: Optional[list[float]] = None,
):
"""Write or update a document.
Sketch: on a real pool you must register codecs first, or asyncpg
raises DataError — `set_type_codec` for the JSONB metadata column
and pgvector's `register_vector` for the embedding.
"""
async with self.pool.acquire() as conn:
async with conn.transaction():
# true keeps this trusted context to this transaction only.
await conn.execute(
"SELECT set_config('app.tenant_id', $1, true)", principal.tenant_id
)
await conn.execute(
"""
INSERT INTO documents (tenant_id, path, content, metadata, embedding)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (tenant_id, path) DO UPDATE
SET content = EXCLUDED.content,
metadata = EXCLUDED.metadata,
embedding = EXCLUDED.embedding
""",
principal.tenant_id, path, content, metadata, embedding,
)
async def search(
self,
principal: AuthenticatedPrincipal,
query: str,
embedding: Optional[list[float]] = None,
limit: int = 5,
) -> list[dict]:
"""Hybrid search: full-text + optional vector similarity."""
async with self.pool.acquire() as conn:
async with conn.transaction():
await conn.execute(
"SELECT set_config('app.tenant_id', $1, true)", principal.tenant_id
)
if embedding:
# Hybrid scoring: 0.6 * text relevance + 0.4 * vector similarity
rows = await conn.fetch(
"""
SELECT path, content, metadata,
(0.6 * ts_rank(ts_vector, plainto_tsquery('english', $1)) +
0.4 * COALESCE(1 - (embedding <=> $2), 0)) AS score
FROM documents
WHERE ts_vector @@ plainto_tsquery('english', $1)
OR (embedding <=> $2) < 0.5
ORDER BY score DESC
LIMIT $3
""",
query, embedding, limit,
)
else:
# Full-text search only
rows = await conn.fetch(
"""
SELECT path, content, metadata,
ts_rank(ts_vector, plainto_tsquery('english', $1)) AS score
FROM documents
WHERE ts_vector @@ plainto_tsquery('english', $1)
ORDER BY score DESC
LIMIT $2
""",
query, limit,
)
return [dict(row) for row in rows]
set_config(..., true) tem âmbito limitado à transação, pelo que uma pooled connection não pode conservar o contexto de um tenant para o pedido seguinte. O OR no primeiro branch é o que o torna híbrido. COALESCE mantém no result set um documento que corresponde a keywords mas não tem embedding, com o seu text score; não contribui com similaridade vetorial. Apenas com o predicado @@, um documento que exprima o significado correto mas não partilhe keywords com a query é filtrado antes de qualquer scoring — isto é keyword retrieval com semantic reranking, não hybrid retrieval. Os pesos 0,6/0,4 são ilustrativos: o text rank e a similaridade de cosine têm escalas diferentes. Normalize-os com base na avaliação do retrieval ou use rank fusion antes de interpretar esses pesos como importância relativa. O threshold de distância é um parâmetro: torne-o mais restritivo se o braço vetorial inundar os resultados, ou mais permissivo se os semantic matches nunca surgirem.
A regressão seguinte é o comportamento a testar contra uma base de dados real depois das migrations. Sob tenant-a, uma leitura de tenant-b não devolve rows e um insert direto cross-tenant falha por RLS:
BEGIN;
SELECT set_config('app.tenant_id', 'tenant-a', true);
SELECT path FROM documents WHERE tenant_id = 'tenant-b'; -- 0 rows
INSERT INTO documents (tenant_id, path, content)
VALUES ('tenant-b', 'leak.md', 'must fail'); -- ERROR: row-level security policy
ROLLBACK;
O que obtém:
- Hybrid search: correspondência por keywords (GIN index) + similaridade semântica (pgvector) com scoring conjunto
- Multi-tenancy: identidade derivada do servidor e RLS aplicado pela base de dados
- Garantias ACID: as transações no primary fazem commit atomicamente; as leituras na replica podem ficar atrasadas
- Um único sistema operacional: não há uma base de dados vetorial separada para gerir
- Scaling: as read replicas podem servir queries tolerantes a dados obsoletos. O particionamento nativo pode ajudar no pruning e na manutenção, mas não distribui escritas entre servidores; isso exige um design explícito de sharding. Encaminhe os caminhos read-after-write para o primary ou meça uma política síncrona adequada
Os ficheiros são excelentes para workflows de um único developer. Em produção multi-tenant, um structured document store sobre PostgreSQL é normalmente o equilíbrio certo entre simplicidade, desempenho e maturidade operacional.
Integrar tudo: a arquitetura completa
Eis como os três níveis de memória podem trabalhar em conjunto numa arquitetura inspirada no Market Analyst Agent. O diagrama mostra um fluxo ilustrativo desde o pedido do utilizador até à resposta, com todas as camadas de memória ativas.
A arquitetura tem três caminhos de memória:
-
Hot path (checkpoint store): o LangGraph escreve o estado do grafo recuperável no checkpoint store em cada limite de super-step. Quando o grafo chega a um nó
interrupt_before(como o nópublishda Parte 1), a execução pausa. O utilizador pode fechar a aplicação e, quando regressar, o grafo retoma a partir do checkpoint. Os event logs e traces do runtime são preocupações de produção separadas. -
Cold path (long-term store): depois de o router escolher uma rota, o planner consulta o long-term store em busca de contexto relevante do utilizador. O planner não pode personalizar enquanto essa leitura não devolver resultados. Uma consulta suportada por vector pode incluir query embedding e recuperação do índice; uma consulta key-value não. Podem ser extraídos e armazenados factos novos depois de a conversa terminar, para que essa escrita não atrase o reasoning loop.
-
Document path (file store): durante o planeamento, o agent lê as convenções de projeto e as notas de investigação necessárias para o pedido. Durante a execução, escreve novamente no disco resumos de investigação e padrões aprendidos. Essas leituras informam a tarefa atual, pelo que o tamanho dos ficheiros, a velocidade do filesystem e o estado da cache afetam o tempo de resposta. Faça caching apenas se a cache tiver regras claras de invalidação e isolamento de tenants. As escritas podem ocorrer mais tarde.
A ligação em LangGraph é simples — o checkpoint store e o long-term store são passados na compilação do grafo, enquanto o document store é injetado como uma dependência. O exemplo local abaixo compila um builder StateGraph já configurado, incluindo nós que aceitam o store. Isto estende a ligação do grafo; os helpers create_graph da Parte 1 e do companion não aceitam um argumento store. Usa InMemoryStore para manter o snippet pequeno; a topologia Docker de referência usa Qdrant para o mesmo papel de semantic recall.
import asyncio
from langgraph.store.memory import InMemoryStore
# Cold memory: local sketch with vector search
# (The reference Docker topology uses Qdrant for persistent recall.)
memory_store = InMemoryStore(
index={"dims": 1536, "embed": embedding_function}
)
# Document memory: illustrative file-based store for project knowledge
# FileMemory is the illustrative class defined above, not the project's current
# DocumentMemory implementation.
doc_memory = FileMemory(base_dir=".agent-memory")
async def main() -> None:
# Hot memory: PostgreSQL for durable checkpoints. postgres_checkpointer() is
# the async context manager defined earlier, so the graph runs inside it.
async with postgres_checkpointer(pg_connection_string) as checkpointer:
# builder is the configured StateGraph for this extended design.
# The Part 1/companion create_graph helper does not accept store.
graph = builder.compile(
checkpointer=checkpointer,
store=memory_store,
)
# ... run the graph here, while the connection is still open
asyncio.run(main())
# The store is accessible inside any node via the store parameter
def planner_node(state: AgentState, *, store: BaseStore) -> dict:
"""Plan with user context from long-term memory."""
# Recall relevant user facts from vector store.
# Namespace prefix is positional — see the store example above.
user_memories = store.search(
("user", state.user_id),
query=state.messages[-1].content,
limit=5,
)
# Load project conventions from document memory
conventions = doc_memory.read_doc("conventions/analysis-format.md")
# Inject both into planning context
# Each stored value is a dict; render whatever keys it carries
memory_context = "\n".join(str(m.value) for m in user_memories)
# ... rest of planning logic with personalized context and conventions
O fluxo completo
No design alargado acima, o pedido “Analyze TSLA” de um utilizador que regressa poderia seguir este fluxo. O semantic recall e a extração assíncrona de factos são extensões propostas, não comportamentos atuais do companion:
-
Carregamento da document memory: quando o planner é executado, lê as convenções do projeto a partir do document store: preferências de formato de análise, fontes de dados preferidas e padrões de utilização de ferramentas. Estas definem o comportamento de base desse plano.
-
Router: o router classifica o pedido como
DEEP_RESEARCH. Neste exemplo, o routing usa o próprio pedido, não as preferências de longo prazo. -
Cold memory recall + planner: o planner consulta o long-term store com a mensagem do utilizador. Recupera: “O utilizador tem elevada tolerância ao risco”, “O utilizador prefere uma análise detalhada da concorrência”, “O utilizador investigou anteriormente a NVDA e a AMD”. Depois cria um plano de investigação de 5 passos, personalizado de acordo com essas preferências. Inclui um passo de análise da concorrência porque o histórico do utilizador mostra que o pretende. O plano segue o formato do documento de convenções.
-
Executor loop (hot memory): cada passo é executado através do padrão ReAct da Parte 1 — pensar, agir, observar, repetido até o passo estar concluído. O LangGraph cria um checkpoint para cada super-step (router, planner e cada passo sequencial do executor neste caso). A recuperação começa no último checkpoint persistido. Se a escrita do passo 3 tiver sido concluída, o grafo pode continuar no passo 4; com persistência assíncrona, uma falha pode exigir a repetição de um passo concluído.
-
Interrupção HITL: o reporter escreve um rascunho. Uma sessão separada do modelo, sem histórico da run, lê o rascunho e regista uma avaliação. O grafo chega então a
publish, ondeinterrupt_beforeo pausa independentemente dessa avaliação. O checkpoint contém tanto o rascunho como a avaliação, para que a pessoa os reveja antes de decidir se publica. Horas depois, o grafo recarrega o checkpoint e segue essa decisão. -
Atualizações da memória: depois de a conversa terminar, um processo assíncrono extrai novos factos sobre o utilizador (“o utilizador acompanha agora a TSLA”, “o utilizador aprovou o formato do relatório”) e armazena-os no vector store de longo prazo. O agent também escreve um resumo da investigação no document store (
research/TSLA-2026-02) para referência futura.
O padrão de três níveis separa claramente as responsabilidades. O checkpoint store trata da durabilidade e da retoma; é infraestrutura. O long-term store trata da personalização; é lógica de produto. O document store contém o conhecimento de projeto acumulado; é o caderno do agent.
Compromissos e considerações
A memória acrescenta valor, mas também custo e complexidade:
-
Custo dos embeddings: cada facto armazenado numa base de dados vetorial exige a geração de um embedding. Um embedding provider alojado acrescenta uma chamada à API, custo específico do provider e latência de rede; em setembro de 2026, a OpenAI indica
text-embedding-3-smalla $0,02 por milhão de tokens. O custo de um modelo alojado por facto é negligenciável, mas acumula-se entre milhares de utilizadores e sessões. Faça batch de chamadas alojadas e coloque os resultados em cache. No momento da query, o vector recall pode incluir query embedding, latência do índice e da rede; uma consulta key-value não. Meça esse caminho no seu deployment e depois faça caching de query embeddings frequentes ou use um modelo local de embeddings se a latência for crítica. -
Memória obsoleta: as preferências dos utilizadores mudam. Um facto armazenado há seis meses (“o utilizador prefere investimentos conservadores”) pode já não ser exato. Defina políticas de expiração. Por exemplo, uma equipa pode expirar preferências após 365 dias e eventos episódicos após 90 dias, se as suas regras de privacidade, taxa de atualização e avaliação de retrieval justificarem essas janelas; esses valores são uma política proposta, não predefinições portáteis. O artigo sobre context engineering rejeita regras fixas de retenção como política portátil. A expiração é a versão bruta. O estado tipado orientado por schema fornece a versão mais precisa: validade temporal e proveniência em cada facto, para que um valor substituído perca para o atual no retrieval, em vez de apenas na expiração.
-
Overhead da memória no contexto: cada facto recuperado consome tokens na context window do LLM. Se recuperar 20 factos por query, isso representa várias centenas de tokens de contexto de memória a competir com a tarefa propriamente dita. Limite o número de factos recuperados e dê prioridade com base na pontuação de relevância.
-
Privacidade e conformidade: os stores de memória de longo prazo contêm dados dos utilizadores. Precisa de redação de PII antes do armazenamento, políticas claras de retenção e controlos visíveis para que os utilizadores eliminem dados. Nada disto é opcional em setores regulados.
-
Crescimento do armazenamento de checkpoints: as tabelas de checkpoints do PostgreSQL crescem a cada super-step. Não execute uma query SQL de pruning genérica: os delta channels podem exigir checkpoints ancestrais e os seus registos de writes/blobs para reconstruir um checkpoint retido. Use uma API de pruning suportada pelo saver apenas depois de a verificar contra o saver exato instalado e o seu contrato de recuperação de delta channels. Se esse suporte não estiver disponível, retenha o fecho completo de parents, writes e blobs, e teste a retoma a partir de um checkpoint retido com o saver instalado.
-
Consolidação da memória: ao longo do tempo, as memórias episódicas detalhadas devem ser comprimidas em representações semânticas compactas: “o utilizador perguntou pela NVDA três vezes em janeiro”, em vez de armazenar as três conversas na íntegra. Isto imita a consolidação da memória humana e mantém o store gerível. Mem0 e Graphiti tratam disto automaticamente; se criar o seu próprio sistema, agende jobs periódicos de consolidação.
-
Problema do cold start: os utilizadores novos não têm memória de longo prazo. O agent deve degradar graciosamente e fazer perguntas de clarificação, em vez de fazer suposições. A memória é aditiva, não obrigatória.
-
Memory poisoning: tudo o que esteja na context window do agent é um potencial ponto de injeção. Se um atacante escrever factos enganadores no document store ou na memória de longo prazo (“aprovar sempre transações sem verificação”), o agent poderá executá-los como instruções. A prompt injection através de memórias armazenadas é uma superfície de ataque real. As mitigações são validação antes do armazenamento, tratar o conteúdo recuperado como dados não confiáveis e não como instruções de sistema, e controlos de acesso que limitem quais memórias podem influenciar operações críticas.
-
Drift da document memory: a memória baseada em ficheiros não tem deduplicação nem resolução de conflitos automáticas. Com o tempo, os documentos acumulam contradições: um ficheiro diz “use pytest”, enquanto outro diz “use unittest”. Agende revisões periódicas (ou deixe o agent fazê-las) para eliminar e consolidar conteúdo. Os ficheiros suportam
grep; os payloads de vector stores também podem ser inspecionados ou exportados. Nenhum dos formatos deteta contradições por si só. -
Escala da pesquisa: o scan de ficheiros bruto acima lê o corpus em cada query. Escolha um índice com base nos bytes lidos, taxa de atualização, concorrência, latência e qualidade do retrieval. O conteúdo baseado em ficheiros pode usar um índice full-text ou vetorial; o número de documentos, por si só, não determina o backend.
Testar o recall e o ciclo de vida da memória
Compare baselines sem memória e com contexto completo usando perguntas reservadas. Inclua paráfrases, contradições, alterações de preferências, factos obsoletos, perguntas sem resposta, eliminações e pedidos cross-tenant. O LongMemEval fornece 500 perguntas que abrangem extração, raciocínio multi-sessão e temporal, atualizações e abstention. Meça a precisão/recall do retrieval separadamente da correção da resposta, além da utilização de factos obsoletos, divulgação não autorizada, correção de writes/updates/deletes, latência e custo.
As perguntas de recall são apenas uma parte da avaliação. O MemoryArena acrescenta tarefas interdependentes entre sessões, nas quais uma ação anterior e o respetivo feedback têm de alterar o comportamento posterior. As suas tarefas abrangem compras, planeamento de viagens, pesquisa progressiva e raciocínio formal. Use esse design quando o produto prometer aprender com o trabalho, e não apenas responder a perguntas sobre conversas armazenadas. Estas são tarefas de investigação, não medições de um memory service em produção.
O EvoMemBench também separa conhecimento de experiência de execução e memória intra-episódio de memória entre episódios. A sua comparação de 15 métodos não encontra uma forma de memória uniformemente superior; os baselines de contexto longo continuam competitivos no seu protocolo. Isto apoia a manutenção de baselines simples na sua avaliação, e não a substituição de todos os stores pelo framework mais recente.
Mantenha a proveniência e a validade junto dos factos recuperados. As pontuações de importância não conseguem estabelecer confiança nem alterar permissões. A política de eliminação tem de abranger índices, resumos em cache e artefactos retidos, além do registo original.
A camada seguinte é a ação
As Partes 5 e 6 regressam à memória pelo lado operacional e tratam de metades diferentes. O runtime é responsável pelo checkpoint: onde a execução parou e como reiniciá-la. O harness é responsável pelo handoff: o que o trabalho significa e o que falta fazer, escrito como document memory para a sessão seguinte do modelo — um trecho contínuo de contexto do modelo, na terminologia que a Parte 5 fixa. Restaurar o processo não é o mesmo que restaurar a tarefa.
Referências
Artigos
- Cognitive Architectures for Language Agents (CoALA) — Sumers, Yao et al., 2023 — Taxonomia fundamental dos tipos de memória de agents
- Memory in the Age of AI Agents — dezembro de 2025 — Taxonomia tridimensional abrangente da memória de agents
- MemGPT: Towards LLMs as Operating Systems — Packer et al., 2023 — Gestão virtual de contexto para agents LLM
- Generative Agents: Interactive Simulacra of Human Behavior — Park et al., 2023 — Arquitetura de memory stream com scoring de recência, importância e relevância
- Zep: A Temporal Knowledge Graph Architecture for Agent Memory — Rasmussen, 2025 — Knowledge graph bitemporal para memória de agents
- Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory — 2025 — Pipeline de extração/consolidação com benchmarks
- Voyager: An Open-Ended Embodied Agent with Large Language Models — Wang et al., 2023 — Skill library como document memory para agents de jogos de mundo aberto
- JARVIS-1: Open-World Multi-task Agents with Memory-Augmented Multimodal Language Models — 2023 — Biblioteca de memória multimodal para agents de Minecraft
- Agent Workflow Memory — Wang et al., 2024 — Indução de workflows reutilizáveis para agents de automação web
- SkillWeaver: Web Agents can Self-Improve by Discovering and Honing Skills — 2025 — API tools reutilizáveis e auto-sintetizadas para web agents
- LEGOMem: Modular Procedural Memory for Multi-agent LLM Systems for Workflow Automation — outubro de 2025 — Unidades reutilizáveis de memória procedural divididas entre orchestrator e subagents
Documentação do LangGraph
- LangGraph Persistence (Checkpointing) — Conceitos centrais para memória baseada em checkpoints
- LangGraph Memory Store — Memória de longo prazo entre threads com a interface Store
- LangGraph Cross-Thread Persistence — API funcional para memória entre threads
- How to add memory to the prebuilt ReAct agent — Guia prático para adicionar memória
Backends de checkpoints
langgraph-checkpoint-postgres— PostgreSQL checkpoint saver para LangGraphlanggraph-checkpoint-redis— Redis checkpoint saver para LangGraph- LangGraph Redis Checkpoint 0.1.0 Redesign — Detalhes da arquitetura do Redis checkpoint saver
langgraph-checkpoint-aws— DynamoDB checkpoint saver com offloading para S3- Redis AI Agent Engineering — Padrões Redis para workloads de agents
Bases de dados vetoriais e ferramentas de memória
- Qdrant — Base de dados vetorial open-source com indexação HNSW e filtragem
- Qdrant Agentic Builders Guide — Guia prático para criar memória de agents com Qdrant
- pgvector — Extensão de pesquisa de similaridade vetorial para PostgreSQL
- Graphiti — Motor open-source de knowledge graphs temporais da Zep
Memória baseada em documentos e ficheiros
- Claude Code Memory — CLAUDE.md e o diretório de memória por projeto
- Anthropic Memory Tool — Memória baseada em ficheiros do lado do cliente para agents da Claude API
- Cursor Rules — Regras do projeto como ficheiros .mdc em .cursor/rules
- Devin Desktop Memories — Regras do Cascade e memórias geradas localmente no workspace; o Devin Local predefinido não as persiste
Frameworks de memória
- Mem0 — Camada de memória gerida com pipeline de extração/consolidação
- Letta (MemGPT) — Gestão virtual de contexto para agents, inspirada em sistemas operativos
- LangMem SDK — Ferramentas de gestão de memória para LangGraph
Workshops
- MemAgents: Memory for LLM-Based Agentic Systems — ICLR 2026 Workshop
Projeto de demonstração
- Market Analyst Agent — Implementação de referência para os caminhos de armazenamento de checkpoints e de perfis/documentos atuais