Sistem Memori Agen: Memberi Konteks Persisten pada AI Anda
Setiap agen AI memiliki masalah memori. Setiap panggilan ke model bersifat stateless. Model tidak mengingat permintaan terakhir. Tidak mengingat nama pengguna, preferensi, atau keputusan sebelumnya. Tanpa memori eksternal, agen Anda mulai dari nol di setiap giliran.
Panduan ini mencakup empat strategi memori — dari yang paling sederhana hingga paling kuat — dan kapan menggunakan masing-masing. Semua contoh menggunakan TypeScript dan Anthropic SDK.
Mengapa Agen Memerlukan Memori
Pertimbangkan agen code review. Pada run pertama, pengguna menjelaskan konvensi tim mereka: tidak ada tipe any, selalu tangani kesalahan secara eksplisit, preferensikan gaya fungsional. Agen menghasilkan review yang baik.
Pada run kedua, pengguna mengirimkan file lain. Agen telah melupakan segalanya. Ia melewatkan pelanggaran konvensi yang sama yang ditandainya kemarin.
Ini adalah masalah inti: agen bersifat stateless, tapi agen yang berguna memerlukan kesinambungan.
Memori memberi agen tiga kemampuan:
- Pemanggilan ulang — mengambil fakta dari sebelumnya dalam percakapan atau dari sesi sebelumnya
- Personalisasi — beradaptasi dengan preferensi pengguna dari waktu ke waktu
- Koordinasi — dalam sistem multi-agen, berbagi status antar agen
Empat Jenis Memori
Sebelum menulis kode, ada baiknya menamai strategi dengan jelas. Memori agen terbagi dalam empat pola:
| Jenis | Di mana disimpan | Ruang lingkup | Terbaik untuk |
|---|---|---|---|
| Buffer | Dalam konteks (array pesan) | Sesi saat ini | Percakapan pendek |
| Ringkasan | Dalam konteks (dikompres) | Sesi saat ini | Percakapan panjang |
| Semantik | Eksternal (vector DB) | Lintas sesi | Pemanggilan pengetahuan |
| Episodik | Eksternal (key-value store) | Lintas sesi | Fakta pengguna, preferensi |
Setiap strategi memiliki trade-off yang berbeda antara kesederhanaan, biaya, dan kemampuan. Pilih minimum yang memecahkan masalah Anda.
Strategi 1: Buffer Percakapan
Strategi paling sederhana adalah meneruskan riwayat percakapan lengkap ke setiap panggilan model. Model melihat setiap pesan sebelumnya dan merespons dengan konteks penuh.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Message { role: "user" | "assistant"; content: string;}
// Buffer menyimpan riwayat percakapan lengkapconst buffer: Message[] = [];
async function chat(userMessage: string): Promise<string> { // Tambahkan pesan pengguna baru ke buffer buffer.push({ role: "user", content: userMessage });
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, // Lewatkan buffer lengkap di setiap panggilan messages: buffer, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Tambahkan balasan asisten ke buffer buffer.push({ role: "assistant", content: assistantMessage });
return assistantMessage;}
// Contoh penggunaanawait chat("My name is Alex and I prefer Python over TypeScript.");await chat("What language should I use for my next project?");// Model mengingat preferensi yang dinyatakan dalam pesan pertamaKapan menggunakannya: Percakapan kurang dari 20 giliran. Chatbot sederhana. Prototipe.
Batasannya: Jendela konteks terbatas. Sesi panjang pada akhirnya melebihi batas token, dan pesan tertua harus dihapus. Model kehilangan konteks awal secara diam-diam, yang menyebabkan perilaku tidak konsisten.
Strategi 2: Ringkasan Bergulir
Ketika percakapan menjadi panjang, rangkum giliran lama alih-alih menghapusnya. Pertahankan ringkasan terkompresi dari apa yang terjadi sebelum jendela geser pesan terbaru.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface ConversationState { summary: string; // Riwayat terkompresi dari giliran lebih lama recentMessages: Array<{ role: "user" | "assistant"; content: string }>; maxRecentTurns: number; // Simpan N giliran terakhir secara verbatim}
const state: ConversationState = { summary: "", recentMessages: [], maxRecentTurns: 6, // 3 giliran pengguna + 3 asisten};
// Kompresi giliran lebih lama ke dalam ringkasan berjalanasync function compressSummary( currentSummary: string, messagesToCompress: Array<{ role: string; content: string }>): Promise<string> { const formatted = messagesToCompress .map((m) => `${m.role.toUpperCase()}: ${m.content}`) .join("\n");
const response = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 512, messages: [ { role: "user", content: `Update the conversation summary by adding the new exchanges below.Return only the updated summary. Keep it under 200 words.
Current summary:${currentSummary || "(none)"}
New exchanges:${formatted}`, }, ], });
return response.content[0].type === "text" ? response.content[0].text : currentSummary;}
async function chat(userMessage: string): Promise<string> { // Ketika buffer melebihi batas, kompres separuh tertua if (state.recentMessages.length >= state.maxRecentTurns * 2) { const toCompress = state.recentMessages.splice(0, state.maxRecentTurns); state.summary = await compressSummary(state.summary, toCompress); }
state.recentMessages.push({ role: "user", content: userMessage });
// Bangun array pesan: ringkasan sistem + giliran verbatim terbaru const messages = [ ...(state.summary ? [ { role: "user" as const, content: `Context from earlier in this conversation:\n${state.summary}`, }, { role: "assistant" as const, content: "Understood. I will use that context.", }, ] : []), ...state.recentMessages, ];
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
state.recentMessages.push({ role: "assistant", content: assistantMessage }); return assistantMessage;}Panggilan ringkasan menggunakan model yang cepat dan murah (Haiku). Percakapan utama menggunakan model yang mampu (Sonnet). Pemisahan ini menjaga biaya tetap rendah sambil mempertahankan kualitas.
Kapan menggunakannya: Sesi lebih dari 20 giliran. Agen dukungan. Agen tugas jangka panjang.
Batasannya: Ringkasan kehilangan detail. Fakta spesifik yang dinyatakan di awal percakapan mungkin dikompresi menjadi pernyataan samar. Untuk pemanggilan ulang fakta spesifik yang tepat, gunakan memori episodik.
Strategi 3: Memori Episodik
Memori episodik menyimpan fakta spesifik tentang pengguna atau sesi dalam key-value store. Agen mengekstrak fakta selama percakapan dan mengambilnya di sesi mendatang.
import Anthropic from "@anthropic-ai/sdk";import fs from "fs/promises";import path from "path";
const client = new Anthropic();
// Dalam produksi, ganti file store ini dengan Redis atau basis dataconst MEMORY_PATH = "./memory.json";
interface EpisodicStore { [userId: string]: Record<string, string>;}
async function loadMemory(): Promise<EpisodicStore> { try { const data = await fs.readFile(MEMORY_PATH, "utf-8"); return JSON.parse(data); } catch { return {}; }}
async function saveMemory(store: EpisodicStore): Promise<void> { await fs.writeFile(MEMORY_PATH, JSON.stringify(store, null, 2));}
// Ekstrak fakta terstruktur dari pertukaran terbaruasync function extractFacts( userMessage: string, assistantReply: string): Promise<Record<string, string>> { const response = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 256, messages: [ { role: "user", content: `Extract any personal facts, preferences, or important decisions from this exchange.Return a JSON object of key-value pairs. Return {} if there is nothing worth remembering.
User: ${userMessage}Assistant: ${assistantReply}`, }, ], });
try { const text = response.content[0].type === "text" ? response.content[0].text : "{}"; const match = text.match(/\{[\s\S]*\}/); return match ? JSON.parse(match[0]) : {}; } catch { return {}; }}
// Format fakta tersimpan sebagai injeksi system promptfunction formatMemory(facts: Record<string, string>): string { const entries = Object.entries(facts); if (entries.length === 0) return ""; return ( "What you know about this user:\n" + entries.map(([k, v]) => `- ${k}: ${v}`).join("\n") );}
async function chat(userId: string, userMessage: string): Promise<string> { const store = await loadMemory(); const userFacts = store[userId] ?? {};
const systemPrompt = formatMemory(userFacts);
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: systemPrompt || undefined, messages: [{ role: "user", content: userMessage }], });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Ekstrak fakta dan pertahankan untuk sesi mendatang const newFacts = await extractFacts(userMessage, assistantMessage); if (Object.keys(newFacts).length > 0) { store[userId] = { ...userFacts, ...newFacts }; await saveMemory(store); }
return assistantMessage;}
// Sesi 1await chat("user-123", "I prefer concise answers. No bullet points unless necessary.");// Sesi 2 (proses berbeda, hari kemudian)await chat("user-123", "Explain how TCP handshakes work.");// Agen mengingat preferensi pemformatan dari sesi 1Kapan menggunakannya: Agen yang menghadap pengguna yang berjalan lintas sesi. Personalisasi. Pelacakan preferensi.
Batasannya: Fakta terakumulasi dari waktu ke waktu. Fakta usang dapat menghasilkan perilaku yang salah (pengguna mengubah bahasa pilihannya; preferensi lama masih ada di store). Tambahkan mekanisme untuk memperbarui atau menghapus fakta yang kedaluwarsa.
Strategi 4: Memori Semantik (Pencarian Vektor)
Memori episodik menyimpan fakta diskret. Memori semantik menyimpan dokumen, kode, atau potongan percakapan yang diindeks berdasarkan makna. Ketika agen memerlukan informasi, ia mencari indeks menggunakan kueri.
Ini adalah fondasi Retrieval-Augmented Generation (RAG).
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Document { id: string; content: string; embedding?: number[];}
// Hasilkan vektor embedding untuk string teksasync function embed(text: string): Promise<number[]> { // Claude tidak mengekspos endpoint embeddings secara langsung. // Gunakan model embedding khusus. Contoh ini menggunakan stub untuk ilustrasi. // Dalam produksi: gunakan text-embedding-3-small (OpenAI), embed-english-v3 (Cohere), // atau model yang di-host sendiri seperti nomic-embed-text. throw new Error("Replace this stub with a real embedding call");}
// Hitung kemiripan kosinus antara dua vektorfunction cosineSimilarity(a: number[], b: number[]): number { const dot = a.reduce((sum, val, i) => sum + val * b[i], 0); const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); return dot / (magA * magB);}
class SemanticMemory { private documents: Document[] = [];
// Indeks dokumen untuk pengambilan nanti async add(id: string, content: string): Promise<void> { const embedding = await embed(content); this.documents.push({ id, content, embedding }); }
// Ambil top-k dokumen paling relevan untuk kueri async search(query: string, topK = 3): Promise<Document[]> { const queryEmbedding = await embed(query); return this.documents .filter((doc) => doc.embedding) .map((doc) => ({ doc, score: cosineSimilarity(queryEmbedding, doc.embedding!), })) .sort((a, b) => b.score - a.score) .slice(0, topK) .map(({ doc }) => doc); }}
const memory = new SemanticMemory();
async function answerWithContext(question: string): Promise<string> { // Ambil dokumen yang relevan const relevant = await memory.search(question); const context = relevant.map((d) => d.content).join("\n\n---\n\n");
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: context ? `Answer the question using the following context:\n\n${context}` : undefined, messages: [{ role: "user", content: question }], });
return response.content[0].type === "text" ? response.content[0].text : "";}
// Contoh: indeks basis pengetahuan dan kueriawait memory.add("doc-1", "The team uses React 18 with the app router pattern.");await memory.add("doc-2", "All API routes must validate input with Zod schemas.");await memory.add("doc-3", "Database queries use Drizzle ORM. Avoid raw SQL.");
const answer = await answerWithContext("How should I write a new API endpoint?");// Agen mengambil doc-2 dan doc-3 dan menghasilkan jawaban yang beralasanUntuk penggunaan produksi, ganti penyimpanan dalam memori dengan basis data vektor yang dibuat khusus. Pgvector (ekstensi PostgreSQL) adalah yang paling sederhana jika Anda sudah menjalankan Postgres. Chroma dan Qdrant adalah opsi mandiri yang bagus.
Kapan menggunakannya: Asisten dokumentasi. Agen pencarian kode. Q&A basis pengetahuan. Apa pun yang memerlukan pemanggilan ulang dari korpus besar.
Menghubungkan Memori ke MCP
Jika Anda mengikuti panduan server MCP, Anda dapat mengekspos memori sebagai sumber daya MCP. Ini membuat lapisan memori Anda dapat diakses oleh klien yang kompatibel MCP mana pun, termasuk Claude Code.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server( { name: "memory-server", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } });
// Alat: simpan faktaserver.setRequestHandler("tools/call", async (request) => { if (request.params.name === "remember") { const { key, value, userId } = request.params.arguments as { key: string; value: string; userId: string; }; await storeEpisodicFact(userId, key, value); return { content: [{ type: "text", text: `Stored: ${key} = ${value}` }] }; }
if (request.params.name === "recall") { const { userId } = request.params.arguments as { userId: string }; const facts = await loadEpisodicFacts(userId); return { content: [{ type: "text", text: JSON.stringify(facts, null, 2) }], }; }});
const transport = new StdioServerTransport();await server.connect(transport);Agen mana pun yang berbicara MCP sekarang dapat memanggil remember dan recall sebagai alat standar. Memori menjadi layanan bersama daripada implementasi per-agen.
Memilih Strategi yang Tepat
Gunakan tabel keputusan ini sebagai titik awal:
| Situasi | Strategi |
|---|---|
| Sesi tunggal, kurang dari 20 giliran | Buffer |
| Sesi tunggal, berjalan lama | Ringkasan |
| Multi-sesi, preferensi pengguna | Episodik |
| Basis pengetahuan besar untuk dikueri | Semantik |
| Semua di atas | Kombinasikan: ringkasan untuk sesi + episodik/semantik lintas sesi |
Mulai dengan buffer. Tambahkan lapisan ringkasan ketika percakapan menjadi panjang. Tambahkan memori episodik ketika pengguna kembali lintas sesi. Tambahkan memori semantik ketika Anda memiliki korpus dokumen untuk diambil.
Jangan tambahkan kompleksitas sebelum Anda membutuhkannya. Buffer percakapan adalah jawaban yang tepat untuk sebagian besar prototipe. Rekayasa berlebihan pada memori di awal menambah biaya, latensi, dan luas permukaan pemeliharaan tanpa manfaat yang proporsional.
Kesimpulan
Memori bukan satu hal — ini adalah tumpukan strategi yang mengatasi masalah berbeda pada ruang lingkup yang berbeda. Buffer menangani sesi saat ini. Ringkasan memperpanjang sesi tersebut. Memori episodik menjembatani sesi. Memori semantik mendasarkan jawaban pada basis pengetahuan.
Pilih lapisan yang memecahkan masalah yang Anda miliki hari ini. Bungkus di balik antarmuka yang bersih sehingga Anda dapat menukar implementasi nanti. Dan jika agen Anda berkomunikasi melalui MCP, ekspos memori sebagai alat bersama — ini membuat setiap agen tetap sederhana sambil memberi sistem secara keseluruhan otak yang persisten.
Artikel Terkait
- Membangun Server MCP Pertama Anda
- Pola Multi-Agen: Orkestrator, Pekerja, dan Pipeline
- Pola Penggunaan Alat: Membangun Antarmuka Agen-Alat yang Andal
- Mengenal Pengembangan Agentik