কাস্টমাইজড RAG সমাধানের বাস্তব উদাহরণ

বাস্তব উদাহরণ দেখায় কীভাবে উপযোগী retrieval-augmented generation সিস্টেম জটিল এন্টারপ্রাইজ জ্ঞান-সমস্যা সমাধান করে.

আজকাল RAG কখনও কখনও বদনাম পায়। কেউ মনে করেন এটি একেবারেই তুচ্ছ বিষয় (শুরু করা সহজ, স্কেলে নেওয়া ততটা নয়), আবার কেউ ভাবেন এটি ‘এজেন্টিক সিস্টেম’-এর কাছে পুরোনো হয়ে গেছে (যদিও অনেক ক্ষেত্রে একটু গভীরে দেখলেই সেগুলো খুব দ্রুত RAG-এর মতোই দেখাতে শুরু করে…).

সাধারণ কিছু চ্যালেঞ্জ আমরা কীভাবে সামলাই, তা দেখাতে এই ব্লগে দু-একটি কাজ করা উদাহরণ দেওয়া হয়েছে, যেমন:

  • মিশ্র টেক্সট + সংখ্যাগত ডেটা সামলানো এবং কেন তা সরল RAG ভেঙে দেয়: কীওয়ার্ডে সংঘর্ষ হয়, আর সংখ্যার নিজস্ব কোনো অর্থগত মানে থাকে না.

  • সামারি-ফার্স্ট এমবেডিং ডিজাইন কেন সহায়ক: প্রতিটি চাঙ্কের জন্য ছোট একটি বর্ণনামূলক সারাংশ তৈরি করুন, তারপর সেই সারাংশে এমবেড/কোয়েরি করুন.

  • প্রাসঙ্গিক সারাংশ কীভাবে তৈরি করবেন: প্যারেন্ট ডকুমেন্টের প্রেক্ষাপট যোগ করুন, যাতে একই ধরনের পরিসংখ্যান আলাদা করে বোঝা যায়.

  • কখন কোড ও Pydantic মডেলের ওপর নির্ভর করবেন: যেখানে হুবহু কনটেন্ট গুরুত্বপূর্ণ, নির্ভরযোগ্যতার জন্য LLM কলের সঙ্গে কাস্টম কোড এবং/অথবা Pydantic মডেল মিলিয়ে ব্যবহার করুন.

কাস্টম RAG সমাধান তৈরি করা

মৌলিক বিষয়গুলো

RAG সিস্টেম সাপোর্ট বট থেকে শুরু করে অভ্যন্তরীণ নলেজ অ্যাসিস্ট্যান্ট পর্যন্ত নানা জিনিস চালায়.

ভেতরের প্রক্রিয়ায় সাধারণত আপনি:

  1. আপনার সোর্স ডকুমেন্টগুলো চাঙ্ক করেন

  2. প্রতিটি চাঙ্ককে একটি ভেক্টর স্পেসে এমবেড করেন

  3. কোয়েরির সময় শীর্ষ-K চাঙ্ক রিট্রিভ করেন

  4. সেই চাঙ্কগুলোর ভিত্তিতে একটি উত্তর তৈরি করেন

LangChain, LlamaIndex, OpenAI-এর Filestore-এর মতো জনপ্রিয় টুলকিট এসব ধাপকে প্রায় তুচ্ছ করে দেয়. কিন্তু বাস্তব দুনিয়ার পাইপলাইনে এমন ডেটার মুখোমুখি হবেন যা শুধু ঘন টেক্সট নয়, আর মৌলিক RAG সেখানে হিমশিম খেতে পারে. পরের অংশগুলোতে আমরা ডেটা-সংক্রান্ত চ্যালেঞ্জের নির্দিষ্ট উদাহরণ দেখাব, এবং জটিলতা বাড়ার সঙ্গে সঙ্গে ধাপে ধাপে সমাধান তৈরি করব.

যখন বিষয়গুলো আরও জটিল হয়ে ওঠে

  1. যখন আপনার ডেটা শুধু টেক্সট নয় (আসলে খুব অস্বাভাবিকও নয়)

গেমিং প্রেক্ষাপটে নিচের ডেটা চাঙ্কটি বিবেচনা করুন:

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

এমবেডিং কাজ করে কারণ শব্দগুলোর মধ্যে অর্থগত মানে ও ব্যাকরণের মাধ্যমে শেখা সম্পর্ক থাকে. উপরের ডেটায় টেক্সট ও সংখ্যার মিশ্রণ আছে; এই নির্দিষ্ট প্রেক্ষাপটের বাইরে সংখ্যাগুলোর সঙ্গে শব্দগুলোর কোনো সম্পর্ক নেই. তাই বলা যায়, এই ডেটা চাঙ্কটি মূলত কিছুটা বর্ণনামূলক শব্দের পর কিছু এলোমেলো সংখ্যার সমন্বয়.

