আগের পোস্টে ভাষা বাছা আর এজেন্টের জন্য আসলে কী কী টুল লাগে তা নিয়ে লিখেছিলাম। যেটাই বেছে নিন, এজেন্ট কয়েক turn চালানোর পরেই প্রথম আসল সমস্যাটা দেখা দেয়: কথোপকথন ক্রমাগত বড় হতে থাকে, প্রতিটা call আগের চেয়ে ধীর আর দামি হয়ে যায়, আর তারপরও এজেন্ট গত সপ্তাহে অন্য একটা সেশনে বলা কিছু ঠিকই ভুলে যায়। এই দুটো আসলে একই নামের দুটো আলাদা সমস্যা।
Short-term memory হলো চলতি কথোপকথন — এই সেশনে যা বলা হয়েছে সবকিছু, প্রতিটা call-এ মডেলকে ফিরিয়ে পাঠানো হয় যাতে তার প্রসঙ্গ থাকে। Long-term memory হলো যা ইচ্ছাকৃতভাবে জমিয়ে রাখা হয়, যাতে সেশন শেষ হয়ে গেলেও সেটা টিকে থাকে আর একদম আলাদা একটা কথোপকথনে পরে আবার ফিরিয়ে আনা যায়। দুটোর যত্ন নেওয়ার ধরন সম্পূর্ণ আলাদা: একটার আকার সীমিত রাখতে হয়, অন্যটার খুঁজে বের করার মতো হতে হয়।
Short-Term Memory মানে সেই চেনা array-ই
এজেন্টের চারটা অংশ নিয়ে পোস্টে এটা প্রথম বলা হয়েছিল: short-term memory মানে প্রতিটা call-এ পাঠানো messages array। এর মধ্যে রহস্যময় কিছু নেই — এটা আক্ষরিক অর্থেই কথোপকথনের ইতিহাস, আর মডেল প্রতিবার পুরোটাই পড়ে, কারণ call-এর মাঝে সে নিজে থেকে কিছুই মনে রাখে না।
স্কেলে গেলে এটাই ভেঙে পড়ে। সেশনে এ পর্যন্ত পাঠানো প্রতিটা মেসেজ প্রতিটা পরের call-এও সাথে যায়: খরচ বাড়ে শুধু চলতি প্রশ্নের সাথে নয়, পুরো কথোপকথনের দৈর্ঘ্যের সাথে, আর যথেষ্ট লম্বা history মডেলের context window-এর সীমায় গিয়ে ঠেকে। একটা সূক্ষ্ম খরচও আছে — অনেক লম্বা history দশ turn আগের কোনো দরকারি বিস্তারিত তথ্যকে বিশাল লেখার মাঝে চাপা দিয়ে ফেলতে পারে, যা মডেলকে বাকি সবকিছুর সাথে তুলনা করেই বুঝতে হয়।
এটা অনেকটা ফোনে কথা বলার সময় একটা নম্বর মাথায় ধরে রাখার মতো, ফোন কেটে দেওয়ার সাথে সাথেই সেটা ভুলে যাওয়া, যদি না আলাদা করে কোথাও লিখে রাখেন। মডেলের short-term memory একইভাবে কাজ করে: এটা ততক্ষণই টিকে থাকে যতক্ষণ array-টা টিকে থাকে, আর array মুছে গেলে বা ছেঁটে ফেলা হলে, ভেতরের সবকিছুই হারিয়ে যায়, দরকারি হোক বা না হোক।
Long-Term Memory পুরোপুরি আলাদা গঠনের জিনিস
Long-term memory মানে message array-এর একটা বড় সংস্করণ না। এটা একটা আলাদা store — একটা ফাইল, database-এর একটা row, একটা vector store — যেখানে এজেন্ট ইচ্ছাকৃতভাবে লেখে, আর বেছে বেছে পড়ে, পুরোটা আবার চালিয়ে না দিয়ে। ফোন নম্বরের উদাহরণেই ফিরি: সেটাই যদি কাজে লাগার মতো মনে হয়, কল শেষ হওয়ার আগেই কেউ সেটা ডায়েরিতে বা কন্টাক্ট লিস্টে টুকে রাখে, যাতে পরের সপ্তাহে দরকার হলে আবার খুঁজে পাওয়া যায়। Long-term memory এজেন্টের জন্য ঠিক সেই ডায়েরির কাজটাই করে।
RAG নিয়ে পোস্টের সাথে মিলিয়ে দেখুন: retrieval augmented generation অন্য কারো লেখা ডকুমেন্ট থেকে প্রাসঙ্গিক অংশ তুলে আনে। Long-term memory ঠিক একই retrieval করে, শুধু লক্ষ্য বদলে যায়: এজেন্ট বা ব্যবহারকারী জমিয়ে রাখার মতো মনে করেছিল এমন তথ্যের দিকে। কয়েকটা নোটের বেশি হয়ে গেলে, এজেন্ট মেমরি মূলত RAG-ই, শুধু বাইরের কোনো ডকুমেন্টের বদলে নিজের দিকেই তাক করা।
দুটোই এক ফাইলে
flowchart TD
U[নতুন ইউজার মেসেজ] --> ST[Short-term: এই কথোপকথনের মেসেজ অ্যারে]
ST -->|বেশি বড় হয়ে গেলে| C[পুরনো অংশ সারাংশ করা, সাম্প্রতিকগুলো রাখা]
C --> ST
U --> LT[Long-term: আগের যেকোনো সেশনের জমানো তথ্য]
LT -->|এই মেসেজের সাথে প্রাসঙ্গিক হলে| R[মিলে যাওয়া তথ্য system prompt-এ যোগ]
ST --> M[মডেল]
R --> MFlow-এর সারসংক্ষেপ: নতুন মেসেজ এলে দুটো memory-ই একসাথে দেখা হয়। short-term array বেশি বড় হয়ে গেলে সংক্ষিপ্ত করা হয়, আর long-term storage-এ প্রাসঙ্গিক কিছু আছে কিনা খোঁজা হয়, যা পেলে তা system prompt-এ যোগ হয়; দুটোই তারপর একসাথে মডেলের কাছে যায়।
npm install @anthropic-ai/sdk দিয়ে SDK ইনস্টল করুন, ANTHROPIC_API_KEY সেট করুন, ফাইলটা agent/memory-agent.ts নামে সেভ করে npx tsx agent/memory-agent.ts চালান।
// agent/memory-agent.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
type Memory = { fact: string; vector: number[] };
const longTerm: Memory[] = [];
function tokenize(text: string): string[] {
return text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
}
function vectorize(text: string, vocabulary: string[]): number[] {
const counts = new Map<string, number>();
for (const token of tokenize(text)) counts.set(token, (counts.get(token) ?? 0) + 1);
return vocabulary.map((word) => counts.get(word) ?? 0);
}
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
if (normA === 0 || normB === 0) return 0;
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
function rememberFact(fact: string): void {
const vocabulary = Array.from(new Set([fact, ...longTerm.map((m) => m.fact)].flatMap(tokenize)));
longTerm.push({ fact, vector: vectorize(fact, vocabulary) });
}
function recallRelevant(query: string, minScore = 0.2): string[] {
if (longTerm.length === 0) return [];
const vocabulary = Array.from(new Set([query, ...longTerm.map((m) => m.fact)].flatMap(tokenize)));
const queryVector = vectorize(query, vocabulary);
return longTerm
.map((m) => ({ fact: m.fact, score: cosineSimilarity(queryVector, vectorize(m.fact, vocabulary)) }))
.filter((m) => m.score >= minScore)
.sort((a, b) => b.score - a.score)
.slice(0, 2)
.map((m) => m.fact);
}
const tools: Anthropic.Tool[] = [
{
name: 'remember',
description: 'Save a fact about the user that should be recalled in future sessions, such as a preference or a standing constraint.',
input_schema: {
type: 'object',
properties: { fact: { type: 'string' } },
required: ['fact'],
},
},
];
const messages: Anthropic.MessageParam[] = [];
const turnBoundaries: number[] = [];
const KEEP_RECENT_TURNS = 2;
const COMPACT_AFTER_TURNS = 4;
async function compactIfNeeded(): Promise<void> {
if (turnBoundaries.length <= COMPACT_AFTER_TURNS) return;
const cutBoundary = turnBoundaries[turnBoundaries.length - KEEP_RECENT_TURNS];
const older = messages.slice(0, cutBoundary);
const recent = messages.slice(cutBoundary);
const summaryResponse = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 200,
system: 'Summarize this conversation in 2-3 sentences, keeping only details that matter for future turns.',
messages: older,
});
const summaryBlock = summaryResponse.content.find((b): b is Anthropic.TextBlock => b.type === 'text');
messages.length = 0;
messages.push({ role: 'user', content: `Earlier in this conversation: ${summaryBlock?.text ?? ''}` });
messages.push({ role: 'assistant', content: 'Got it, continuing from there.' });
messages.push(...recent);
const shift = 2 - cutBoundary;
const kept = turnBoundaries.slice(turnBoundaries.length - KEEP_RECENT_TURNS);
turnBoundaries.length = 0;
turnBoundaries.push(...kept.map((i) => i + shift));
}
async function runAgent(userText: string): Promise<string> {
const relevant = recallRelevant(userText);
turnBoundaries.push(messages.length);
messages.push({ role: 'user', content: userText });
await compactIfNeeded();
for (let step = 0; step < 4; step++) {
const response = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 500,
...(relevant.length > 0 ? { system: `Known about the user from earlier sessions: ${relevant.join('; ')}` } : {}),
tools,
messages,
});
messages.push({ role: 'assistant', content: response.content });
if (response.stop_reason !== 'tool_use') {
const block = response.content.find((b): b is Anthropic.TextBlock => b.type === 'text');
return block?.text ?? '';
}
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === 'tool_use') {
if (block.name === 'remember') {
rememberFact((block.input as { fact: string }).fact);
}
results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify({ ok: true }) });
}
}
messages.push({ role: 'user', content: results });
}
return 'Stopped: step limit reached';
}
runAgent('I only drink green tea, never coffee.').then(console.log);
recallRelevant RAG-এর পোস্টের সেই tokenize-আর-cosine-similarity retriever-টাই আবার ব্যবহার করে, শুধু document array-এর বদলে longTerm-এর দিকে তাক করা। compactIfNeeded হলো short-term দিকটা: কথোপকথন চার turn পার হয়ে গেলে, সাম্প্রতিক দুটো বাদে বাকি সবকিছুর একটা সারাংশ বানায় আর পুরনো মেসেজগুলোর জায়গায় একটামাত্র সাজানো বিনিময় বসিয়ে দেয়, প্রতিটা turn-এর ঠিক সীমানা turnBoundaries-এ রেখে, যাতে compaction কখনো কোনো tool call আর তার ফলাফলের মাঝখানে কেটে না বসে — একটা সত্যিকারের ঝুঁকি, কারণ কোনো turn-এ শুধু প্রশ্ন-উত্তরের চেয়ে বেশি কিছু থাকলে, tool call-এর সাথে মিলে যাওয়া ফলাফল ছাড়া মডেলের API সরাসরি প্রত্যাখ্যান করে।
টেস্ট করতে গিয়ে আসলে যা পেলাম
এটা লেখার আগে ফাইলটা কয়েক turn চালিয়ে দেখলাম: একটাতে এজেন্ট remember tool দিয়ে একটা পছন্দ জমিয়ে রাখল, তারপর কয়েকটা অপ্রাসঙ্গিক বিনিময় দিয়ে জোর করে একটা compaction ঘটালাম, আর শেষে সেই একই পছন্দ নিয়ে আবার প্রশ্ন করলাম। Compaction একদম ঠিকভাবে কাজ করল। Recall করল না।
জমানো তথ্যটা ছিল drinks only green tea, never coffee। পরের প্রশ্নটা ছিল What do I usually drink?। দুটোর মধ্যে একটা শব্দও মেলেনি — প্রশ্নে drink, তথ্যে drinks, আর একটা সাধারণ word-count vector এদের একদম আলাদা দুটো token হিসেবে ধরে। Similarity score এল ঠিক শূন্য, threshold-এর অনেক নিচে, তাই system prompt-এ কিছুই যোগ হলো না, আর এজেন্টের কোনো ধারণাই ছিল না যে তাকে এটা কখনো বলা হয়েছিল।
RAG-এর পোস্টে যে সীমাবদ্ধতা পেয়েছিলাম, এখানেও সেটাই, বরং আরও খারাপ ভাবে: একটা ডকুমেন্ট সাধারণত তার নিজের গুরুত্বপূর্ণ শব্দগুলো কোথাও না কোথাও আবার বলে, কিন্তু একটা memory জমা হয় একবার, তখনকার যা শব্দ মাথায় এসেছিল সেভাবে, আর পরে জিজ্ঞেস করা হয় পুরোপুরি অন্য শব্দে। শুধু mechanism দেখানোর জন্য বানানো একটা toy retriever এই সমস্যায় দ্রুতই পড়ে। এটা আসলে ঠিক করতে হলে RAG-এর পোস্টে যা বলা হয়েছিল সেটাই লাগবে — অর্থ বোঝে এমন একটা আসল embedding model, শুধু মিলে যাওয়া অক্ষর নয়, কারণ Anthropic-এর নিজস্ব কোনো embedding model নেই বলে তাদের ডকুমেন্টেশনই ঠিক এই কাজের জন্য Voyage AI recommend করে। এই একই toy retriever দুটো পোস্টেই ধরা পড়েছে বলেই বুঝলাম, mechanism শেখার জন্য এটা যথেষ্ট, কিন্তু কোনো real memory system-এ এটা হুবহু বসিয়ে দেওয়া ঠিক না — সেখানে ভুলটা নীরবে ঘটে, কোনো error ছাড়াই, শুধু এজেন্ট এমনভাবে উত্তর দেয় যেন আপনি তাকে কখনো কিছু বলেনইনি।
Agent Memory-তে সাধারণ ভুল
- সবকিছুকেই long-term-এ রাখার মতো ধরে নেওয়া। কথোপকথনের বেশিরভাগই শুধু সেই কথোপকথনের ভেতরেই কাজের। কয়েক সপ্তাহ পরেও যা গুরুত্বপূর্ণ থাকবে সেটাই জমান — পছন্দ, শর্ত, সিদ্ধান্ত — চলতি বিনিময়ের খুঁটিনাটি না, যা short-term memory ইতিমধ্যেই ওই কথোপকথনের জন্য ধরে রাখে।
- Save করার সময় duplicate চেক না করা। একই টেস্টে এজেন্ট দুটো আলাদা turn-এ হুবহু একই তথ্য দিয়ে দুইবার
rememberডেকেছিল, আর কোড দুইবারই খুশিমনে সেভ করে ফেলেছিল। একটা আসল সিস্টেমে নতুন entry লেখার আগে কাছাকাছি duplicate আছে কিনা চেক করা উচিত। - নির্দিষ্ট সংখ্যক মেসেজের ভিত্তিতে compact করা, turn-এর সীমানা না দেখে। একটা নির্দিষ্ট সংখ্যক মেসেজ ধরে কাটলে সেটা কোনো tool call আর তার ফলাফলের মাঝখানে গিয়ে পড়তে পারে, যা API সরাসরি প্রত্যাখ্যান করবে। এখানে
turnBoundariesযেভাবে করেছে, সেভাবে turn-এর সীমানা স্পষ্টভাবে ট্র্যাক করুন। - সারাংশ সবকিছু ধরে রাখবে ধরে নেওয়া। ধরে রাখবে না, এটাই তো সারাংশ করার আসল কারণ। যা সত্যিই হারানো চলবে না, সেটা long-term memory-তে স্পষ্ট একটা তথ্য হিসেবে থাকা উচিত, কোনো পুরনো কথোপকথনের সারাংশের ভেতরে টিকে থাকবে এই আশায় নয়।
এরপর
Memory এজেন্টকে জানায় সে কী জানে। সেটা ব্যবহার করে ভরসাযোগ্যভাবে একাধিক ধাপ ধরে একটা লক্ষ্যের দিকে এগোনো, একবারে একটা call-এ react করার বদলে — এটা নিয়ে আলাদা একটা পোস্ট লেখার মতো যথেষ্ট আছে।
প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)
Long-term memory কি আসলে RAG-এরই মতো জিনিস?
Mechanism-এর দিক থেকে, হ্যাঁ, কয়েকটা নোটের বেশি হয়ে গেলে। দুটোই কিছু লেখাকে embed করে, জমানো একটা সংগ্রহের সাথে মিলিয়ে দেখে, আর সবচেয়ে কাছের মিলগুলো তুলে আনে। RAG সেটা তাক করে অন্য কারো লেখা ডকুমেন্টের দিকে; long-term memory ঠিক একই mechanism তাক করে এজেন্ট বা ব্যবহারকারীর জমিয়ে রাখা তথ্যের দিকে।
Long-term memory-তে কী রাখার মতো, সেটা কীভাবে ঠিক করব?
প্রতিটা মেসেজ না — কথোপকথনের বেশিরভাগই শুধু সেই কথোপকথনের ভেতরেই কাজের, যা short-term memory ইতিমধ্যে তার দৈর্ঘ্য পর্যন্ত সামলায়। কয়েক সপ্তাহ পরেও যা সত্যি আর কাজের থাকবে সেটাই জমান: বলা কোনো পছন্দ, স্থায়ী কোনো শর্ত, এমন কোনো সিদ্ধান্ত যা পরের বার আবার ব্যাখ্যা করার দরকার হওয়া উচিত না।
কথোপকথন compact করলে কি গুরুত্বপূর্ণ কিছু হারিয়ে যায় না?
কিছুটা যায়, হ্যাঁ, আর সেটা ইচ্ছাকৃত একটা trade-off, কোনো bug না। সাম্প্রতিক turn-গুলো হুবহু রাখুন যাতে কাছের প্রসঙ্গ নির্ভুল থাকে, আর পুরনোগুলোই শুধু সারাংশ করুন। যা সত্যিই কখনো হারানো চলবে না, সেটা long-term memory-তে একটা স্পষ্ট তথ্য হিসেবে রাখুন, কোনো পুরনো কথোপকথনের সারাংশের ভেতরে কাকতালীয়ভাবে টিকে থাকবে এই ভরসায় না।



কাস্টম ওয়েব অ্যাপ বা এআই অটোমেশন বানাতে চান?
আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (Next.js, React, Node, PostgreSQL) এবং ইন্টেলিজেন্ট এআই এজেন্ট সলিউশন তৈরি করি। আপনার প্রজেক্টের আইডিয়া নিয়ে কথা বলা যাক!
আর্টিকেলটি কি আপনার ভালো লেগেছে?
তন্ময়ের কাজকে সাপোর্ট করতে তালি (Clap) দিয়ে উৎসাহিত করুন!
জাহিদ হাসান তন্ময়
সফটওয়্যার ডেভেলপার
MERN ফুল-স্ট্যাক ডেভেলপার এবং AI এজেন্ট ডেভেলপার, ঢাকা, বাংলাদেশ। ওয়েব ডেভেলপমেন্ট, রিঅ্যাক্ট, লারাভেল এবং আমার লার্নিং জার্নি নিয়ে লিখছি।