Arquitetura de memória para AI Agents: Checkpoints e Vector Stores
Tradução automática Este artigo foi traduzido automaticamente a partir da versão original em inglês.
Um reasoning loop só sobrevive a um pedido se o seu estado for armazenado fora do worker. Sem memória do agent, 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 de que estado 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 raw Markdown são designs ilustrativos que mostram extensões ainda não implementadas pelo projeto atual. Depois, abordarei quando faz sentido usar PostgreSQL, Redis, Qdrant, key-value stores e simples ficheiros Markdown.
Em resumo: Separe a memória por padrão de acesso. A hot memory é o estado de checkpoint por conversa, usado para pausar e retomar. A cold memory contém factos entre sessões num key-value ou vector store. A document memory mantém o conhecimento do projeto em ficheiros inspecionáveis. Comece pela falha que precisa de corrigir e escolha depois o store. Não coloque factos exatos num sistema de retrieval difuso nem trate um checkpoint como audit log.
Todos os stores abaixo são lidos pelo harness, o código que conduz o loop em torno do modelo. O harness decide que conteúdos chegam à context window; os stores não. Este artigo aborda onde esse estado reside antes de o harness o consultar. A Parte 3 e a Parte 4 cobrem o que o harness faz depois com o prompt.
O que é a memória de um AI agent?
A memória de um AI agent é a camada de estado que permite a um agent preservar o progresso de uma tarefa, obter conhecimento anterior e atualizar o que sabe entre execuções. Em produção, não é uma única vector database. É uma combinação de hot checkpoints, stores semânticos ou estruturados de longa duração e document memory legível por humanos.
| 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 |
| Recall semântico entre threads | Qdrant ou pgvector | Obtém memórias pelo significado, não apenas por chaves exatas |
| Factos estruturados do utilizador | PostgreSQL ou key-value store | Atualizações determinísticas são melhores do que retrieval difuso para preferências e IDs |
| Convenções e procedimentos aprendidos | 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. Esse 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 em checkpoints, o agent recomeça do princípio.
- Coerência entre turnos: numa conversa longa, o agent tem de se lembrar das tools que chamou, dos dados que recolheu e dos passos do plano que concluiu.
- Personalização: um utilizador recorrente espera que o agent conheça a sua tolerância ao risco, o nível de profundidade da análise que prefere e as interações anteriores.
- Human-in-the-loop (HITL): o agent reúne as evidências e espera 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 relatório preliminar. Quando o utilizador responde “parece bem, mas acrescenta uma análise da concorrência”, um checkpoint store permite ao agent carregar o estado do último passo concluído e adicionar o passo de análise da concorrência. Sem estado guardado em checkpoints, não consegue determinar a que se refere “parece bem” e tem de recomeçar.
A memória de longa duração 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 obter 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; as fronteiras de armazenamento que define generalizam-se a qualquer framework. O LangGraph separa a memória por âmbito. Cada execução do grafo decorre dentro de uma thread, isto é, uma conversa ou tarefa. O estado persistido nessa thread é short-term memory. O estado partilhado entre threads é long-term memory. Abaixo, “thread” e “conversation” são intercambiáveis; evito “session” para esse intervalo porque a Parte 5 reserva o termo para o log durável de uma execução, podendo várias execuções acumular-se numa thread. O contexto atual do modelo e as variáveis em processo formam a camada de working memory acima de ambos os stores.
Uma taxonomia da memória de AI Agents
Antes de abordar a 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 scoping 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 calls, 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 do projeto, resumos de investigação, padrões aprendidos | File store (Markdown/JSON) |
| Procedural | Todo o sistema | Permanente | “Ao analisar ações, verificar sempre os documentos da SEC” | Config / system prompt |
A working memory é aquilo com que o LLM está a raciocinar ativamente naquele momento: variáveis Python na função atual, conteúdos da context window e argumentos de tool calls durante a execução. É a camada mais rápida e mais efémera. Nada persiste para além do passo atual. A working memory é limitada pela context window do modelo, o que faz dela o bottleneck. Tudo o que o agent “sabe” no momento da decisão tem de caber aqui, quer tenha vindo do checkpoint store, de uma consulta vetorial ou da leitura de um ficheiro. Os outros níveis existem para alimentar a working memory com a informação certa, no momento certo.
A short-term memory é o checkpoint que o LangGraph escreve depois de 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 do projeto, resumos de investigação e convenções aprendidas em ficheiros que pessoas e agents podem inspecionar. A procedural memory reside nas instruções do sistema e nas definições das tools, em vez de mudar para cada utilizador.
Para efeitos de implementação, cinco dessas seis categorias agregam-se em três níveis de armazenamento. A short-term memory torna-se hot memory, o checkpoint da thread atual. As memórias episodic e semantic tornam-se cold memory, usada para recall entre threads. A document memory mantém o conhecimento acumulado do projeto legível e diretamente editável. A working memory aparece agrupada com o nível hot na taxonomia acima, mas é a única que nunca é realmente armazenada: existe durante um único passo, em processo, e corresponde à context window para a qual os três níveis de armazenamento carregam dados. A procedural memory fica fora dos três: reside no system prompt e nas definições das tools, sendo distribuída com o agent em vez de ser armazenada e obtida.
A CoALA classifica as memórias working, episodic, semantic e procedural. O inquérito Memory in the Age of AI Agents dá ênfase a vector stores e knowledge graphs, enquanto o LangGraph documenta checkpoints e a sua interface Store. O conhecimento de projeto suportado por ficheiros fica fora dessas taxonomias, apesar de Claude Code, Cursor e Devin Desktop carregarem ficheiros persistentes de projeto.
O mesmo padrão de armazenamento surge noutros domínios. Um agent de Minecraft (Voyager) armazena skills de jogo reutilizáveis como bibliotecas de código; equipas numa competição empresarial de document-QA iteram sobre documentos de prompts procedurais; e web agents inferem workflows de browsing reutilizáveis a partir de execuções bem-sucedidas. Voltarei aos três casos mais adiante; o ponto aqui é que os ficheiros tornam esse conhecimento inspecionável e versionável sem um serviço de embeddings separado.
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 deve obter.
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.
Memória de curto prazo de AI Agents: o checkpoint store
Sempre que o LangGraph termina um super-step — um node ou um conjunto de nodes executados em paralelo — o framework serializa o estado completo do grafo e escreve-o num checkpoint store. Essa é a base das workflows de pausa/retoma, da depuração com time travel e dos fluxos 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. Em paralelo, o LangGraph armazena o seu próprio bookkeeping: o ID e timestamp do checkpoint, uma versão por channel (o nome que o LangGraph dá a cada chave individual do estado) e um registo separado dos channel versions que cada node já viu. O número do passo reside nos metadados do checkpoint, não no checkpoint em si. É comparando esses dois elementos que o grafo determina o que deve executar em seguida. Depois de um HITL interrupt ou de um reinício do processo, o grafo carrega o checkpoint escrito no último limite concluído e reentra no node seguinte. Não retoma a partir de uma linha Python arbitrária. Um checkpoint também é diferente de um event log append-only ou de um trace; a Parte 5 separa explicitamente essas superfícies de observabilidade do runtime.
Como funciona o checkpointing no LangGraph
O BaseCheckpointSaver do LangGraph é uma interface simples: put() escreve um checkpoint, get_tuple() lê o mais recente para uma thread e list() devolve o histórico. Cada checkpoint é identificado por (thread_id, checkpoint_ns, checkpoint_id), em que thread_id identifica a conversa, checkpoint_ns trata do namespacing de subgrafos e checkpoint_id é uma versão única.
A decisão importante é o backend que ficará por trás desta interface. PostgreSQL e Redis são duas escolhas 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: log de comandos 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 |
| Principal restrição | Latência de escrita da database e crescimento das tabelas | Utilização de RAM, eviction e configuração de persistência |
| Adequação operacional | Equipas que já operam databases relacionais | Equipas que já operam Redis com elevado throughput |
| Melhor predefinição para | Retoma durável e debugging reproduzível | Estado de sessão sensível à latência e recuperável |
Benchmarks genéricos de databases não permitem prever o desempenho dos 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 simplifica a depuração com time travel.
Uma versão simplificada da configuração de checkpoints em memory/hot.py. Em produção, defina LANGGRAPH_STRICT_MSGPACK=true ou configure uma allowlist explícita de allowed_msgpack_modules, para que a desserialização dos checkpoints permita apenas tipos seguros ou declarados; a predefinição permissiva avisa sobre tipos não registados, mas permite-os.
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() -> 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(
{"messages": [HumanMessage(content="Analyze NVDA")]}, config
)
# Resume later on the same thread_id — loads the latest checkpoint.
result = await graph.ainvoke(
{"messages": [HumanMessage(content="approved")]}, config
)
asyncio.run(main())
O AsyncPostgresSaver usa o package langgraph-checkpoint-postgres, que cria quatro tabelas: checkpoints (o estado serializado), checkpoint_blobs (dados binários de grandes dimensões), checkpoint_writes (writes pendentes para recuperação após falhas) e checkpoint_migrations (versão do schema). Os writers concorrentes são separados pela primary key (thread_id, checkpoint_ns, checkpoint_id) e por upserts, em vez de locks — dois workers na mesma thread não corrompem os dados uns dos outros, mas também não coordenam entre si.
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.
Trade-off: less durable than PostgreSQL unless you enable AOF,
Redis's append-only log — snapshots alone lose recent writes on a crash.
"""
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 um documento RedisJSON independente, sob a mesma chave (thread_id, checkpoint_ns, checkpoint_id) que o saver do Postgres. O redesign da v0.1.0 substituiu várias operações de pesquisa por uma única chamada JSON.GET, reduzindo significativamente a latência. O Redis 8.0+ inclui RedisJSON e RediSearch por predefinição — não é necessário instalar módulos adicionais.
Em deployments com memória limitada, 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 opção
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 repetir horas de computação
- Quer um unified data store — PostgreSQL com pgvector pode ser um único backend para checkpoints, memória de longa duração 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 ao checkpoint está num caminho crítico de latência medido
- Precisa de scaling horizontal para muitas threads concorrentes
- Tem padrões de fan-out de elevada concorrência em que vários agents partilham estado
- Tem 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 — checkpoints pequenos (<350 KB) permanecem no DynamoDB e os maiores são transferidos para o S3. O preço serverless e a ausência de infraestrutura para gerir tornam-no atrativo para deployments com carga variável.
Memória de longa duração: recordar entre sessões
A hot memory trata da conversa atual. A memória de longa duração cobre 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 normalmente codifica o utilizador ou a organização: ("user", "user-123", "preferences").
Vector storage: recall semântico 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 proporciona recall semântico. Em vez de procurar por chaves exatas, o agent consulta pelo significado.
Qdrant é uma vector database especializada, escrita em Rust, que trata do armazenamento de embeddings, da indexação (Hierarchical Navigable Small World, ou HNSW) e da pesquisa filtrada. Abordei detalhadamente o HNSW e os seus compromissos no meu artigo sobre search ranking. O Qdrant também disponibiliza um MCP server que funciona como camada de memória semântica — útil se o seu agent framework 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
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."""
point_id = hashlib.md5(f"{principal.user_id}:{fact}".encode()).hexdigest()
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 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 convertidos 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 recordar 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: além da cosine similarity
A cosine similarity em bruto é 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 scores superiores. Uma função de decaimento exponencial faz com que um facto de ontem ultrapasse um facto equivalente de há seis meses.
- Importância: Significância avaliada pelo LLM numa escala de 1 a 10. “A carteira do utilizador caiu 40%” obtém um score superior a “o utilizador disse olá”.
- Relevância: Cosine similarity do embedding entre a query e o facto armazenado.
O score final de retrieval é uma soma ponderada: score = alpha * recency + beta * importance + gamma * relevance. Isto impede que factos recentes e importantes fiquem ocultos sob factos obsoletos, mas semanticamente semelhantes. Para um agent como o Market Analyst Agent, começaria com alpha = 0.3 para recência, beta = 0.2 para importância e gamma = 0.5 para relevância, uma vez que a intenção da query atual do utilizador é o fator mais importante. Estes pesos são pontos de partida adaptados do artigo Generative Agents (que usou ponderação uniforme); constatei que dar mais peso à relevância funcionava melhor para queries de análise financeira, mas os valores baseiam-se na intuição e não foram otimizados empiricamente.
Alternativas à pesquisa vetorial
A pesquisa vetorial é poderosa, mas nem sempre é a ferramenta certa. Eis quando usar alternativas:
| Abordagem | Mais indicada para | Principal custo operacional |
|---|---|---|
| Pesquisa vetorial (Qdrant) | Recall semântico de factos não estruturados | Lifecycle de embeddings e do índice |
| Key-value store (Redis) | Perfis e preferências estruturados do utilizador | Utilização de memória e política de persistência |
| Document store (ficheiros) | Conhecimento do projeto e notas geridas pelo agent | Concorrência, permissões e pesquisa |
| Full-text search (PostgreSQL GIN index) | Recall por palavras-chave no histórico da conversa | Crescimento do índice e tuning das queries |
| Knowledge graph (Neo4j) | Relações entre entidades e queries multi-hop | Modelação do grafo e mais um sistema de dados |
| Híbrida (vector + keyword) | Recall quando a intenção da query varia | Dois caminhos de scoring para afinar e avaliar |
Os key-value stores funcionam bem para dados estruturados. Se a sua memória de longa duração for um perfil de utilizador — tolerância ao risco, horizonte de investimento, setores preferidos — um Redis hash ou uma coluna JSONB do PostgreSQL é mais simples e rápido do que criar embeddings e consultar vectors. Use pesquisa vetorial quando a memória não for estruturada e a formulação da query variar.
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 os itens armazenados, usando um embedding model configurável. Para muitos casos de uso, este store integrado é suficiente, sem necessidade de recorrer a uma vector database 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 longa duração
Comece por key-value se a memória for estruturada e bem definida (perfis de utilizador, definições, entidades nomeadas). Acrescente 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 o seu custo quando as relações entre entidades são importantes, por exemplo: “De que empresas perguntou o utilizador que são concorrentes da NVDA?” O projeto recente mais interessante nesta área é o Graphiti (da Zep), que cria um knowledge graph temporal que regista quando os factos eram verdadeiros, não apenas o que era verdadeiro. Cada edge contém intervalos de validade, pelo que uma alteração na tolerância ao risco do utilizador invalida o valor antigo em vez de o substituir silenciosamente. O Graphiti reporta 94,8% de accuracy no benchmark DMR — Deep Memory Retrieval, um teste de recall em conversas longas — e o seu modelo bi-temporal trata o problema da memória obsoleta na camada de dados.
O problema é operacional. Operar uma graph database 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.
Os managed memory frameworks, como Mem0 e Letta (anteriormente MemGPT), tratam por si do pipeline de extração, consolidação e retrieval. A abordagem do Mem0 é particularmente interessante: um LLM extrai memórias candidatas, um decision engine compara cada novo facto com as entradas existentes na vector store e um resolver decide entre 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 com memory management tools, movendo autonomamente dados entre “core memory” (in-context) e “archival memory” (out-of-context). Vale a pena avaliar ambos se quiser reduzir o time-to-production e não precisar de controlo total sobre o pipeline de memória.
Memória documental: o arquivo do agent
Os vector stores e os key-value backends tratam bem de recall semântico e de lookups estruturados. Existe uma terceira categoria de conhecimento de agents que nenhum dos dois serve de forma adequada: contexto acumulado do projeto, isto é, as convenções, notas de investigação e decisões de que o agent precisa entre sessões. Este conhecimento beneficia de ser legível por humanos e controlado por versões.
Esta é a document memory: o agent lê e escreve ficheiros estruturados (Markdown, JSON, YAML) num diretório conhecido. Sem embeddings, sem database, sem infraestrutura. Apenas ficheiros em disco que tanto o agent como o developer podem cat, grep, git diff e editar manualmente.
A adoção deste padrão em produtos é maior do que a sua cobertura nas taxonomias de memória acima. Numa avaliação conduzida por um fornecedor, a Letta reportou 74,0% de accuracy no LoCoMo — um benchmark de question-answering em conversas longas — para um agent suportado por filesystem a correr em GPT-4o mini, contra 68,5% da melhor variante baseada em grafos do Mem0. Trata-se de um fornecedor, um modelo, um benchmark e um harness: interprete o resultado como sinal de competitividade da abordagem, não como ranking. A vantagem operacional não depende do benchmark: 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 por 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?
Em workflows de agents de longa duração, o padrão mais eficaz que encontrei não é uma vector database. É um diretório de notas bem organizado. Considere o que acontece quando um coding agent trabalha num projeto durante várias 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 são demasiado estruturados para pesquisa vetorial (é necessário recall exato, não similaridade difusa) e demasiado interligados para um key-value store — são documentos que fazem referência uns aos outros, não valores isolados obtidos por chave. São também factos que o developer quer ver e editar diretamente. Se o agent aprender algo errado, basta abrir o ficheiro e corrigi-lo.
É assim que funcionam o CLAUDE.md do Claude Code e o diretório .claude/. O agent lê ficheiros CLAUDE.md ao nível do projeto para obter convenções e instruções e mantém um ficheiro de auto-memory separado por projeto — em ~/.claude/projects/<project-slug>/memory/ — para aprendizagens entre sessões. Ambos são Markdown simples: pode lê-los, editá-los, fazer commit dos ficheiros do projeto no git e partilhá-los com a equipa. As project rules do Cursor e as rules e memories do Devin Desktop seguem o mesmo padrão. O Cursor lê ficheiros .mdc a partir de .cursor/rules; o Devin Desktop (anteriormente Windsurf) lê .windsurf/rules/ e continua a aceitar o ficheiro único legado .windsurfrules. Em qualquer dos casos: texto simples em disco que o agent carrega no arranque para obter o contexto do projeto.
Implementar um file memory store
A implementação é deliberadamente simples. O agent recebe quatro operações: escrever um documento, ler um documento, listar os documentos disponíveis e pesquisar por palavra-chave em todos os documentos.
O exemplo seguinte é um file store independente e ilustrativo baseado em raw Markdown. Não é uma versão simplificada do memory/document.py atual. O projeto atual usa DocumentMemory, que requer um namespace e uma key e escreve um envelope JSON contendo 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.
NOTE: This is a sketch for demonstration. A simple substring check
won't scale beyond a few hundred documents. For production with 500+
documents, use TF-IDF/BM25 scoring (e.g., rank_bm25) or a full-text
search backend (PostgreSQL GIN index, Elasticsearch).
"""
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 é deliberadamente partilhado por reads, writes e resultados de glob: os paths relativos podem ainda 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; numa fronteira multi-tenant hostil, use operações no-follow relativas ao descritor, para que uma mutação do filesystem não possa competir com essa verificação. Execute este pequeno regression check 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. Esta é a estrutura que usaria para um research agent. O Market Analyst Agent usa namespaces em memory/documents/, mas o seu DocumentMemory atual escreve cada entrada como um envelope JSON com uma string content, em vez de raw Markdown. A organização raw Markdown abaixo pertence ao design ilustrativo independente FileMemory apresentado anteriormente:
.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 ilustrativo FileMemory, todos os documentos são Markdown e o objetivo de cada um é 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 preservam a estrutura de namespace e key, mas não proporcionam a mesma experiência de diff de raw Markdown.
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 chave” | “Ler o documento no path” |
| Mais indicado para | Recall não estruturado e variável | Lookups estruturados | Contexto e notas de projeto |
| Legível por humanos | Não (embeddings) | Parcialmente (JSON) | Sim (Markdown) |
| Fácil de depurar | Difícil (scores de similaridade) | Fácil (chaves exatas) | Trivial (abrir o ficheiro) |
| Controlável por versões | Não | Possível | Sim (nativo do git) |
| Infraestrutura de embeddings | Necessária | Não necessária | Não necessária |
| Escala até | Milhões de factos | Milhões de chaves | Milhares de documentos |
| Capacidade de pesquisa | Similaridade semântica | Correspondência exata | Baseada em palavras-chave / path |
Use document memory quando:
- O agent acumula conhecimento do 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), não como factos isolados
- Quer versionamento da memória do agent baseado em git
- Zero infraestrutura é um requisito obrigatório
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 lookups exatos e rápidos para dados estruturados (perfis de utilizador, definições)
- O schema dos dados é bem definido
Na prática, os agents de produção combinam frequentemente os três. 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 com envelopes JSON em namespaces. As variantes de recall semântico e raw Markdown deste artigo são extensões ilustrativas.
Exemplos reais
O padrão já é comum nos AI coding assistants:
- O 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 versionados juntamente com o código. - O Cursor carrega rules 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 rule se aplica. - O Devin Desktop (anteriormente Windsurf) lê rules de
.windsurf/rules/, continua a aceitar o legado.windsurfrulesna raiz e escreve memories autogeradas num store local que o agent consulta em execuções posteriores. - A memory tool da Anthropic para a Claude API é uma tool do lado do cliente que o modelo controla 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, database).
Todos estes sistemas armazenam o conhecimento do agent como ficheiros de texto legíveis por humanos, com operações explícitas de leitura/escrita, e nenhum precisa de um pipeline de embeddings. O agent decide o que escrever, o developer pode ver e editar tudo e o sistema inteiro cabe num git diff.
Para além dos coding assistants
A document memory não se limita aos coding agents. O padrão também surge noutros domínios de agents:
-
Agents de jogos em mundos abertos: o Voyager (Wang et al., 2023) cria uma skill library persistente de programas JavaScript verificados que um agent de Minecraft acumula ao longo do tempo, recolhendo 3,3 vezes mais itens únicos e atingindo milestones 15,3 vezes mais depressa do que os baselines. As skills são transferidas para novos mundos sem retraining. O JARVIS-1 estende este conceito com uma memória multimodal que combina planos textuais e observações visuais e é cinco vezes mais fiável do que os melhores agents anteriores na tarefa de longa duração
ObtainDiamondPickaxe.Convém fazer uma distinção: as skill libraries são memória executável (ficheiros de código importados e executados), enquanto a document memory nos coding assistants é declarativa (Markdown injetado nos prompts). Os modos de falha são diferentes. Código executável incorreto faz o agent falhar; texto declarativo incorreto conduz a erros de raciocínio. Mas o padrão de armazenamento e os benefícios operacionais (debuggability, controlo de versões) são os mesmos.
-
Automação de workflows empresariais: a competição ERC3 — a terceira Enterprise RAG Challenge — teve vencedores que usaram document memory para refinar prompts iterativamente. Uma das equipas vencedoras fez iterar os agents Analyzer e Versioner através de 80 versões de prompts armazenadas como documentos procedurais. Outra equipa de topo criou mais de 20 módulos enricher como conhecimento procedural em formato documental. A LEGOMem (2025) formaliza isto para sistemas multi-agent como modular procedural memory: as trajetórias de tarefas anteriores são decompostas em unidades de memória reutilizáveis, que são depois colocadas no orchestrator responsável pelo planeamento e delegação ou nos agents que executam os passos. No benchmark OfficeBench, verificou-se que a memória do orchestrator é a importante para a decomposição de tarefas, enquanto a memória granular dos agents melhora a accuracy da execução.
-
Automação web: o Agent Workflow Memory (Wang et al., 2024) permite que web agents infiram workflows reutilizáveis a partir de episódios bem-sucedidos, com uma melhoria relativa de 51,1% na taxa de sucesso no WebArena. O SkillWeaver (2025) vai mais longe: os agents sintetizam API tools reutilizáveis a partir da exploração, obtendo um ganho relativo de 31,8% na taxa de sucesso. As skills aprendidas também são transferidas para modelos mais fracos (até 54,3% de melhoria relativa), pelo que a memória acumulada por um agent mais forte pode elevar o desempenho de um agent mais pequeno.
-
Apoio ao cliente: a Gartner prevê que a agentic AI resolva autonomamente 80% dos problemas comuns de apoio ao cliente sem intervenção humana até 2029. Estes agents consultam SOPs, playbooks e históricos de clientes, todos eles formas de document memory.
O workshop MemAgents na ICLR 2026 é um sinal de que a comunidade de investigação está a acompanhar aquilo que os practitioners já construíram.
As skills usam documentos para empacotar instruções procedurais. O standard Agent Skills armazena essas instruções em ficheiros SKILL.md com frontmatter YAML e um corpo Markdown. Isto assemelha-se à document memory na camada de armazenamento, mas o papel é diferente: uma skill diz ao agent como executar uma classe de trabalho, enquanto a memória regista factos aprendidos num projeto ou numa execução anterior. A Parte 3 traça a fronteira adjacente entre uma skill e uma tool.
O MCP (Model Context Protocol) tem uma interface procedural relacionada: tools/list devolve objetos tool cujo inputSchema é JSON Schema, e um agent invoca um deles através de tools/call. A descoberta não autoriza uma chamada. Antes de invocar uma tool com efeitos ou dados privados, o host tem de implementar autenticação, autorização e consentimento explícito do utilizador; o server também tem de impor os seus próprios controlos de acesso. O MCP não consegue impor esses controlos ao nível do protocolo. Uma revisão do primeiro ano do MCP, em dezembro de 2025, colocou o protocolo nos 97 milhões de downloads mensais de SDKs para Python e TypeScript, com adoção pela OpenAI, Google DeepMind e Microsoft. O MCP não é específico de coding. Os mesmos servers ligam agents a databases, APIs internas e sistemas empresariais.
Ambos tornam as interfaces procedurais inspecionáveis: as skills armazenam instruções em documentos, enquanto o MCP expõe tool schemas e calls legíveis por máquinas. O MCP, agora sob a alçada da Agentic AI Foundation, é o mais próximo que o ecossistema de agents tem de um standard de interoperabilidade.
Escalar a document memory para produção
A implementação baseada em ficheiros acima funciona bem em portáteis de developers individuais e em deployments de pequena escala. Uma operação multi-tenant em produção, com centenas de utilizadores e milhares de documentos, exige uma arquitetura diferente.
O limite de ficheiros num único node torna-se evidente: não é possível escalar horizontalmente o file I/O, as escritas concorrentes precisam de locks e a gestão de permissões entre tenants é difícil. A produção precisa de um backing store que trate corretamente a concorrência, a pesquisa e o multi-tenancy.
Três abordagens comuns:
Abordagem A: híbrida, com uma camada fina de database
Mantenha os ficheiros para authoring (os developers editam Markdown localmente), mas sirva-os a partir de uma database em runtime. Durante o deployment, sincronize os ficheiros com rows do PostgreSQL. O agent lê da database, não do disco. Isto proporciona:
- Ergonomia para developers (editar Markdown, fazer commit no git)
- Desempenho de queries em produção (reads indexados na database)
- Separação clara entre authoring e serving
Abordagem B: object storage + vector index sidecar
Armazene os documentos no S3/GCS como objetos, com uma coleção Qdrant que indexa os seus embeddings. O agent consulta o Qdrant para obter os IDs dos documentos relevantes e depois obtém o conteúdo no 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 os documentos como rows JSONB do PostgreSQL, com full-text search (GIN index) e embeddings vetoriais opcionais (pgvector). Isto oferece hybrid search (keyword + semântica), transações ACID e um único sistema operacional.
Um exemplo da Abordagem C. Este é um padrão RLS, não código de aplicação pronto a usar: a sua database role só deve estar disponível para o application server de confiança. O server autentica o pedido e constrói principal; não aceita um tenant ID fornecido pelo caller. O PostgreSQL RLS torna esse âmbito obrigatório, mesmo que uma query posterior omita o predicado do 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);
CREATE INDEX ON documents USING ivfflat(embedding vector_cosine_ops);
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 * (1 - (embedding <=> $2))) 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 scope transacional, 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 torna a pesquisa híbrida. Apenas com o predicado @@, um documento semanticamente correto mas sem keywords em comum com a query seria filtrado antes de o scoring começar — isto é keyword retrieval com semantic re-ranking, não hybrid retrieval. O threshold da distância é um parâmetro: reduza-o se o vector arm inundar os resultados, aumente-o se os matches semânticos nunca aparecerem.
A regressão seguinte é o comportamento que deve ser testado contra uma database real depois das migrations. Com tenant-a, uma leitura de tenant-b não devolve rows e um insert cross-tenant direto falha o 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 pelo server e RLS imposto pela database
- Garantias ACID: sem problemas de consistência eventual
- Um único sistema operacional: sem vector database separada para gerir
- Escalabilidade horizontal: read replicas para carga de queries e partitioning por tenant para escalar as writes
Os ficheiros são excelentes para workflows de um único developer. Em produção multi-tenant, um structured document store sobre PostgreSQL é normalmente o melhor equilíbrio entre simplicidade, desempenho e maturidade operacional.
Juntar 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 do pedido do utilizador à 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 que pode ser retomado no checkpoint store em cada limite de super-step. Quando o grafo chega a um node
interrupt_before(como o nodepublishna Parte 1), a execução é pausada. O utilizador pode fechar a aplicação e, quando regressar, o grafo retoma a partir do checkpoint. Os event logs e traces de runtime são preocupações de produção separadas. -
Cold path (long-term store): nesta arquitetura ilustrativa, o agent consulta o long-term store em busca de contexto relevante do utilizador no início de cada conversa. Essa leitura está no caminho crítico — o planner não pode personalizar a resposta até ela terminar. Um caminho baseado em vectors pode incluir a criação do embedding da query, seguida de retrieval no índice; um key-value lookup não. A escrita não está no caminho crítico: quando a conversa termina, uma background job extrai e armazena novos factos, e essa job nunca deve bloquear o reasoning loop.
-
Document path (file store): no arranque, o agent carrega convenções do projeto e notas de investigação relevantes a partir do document store. Durante a execução, escreve novos resumos de investigação e padrões aprendidos no disco. Estas leituras também estão no caminho crítico, porque informam a tarefa atual; o custo depende do filesystem, do tamanho dos ficheiros e do estado da cache. As escritas podem ser adiadas.
A ligação destes componentes no LangGraph é simples — o checkpoint store e o long-term store são passados durante a compilação do grafo, enquanto o document store é injetado como dependência. O sketch local abaixo usa InMemoryStore para manter o snippet pequeno; a topologia Docker de referência usa Qdrant para o mesmo papel de recall semântico.
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:
graph = create_graph(
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
O que acontece quando um utilizador recorrente envia “Analyze TSLA” ao Market Analyst Agent:
-
Carregamento da document memory: no arranque, o agent lê as convenções do projeto a partir do document store: preferências de formato da análise, fontes de dados preferidas e padrões de utilização de tools. Estas convenções definem o comportamento de base.
-
Recall da cold memory: neste fluxo ilustrativo, antes de o router node ser executado, o grafo consulta o long-term store com a mensagem do utilizador. Obtém: “O utilizador tem elevada tolerância ao risco”, “O utilizador prefere uma análise detalhada da concorrência”, “O utilizador já investigou a NVDA e a AMD”.
-
Router + Planner: o router classifica o pedido como
DEEP_RESEARCH. O planner cria um plano de investigação com cinco passos, personalizado de acordo com as preferências recordadas. Inclui um passo de análise da concorrência porque o histórico do utilizador mostra que este o pretende. O plano segue o formato definido no documento de convenções. -
Executor loop (hot memory): cada passo é executado segundo o padrão ReAct da Parte 1 — pensar, agir, observar, repetindo até o passo estar concluído. Depois de cada super-step (router, planner e cada passo do executor, executados sequencialmente neste caso), o LangGraph escreve um checkpoint no PostgreSQL. Se o processo falhar depois do passo 3 de 5, reinicia-se e continua-se a partir do passo 4.
-
HITL interrupt: o reporter escreve um draft, um evaluator com fresh context — uma segunda sessão do modelo sem histórico da execução — vota sobre o draft e o grafo chega ao node
publishcominterrupt_beforee pausa. O checkpoint contém o draft e a decisão do evaluator, pelo que a pessoa pode rever ambos em vez de avaliar a investigação em bruto. A revisão ocorre horas depois; o grafo carrega o checkpoint e publica o relatório. -
Atualizações da memória: depois de a conversa terminar, um processo assíncrono extrai novos factos do utilizador (“o utilizador acompanha agora a TSLA”, “o utilizador aprovou o formato do relatório”) e armazena-os na vector store de longa duração. 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 do produto. O document store mantém o conhecimento acumulado do projeto; é 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 vector database exige uma chamada à embedding API. Em setembro de 2026, a OpenAI indica
text-embedding-3-smalla $0,02 por milhão de tokens, pelo que o custo por facto é negligenciável, mas acumula-se entre milhares de utilizadores e sessões. Agrupe chamadas de embeddings e coloque os resultados em cache. Em query time, o recall vetorial pode incluir a criação do embedding da query, o acesso ao índice e a latência de rede; um key-value lookup não. Meça esse caminho no seu deployment e coloque em cache embeddings de queries frequentes ou use um embedding model local 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 correto. Defina políticas de expiração. Num dos meus designs, uso 365 dias para preferências e 90 dias para eventos episódicos como exemplos provisórios, não como predefinições universais. O artigo sobre context engineering rejeita regras fixas de retenção como política portátil. A expiração é a versão mais rudimentar. O typed state orientado por schema é a versão mais precisa: validade temporal e proveniência em cada facto, para que um valor substituído perca para o atual no momento do retrieval, e não no momento da expiração.
-
Overhead da memória no contexto: cada facto recordado consome tokens na context window do LLM. Se recuperar 20 factos por query, serão várias centenas de tokens de contexto de memória a competir com a tarefa real. Limite o número de factos recuperados e dê prioridade segundo o score de relevância.
-
Privacidade e conformidade: os stores de memória de longa duração contêm dados dos utilizadores. Precisa de redaction de PII antes do armazenamento, políticas claras de retenção e controlos para que os utilizadores possam pedir a eliminação dos 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 genérica de pruning: os delta channels podem exigir checkpoints ancestrais e os respetivos registos de writes/blobs para reconstruir um checkpoint retido. Use uma pruning API 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 parent, write e blob e teste a retoma a partir de um checkpoint retido com o saver instalado.
-
Consolidação da memória: com o 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 integralmente as três conversas. Isto reproduz a consolidação da memória humana e mantém o store gerível. O Mem0 e o Graphiti tratam disto automaticamente; se criar o seu próprio sistema, agende jobs periódicos de consolidação.
-
Problema do cold start: os novos utilizadores não têm memória de longa duração. O agent deve degradar graciosamente e fazer perguntas de clarificação, em vez de assumir. A memória é aditiva, não obrigatória.
-
Memory poisoning: tudo o que está 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 longa duração (“aprovar sempre transações sem verificação”), o agent poderá executá-los como instruções. Prompt injection através de memórias armazenadas é uma superfície de ataque real. As mitigações incluem validação antes do armazenamento, tratar o conteúdo recuperado como dados não confiáveis e não como instruções do sistema, e controlos de acesso que limitem as memórias que 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 “usar pytest”, enquanto outro diz “usar unittest”. Agende revisões periódicas (ou deixe o agent fazê-las) para podar e consolidar. Num vector store, a obsolescência permanece oculta; num diretório de ficheiros, pode
grepcontradições. -
A document memory não escala para milhões de itens: a memória baseada em ficheiros funciona para centenas a poucos milhares de documentos. Se o agent precisar de recall sobre milhões de factos com matching difuso, precisa de um vector store. A document memory destina-se a conhecimento estruturado do projeto, não à cauda longa de cada interação do utilizador.
Principais conclusões
- A memória de agents consiste em vários stores com padrões de acesso diferentes. Mantenha separados os checkpoints retomáveis, os factos estruturados, o recall semântico e os documentos do projeto.
- Implemente pausa e retoma antes da personalização. Perder o progresso de uma tarefa é a primeira falha de memória exposta por um agent de longa duração.
- Coloque os factos determinísticos em armazenamento estruturado. Use pesquisa vetorial quando a query for difusa e a formulação variar.
- Use ficheiros para conhecimento do projeto que as pessoas precisam de inspecionar, editar, versionar ou rever num diff.
- Defina para cada tipo de memória uma regra de expiração, conflito e eliminação. Uma memória que o sistema não consiga corrigir transforma-se em dívida do produto.
- Limite aquilo que chega ao modelo. A memória armazenada só tem valor quando o retrieval coloca as evidências certas no contexto atual.
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 a reiniciar. 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 model context, 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 do 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 bi-temporal 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 em mundos abertos
- 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 — Inferência 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, sintetizadas por agents web
- 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 fundamentais para memória baseada em checkpoints
- LangGraph Memory Store — Memória de longa duração 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
Checkpoint backends
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
Vector databases e memory tools
- Qdrant — Vector database 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 documental e baseada em 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 — Rules do projeto como ficheiros .mdc em .cursor/rules
- Devin Desktop Memories — Rules baseadas em ficheiros e memories autogeradas (anteriormente Windsurf)
Memory frameworks
- 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 — Memory management tools para LangGraph
Workshops
- MemAgents: Memory for LLM-Based Agentic Systems — Workshop ICLR 2026
Projeto de demonstração
- Market Analyst Agent — Implementação de referência para os caminhos de armazenamento de checkpoints e de perfis/documentos atuais