আমাদের কাছে যদি শুধু এ ধরনের ডেটাই থাকত, তাহলে আসলে এটি সমস্যা হতো না, কারণ অল্প যে বর্ণনামূলক শব্দ আছে সেগুলোর এমবেডিং দিয়েও আমরা রিট্রিভ করতে পারতাম (অথবা সরাসরি text-to-sql ব্যবহার করতে পারতাম). কিন্তু যদি এই চাঙ্কটি এমন অনেক টেক্সট-ঘন চাঙ্কের মধ্যে চাপা পড়ে থাকে, যেখানে একই শব্দগুলোও আছে, তাহলে কী হবে. উদাহরণস্বরূপ:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

এখন ধরুন আমরা রিট্রিভ করতে চাই, “Draconic Ascension থাকলে আক্রমণের পাল্লা কত.” খুব সম্ভবত আমরা যে প্রাসঙ্গিক চাঙ্কটি চাই তা রিট্রিভ করতে পারব না, কারণ একই কীওয়ার্ড থাকা অন্য চাঙ্কের শব্দের ভিড়ে সেটি গভীরে চাপা পড়ে আছে.

মূল সমস্যা হলো, এসব ডেটা চাঙ্ক একই বিষয়ের ভিন্ন ধরনের তথ্য বহন করলেও আমরা সেগুলোকে ভালোভাবে আলাদা করতে পারি না. আমরা কি কোনোভাবে সেটিকে সমৃদ্ধ বা উন্নত করতে পারি. অবশ্যই পারি:smile:

  1. সারাংশ করে আপনার ডেটা সমৃদ্ধ করুন, হ্যাঁ, ঠিকই পড়েছেন

চাঙ্কটিকে সরাসরি এমবেড করার বদলে আগে আমরা ডেটাটি কী নিয়ে তা ব্যাখ্যা করে একটি সারাংশ তৈরি করতে পারি, তারপর সেই সারাংশে এমবেড ও রিট্রিভ করতে পারি. জেনারেশন ধাপে আমরা তবুও সারাংশের সঙ্গে যুক্ত মূল ডেটাই ব্যবহার করব.

তাই উপরে দেখানো দুই চাঙ্কের উদাহরণের জন্য আমরা এমন কিছু সারাংশ তৈরি করতাম:

  1. পাল্লা, গতি ও ক্ষতির আক্রমণ-পরিসংখ্যান (ডিফল্ট এবং Draconic Ascension সহ).

  2. Draconic Ascension-এর বর্ণনা ও বিশদ, যার মধ্যে সক্রিয় হওয়ার শর্ত, ভিজ্যুয়াল এফেক্ট এবং লোর রয়েছে.

এরপর আমরা কোয়েরিটিকেও সারাংশের সঙ্গে “মেলাতে” বাড়তি তথ্য দিই. যেমন, আমরা “Draconic Ascension থাকলে আক্রমণের পাল্লা কত.” বদলে “Draconic Ascension থাকলে আক্রমণের পাল্লার পরিসংখ্যান কী.” করতাম। এটি বিশেষভাবে গুরুত্বপূর্ণ যখন রিট্রিভাল কোয়েরি আসে প্রযুক্তিগত ক্ষেত্রের বাইরে থাকা ব্যবহারকারীদের কাছ থেকে, যারা ~~“ফ্রিস্টাইল”~~ স্বাভাবিক মানুষের ভাষায় প্রশ্ন করেন; কারণ শেষ পর্যন্ত প্রিসিশন/রিকল সর্বাধিক করতে RAG কীভাবে কাজ করে, তা জানা তাদের দায়িত্ব বা আগ্রহের বিষয় নয়.

বিষয়গুলো কখন আরও জটিল হয়ে ওঠে তা দেখানো ডায়াগ্রাম.

  1. প্রেক্ষাপটের বাইরে কিছু নেবেন না (জীবনেও সাধারণভাবে প্রযোজ্য)

এখন পরের পরিস্থিতি হলো নিচের মতো দেখতে একরকম বিপুল সংখ্যক ডেটা চাঙ্ক সামলানো:

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

