আগের পোস্টে tool calling নিয়ে লিখেছিলাম — এজেন্ট কীভাবে একটা ফাংশনের মধ্য দিয়ে বাইরের জগতে হাত বাড়ায়, কাজ করে। এবার একটু শান্ত একটা সমস্যা নিয়ে কথা বলব: এজেন্টের কাছে দরকারি সব tool থাকতে পারে, তবু সে ভুল উত্তর দিতে পারে, শুধু কারণ সে একটা জিনিস জানে না। জানতে পারত না বলে নয় — সেই তথ্যটা কখনো তার training data-তে ছিলই না, বা training শেষ হওয়ার পর বদলে গেছে, বা এমন একটা ডকুমেন্টে আছে যা শুধু আপনার নিজের কোম্পানির কাছেই আছে।
গত সপ্তাহে আপনার টিম যে policy লিখেছে, সেটা নিয়ে মডেলকে জিজ্ঞেস করুন — সে হয় বলবে জানে না, নয়তো আরও খারাপ, পুরো আত্মবিশ্বাসের সাথে আন্দাজ করে বলে দেবে। প্রতিবার কোনো ডকুমেন্ট বদলালেই মডেলকে নতুন করে ট্রেন করা বাস্তবসম্মত না। Retrieval Augmented Generation-এর পুরো উত্তরটাই হলো: ট্রেনিং ছাড়াই এটা সমাধান করা যায়।
RAG আসলে কী?
Retrieval Augmented Generation, বা RAG, এমন একটা পদ্ধতি যেখানে প্রশ্ন করার ঠিক মুহূর্তেই কোনো জ্ঞানভাণ্ডার থেকে প্রাসঙ্গিক লেখা তুলে এনে মডেলের prompt-এ ঢুকিয়ে দেওয়া হয়, উত্তর দেওয়ার আগেই। মডেল শুধু training-এ শেখা জিনিসের ওপর নির্ভর করে না; প্রশ্নের সাথে সাথে তুলে আনা কয়েকটা passage-ও পড়ে, আর সেখান থেকেই উত্তর লেখে। মূল ডকুমেন্ট বদলে দিলেই পরের উত্তর সাথে সাথে সেই বদলটা প্রতিফলিত করে, কোনো নতুন ট্রেনিং লাগে না।
এজেন্টের এটা কেন লাগে
মডেলের জ্ঞান ট্রেনিং শেষ হওয়ার মুহূর্তেই জমে যায়, আর তার ভেতরেও একটা নির্দিষ্ট কোম্পানির প্রতিটা internal document, changelog বা price sheet মুখস্থ রাখা তার পক্ষে সম্ভব না। এই ফাঁক ভরাট করার দুটো পথ আছে। Fine-tuning নতুন তথ্য মডেলের weight-এর ভেতরেই বসিয়ে দেয়, অতিরিক্ত training চালিয়ে — খরচ বেশি, সময় লাগে, আর মূল ডকুমেন্ট একটু বদলালেই আবার করতে হয়। RAG মডেলকে একদম অক্ষত রেখে, প্রশ্নের মুহূর্তেই দরকারি লেখা তুলে আনে। যেসব তথ্য নিয়মিত বদলায় — ডকুমেন্টেশন, policy, দাম, কোডের নোট — সেসবের জন্য RAG প্রায় সবসময়ই সস্তা আর দ্রুত আপডেট করা যায় এমন পথ।
লাইব্রেরির একজন রেফারেন্স লাইব্রেরিয়ানের কথা ভাবুন। তিনি লাইব্রেরির প্রতিটা বইয়ের প্রতিটা পাতা মুখস্থ রাখেন না, আর সেটা রাখারও দরকার নেই। আপনি একটা প্রশ্ন করলে তিনি জানেন কোন তাকে, কোন বইয়ে, এমনকি কোন পাতায় উত্তরটা পাওয়া যাবে; সেই পাতাটা বের করে এনে তারপর তার ভিত্তিতে উত্তর দেন। মডেলও ঠিক এভাবেই কাজ করে যখন RAG ব্যবহার করা হয় — পুরো লাইব্রেরি মাথায় রাখার বদলে, ঠিক জায়গামতো খুঁজে বের করার দক্ষতাটাই আসল।
তিনটা আলাদা অংশ
একটা RAG pipeline-এর তিনটা কাজ থাকে, আর এগুলোকে আলাদা করে ভাবাই ভালো, যদিও কোড একই ফাইলে থাকতে পারে।
Embedding। কোনো লেখাকে একটা vector-এ বদলানো — সংখ্যার একটা তালিকা, এমনভাবে সাজানো যাতে কাছাকাছি অর্থের লেখাগুলো সেই সংখ্যার জগতে একে অন্যের কাছাকাছি থাকে। আপনার ডকুমেন্ট আর আসা প্রশ্ন, দুটোই এই ধাপ পার হয়।
Retrieval। প্রশ্নের vector-টাকে জমানো প্রতিটা document-এর vector-এর সাথে মিলিয়ে দেখা, সবচেয়ে বেশি ব্যবহৃত পদ্ধতি cosine similarity দিয়ে, আর শুধু সবচেয়ে কাছের মিলগুলোই রাখা।
Augmentation। সেই মিলগুলো prompt-এর ভেতরে context হিসেবে বসিয়ে দেওয়া, তারপর মডেলকে শুধু প্রশ্ন নয়, প্রশ্ন আর সেই context দুটো মিলিয়েই উত্তর লিখতে দেওয়া।
flowchart LR
Q[ব্যবহারকারীর প্রশ্ন] --> E[প্রশ্নটাকে একটা vector-এ বদলানো]
E --> S[জমানো প্রতিটা document-এর vector-এর সাথে তুলনা]
S --> K[থ্রেশহোল্ডের ওপরে থাকা সবচেয়ে কাছের মিলগুলো রাখা]
K --> P[সেই chunk-গুলো prompt-এর context হিসেবে যোগ করা]
P --> M[মডেল উত্তর লেখে]Flow-এর সারসংক্ষেপ: প্রশ্নটা একটা vector হয়ে যায়, জমানো প্রতিটা document-এর vector-এর সাথে মেলানো হয়, আর থ্রেশহোল্ডের ওপরে থাকা সবচেয়ে কাছের মিলগুলোই শুধু prompt-এ context হিসেবে যোগ হয়, তারপর মডেল উত্তর লেখে।
আজই চালানো যায় এমন একটা উদাহরণ
Anthropic নিজস্ব কোনো embedding model দেয় না — তাদের নিজেদের ডকুমেন্টেশনই ডেভেলপারদের Voyage AI-কে recommended provider হিসেবে দেখায়। বাস্তব কোনো কাজের জন্য এটাই ঠিক পথ, কিন্তু তার মানে শুধু retrieval দেখানোর জন্যও একটা দ্বিতীয় API key লাগবে। তাই নিচের সংস্করণে আসল embedding model-এর জায়গায় নিজের হাতে বানানো একটা ছোট্ট বিকল্প বসিয়েছি: শব্দ গোনা থেকে vector, cosine similarity দিয়ে তুলনা। বড় স্কেলে এভাবে retrieval করবেন না, কিন্তু ধাপ তিনটা একই থাকে, আর পুরোটাই চলে শুধু একটা Anthropic key দিয়ে।
npm install @anthropic-ai/sdk দিয়ে SDK ইনস্টল করুন, ANTHROPIC_API_KEY সেট করুন, ফাইলটা agent/simple-rag.ts নামে সেভ করে npx tsx agent/simple-rag.ts চালান।
// agent/simple-rag.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const knowledgeBase: string[] = [
'This blog is a Next.js app deployed on Vercel. Pushing to the main branch triggers a new production deployment automatically.',
'Blog posts are stored as JSON files under content/posts and read at build time, so adding a new post requires a redeploy before it appears live.',
'The blog index page uses Incremental Static Regeneration with a 60 second revalidate window.',
'Environment variables such as API keys are set in the Vercel dashboard under Project Settings, Environment Variables, never committed to git.',
'Every pull request gets its own preview deployment with a unique URL, separate from the production domain.',
];
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 retrieve(query: string, topK = 2, minScore = 0.22): string[] {
const vocabulary = Array.from(new Set([query, ...knowledgeBase].flatMap(tokenize)));
const queryVector = vectorize(query, vocabulary);
return knowledgeBase
.map((doc) => ({ doc, score: cosineSimilarity(queryVector, vectorize(doc, vocabulary)) }))
.sort((a, b) => b.score - a.score)
.filter((s) => s.score >= minScore)
.slice(0, topK)
.map((s) => s.doc);
}
async function askWithContext(question: string): Promise<string> {
const context = retrieve(question);
if (context.length === 0) {
return "I don't have anything in the knowledge base about that.";
}
const response = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 512,
system:
'Answer only using the context below. If the context does not contain the answer, say you do not know.\n\nContext:\n' +
context.map((c, i) => `${i + 1}. ${c}`).join('\n'),
messages: [{ role: 'user', content: question }],
});
const text = response.content.find((b): b is Anthropic.TextBlock => b.type === 'text');
return text?.text ?? '';
}
askWithContext('How do new blog posts actually go live on the site?').then(console.log);
tokenize আর vectorize এখানে আসল embedding model-এর জায়গা নিয়েছে। cosineSimilarity আর retrieve ঠিক production সিস্টেমের মতোই retrieval ধাপ করে, শুধু পঞ্চাশ লক্ষের বদলে পাঁচটা string-এর ওপর। askWithContext হলো augmentation ধাপ: retrieve যা খুঁজে পেয়েছে তা দিয়ে একটা system prompt বানায়, তারপরই মডেলকে ডাকে।
যে ভুলটা ধরতে আসলেই টিউনিং লেগেছিল
retrieve-এর প্রথম সংস্করণে কোনো minScore ছিলই না — যা-ই স্কোর হোক, সবসময় সেরা দুটো মিলই ফেরত দিত। ব্লগের deployment-এর সাথে কোনো সম্পর্কই নেই এমন একটা প্রশ্ন দিয়ে টেস্ট করলাম, রান্না নিয়ে। তবুও সে দুটো chunk ফেরত দিল। স্কোর শূন্য হয়নি, কারণ “is”, “the”-এর মতো কিছু সাধারণ শব্দ প্রশ্ন আর ডকুমেন্টের মধ্যে মিলে গিয়েছিল, আর cosine similarity জানে না এই শব্দগুলোর আসলে কোনো অর্থ বহন করে না। মডেল তখন রান্নার প্রশ্নের উত্তর দিল deployment-এর নোট ব্যবহার করে, যেন সেটাই প্রাসঙ্গিক, একদম শান্তভাবে আর আত্মবিশ্বাসের সাথে।
সমাধানটা জটিল কিছু না — একটা সর্বনিম্ন স্কোরের নিচে যা আসে তা বাদ দেওয়া — কিন্তু সংখ্যাটা ঠিক করতে সত্যিকারের টেস্ট লেগেছে, আন্দাজ না। আমার চারটা আসল প্রশ্ন চালিয়ে প্রতিটার স্কোর প্রিন্ট করলাম, আর দেখলাম কোথায় একটা রেখা টানা যায় “সত্যিই সম্পর্কিত” আর “শুধু কয়েকটা সাধারণ শব্দ মিলে যাওয়া”-র মাঝে। এই ছোট্ট জ্ঞানভাণ্ডারের জন্য ০.২২ ঠিক তার মাঝখানে পড়ল: প্রাসঙ্গিক প্রতিটা প্রশ্ন এখনও ঠিক উত্তর পায়, আর রান্নার প্রশ্নটা এখন ঠিকভাবেই কিছু ফেরত দেয় না, দুটো অপ্রাসঙ্গিক chunk-এর বদলে। সংখ্যাটা সার্বজনীন কিছু না — এই নির্দিষ্ট ডেটার জন্য টিউন করা, অন্য কোনো জ্ঞানভাণ্ডার বা অন্য embedding পদ্ধতির জন্য এটা আবার নতুন করে টিউন করতে হবে।
RAG-এ সাধারণ ভুল
- স্কোরের নিচের সীমা ছাড়া নির্দিষ্ট সংখ্যক top-k-তে ভরসা রাখা। কিছুই ঠিকভাবে না মিললেও সেরা দুটো মিল ফেরত দিলে মডেল অপ্রাসঙ্গিক context হাতে পায়, আর সেটা ব্যবহারও করে ফেলে। শুধু rank নয়, স্কোরের ওপর একটা threshold বসান।
- Chunk খুব বড় বা খুব ছোট করা। পুরো একটা পাতা একটা chunk করলে দরকারি বাক্যটা অনেক শব্দের ভিড়ে হারিয়ে যায়; আবার প্রতি বাক্যে একটা chunk করলে বোঝার জন্য দরকারি চারপাশের প্রসঙ্গটাই হারিয়ে যায়। শুরুতে paragraph বা section অনুযায়ী chunk করুন, তারপর কোনটা ভুল retrieve হচ্ছে দেখে সেই অনুযায়ী বদলান।
- কী retrieve হলো তা কখনো ফিরে দেখা। development-এর সময় উত্তরের সাথে সাথে retrieve হওয়া chunk-গুলোও লগ করুন। মডেল ভুল উত্তর দিলে, prompt নিয়ে ঘাঁটাঘাঁটি শুরু করার আগে প্রথম প্রশ্নটা হওয়া উচিত: retrieval কি ভুল লেখা তুলে দিয়েছিল?
- Production-এ আসল embedding model বাদ দেওয়া। RAG-এর গঠনটা শিখতে word-overlap vector দিয়ে চলে যায়। কিন্তু synonym বা অন্যভাবে বলা একই কথা এগুলো পুরোপুরি ধরতে পারে না — “taka” আর “BDT” একটা word-count vector-এর চোখে একেবারেই আলাদা, অসম্পর্কিত। ডকুমেন্টের সংখ্যা বাড়লে আসল একটা embedding model, আর একটা ঠিকঠাক vector store-ই retrieval-কে আসলে ভরসাযোগ্য করে তোলে।
এরপর
Tool calling এজেন্টকে হাত দেয়, আর RAG দেয় training-এর বাইরের জিনিস জানার একটা উপায়। যেটা এখনও বাকি তা হলো একটামাত্র নির্দেশকে এজেন্ট আসলে ধাপে ধাপে অনুসরণ করার মতো একটা পরিকল্পনায় বদলানো — এটা নিয়ে আলাদা একটা পোস্ট লেখার মতো যথেষ্ট আছে।
প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)
RAG আর fine-tuning কি একই জিনিস?
না। Fine-tuning অতিরিক্ত training চালিয়ে মডেলের weight বদলে দেয়, যা আপডেট করতে সময় লাগে, আর মডেলকে নতুন লেখার স্টাইল বা আচরণ শেখানোর জন্য বেশি উপযোগী। RAG মডেলকে অক্ষত রেখে প্রশ্নের মুহূর্তেই প্রাসঙ্গিক লেখা তুলে আনে, যার ফলে মূল তথ্য ঘন ঘন বদলালেও এটা আপডেট রাখা অনেক সস্তা।
Retrieval না করে পুরো ডকুমেন্টটাই তো prompt-এ বসিয়ে দেওয়া যায়, তাহলে কেন করব?
কয়েক পাতার জন্য এটাই সহজ, RAG-এর দরকারই নেই তখন। জ্ঞানভাণ্ডার context window-তে ধরার চেয়ে বড় হয়ে গেলে, বা তার বেশিরভাগই কোনো একটা নির্দিষ্ট প্রশ্নের সাথে অপ্রাসঙ্গিক হলে, তখন retrieval কাজে লাগে — প্রতিবার সবকিছু না পাঠিয়ে শুধু প্রাসঙ্গিক অংশটুকুই পাঠানো যায়।
এটা চেষ্টা করতে কি আসলেই vector database লাগবে?
শুধু গঠনটা শিখতে লাগবে না। একটা array আর একটা similarity function, ওপরের উদাহরণের মতো, retrieval আর generation কীভাবে জোড়া লাগে তা বুঝতে যথেষ্ট। ডকুমেন্টের সংখ্যা memory-তে আরামে ধরার চেয়ে বেড়ে গেলে, বা server restart-এর পরও টিকে থাকতে হলে, তখন আসল embedding model আর একটা ঠিকঠাক vector database-এ যান।



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