Sem memória, todo agente é um estagiário novo a cada conversa. Ele pode ser brilhante por 10 minutos, mas esquece seu gosto, suas regras, seus erros passados e até o que você pediu na mensagem anterior. A diferença entre um chatbot descartável e um agente que vale dinheiro é saber o que lembrar, por quanto tempo e como atualizar o que sabe.
Este guia mostra como construir memória de longo prazo para agentes de IA em produção. Vamos cobrir RAG como memória de referência, Knowledge Graphs como memória estrutural e memória episódica como memória de conversas. Tudo com código Python real, sem abstrações mágicas.
Você vai aprender:
- Por que
context windownão é memória - Os 3 tipos de memória que agentes sérios precisam
- Como combinar RAG, Knowledge Graph e memória episódica
- Código de um agente com memória híbrida
- Armadilhas reais de produção: staleness, privacidade e "alucinação de memória"
O Problema do Agente Amnésico
A maioria dos tutoriais de agentes ensina algo assim: o usuário envia uma mensagem, o LLM responde, fim. O estado dura exatamente o tempo de uma requisição. Se o usuário perguntar "continua", o agente olha para você como se nada tivesse acontecido.
Alguns avançam um passo e mantêm a memória de trabalho — as últimas mensagens da conversa, enviadas de volta ao LLM a cada turno. Isso funciona para diálogos curtos, mas tem limites óbvios:
- Context window limitada: você não pode enviar 10 mil mensagens de volta
- Custo: cada token de contexto custa dinheiro
- Ruido: conversas antigas poluem a tomada de decisão atual
- Aprendizado zero: o agente nunca melhora com o tempo
Memória de trabalho é RAM. Você também precisa de disco.
Um agente em produção precisa lembrar coisas que vão muito além da conversa atual:
- Preferências do usuário ("sempre envie relatórios às segundas")
- Fatos sobre o negócio ("nosso SLA é 4 horas")
- Interações passadas ("na última vez, você preferiu opção B")
- Relacionamentos entre entidades ("o projeto X pertence ao cliente Y")
- Procedimentos aprendidos ("quando o usuário pedir Z, primeiro valide W")
Os 3 Tipos de Memória que Agentes Precisam
Existem dezenas de taxonomias. Na prática, você precisa de três camadas bem separadas:
| Tipo | O que guarda | Quando usar | Tecnologia típica |
|---|---|---|---|
| Semântica / Factual | Fatos, entidades, relacionamentos | "Quem é o cliente? Qual o SLA?" | Knowledge Graph, banco relacional |
| Episódica | Eventos, conversas, interações passadas | "Na última vez, o usuário preferiu B" | Vector DB + sumarização |
| Procedural | Como fazer as coisas, skills, workflows | Sempre que o agente executa uma tarefa | Código, prompts, skills do Hermes |
Além delas, existe a memória de trabalho — o contexto imediato que vive no prompt. Ela é curta, cara e volátil. O objetivo das outras três é alimentar a memória de trabalho com apenas o que importa no momento certo.
Memória semântica: o banco de fatos
A memória semântica guarda conhecimento estruturado sobre o mundo. Não é uma conversa, são fatos:
- Cliente A está no plano Enterprise
- O produto B não integra com o produto C
- O limite de crédito do cliente D é R$ 50 mil
A melhor forma de armazenar isso é um Knowledge Graph — um grafo de entidades e relacionamentos. Diferente de texto solto em um vector DB, um KG permite inferência: se A é dono de B e B depende de C, então problemas em C afetam A.
Memória episódica: o diário do agente
A memória episódica guarda experiências passadas. Cada interação relevante vira um registro:
- Data, usuário, intenção, ação tomada, resultado, feedback
- "Usuário X pediu relatório Y. Enviei às 9h. Ele respondeu que faltou o campo Z."
Isso permite que o agente aprenda com erros e acertos. O desafio é que episódios são volumosos. Você não pode jogar tudo no prompt. A solução é sumarização seletiva + recuperação por similaridade.
Memória procedural: o manual de instruções
A memória procedural é o que o agente sabe fazer. No Hermes Agent, são as skills. Em código puro, são funções, prompts e workflows. Ela é a mais estável e normalmente versionada no Git.
A regra de ouro: procedural no código, semântica no grafo, episódica no vector DB.
RAG: Memória de Curto e Médio Prazo
RAG (Retrieval-Augmented Generation) é a forma mais comum de dar "memória" a um agente. Você joga documentos em um vector DB e, na hora da pergunta, recupera os trechos mais similares.
RAG é ótimo para:
- Documentos grandes que não cabem no context window
- Bases de conhecimento estáticas (manuais, help centers)
- Perguntas factuais sobre texto
RAG é ruim para:
- Informações que mudam com frequência
- Relacionamentos complexos entre entidades
- Memórias que precisam ser atualizadas, não apenas anexadas
RAG não é memória. RAG é consulta a documentos. Se você precisa que o agente aprenda com o tempo, RAG sozinho não resolve.
Ainda assim, RAG é a base da camada episódica. Cada episódio vira um documento vetorizado. Na hora da consulta, você recupera os episódios mais relevantes e os injeta no prompt.
Knowledge Graphs: Memória Estrutural
Knowledge Graphs resolvem o problema que RAG não resolve: relacionamentos. Em um KG, você armazena entidades como nós e relacionamentos como arestas.
(Usuário: Ana) --[TRABALHA_EM]--> (Empresa: Acme)
(Empresa: Acme) --[USA]--> (Produto: Hermes)
(Produto: Hermes) --[TEM_PLANO]--> (Plano: Enterprise)
Com esse grafo, você pode responder perguntas que exigem múltiplos saltos:
- "Qual plano a Ana tem acesso?" → Ana → Acme → Hermes → Enterprise
- "Quem mais usa Hermes Enterprise?" → percorrer arestas reversas
Para agentes, KG é especialmente útil para:
- Modelar permissões e escopo de ação
- Detectar conflitos e dependências
- Manter dados atualizados de forma granular
- Explicar decisões ("você não pode fazer X porque Y depende de Z")
Arquitetura de Memória Híbrida
A arquitetura que recomendo para produção combina as três camadas:
O fluxo é:
- Usuário envia mensagem
- Um router de memória decide quais camadas consultar
- Cada camada retorna trechos relevantes
- Os trechos são combinados, deduplicados e ranqueados
- O resultado vira contexto para o LLM
- A resposta do LLM e a ação tomada são salvas como novo episódio
Código na Prática: Agente com Memória Híbrida
Vamos construir um agente minimalista com memória híbrida. A stack:
- SQLite para persistir episódios e entidades
- NetworkX para o Knowledge Graph
- sentence-transformers para embeddings (pode trocar por OpenAI, Cohere, etc.)
Nota: este código é didático. Em produção, você usaria Chroma, LanceDB, Neo4j ou um servidor MCP de memória. O importante é entender os padrões.
1. Knowledge Graph com NetworkX
import networkx as nx
from typing import List, Tuple
class AgentMemoryGraph:
def __init__(self):
self.g = nx.DiGraph()
def add_fact(self, entity_a: str, relation: str, entity_b: str, metadata: dict = None):
self.g.add_node(entity_a, type=metadata.get("type_a", "entity") if metadata else "entity")
self.g.add_node(entity_b, type=metadata.get("type_b", "entity") if metadata else "entity")
self.g.add_edge(entity_a, entity_b, relation=relation, metadata=metadata or {})
def query(self, entity: str, depth: int = 2) -> List[Tuple[str, str, str]]:
"""Retorna caminhos a partir de uma entidade."""
results = []
if entity not in self.g:
return results
for target in nx.single_source_shortest_path_length(self.g, entity, cutoff=depth):
if target == entity:
continue
paths = list(nx.all_simple_edge_paths(self.g, entity, target, cutoff=depth))
for path in paths[:3]:
edges = []
for u, v in path:
rel = self.g[u][v]["relation"]
edges.append(f"{u} --[{rel}]--> {v}")
results.append(" → ".join(edges))
return results
def update_fact(self, entity_a: str, relation: str, entity_b: str, metadata: dict = None):
"""Atualiza uma aresta existente em vez de duplicar."""
if self.g.has_edge(entity_a, entity_b):
self.g[entity_a][entity_b].update({"relation": relation, "metadata": metadata or {}})
else:
self.add_fact(entity_a, relation, entity_b, metadata)
2. Memória Episódica com SQLite + Embeddings
import sqlite3
import json
import numpy as np
from sentence_transformers import SentenceTransformer
class EpisodicMemory:
def __init__(self, db_path: str = "episodes.db"):
self.conn = sqlite3.connect(db_path)
self._init_table()
self.model = SentenceTransformer("all-MiniLM-L6-v2")
def _init_table(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS episodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
user_id TEXT,
role TEXT,
content TEXT,
action TEXT,
outcome TEXT,
embedding BLOB
)
""")
self.conn.commit()
def add(self, user_id: str, role: str, content: str, action: str = "", outcome: str = ""):
text = f"{role}: {content}. action: {action}. outcome: {outcome}"
embedding = self.model.encode(text).astype(np.float32).tobytes()
self.conn.execute("""
INSERT INTO episodes (user_id, role, content, action, outcome, embedding)
VALUES (?, ?, ?, ?, ?, ?)
""", (user_id, role, content, action, outcome, embedding))
self.conn.commit()
def recall(self, user_id: str, query: str, top_k: int = 3) -> List[dict]:
query_vec = self.model.encode(query).astype(np.float32)
cursor = self.conn.execute(
"SELECT * FROM episodes WHERE user_id = ? ORDER BY id DESC LIMIT 200",
(user_id,)
)
episodes = cursor.fetchall()
scored = []
for ep in episodes:
ep_vec = np.frombuffer(ep[7], dtype=np.float32)
score = float(np.dot(query_vec, ep_vec) / (np.linalg.norm(query_vec) * np.linalg.norm(ep_vec)))
scored.append((score, ep))
scored.sort(reverse=True, key=lambda x: x[0])
return [
{
"timestamp": ep[1],
"role": ep[3],
"content": ep[4],
"action": ep[5],
"outcome": ep[6],
"score": round(score, 3),
}
for score, ep in scored[:top_k]
]
3. Agente com Memória Híbrida
class AgentWithMemory:
def __init__(self):
self.kg = AgentMemoryGraph()
self.episodes = EpisodicMemory()
def remember_fact(self, a: str, relation: str, b: str):
self.kg.update_fact(a, relation, b)
def recall(self, user_id: str, message: str) -> dict:
# Recupera fatos relacionados a entidades mencionadas
# Em produção, use NER para extrair entidades
entities = [word for word in message.split() if len(word) > 4]
facts = []
for entity in entities:
facts.extend(self.kg.query(entity, depth=2))
# Recupera episódios similares
episodes = self.episodes.recall(user_id, message, top_k=3)
return {
"facts": list(set(facts))[:5],
"episodes": episodes,
}
def run(self, user_id: str, message: str, llm_call) -> str:
context = self.recall(user_id, message)
system_prompt = f"""
Você é um agente de atendimento. Use o contexto abaixo para responder.
FATOS RELEVANTES:
{chr(10).join(context['facts'])}
EPISÓDIOS RELEVANTES:
{chr(10).join([f"- {e['timestamp']}: {e['role']} disse '{e['content']}' (ação: {e['action']}, resultado: {e['outcome']})" for e in context['episodes']])}
Responda de forma direta. Se não souber, diga que vai verificar.
"""
response = llm_call(system_prompt, message)
# Salva este turno como episódio
self.episodes.add(
user_id=user_id,
role="user",
content=message,
)
self.episodes.add(
user_id=user_id,
role="assistant",
content=response,
action="respondeu",
outcome="pendente",
)
return response
4. Exemplo de Uso
agent = AgentWithMemory()
# Popula o knowledge graph
agent.remember_fact("Ana", "TRABALHA_EM", "Acme Corp")
agent.remember_fact("Acme Corp", "USA", "Hermes Agent")
agent.remember_fact("Hermes Agent", "TEM_PLANO", "Enterprise")
# Simula uma interação anterior
agent.episodes.add(
user_id="ana@acme.com",
role="user",
content="Quero um relatório de uso semanal sempre às segundas de manhã.",
action="salvou_preferencia",
outcome="confirmado",
)
# Agora pergunta algo que exige memória
response = agent.run(
user_id="ana@acme.com",
message="Qual o meu plano e quando devo receber o relatório?",
llm_call=lambda system, user: f"[MOCK] Ana, seu plano é Enterprise e o relatório chega às segundas."
)
print(response)
O agente consegue responder porque consultou tanto o Knowledge Graph (plano Enterprise) quanto a memória episódica (preferência do relatório).
Sumarização e Esquecimento Controlado
Memória episódica infinita é um problema. Você não pode guardar cada mensagem para sempre. Estratégias de produção:
1. Janela deslizante com sumarização
Mantenha os últimos N episódios completos. Episódios mais antigos são sumarizados em "memórias de alto nível".
def summarize_old_episodes(self, user_id: str):
old = self.get_episodes_before(user_id, days=30)
if not old:
return
summary = self.llm.summarize(old)
self.add_episodic_memory(user_id, "summary", summary)
self.archive_episodes(old)
2. Política de esquecimento
Defina regras de retenção:
- Episódios de erro crítico: nunca esquecer
- Preferências do usuário: reter permanentemente
- Conversas rotineiras: resumir após 30 dias, arquivar após 90
- Dados pessoais sensíveis: apagar sob demanda (LGPD/GDPR)
3. Confirmação antes de gravar
Nem toda interação merece memória. Use um classificador leve para decidir:
def should_remember(self, content: str) -> bool:
"""Classifica se o episódio é digno de memória de longo prazo."""
prompt = f"Este conteúdo deve ser lembrado por um agente no futuro? Responda SIM ou NÃO.\n\n{content}"
return self.llm.classify(prompt) == "SIM"
Integração com Hermes Agent e MCP
No ecossistema Hermes, você pode expor memória como um servidor MCP. Outros agentes conectam a esse servidor e leem/escrevem memória de forma padronizada.
Estrutura sugerida:
hermes/
├── skills/
│ └── memory_search.py # skill que consulta memória
├── mcp-servers/
│ └── memory-server/ # servidor MCP de memória
│ ├── main.py
│ └── memory_store.py
Ferramentas MCP que o servidor pode expor:
memory_search(query, user_id, k)memory_add_fact(entity_a, relation, entity_b)memory_add_episode(user_id, content, action, outcome)memory_forget(user_id, pattern)
Isso permite que vários agentes compartilhem a mesma memória sem acoplamento direto.
Armadilhas de Produção
1. Alucinação de memória
O agente pode confundir fatos. Se o KG tiver dados errados, o agente os propagará com confiança. Solução: versione fatos, exija fontes e permita correção pelo usuário.
2. Staleness (dados velhos)
Um fato atualizado no sistema legado pode não refletir no KG. Solução: sync periódico, TTL em arestas e validação antes de usar fatos críticos.
3. Privacidade e compliance
Memória episódica guarda conversas. Isso pode violar LGPD/GDPR se não for tratado. Solução: anonimização, consentimento, delete sob demanda, retenção limitada.
4. Custo de embeddings
Cada episódio precisa de embedding. Em escala, isso custa caro. Solução: sumarize antes de vetorizar, use modelos locais pequenos, batch de inserções.
5. Ruído no contexto
Recuperar muitos episódios pode poluir o prompt. Solução: rerank, filtragem por relevância e limite rígido de tokens de contexto.
Checklist Antes de Colocar Memória em Produção
- [ ] Definiu quais dados são memória semântica, episódica e procedural
- [ ] Escolheu bancos adequados para cada camada (não tudo no vector DB)
- [ ] Implementou sumarização e política de esquecimento
- [ ] Adicionou rastreabilidade: qual fato veio de onde?
- [ ] Garante anonimização de dados pessoais
- [ ] Tem processo de delete sob demanda (LGPD/GDPR)
- [ ] Mede se a memória melhora ou piora a qualidade das respostas
- [ ] Limita tokens de contexto recuperado
- [ ] Versiona fatos e permite correção
- [ ] Testou recuperação com dados ausentes (fallbacke graceful)
Próximos Passos
Memória é só uma peça da produção. Depois de implementar, você precisa:
- Testar se a memória não introduz regressão (próximo post da série)
- Rotear tarefas para modelos certos sem explodir custo (LLM Gateway)
- Observar o que o agente recupera e por quê
Se você já tem um agente rodando, comece pelo mais simples: memória episódica com sumarização. Ela entrega 80% do valor com 20% da complexidade. Depois, adicione o Knowledge Graph para fatos estruturais.
Regra final: memória boa não é memória infinita. Memória boa é memória relevante.