একই পদ্ধতিতে থাকলে, কল্পনা করুন কেউ জিজ্ঞেস করছে, “চরিত্র X-এর আক্রমণের পাল্লা কত.” তখন আমরা সদ্য তৈরি করা এসব সারাংশ নিয়ে ভাগ্যনির্ভর অনুমানের খেলায় নেমে পড়তাম, কারণ সেগুলোও দেখতে খুব মিল হতো. তাহলে আমরা কীভাবে সেগুলো আলাদা করব.

সহজ উত্তর হলো, প্রেক্ষাপট দিন. আমরা ডেটা চাঙ্কে চাঙ্কটির প্যারেন্ট ডকের একটি রেফারেন্স সহজেই যোগ করতে পারি, যেমন এই ক্ষেত্রে {”character”: “X”}. তাহলে চরিত্র Y ও Z-এর জন্য একই ডেটা থাকলেও আমরা চরিত্র X-এর সঠিক ডেটা নির্ভুলভাবে রিট্রিভ করতে পারব.

তবে আরও ভালো এবং বেশি সাধারণীকরণযোগ্য পদ্ধতি হবে চাঙ্কটির প্রাসঙ্গিক সারাংশ তৈরি করা. অর্থাৎ শুধু ডেটা চাঙ্কটির সারাংশ তৈরির বদলে আমরা তার প্যারেন্ট ডকুমেন্ট এবং চাঙ্ক—দুটিই দিয়ে সাধারণ প্রাসঙ্গিক সারাংশ তৈরি করতে পারি, যেখানে সারাংশে বলা হবে এই চাঙ্কটি প্যারেন্ট ডকে কীভাবে বসে, যেমন:

  1. এই চাঙ্কটি চরিত্র X-এর জন্য …-এর বিস্তারিত পরিসংখ্যান দেয়. সম্পূর্ণ ডকুমেন্টে এই চাঙ্কটির ভূমিকা হলো X-এর আক্রমণগতির শক্তি দেখানো…

  2. এই চাঙ্কটি চরিত্র Y-এর জন্য …-এর বিস্তারিত পরিসংখ্যান দেয়. সম্পূর্ণ ডকুমেন্টে এই চাঙ্কটির ভূমিকা হলো Y-এর বিশেষ ক্ষমতায় বাড়ানো স্ট্যাট দেখানো…

  3. এই চাঙ্কটি চরিত্র Z-এর জন্য …-এর বিস্তারিত পরিসংখ্যান দেয়. সম্পূর্ণ ডকুমেন্টে এই চাঙ্কটির ভূমিকা হলো Z-এর এমন স্ট্যাট দেখানো, যা টিম ম্যাচে ট্যাঙ্ক হিসেবে খুব উপযুক্ত…

এই পদ্ধতিটি (যা আংশিকভাবে Anthropic থেকে অনুপ্রাণিত) উপরের উদাহরণের জন্য বাড়াবাড়ি মনে হতে পারে, তবে যেসব চাঙ্ক “প্রেক্ষাপটের বাইরে” ভুল বোঝা যেতে পারে, সেগুলোর জন্য এটি খুব কার্যকর; পাশাপাশি এটি সব চাঙ্কে কাজ করে এমন একীভূত পদ্ধতি দেয়, ফলে ইঞ্জিনিয়ারিং পাইপলাইন পরিচ্ছন্ন থাকে.

বিষয়গুলো কখন আরও জটিল হয়ে ওঠে তা দেখানো ডায়াগ্রাম.

  1. যখন আপনাকে ~~সবকিছু নিয়ন্ত্রণে রাখতে চাওয়া মানুষ~~ কঠোর হতে হয়

সাধারণত আমরা পূর্ণাঙ্গ অংশে ডেটা পাই এবং RAG সিস্টেমের জন্য সেগুলো চাঙ্কে ভাঙি. এই উদাহরণে আমরা একটু ভিন্ন কিছু দেখাচ্ছি—ডেটা চাঙ্কে ভাঙা আছে, কিন্তু চাঙ্কগুলো খারাপ; এগুলো আসলে একটি লজিক্যাল চাঙ্কের এলোমেলো অংশ, যেগুলো আবার একসঙ্গে গ্রুপ করতে হবে. লজিক্যাল চাঙ্ক বলতে এমন কনটেন্ট-চাঙ্ক বোঝায় যা স্বাভাবিকভাবেই একসঙ্গে থাকার কথা, যেমন কোনো ডকুমেন্টের উপ-অংশ বা সুসংগত অনুচ্ছেদ.

বিষয়গুলো কখন আরও জটিল হয়ে ওঠে তা দেখানো ডায়াগ্রাম.

এই ডেটা নিয়ে আমাদের প্রথম চেষ্টা ছিল সবকিছু একটি LLM কলে দেওয়া, তাকে নিজের মতো করে গ্রুপ করতে বলা, তারপর গ্রুপ করা কনটেন্ট ফেরত নিতে বলা. LLM তো এতে বেশ ভালো হওয়ার কথা, তাই না. আসলে, হ্যাঁ এবং না.

আমরা দেখেছি, অন্য কয়েকটি ক্ষেত্রেও, LLM-গুলো অলসভাবে আচরণ করতে পারে এবং পুরো ও হুবহু কনটেন্ট প্রয়োজন হলে নির্ভরযোগ্য থাকে না, বিশেষ করে প্রেক্ষাপট দীর্ঘ হলে. যা একদমই যুক্তিযুক্ত. কিন্তু এই নির্দিষ্ট ব্যবহারের ক্ষেত্রে সেটাই বড় বাধা ছিল, কারণ আমাদের সত্যিই শব্দে শব্দে হুবহু কনটেন্ট দরকার—কোনো সারাংশ নয়, মূল কনটেন্টের কোনো অংশ বাদ নয়. আমরা কোনো বিস্তারিত বাদ দিতে পারি না.

আর অবশ্যই “হ্যাঁ” অংশটি ছিল যে ভাঙা চাঙ্কগুলোর অর্থ ও কাঠামো বোঝায় এটি দারুণ কাজ করেছিল. যদি না এটি হুবহু কনটেন্ট উদ্ধৃত করে ফেরত দিতে অস্বীকার করে. ধুর:/

তাহলে LLM যা ভালো পারে তা কীভাবে কাজে লাগাব, কিন্তু যেখানে এটি অবিশ্বস্ত তা এড়াব. আমরা ফিরে গেলাম আমাদের পুরনো ভালো বন্ধু, কোডের কাছে (মানে কাস্টমাইজড python ফাংশন). আর একটি “এর চেয়ে সহজ হতে পারে না” ধরনের pydantic মডেল. সমাধানটি হলো:

  • একটি বর্তমান লজিক্যাল চাঙ্ক ধরে রেখে সেকশনগুলোর মধ্য দিয়ে ইটারেট করুন

  • প্রতিটি সেকশনে LLM-কে জিজ্ঞেস করুন: এই সেকশন কি বর্তমান লজিক্যাল চাঙ্কের অংশ, হ্যাঁ না না উত্তর দাও (pydantic মডেল অনুসরণ করে).

  • হ্যাঁ হলে সেকশনটি চাঙ্কে যুক্ত করুন; না হলে বর্তমান লজিক্যাল চাঙ্কটি সম্পূর্ণ ধরে বের করে দিন, তারপর ওই সেকশন দিয়ে নতুন চাঙ্ক শুরু করুন.

বিষয়গুলো কখন আরও জটিল হয়ে ওঠে তা দেখানো ডায়াগ্রাম.

অবশ্যই এখানে পুরো কনটেন্টের একবারের পাসের চেয়ে একটু বেশি টোকেন ব্যবহার করছি, কিন্তু যেখানে হুবহু কনটেন্ট ধরে রাখা সর্বোচ্চ অগ্রাধিকার, সেই নির্দিষ্ট ব্যবহারের ক্ষেত্রে এই সামান্য বাড়তি খরচ পুরোপুরি সার্থক ছিল.

এটি খুব সহজ সমাধান, কিন্তু একটি গুরুত্বপূর্ণ নীতি মানে: কঠোরতা দরকার হলে আমরা শুধু LLM-এর ওপর নির্ভর করতে চাই না, কারণ শেষ পর্যন্ত সেগুলো সম্ভাবনাভিত্তিক.

কাস্টম কোড/ফাংশন এবং pydantic মডেল ব্যবহার করে পূর্বানুমেয় ও নির্ভরযোগ্য ফল পাওয়া যায়, একই সঙ্গে LLM-এর সক্ষমতাও কাজে লাগানো যায়.

শেষ কথা

একটি gen AI সমাধান তৈরি করা যতটা AI চ্যালেঞ্জ, ততটাই ইঞ্জিনিয়ারিং চ্যালেঞ্জ. আশা করি এই উদাহরণগুলো আপনাকে নিজের অনন্য চ্যালেঞ্জ মোকাবিলায় অনুপ্রাণিত করেছে. ইঞ্জিনিয়ারিং-প্রথম gen AI সমাধান সম্পর্কে আরও পড়তে, রাউটার-ভিত্তিক এজেন্টিক সিস্টেম ডিজাইন নিয়ে আমাদের ব্লগ পোস্ট দেখুন.

লেখক

Cynthia Yu