সরাসরি পর্বে যান
পর্ব ৭০ · ৯১-এর মধ্যে · মডিউল ১১

RAG মূল্যায়ন — retrieval metric, faithfulness আর বাংলা ডকুমেন্ট

Evaluating RAG — Retrieval Metrics, Faithfulness, Error Taxonomy and LLM Judges on Bangla Documents

Taught in Bangla, this lesson evaluates retrieval and generation separately. It computes recall@k, precision@k, MRR and nDCG by hand and on real rankings comparing e5-small with BM25, builds a Bangla eval set with answerable, unanswerable, multi-document, numeric and Banglish questions, and uses an oracle context, varying k and chunk size to locate errors with paired bootstrap. It then checks a small LLM faithfulness judge against hand labels with Cohen's kappa, all from a real desktop run with in-page Python.

এই পর্বে যা শিখবেন

  • Recall@k, precision@k, MRR আর nDCG হাতে আর সত্যিকারের ranking-এ হিসাব করা
  • উত্তরের সঠিকতা, faithfulness, প্রাসঙ্গিকতা আর citation-এর মাপ আলাদা করে চেনা
  • উত্তর আছে, উত্তর নেই, বহু নথি, সংখ্যা আর Banglish প্রশ্ন দিয়ে একটি বাংলা eval set বানানো
  • Oracle context দিয়ে retrieval আর generation-এর ভুল আলাদা করা, k আর chunk size-এর প্রভাব paired bootstrap-এ মাপা
  • ভুলের শ্রেণিবিন্যাস, আর LLM বিচারককে নিজের লেবেল ও Cohen's kappa দিয়ে যাচাই করা

১ · দুই অংশ, দুই রকম ভুল — কেন অংশ ধরে মাপবেন

ডাক্তার রোগ ভুল ধরলেন। দোষ কার? হতে পারে ল্যাবের রিপোর্টেই ভুল ছিল। হতে পারে রিপোর্ট ঠিক ছিল, কিন্তু ডাক্তার পড়তে ভুল করেছেন। আবার রিপোর্টে দরকারি পরীক্ষাটাই ছিল না। রোগীর কাছে ফল একই, “ভুল রোগনির্ণয়”। কিন্তু হাসপাতাল ঠিক করতে চাইলে জানতে হবে কোন ধাপে ভুল।

RAG-এও একই কথা। পর্ব ৬৯-এ (RAG — chunking, retrieval, reranking আর উত্তর তৈরি) পুরো pipeline তৈরি হচ্ছে: নথি ভাগ করা, খোঁজা, prompt বানানো, উত্তর লেখা। এখানে প্রশ্ন একটাই: কতটা ভালো কাজ করছে, আর খারাপ হলে কোথায়? দুটি অংশ আলাদা মাপতে হয়:

  • Retriever: প্রশ্নের উত্তর যে নথিতে আছে, সেটি প্রথম k-টির মধ্যে এল কি না, আর কত ওপরে এল।
  • Generator: আনা নথি হাতে পেয়ে LLM ঠিক উত্তর লিখল কি না, উৎসের বাইরে কিছু বানাল কি না, ঠিক উৎসের নম্বর দিল কি না, আর উত্তর না থাকলে “জানি না” বলল কি না।

এই ভাগকে বলে component-wise evaluation. এর সবচেয়ে কাজের কৌশল oracle context: retriever বাদ দিয়ে ঠিক নথিটি নিজে হাতে generator-কে দেওয়া। তখনও উত্তর ভুল হলে দোষ generator-এর; oracle-এ ঠিক কিন্তু সাধারণ pipeline-এ ভুল হলে দোষ retriever-এর।

RAG-এর অংশ ধরে মূল্যায়ন প্রশ্ন Retriever Oracle Context (top-k) Generator (LLM) উত্তর recall@k · MRR · nDCG gold নথি সরাসরি প্রমাণ আছে কি? (context recall) Qwen2.5-1.5B-Instruct সঠিকতা · faithfulness উত্তরের মাপ: • সঠিকতা: gold-এর সঙ্গে • faithfulness: উৎসের সঙ্গে • প্রাসঙ্গিকতা: প্রশ্নের সঙ্গে • citation: ঠিক নম্বর? • উত্তর নেই → “জানি না” ভাঙা দাগ = retriever বাদ
প্রতিটি ধাপে আলাদা মাপ। Oracle পথে retriever-কে বাদ দিলে generator-এর নিজের দুর্বলতা আলাদা করে দেখা যায়।

শব্দগুলো অন্য কোর্সে দেখেছেন। মেশিন লার্নিং কোর্সের পর্ব ৫৩-এ recommender-এর তালিকা মাপতে precision@k আর nDCG এসেছে; পর্ব ৪৫-এ মানুষের বিচার আর kappa; পর্ব ৫৮-এ LLM-as-judge-এর ফাঁদ; পর্ব ৬৬-এ eval set আর paired bootstrap. এখানে সবগুলো এক জায়গায়, বাজারঘরের হেল্প সেন্টারের ৪০টি বানানো বাংলা, ইংরেজি আর Banglish নথিতে, সত্যিকারের model দিয়ে।

২ · Retrieval মাপার চারটি metric — recall@k, precision@k, MRR, nDCG

Retriever একটি সাজানো তালিকা দেয়। প্রতিটি প্রশ্নের জন্য আগে থেকে হাতে ঠিক করা থাকে কোন নথিগুলো প্রাসঙ্গিক; এগুলোকে বলে gold নথি। ধরা যাক প্রথম k-টির মধ্যে r-টি gold, আর মোট gold g-টি।

  • Recall@k = r ÷ g. প্রয়োজনীয় নথির কত ভাগ ধরা পড়ল। RAG-এ এটিই প্রথম শর্ত, কারণ যা আনা হয়নি, generator তা দেখতেই পায় না।
  • Precision@k = r ÷ k. আনা নথির কত ভাগ কাজের। কম হলে prompt-এ আবর্জনা বাড়ে, token-এর খরচ বাড়ে, আর model ভুল নথি থেকে উত্তর তুলতে পারে।
  • MRR: প্রথম gold নথি rank p-তে এলে reciprocal rank = ১ ÷ p; সব প্রশ্নের গড় MRR. প্রথম ঠিক নথি কত ওপরে, শুধু সেটুকু দেখে।
  • nDCG@k: প্রতিটি অবস্থানের প্রাসঙ্গিকতাকে ১ ÷ log2(i + ১) দিয়ে ছাড় দিয়ে যোগ (DCG), তারপর সবচেয়ে ভালো সম্ভাব্য সাজানোর যোগফল (IDCG) দিয়ে ভাগ। মান ০ থেকে ১. একাধিক gold নথি আর graded relevance (যেমন ২ = পুরো উত্তর, ১ = আংশিক) দুটোই সামলায়।

নিচে আমাদের সত্যিকারের run-এর একটি প্রশ্ন: “otp keu chaile ki korbo”. এখানে দুটি নথি প্রাসঙ্গিক, bh20 (OTP-র নিয়ম) আর bh21 (প্রতারণা থেকে সাবধান)।

সত্যিকারের প্রথম ৫টি ফল (rank ১ → ৫)
ধাপ
BM25
e5-small
  1. তালিকা আর rel. প্রতিটি অবস্থানে gold হলে ১, না হলে ০. BM25 rank ১ আর ৩-এ দুটি gold পেয়েছে; e5-small rank ২ আর ৪-এ।
  2. Recall আর precision. দুজনেরই R@৫ = ১, P@৫ = ০.৪. এই দুটি metric অবস্থান দেখে না, তাই এখানে কোনো পার্থক্য ধরা পড়ে না। R@১-এ শুধু BM25 অর্ধেক পায়।
  3. MRR. প্রথম gold-এর rank-এর উল্টো: BM25 ১, e5 ০.৫. দ্বিতীয় gold কোথায়, MRR তা দেখে না।
  4. ছাড়. rank i-এর ওজন ১ ÷ log2(i + ১): rank ১-এ ১, ২-এ ০.৬৩১, ৩-এ ০.৫০০, ৪-এ ০.৪৩১. নিচে যত, ওজন তত কম।
  5. DCG. শুধু gold-এর ঘরের ওজন যোগ: BM25 ১.৫০০, e5 ১.০৬২.
  6. nDCG. সেরা সম্ভাব্য সাজানো (দুটি gold rank ১ আর ২-এ) দেয় IDCG = ১ + ০.৬৩১ = ১.৬৩১. ভাগ করলে BM25 ০.৯২০, e5 ০.৬৫১. দুটি gold-ই কত ওপরে, nDCG সেটা একসঙ্গে ধরে।

এবার ছয়টি সত্যিকারের প্রশ্নে চারটি metric একসঙ্গে। Rank-গুলো পরের অংশের run থেকে নেওয়া; খালি তালিকা মানে gold নথি প্রথম ১০-এ আসেনি।

import numpy as np
# সত্যিকারের run থেকে: প্রতিটি প্রশ্নে gold নথি কোন rank-এ এল ([] = প্রথম ১০-এ নেই)
runs = {
    "BM25":     {"নারায়ণগঞ্জ": [3], "ঢাকায় ফ্রি": [3], "otp keu chaile": [1, 3],
                 "order cancel": [], "রংপুর": [], "delivery Dhaka": [7]},
    "e5-small": {"নারায়ণগঞ্জ": [1], "ঢাকায় ফ্রি": [2], "otp keu chaile": [2, 4],
                 "order cancel": [4], "রংপুর": [], "delivery Dhaka": [4]},
}
n_gold = {"otp keu chaile": 2}          # বাকিগুলোতে gold নথি ১টি
disc = 1 / np.log2(np.arange(2, 12))     # rank 1..10-এর discount

def scores(pos, g, k=5):
    rel = np.zeros(10)
    rel[[p - 1 for p in pos]] = 1
    dcg = (rel[:k] * disc[:k]).sum()
    idcg = disc[:min(g, k)].sum()
    return [rel[:1].sum() / g, rel[:k].sum() / g, rel[:k].sum() / k,
            1 / min(pos) if pos else 0.0, dcg / idcg]

print(f"{'':16}{'R@1':>6}{'R@5':>6}{'P@5':>6}{'RR':>6}{'nDCG@5':>8}")
for name, qs in runs.items():
    print(name)
    rows = []
    for q, pos in qs.items():
        s = scores(pos, n_gold.get(q, 1))
        rows.append(s)
        print(f"  {q:14}" + "".join(f"{v:6.2f}" for v in s[:4]) + f"{s[4]:8.3f}")
    m = np.mean(rows, axis=0)
    print(f"  {'গড়':14}" + "".join(f"{v:6.2f}" for v in m[:4]) + f"{m[4]:8.3f}")

আউটপুট (পাতার Python-এ চালানো):

                   R@1   R@5   P@5    RR  nDCG@5
BM25
  নারায়ণগঞ্জ     0.00  1.00  0.20  0.33   0.500
  ঢাকায় ফ্রি     0.00  1.00  0.20  0.33   0.500
  otp keu chaile  0.50  1.00  0.40  1.00   0.920
  order cancel    0.00  0.00  0.00  0.00   0.000
  রংপুর           0.00  0.00  0.00  0.00   0.000
  delivery Dhaka  0.00  0.00  0.00  0.14   0.000
  গড়             0.08  0.50  0.13  0.30   0.320
e5-small
  নারায়ণগঞ্জ     1.00  1.00  0.20  1.00   1.000
  ঢাকায় ফ্রি     0.00  1.00  0.20  0.50   0.631
  otp keu chaile  0.00  1.00  0.40  0.50   0.651
  order cancel    0.00  1.00  0.20  0.25   0.431
  রংপুর           0.00  0.00  0.00  0.00   0.000
  delivery Dhaka  0.00  1.00  0.20  0.25   0.431
  গড়             0.17  0.83  0.20  0.42   0.524

তিনটি জিনিস খেয়াল করুন। এক, “delivery Dhaka”-তে BM25-এর gold নথি rank ৭-এ: RR ০.১৪ পায় (MRR@১০ ধরে), কিন্তু nDCG@৫ শূন্য। কোন k-তে মাপছেন, সেটি সবসময় লিখবেন। দুই, “ঢাকায় ফ্রি” প্রশ্নে দুই পদ্ধতিরই প্রথম ফল ছিল bh40, যা ২০২৫ সালের বাতিল করা পুরোনো নথি; retrieval metric এটিকে শুধু “gold নয়” বলে, ক্ষতিটা ধরা পড়ে উত্তরে (অংশ ৭)। তিন, “রংপুর” কোনো পদ্ধতিতেই আসেনি: নথিতে লেখা “অন্যান্য জেলা”, জেলার নাম নেই। এমন প্রশ্নে কোনো metric নয়, নথি লেখার ধরন বদলাতে হয়।

৩ · সত্যিকারের retrieval মাপ — ৬০টি প্রশ্ন, আর chunk-এর আকার

মডিউল ১১-এর পর্বগুলো বাজারঘরের হেল্প সেন্টারের ৪০টি নথির একটি সাধারণ সংগ্রহ ভাগ করে ব্যবহার করে (সব বানানো; ৩৫টি বাংলা, ৩টি ইংরেজি, ২টি Banglish; bh40 পুরোনো, বাতিল)। পর্ব ৬৭-এ এই নথির ওপর ৬০টি প্রশ্ন আর তাদের gold নথি লেখা হয়েছে। সংখ্যা যাতে মেলানো যায়, সেই প্রশ্নই আবার চালালাম, এবার nDCG আর precision-সহ। সত্যিকারের run (CPU-তে, ৩টি thread; Python 3.12.13, torch 2.15.0, sentence-transformers 6.2.0; model intfloat/multilingual-e5-small, MIT লাইসেন্স):

~/bazarghar-dl$ uv run python retrieval.py
python 3.12.13, torch 2.15.0+cu132, transformers 5.22.2, sentence-transformers 6.2.0
doc-level: 40 docs, 60 questions, time 1.2s
              R@1     R@3     R@5    R@10     P@3  MRR@10 nDCG@10
BM25        0.392   0.550   0.667   0.750   0.189   0.502   0.559
e5-small    0.517   0.742   0.817   0.883   0.250   0.651   0.708
e5 - BM25 MRR@10: +0.149  95% CI [+0.050, +0.255]  (paired bootstrap, 2000)
e5 - BM25 nDCG@10: +0.149  95% CI [+0.058, +0.248]  (paired bootstrap, 2000)

chunk-level, e5-small, 34 answerable of 40 questions
chunking  chunks avgchr  hit@1  hit@3  hit@5  all@3    MRR
sent40        47    179  0.794  0.971  0.971  0.941  0.873
fixed80      143     72  0.735  0.882  0.941  0.794  0.817
fixed160      78    124  0.765  0.912  0.971  0.853  0.837
fixed320      41    201  0.794  0.971  0.971  0.941  0.873

প্রথম অংশ পর্ব ৬৭-এর সঙ্গে হুবহু মিলেছে: MRR@১০ BM25 ০.৫০২, e5-small ০.৬৫১. একই ডেটা, একই কোড, একই সংখ্যা; এটি নিজেই একটি পরীক্ষা যে মাপার যন্ত্র ঠিক আছে। P@৩ কম (০.২৫) দেখে ঘাবড়াবেন না: বেশিরভাগ প্রশ্নে gold নথি মাত্র একটি, তাই P@৩-এর সর্বোচ্চ মানই ০.৩৩.

দ্বিতীয় অংশ পর্ব ৭০-এর নিজের ৪০টি প্রশ্নে (পরের অংশে বিস্তারিত), এবার chunk ধরে। Chunking চার রকম: sent40 মানে শিরোনাম ধরে ভাগ, তারপর প্রায় ৪০ শব্দের বাক্যগুচ্ছ, ১টি বাক্য overlap; fixedN মানে N অক্ষরের টুকরো, ২৫% overlap. hit@k = প্রথম k-টি chunk-এর অন্তত একটি gold নথি থেকে; all@3 = বহু-নথির প্রশ্নে সব gold নথি প্রথম ৩-এ।

  • ৮০ অক্ষরের ছোট টুকরোয় hit@৩ নেমে ০.৮৮২ আর all@৩ ০.৭৯৪. টুকরো এত ছোট যে শব্দ মাঝখানে কাটে (“ে যায়। ভিসা বা…”), আর একটি তথ্যের দুই অংশ দুই chunk-এ চলে যায়।
  • বাক্য ধরে ভাগ আর ৩২০ অক্ষরের টুকরো একই ফল দিল। আমাদের নথি ছোট (গড়ে প্রায় ২০০ অক্ষর), তাই ৩২০ অক্ষরে প্রায় প্রতিটি নথি একটি chunk. বড় নথিতে এই ফল খাটবে না; নিজের ডেটায় মাপবেন।

Retrieval ঠিক, তবু context-এ উত্তর না-ও থাকতে পারে। hit@k নথি দেখে, chunk-এর ভেতরের লেখা দেখে না। তাই আমরা আরেকটি মাপ রাখব: আনা chunk-এর লেখায় উত্তরের প্রমাণ (যেমন “৯৯৯ টাকা”) সত্যিই আছে কি না। একে বলা যায় context recall. RAGAS-এর মতো কাঠামোতে একই নামের metric আছে, যদিও সেখানে হিসাবের পদ্ধতি আলাদা হতে পারে।

৪ · একটি বাংলা RAG eval set বানানো

Retrieval-এর জন্য দরকার প্রশ্ন আর gold নথি। উত্তর মাপতে আরও দুটি জিনিস লাগে: একটি রেফারেন্স উত্তর, আর উত্তর স্বয়ংক্রিয়ভাবে যাচাইয়ের একটি নিয়ম। আমি হাতে ৪০টি প্রশ্ন লিখেছি (eval set), ছয় ধরনের:

ধরনসংখ্যাউদাহরণকী পরীক্ষা করে
এক নথির তথ্য১৮OTP কত মিনিট বৈধ থাকে?মূল সামর্থ্য
সংখ্যার হিসাব৪চট্টগ্রামে ৩ কেজির পার্সেলের চার্জ কত? (১২০ + ২ × ২০)উৎস থেকে নিয়ে হিসাব
বহু নথি৪মন বদলে ফেরতে কত কাটে, আর Nagad-এ রিফান্ড কত দিনে?দুটি নথি জোড়া
Banglish৪gazipur e delivery charge koto?লিপি পেরিয়ে খোঁজা
ইংরেজি৪How much extra does express delivery cost?উত্তরের ভাষা
উত্তর নেই৬বাজারঘরের প্রধান নির্বাহী কে?Abstention, বানিয়ে না বলা

প্রতিটি সারিতে প্রশ্ন, gold নথি, রেফারেন্স উত্তর আর কয়েকটি key: উত্তরে যে সংখ্যা বা শব্দ থাকতেই হবে। উত্তর নেই এমন প্রশ্নে key নেই; সেখানে ঠিক উত্তর মানে “উৎসে এই তথ্য নেই”-এর মতো বাক্য। তিনটি সারি (ফাইল থেকে হুবহু, সংক্ষিপ্ত):

("n01", "চট্টগ্রামে ৩ কেজির একটি পার্সেলের ডেলিভারি চার্জ কত হবে?", ["bh06"], "১৬০ টাকা (১২০ + ২ × ২০)", "numeric", [r"\b160\b"]),
("m01", "মন বদলে ঢাকা সিটি থেকে পণ্য ফেরত দিলে কত টাকা কাটা যায়, আর Nagad-এ রিফান্ড কত দিনে আসে?", ["bh03", "bh02"], "৬০ টাকা; ৩ কর্মদিবস", "multi", [r"\b60\b", r"\b3\b"]),
("u04", "Nagad-এ টাকা দিলে কত শতাংশ ক্যাশব্যাক পাব?", [], "উৎসে নেই", "unanswerable", []),

বাংলা ডকুমেন্টে key মেলানোর আগে উত্তরকে normalise করতে হয়, নইলে ঠিক উত্তরও ভুল গোনা হবে:

  • অঙ্কের লিপি: model কখনো “৭ দিন”, কখনো “7 দিন” লেখে (একই run-এ দুটোই এসেছে)। বাংলা অঙ্ক Latin-এ বদলে নিন।
  • হাজারের কমা: “১,৫০০” আর “1500” এক করতে সংখ্যার ভেতরের কমা বাদ দিন।
  • শব্দসীমা: Python-এর regex-এ বাংলা অক্ষরও “word” অক্ষর, তাই \b7\b “৭টি”-র ভেতরের ৭ ধরে না, কারণ ৭ আর ট-এর মাঝে কোনো সীমা নেই। উপরের ফাইলে key-গুলো \b দিয়ে লেখা, তাই মেলানোর কোড আগে সেগুলোকে অঙ্কের সীমায় বদলে নেয়: (?<![0-9])7(?![0-9]), অর্থাৎ দুপাশে অন্য অঙ্ক নেই।
  • উত্তর নেই চেনা: “উৎসে এই তথ্য নেই”, “উৎসে নেই”, “উত্তর নেই”, একই কথার তিন রূপ এসেছে।

দুটি বিপদ। এক, eval set লেখার সময় retriever-এর ফল দেখে প্রশ্ন বদলালে প্রশ্নগুলো নিজের system-এর দিকে হেলে যায়; আগে লিখুন, পরে চালান। দুই, key-মেলানো সহজ বলে লোভনীয়, কিন্তু অন্ধ: “৫ মিনিট” থাকলেই ঠিক ধরবে, পাশে বানানো “৩০ সেকেন্ড” থাকলেও। তাই অন্তত একটি অংশ মানুষকে পড়তেই হয় (অংশ ৬)।

৫ · উত্তরের মাপ — সঠিকতা, faithfulness, প্রাসঙ্গিকতা আর citation

Generator-এর উত্তর চারটি আলাদা প্রশ্নে মাপা হয়। এগুলো একে অন্যের বদলি নয়:

সঠিকতা

Answer correctness: রেফারেন্স উত্তরের সঙ্গে মেলে কি না। Gold উত্তর লাগে।

উৎস ভুল হলে এটিও ভুল

Faithfulness

Faithfulness (groundedness নামেও চলে): উত্তরের প্রতিটি দাবি দেওয়া উৎসে আছে কি না। Gold উত্তর লাগে না।

বানানো তথ্য ধরে

প্রাসঙ্গিকতা

Answer relevance: যা জিজ্ঞেস করা হয়েছে, উত্তর সেটির কথা বলছে কি না।

সত্য কিন্তু অবান্তর উত্তর ধরে

Citation

Citation precision: উদ্ধৃত উৎসের কত ভাগ সত্যিই সমর্থন করে; citation recall: দরকারি উৎসের কত ভাগ উদ্ধৃত।

পাঠক যাচাই করতে পারে কি না

এগুলো কেন আলাদা, আমাদের run-এর দুটি উত্তরেই দেখা যায়। “ঢাকা সিটিতে কত টাকার অর্ডারে ডেলিভারি ফ্রি?” প্রশ্নে model লিখল “১,০০০ টাকা”, যা হাতে দেওয়া পুরোনো নথিতে হুবহু আছে: faithful কিন্তু ভুল। আবার “বাজারঘরের অ্যাপ কোন সালে চালু হয়েছে?” প্রশ্নে লিখল “বাজারঘর প্লাস সদস্যপদের ফি বছরে ৯৯৯ টাকা”: উৎসে আছে, কিন্তু প্রশ্নের সঙ্গে সম্পর্কহীন, অর্থাৎ faithful কিন্তু অপ্রাসঙ্গিক।

Faithfulness মাপার প্রচলিত পথ হলো claim decomposition: উত্তরকে ছোট ছোট দাবিতে ভাঙা, তারপর প্রতিটি দাবি উৎসে খোঁজা। একটি সত্যিকারের উত্তরে (প্রশ্ন: “বাজারঘর প্লাসের বার্ষিক ফি কত, আর ঢাকার বাইরে সদস্যরা মাসে কয়টি অর্ডারে ফ্রি ডেলিভারি পান?”):

উৎস [1]: “ফি বছরে ৯৯৯ টাকা… ঢাকার বাইরে সদস্যরা মাসে ৪টি অর্ডারে ফ্রি ডেলিভারি পান।”
ধাপ
দাবি
রায়
  1. উত্তর. Model-এর লেখা হুবহু (greedy decoding, ১৬০ token-এ থেমেছে, তাই শেষটা কাটা)। প্রথমে উত্তরকে আলাদা দাবিতে ভাঙুন।
  2. দাবি ক. “৯৯৯ টাকা” উৎসের প্রথম বাক্যে আছে। সমর্থিত।
  3. দাবি খ. উৎস বলে ৪টি অর্ডার, উত্তর বলে ৩টি। উৎসে নেই শুধু নয়, উৎসের বিপরীত। এটিই ক্ষতিকর ভুল।
  4. দাবি গ. “১৪ দিন” দ্বিতীয় উৎসে আছে, কিন্তু বাক্য শেষ হয়নি। এমন সীমান্তের ঘটনা আগে থেকে নিয়মে লিখে রাখুন; আমি সংখ্যা মিললে সমর্থিত ধরেছি।
  5. হিসাব. RAGAS-ধাঁচের ভগ্নাংশে ২ ÷ ৩ ≈ ০.৬৭. আমার বাইনারি লেবেল “একটি দাবিও উৎসের বাইরে হলে ০”, তাই ০. কোন সংজ্ঞা নিলেন, ফলের সঙ্গে লিখবেন।

দাবি ভাঙার কাজটি সাধারণত আরেকটি LLM করে; সেটি অংশ ৮-এ। তার আগে একটি সস্তা, স্বচ্ছ পরীক্ষা: উত্তরের প্রতিটি বাক্যের সংখ্যা উৎসে আছে কি না, আর শব্দের কত ভাগ উৎসের সঙ্গে মেলে। নিচে চারটি সত্যিকারের উত্তর।

import re
# Qwen2.5-1.5B-Instruct-এর সত্যিকারের উত্তর আর তাকে দেওয়া উৎস (ছোট করতে উৎসের অপ্রাসঙ্গিক বাক্য বাদ)
cases = [
 ("m03", "বাজারঘর প্লাস সদস্যপদের ফি বছরে ৯৯৯ টাকা। ঢাকার বাইরে সদস্যরা মাসে ৪টি অর্ডারে ফ্রি ডেলিভারি পান। সদস্যপদ নেওয়ার ১৪ দিনের মধ্যে কোনো ফ্রি ডেলিভারি ব্যবহার না করলে পুরো ফি ফেরত দেওয়া হয়।",
  "বাজারঘর প্লাসের বার্ষিক ফি: ৯৯৯ টাকা\n\nঢাকার বাইরে সদস্যরা মাসে কয়টি অর্ডারে ফ্রি ডেলিভারি পান:\n\n- ৩টি অর্ডারে ফ্রি ডেলিভারি পান (সদস্যপদ নেওয়ার ১৪ দিনের"),
 ("n02", "চেকআউটের সময় গিফট র‍্যাপ বেছে নেওয়া যায়, খরচ প্রতি পণ্যে ৫০ টাকা। সঙ্গে ১৫০ অক্ষর পর্যন্ত একটি বার্তা কার্ডে ছাপা হয়।",
  "উৎস: [1]\n\nপ্রশ্ন: দুটি পণ্যে গিফট র‍্যাপ নিলে মোট র‍্যাপের খরচ কত?\n\nউত্তর: ৫০ টাকা × ২ = ৩০০ টাকা"),
 ("s10", "কাস্টমার কেয়ার প্রতিদিন সকাল ৯টা থেকে রাত ১০টা পর্যন্ত খোলা। শুক্রবার সময় বিকেল ৩টা থেকে রাত ১০টা।",
  "উত্তর: শুক্রবার কাস্টমার কেয়ার খোলা হয় রাত ১০টা।"),
 ("b02", "লগইনের সময় নিবন্ধিত মোবাইল নম্বরে একটি OTP যায়, যার মেয়াদ ৫ মিনিট। পাসওয়ার্ড",
  "OTP অ্যাকাউন্টে পাওয়া যায়। OTP যখন মেয়াদ ৫ মিনিট হবে, তখন তার প্রতি মিনিটে ৩০ সেকেন্ড পর্যন্ত পাওয়া যায়।"),
]
BN = str.maketrans("০১২৩৪৫৬৭৮৯", "0123456789")
def norm(s): return re.sub(r"(?<=\d),(?=\d{3})", "", s.translate(BN))
def nums(s): return set(re.findall(r"\d+", re.sub(r"\[\d+\]", " ", norm(s))))
def words(s): return set(w for w in re.findall(r"[\wঀ-৿]+", norm(s)) if len(w) > 1)

for qid, src, ans in cases:
    print(f"== {qid}")
    S_num, S_w = nums(src), words(src)
    for sent in re.split(r"(?<=[।?!])\s*|\n+", ans):
        if not words(sent) or sent.startswith(("উৎস:", "প্রশ্ন:")):
            continue
        extra = sorted(nums(sent) - S_num)
        overlap = len(words(sent) & S_w) / len(words(sent))
        verdict = "সংখ্যা মেলেনি " + str(extra) if extra else "সংখ্যা ঠিক"
        print(f"  {overlap:4.2f}  {verdict:22}  {sent.strip()[:38]}")

আউটপুট (পাতার Python-এ চালানো):

== m03
  0.67  সংখ্যা ঠিক              বাজারঘর প্লাসের বার্ষিক ফি: ৯৯৯ টাকা
  0.89  সংখ্যা ঠিক              ঢাকার বাইরে সদস্যরা মাসে কয়টি অর্ডারে
  0.89  সংখ্যা মেলেনি ['3']     - ৩টি অর্ডারে ফ্রি ডেলিভারি পান (সদস্য
== n02
  0.50  সংখ্যা মেলেনি ['2', '300']  উত্তর: ৫০ টাকা × ২ = ৩০০ টাকা
== s10
  0.75  সংখ্যা ঠিক              উত্তর: শুক্রবার কাস্টমার কেয়ার খোলা হ
== b02
  0.50  সংখ্যা ঠিক              OTP অ্যাকাউন্টে পাওয়া যায়।
  0.29  সংখ্যা মেলেনি ['30']    OTP যখন মেয়াদ ৫ মিনিট হবে, তখন তার প্

সংখ্যা-পরীক্ষা ধরল m03-এর “৩টি”, n02-এর “৩০০” আর b02-এর বানানো “৩০ সেকেন্ড”। কিন্তু দুটি দুর্বলতাও দেখুন। n02-তে “২” আটকেছে, যদিও প্রশ্নে “দুটি” লেখা ছিল, অঙ্কে নয়: সংখ্যা কথায় লেখা থাকলে এই পরীক্ষা বোঝে না। আর s10-এর “খোলা হয় রাত ১০টা” পাশ করে গেল, কারণ ১০ উৎসে আছে; ভুলটা সংখ্যায় নয়, অর্থে (খোলে বিকেল ৩টায়, রাত ১০টায় বন্ধ হয়)। ৬৩টি উত্তরে আমার নিজের লেবেলের সঙ্গে এই পরীক্ষার kappa মাত্র ০.৩২৪: ১৯টি unfaithful উত্তরের ১৩টি সে ধরতে পারেনি (অংশ ৬-এর run).

আরও গভীরে: RAGAS (Es ও সহকর্মী, ২০২৩) রেফারেন্স ছাড়া (reference-free) RAG মাপার জন্য metric-এর একটি সংকলন প্রস্তাব করে: retrieval-এর প্রাসঙ্গিকতা, উত্তরের faithfulness আর উত্তরের মান; ভেতরের কাজগুলো LLM করে। ARES (Saad-Falcon, Khattab, Potts ও Zaharia; NAACL ২০২৪) সিনথেটিক ডেটায় ছোট বিচারক model fine-tune করে, আর কয়েকশো মানুষের লেবেল দিয়ে prediction-powered inference পদ্ধতিতে বিচারকের ভুল শুধরে ব্যবধান দেয়। TruLens-এর নথি (লেখার সময়ে, নভেম্বর ২০২৬; পরিবর্তন হতে পারে) একে “RAG triad” বলে: context relevance, groundedness আর answer relevance. নামগুলো আলাদা, ধারণা একই তিন ধাপ।

৬ · অংশ ধরে সত্যিকারের run — k, oracle আর chunk-এর আকার

এবার পুরো pipeline. Generator Qwen/Qwen2.5-1.5B-Instruct (Apache 2.0), retriever e5-small, chunk sent40. পাঁচটি অবস্থা: প্রথম ১, ৩ আর ৫টি chunk; oracle (gold নথির সব chunk; উত্তর নেই এমন প্রশ্নে k = ৩-এর একই chunk, কারণ সেখানে কোনো gold নেই); আর ৮০ অক্ষরের টুকরোর প্রথম ৩টি। Prompt সব অবস্থায় একই:

আপনি বাজারঘরের গ্রাহকসেবা সহায়ক। শুধু নিচের উৎসগুলোর তথ্য দিয়ে প্রশ্নের ভাষায় সংক্ষেপে উত্তর দিন। প্রতিটি তথ্যের পরে [1] বা [2]-এর মতো করে উৎসের নম্বর দিন। উৎসে উত্তর না থাকলে শুধু লিখুন: উৎসে এই তথ্য নেই।

তারপর user বার্তায় “উৎস: [1] (শিরোনাম, হালনাগাদ তারিখ) লেখা …” আর প্রশ্ন। Decoding greedy (do_sample=False, seed 0), সর্বোচ্চ ১৬০টি নতুন token, bfloat16, GPU-তে। ৮০টি উত্তর (k৩ আর oracle) মোট ৮২ সেকেন্ডে; সর্বোচ্চ GPU memory ৩,০৫১ MB. সত্যিকারের run, ফল গোনা (সংক্ষিপ্ত):

~/bazarghar-dl$ uv run python score.py
condition    acc   ans-acc  unans-abst  ret-hit  all-gold  cite-only  avg-in-tok  avg-sec
k1          0.450    0.353       1.000    0.794     0.706         1         490     0.62
k3          0.525    0.529       0.500    0.971     0.941         7         959     1.17
k5          0.475    0.529       0.167    0.971     0.941         5        1389     1.55
oracle      0.550    0.559       0.500    1.000     1.000         2         653     0.88
fixed80_k3  0.525    0.559       0.333    0.882     0.794         2         638     1.63

by question type, auto acc (k3 | oracle | fixed80_k3):
  single        n=18  0.67  0.56  0.72
  numeric       n= 4  0.00  0.00  0.00
  multi         n= 4  0.00  0.25  0.25
  banglish      n= 4  0.50  1.00  0.50
  english       n= 4  1.00  1.00  0.75
  unanswerable  n= 6  0.50  0.50  0.33

citations (answerable answers that cite at least one source):
  k3          n= 8  precision 0.750  recall 0.688
  k5          n=10  precision 0.600  recall 0.600
  oracle      n= 2  precision 1.000  recall 0.750
  fixed80_k3  n=21  precision 0.810  recall 0.762
…
auto vs my labels:
  correctness k3          human acc 0.525  auto acc 0.525  agree 1.000  kappa 1.000
  correctness fixed80_k3  human acc 0.450  auto acc 0.525  agree 0.925  kappa 0.851
     disagree: ['s18', 'm04', 'b02']
  number-check faithfulness vs my F labels: n=63  my unfaithful 19  check says unfaithful 8  agree 0.762  kappa 0.324

এখান থেকে পাঁচটি শিক্ষা। মনে রাখবেন, এগুলো একটি ছোট model আর ৪০টি প্রশ্নের একটি local run, সাধারণ সত্য নয়।

  1. বাধা retriever-এ নয়। k = ৩-এ ঠিক নথি এসেছে ৯৭% প্রশ্নে, অথচ উত্তর ঠিক ৫৩%. Oracle-এ retrieval নিখুঁত, তবু উত্তর ঠিক ৫৬%. অর্থাৎ বাকি ভুলের প্রায় সবটাই generator-এর। এই সিদ্ধান্ত oracle ছাড়া নেওয়া যেত না; শুধু শেষ সংখ্যা ৫২.৫% দেখে কেউ হয়তো retriever বদলাতে সপ্তাহ খরচ করতেন।
  2. k একটি দাঁড়িপাল্লা। k = ১-এ model উত্তর নেই এমন ৬টি প্রশ্নেই ঠিকভাবে “উৎসে নেই” বলেছে, কিন্তু উত্তর আছে এমন ৩৪টি প্রশ্নের ১৬টিতেও তাই বলেছে। k = ৫-এ উল্টো: বেশি লেখা হাতে পেয়ে উত্তর নেই এমন ৬টির ৫টিতে “উৎসে নেই” বলেনি, আর তার ৪টিতে কাছাকাছি তথ্য জুড়ে উত্তর বানিয়েছে। Prompt-এর দৈর্ঘ্যও ৪৯০ থেকে ১,৩৮৯ token.
  3. সংখ্যার হিসাব আর বহু নথি সবচেয়ে দুর্বল। চারটি হিসাবের প্রশ্নের একটিও কোনো অবস্থায় ঠিক হয়নি। এই কাজে 1.5B model-এর সীমা স্পষ্ট; হিসাবের অংশ কোডে করা (পর্ব ৭১-এর tool) একটি বাস্তব সমাধান।
  4. Citation বেশি দেখাই ভালো নয়। Citation precision হিসাব হয় শুধু যেসব উত্তরে নম্বর আছে তাদের ওপর: k৩-এ মাত্র ৮টি. ছোট n-এর হার আলাদা করে লিখুন। আরেকটি বিপদ: আমার নিজের গোনায় k৩-এর ৫টি উত্তর ছিল শুধু “উত্তর: [1]” বা “উৎস: [1]”, উত্তর ছাড়াই নম্বর। স্বয়ংক্রিয় cite-only কলাম ৭ দেখায়, কারণ সেটি “৩” বা “উত্তর: 1”-এর মতো শুধু-সংখ্যার ঠিক উত্তরকেও এই দলে ফেলেছে; আরেকটি নীরব মাপ-ভুল।
  5. স্বয়ংক্রিয় মাপ নিজে যাচাই করুন। k৩-এর ৪০টি উত্তরে key-মেলানো আর আমার নিজের রায় হুবহু মিলেছে, কিন্তু ৮০ অক্ষরের অবস্থায় ৩টি অমিল: b02-তে “৫ মিনিট” আছে বলে key পাশ, অথচ পাশে বানানো “৩০ সেকেন্ড”।

পার্থক্যগুলো কি আসল? একই ৪০টি প্রশ্নে মাপা, তাই paired bootstrap (মেশিন লার্নিং কোর্সের পর্ব ২১ আর পর্ব ২২)। নিচের block সত্যিকারের প্রতি-প্রশ্ন ফল থেকে ভুলের শ্রেণি গোনে আর ব্যবধান বের করে; desktop-এ একই seed-এ একই সংখ্যা এসেছিল।

import numpy as np
from collections import Counter
# সত্যিকারের run: ৪০টি প্রশ্নে স্বয়ংক্রিয় key-মিলের ফল (১ = ঠিক), একই প্রশ্ন-ক্রমে
runs = {
    "k1":     "0001011000111101000000000000101101111111",
    "k3":     "1001011010111101110000000000111111011001",
    "k5":     "1001001010111111100000001000111111010000",
    "oracle": "1101011000111101000000001011111111011001",
}
R = {c: np.array([int(ch) for ch in s]) for c, s in runs.items()}
# k3-এর ৪০টি উত্তরে আমার হাতে দেওয়া ভুল-শ্রেণি
kinds = ("ok cite_only cite_only ok stale ok ok cite_only ok unsupported ok ok ok ok "
         "cite_only ok ok ok ignored number number number partial unsupported number "
         "partial miss unsupported ok ok ok ok ok ok cite_only ok ok halluc_un halluc_un ok").split()
print("k3 ভুল-শ্রেণি:", Counter(kinds).most_common())

rng = np.random.default_rng(0)
idx = rng.integers(0, 40, (10000, 40))       # একই resample সব তুলনায়
base = R["k3"]
for c in ("k1", "k5", "oracle"):
    d = R[c] - base
    bs = d[idx].mean(axis=1)
    lo, hi = np.percentile(bs, [2.5, 97.5])
    win, lose = int(((R[c] == 1) & (base == 0)).sum()), int(((R[c] == 0) & (base == 1)).sum())
    print(f"{c:6} - k3: {d.mean():+.3f}  95% CI [{lo:+.3f}, {hi:+.3f}]  ভালো {win}, খারাপ {lose}")
# শুধু উত্তর নেই এমন ৬টি প্রশ্ন (শেষের ৬টি): abstain-এর হার
print("abstain, উত্তর নেই এমন ৬টি:", {c: int(R[c][-6:].sum()) for c in R})

আউটপুট (পাতার Python-এ চালানো):

k3 ভুল-শ্রেণি: [('ok', 21), ('cite_only', 5), ('number', 4), ('unsupported', 3), ('partial', 2), ('halluc_un', 2), ('stale', 1), ('ignored', 1), ('miss', 1)]
k1     - k3: -0.075  95% CI [-0.225, +0.075]  ভালো 3, খারাপ 6
k5     - k3: -0.050  95% CI [-0.175, +0.075]  ভালো 2, খারাপ 4
oracle - k3: +0.025  95% CI [-0.100, +0.150]  ভালো 4, খারাপ 3
abstain, উত্তর নেই এমন ৬টি: {'k1': 6, 'k3': 3, 'k5': 1, 'oracle': 3}

তিনটি ব্যবধানেই ০ আছে। Oracle k৩-এর চেয়ে ৪টি প্রশ্নে ভালো, ৩টিতে খারাপ; মোট পার্থক্য মাত্র একটি প্রশ্ন। ৪০টি প্রশ্নে ±০.১৫-এর মতো ব্যবধান মানে ৫ শতাংশ বিন্দুর পার্থক্য ধরা অসম্ভব। দুটি জিনিস বলা যায়: ফলের পার্থক্য প্রশ্নভেদে এলোমেলো, আর oracle-এও ভালো না হওয়া মানে generator-ই সীমা। আরও প্রশ্ন লিখুন, অথবা একই প্রশ্নের ধরনভিত্তিক ফল দেখুন।

৭ · ভুলের শ্রেণিবিন্যাস — একটি ভুল কোন ধাপের

সংখ্যা বলে কতটা খারাপ, error analysis বলে কেন। প্রতিটি ভুল উত্তরে পরপর চারটি প্রশ্ন করুন। প্রথম “না”-তেই শ্রেণি ঠিক হয়ে যায়। নিচের পাঁচটি ঘটনা সব আমাদের run-এর।

প্রথম “না” যেখানে, শ্রেণি সেখানে
প্রশ্ন
১ gold নথি এল?
২ প্রমাণ chunk-এ?
৩ উত্তর দিল?
৪ উত্তর উৎসের সঙ্গে ঠিক?
শ্রেণি
  1. Retrieval miss. Banglish প্রশ্নে e5 ইংরেজি আর Banglish নথি আনল, গাজীপুরের চার্জের বাংলা নথি আনল না। Model লিখল জোড়াতালির বাক্য। সমাধান retriever-এ: hybrid খোঁজ, query rewriting (পর্ব ৬৯)।
  2. Wrong chunk. ঠিক নথি এসেছে, কিন্তু ৮০ অক্ষরের যে টুকরোয় “৯৯৯ টাকা”, সেটি আসেনি। Model লিখল “বার্ষিক ফি ছিল: - টাকা”। hit@k-এ এটি সফল দেখায়; ধরা পড়ে শুধু context recall-এ। সমাধান chunking-এ।
  3. Context উপেক্ষা. প্রমাণ হাতে, তবু “উৎসে এই তথ্য নেই”। হিসাব লাগবে বলে model উত্তর এড়াল। একই ধরনের আরও ৫টি k৩-উত্তর ছিল শুধু উৎসের নম্বর। সমাধান generator-এ: prompt, উদাহরণ, বড় model, বা হিসাব কোডে।
  4. বানানো সংখ্যা. ৫০ আর ২ ঠিক তুলেছে, গুণফল ভুল। Faithfulness-এর সংখ্যা-পরীক্ষা এটি ধরে (অংশ ৫)। ছোট model-এ বাংলা অঙ্কের হিসাব বিশেষ ঝুঁকির।
  5. পুরোনো নথি. সবচেয়ে চতুর ভুল: উত্তর উৎসে হুবহু আছে, তাই faithfulness পূর্ণ, citation-ও ঠিক হতে পারত। দোষ ingest-এর: বাতিল নথি index-এ রয়ে গেছে। সমাধান metadata ফিল্টার (status = active) আর তারিখ।

আরেকটি পরিচিত শ্রেণি “ভুল ভাষা”। আমাদের run-এ k৩-এ চারটি ইংরেজি প্রশ্নের উত্তর ইংরেজিতেই এসেছে; Banglish প্রশ্নের উত্তর এসেছে বাংলা লিপিতে। সেটি ভুল কি না, তা আপনার নীতির ওপর নির্ভর করে, তাই নিয়মটি আগে লিখে রাখুন। দুই ধরনের ৪০টি করে উত্তরে আমার নিজের শ্রেণি গোনা (একজন লেবেলার, তাই নিজেই পক্ষপাতদুষ্ট হতে পারি):

my error tally:
  k3          {'ok': 21, 'cite_only': 5, 'number': 4, 'unsupported': 3, 'partial': 2, 'halluc_un': 2, 'stale': 1, 'ignored': 1, 'miss': 1}
  fixed80_k3  {'ok': 18, 'miss': 4, 'halluc_un': 4, 'cite_only': 3, 'partial': 3, 'number': 2, 'unsupported': 2, 'stale': 1, 'ignored': 1, 'wrongchunk': 1, 'garbled': 1}

ছবিটা পরিষ্কার: বাক্য-ভিত্তিক chunk-এ retrieval-এর ভুল মাত্র ১টি, বাকি সব generator-এর (শুধু নম্বর, বানানো সংখ্যা, উৎসে নেই এমন দাবি, অর্ধেক উত্তর)। ৮০ অক্ষরের টুকরোয় retrieval miss বেড়ে ৪টি। কোথায় খাটবেন, সেটি এই তালিকাই বলে।

৮ · LLM-as-judge দিয়ে faithfulness — আর তাকে যাচাই

৬৩টি উত্তর হাতে পড়ে লেবেল দেওয়া যায়, কিন্তু হাজার উত্তরে সেটি ধীর আর দামি, তাই প্রচলিত পথ LLM-as-judge: আরেকটি model-কে উৎস আর উত্তর দিয়ে জিজ্ঞেস করা, “প্রতিটি দাবি কি উৎসে আছে?” পর্ব ৫৮-এ দেখেছেন বিচারকের পক্ষপাত: position bias, লম্বা উত্তরের দিকে ঝোঁক, self-preference. এখানে আমরা মাপব।

বিচারক একই Qwen2.5-1.5B-Instruct (অর্থাৎ নিজের উত্তর নিজে বিচার করছে; এটি ইচ্ছাকৃত দুর্বল পরীক্ষা, বড় বিচারক এখানে চালানো যায়নি)। Prompt-এ বলা হলো শুধু YES বা NO লিখতে; পরের token-এর logits থেকে P(YES) নিয়েছি, ০.৫-এর বেশি হলে YES. প্রতিটি উত্তর দুবার: একবার উৎস আগে, উত্তর পরে; একবার উল্টো ক্রমে। তুলনা আমার নিজের faithfulness লেবেলের সঙ্গে (৬৩টি উত্তর যাতে কোনো দাবি আছে; ৪৪টি faithful, ১৯টি নয়)। সত্যিকারের run (GPU-তে, ১২৬টি বিচার ৩০.৫ সেকেন্ডে, সর্বোচ্চ ৩,২৭৯ MB; সংক্ষিপ্ত):

~/bazarghar-dl$ uv run python judge.py
token ids YES [14004] NO [8996]
k3 s01 1 0.991 'YES'  0.349 'NO'
k3 s04 1 0.010 'NO'  0.068 'NO'
k3 s10 0 0.755 'YES'  0.068 'NO'
k3 n02 0 0.438 'NO'  0.042 'NO'
k3 m02 0 0.562 'YES'  0.107 'NO'
k3 e02 1 0.958 'YES'  0.026 'NO'
…
fixed80_k3 s16 1 0.500 'NO'  0.706 'YES'
fixed80_k3 b02 0 0.060 'NO'  0.321 'NO'
fixed80_k3 u06 0 0.202 'NO'  0.531 'YES'
meta {'n': 63, 'sec': 30.5, 'peak_mb': 3279}

প্রতিটি লাইনে: অবস্থা, প্রশ্ন, আমার লেবেল, উৎস-আগে P(YES) আর লেখা উত্তর, উত্তর-আগে P(YES) আর লেখা উত্তর। s04-এর মতো একদম ঠিক উত্তরেও (“90 টাকা”, উৎসে আছে) বিচারক NO বলেছে; s10-এর ভুল উত্তরে YES. এখন মিল মাপি। শুধু “কতবার একমত” যথেষ্ট নয়, কারণ দুজনেই বেশিরভাগ সময় একই উত্তর দিলে কাকতালীয় মিলও বেশি হয়। Cohen's kappa সেই কাকতালীয় অংশ বাদ দেয়।

উৎস আগে, উত্তর পরে (সত্যিকারের সংখ্যা)
ধাপ
হিসাব
  1. সারণি. ৬৩টি উত্তরে দুজনের রায় চার ঘরে। বিচারক আমার ৪৪টি faithful-এর ২১টিকে NO বলেছে, আর ১৯টি unfaithful-এর ৪টিকে YES.
  2. দেখা মিল. একমত ২৩ + ১৫ = ৩৮টিতে: po = ০.৬০৩. শুনতে মন্দ নয়।
  3. দুজনের হার. বিচারক YES বলে ৪২.৯% সময়, আমি faithful বলি ৬৯.৮% সময়।
  4. কাকতালীয় মিল. দুজন যদি নিজের হারে এলোমেলো রায় দিত, তবুও মিল হতো pe = ০.৪৭২. অর্থাৎ ৬০% মিলের বেশিরভাগই এমনিতেই আসত।
  5. Kappa. κ = (po − pe) ÷ (১ − pe) = ০.২৪৯. Landis আর Koch-এর (১৯৭৭) প্রচলিত ভাগে এটি “fair”, অর্থাৎ দুর্বল; ভাগগুলো নিজেও বিতর্কিত, শুধু মোটা দাগের দিক নির্দেশক।
  6. ক্রম উল্টালে. শুধু উত্তর আগে, উৎস পরে দিলে বিচারক প্রায় সবকিছুতে NO বলে, kappa −০.০৫১, অর্থাৎ এলোমেলোর চেয়েও খারাপ। ৬৩টির ৩০টি রায় শুধু ক্রমের কারণে উল্টেছে: position bias-এর সরাসরি মাপ।

একই হিসাব কোডে, সঙ্গে kappa-র bootstrap ব্যবধান। সংখ্যাগুলো বিচারকের সত্যিকারের P(YES), তিন দশমিকে কাটা।

import numpy as np
# আমার faithfulness লেবেল (১ = সব দাবি উৎসে আছে), ৬৩টি উত্তর
H = np.array([int(c) for c in "111111011111110001001001111110111111111111011100010100101111010"])
# Qwen2.5-1.5B বিচারকের P(YES), দুই ক্রমে (সত্যিকারের run)
p_src = [.991,.010,.321,.438,.018,.818,.755,.076,.014,.777,.349,.223,.269,.148,.438,.018,.294,.202,.562,.085,.047,
         .085,.053,.076,.651,.651,.958,.042,.438,.085,.867,.881,.706,.798,.915,.818,.731,.852,.924,.085,.593,.469,
         .245,.500,.378,.940,.085,.500,.706,.562,.562,.562,.148,.060,.068,.245,.836,.940,.867,.269,.245,.798,.202]
p_ans = [.349,.068,.223,.182,.042,.095,.068,.020,.060,.068,.223,.042,.076,.294,.042,.182,.531,.294,.107,.119,.223,
         .033,.085,.020,.042,.164,.026,.294,.119,.469,.245,.037,.095,.076,.042,.107,.053,.269,.085,.026,.068,.202,
         .294,.706,.245,.223,.245,.107,.107,.245,.223,.060,.294,.321,.026,.076,.148,.133,.321,.223,.438,.053,.531]

def kappa(a, b):
    po = (a == b).mean()
    pe = a.mean() * b.mean() + (1 - a.mean()) * (1 - b.mean())
    return po, pe, (po - pe) / (1 - pe)

rng = np.random.default_rng(0)
idx = rng.integers(0, len(H), (2000, len(H)))
for name, p in (("উৎস আগে", p_src), ("উত্তর আগে", p_ans)):
    J = (np.array(p) > 0.5).astype(int)
    po, pe, k = kappa(J, H)
    ks = [kappa(J[i], H[i])[2] for i in idx]
    lo, hi = np.percentile(ks, [2.5, 97.5])
    print(f"{name}: YES-এর হার {J.mean():.3f}  মিল {po:.3f}  pe {pe:.3f}  kappa {k:.3f}  95% CI [{lo:.2f}, {hi:.2f}]")
    print(f"   সারণি: মানুষ ১/বিচারক ১ = {((J==1)&(H==1)).sum()}, ১/০ = {((J==0)&(H==1)).sum()},"
          f" ০/১ = {((J==1)&(H==0)).sum()}, ০/০ = {((J==0)&(H==0)).sum()}")
A, B = (np.array(p_src) > 0.5), (np.array(p_ans) > 0.5)
print("শুধু ক্রম বদলে রায় উল্টেছে:", int((A != B).sum()), "/", len(H))

আউটপুট (পাতার Python-এ চালানো):

উৎস আগে: YES-এর হার 0.429  মিল 0.603  pe 0.472  kappa 0.249  95% CI [0.05, 0.45]
   সারণি: মানুষ ১/বিচারক ১ = 23, ১/০ = 21, ০/১ = 4, ০/০ = 15
উত্তর আগে: YES-এর হার 0.048  মিল 0.286  pe 0.320  kappa -0.051  95% CI [-0.16, 0.03]
   সারণি: মানুষ ১/বিচারক ১ = 1, ১/০ = 43, ০/১ = 2, ০/০ = 17
শুধু ক্রম বদলে রায় উল্টেছে: 30 / 63

উৎস-আগে ক্রমে kappa-র ৯৫% ব্যবধান ০.০৫ থেকে ০.৪৫: শূন্যের একটু ওপরে, কিন্তু কাজে লাগানোর মতো নয়। এর চেয়ে সস্তা সংখ্যা-পরীক্ষার kappa ছিল ০.৩২৪. সিদ্ধান্ত তিনটি। এক, বিচারকের রায় কখনো সরাসরি রিপোর্ট করবেন না; আগে ৫০–১০০টি উত্তরে মানুষের লেবেলের সঙ্গে kappa মাপুন। দুই, ক্রম, ভাষা আর prompt বদলে বিচারকের স্থিরতা দেখুন। তিন, বিচারক বড় হলে সাধারণত ভালো হয় বলে রিপোর্ট আছে, কিন্তু সেটিও নিজের ডেটায় মাপতে হয়; বাংলায় বড় model-ও একইভাবে যাচাই না করে বিশ্বাসযোগ্য ধরা যায় না।

৯ · AI-এর যুগে — AI-এর বানানো RAG মূল্যায়ন যাচাই

AI assistant এক মিনিটে একটি RAG evaluation script লিখে দেয়, সঙ্গে সুন্দর একটি সংখ্যা। ভুলগুলো প্রায় সবই নীরব।

যে ভুল প্রায়ই দেখা যায়ফলকীভাবে ধরবেন
শুধু শেষ উত্তরের একটি “accuracy”ভুল কোন ধাপের, জানা যায় নাRetrieval metric, oracle, ভুলের শ্রেণি আলাদা
LLM বিচারকের স্কোর সরাসরি রিপোর্টপক্ষপাত ধরা পড়ে নামানুষের লেবেলের সঙ্গে kappa, ক্রম উল্টে পরীক্ষা
বাংলা অঙ্ক বা কমা normalise না করা; \b-এর ভরসাঠিক উত্তর ভুল গোনা৫–১০টি উত্তর হাতে পড়ে মেলানো
উত্তর নেই এমন প্রশ্ন eval set-এ না রাখাবানানো উত্তর দেখা যায় নাঅন্তত ১০–১৫% এমন প্রশ্ন
পুরোনো বা বানানো API (RAGAS-এর পুরোনো ফাংশনের নাম ইত্যাদি)Error, বা নীরবে অন্য metricনিজের version-এর নথি পড়া
একটি run, n ছাড়া, ব্যবধান ছাড়া “১০% উন্নতি”ভাগ্যের ফলPaired bootstrap, ভালো আর খারাপ প্রশ্ন আলাদা গোনা
বানানো benchmark সংখ্যা বা ভুল citationভুল সিদ্ধান্তপ্রবন্ধ খুলে মেলানো

AI-কে কোডের সঙ্গে প্রমাণও চাইতে বলুন:

আমার একটি বাংলা হেল্প সেন্টারের RAG আছে (৪০টি নথি, ৪০টি হাতে লেখা প্রশ্ন, প্রতিটিতে gold নথি আর key সংখ্যা)। Python-এ একটি মূল্যায়ন script লিখে দাও। ১. Retrieval: recall@k, precision@k, MRR@10, nDCG@10; প্রতিটির সূত্র কমেন্টে। ২. বাংলা অঙ্ক Latin-এ, সংখ্যার কমা বাদ; regex-এ \b ব্যবহার করবে না, অঙ্কের সীমা lookaround দিয়ে। ৩. উত্তর নেই এমন প্রশ্নে abstain ঠিক কি না আলাদা গোনা। ৪. Oracle context-এর অবস্থা, আর k3 বনাম অন্যগুলোর paired bootstrap ব্যবধান (seed দিয়ে)। ৫. LLM বিচারক থাকলে আমার হাতের লেবেলের সঙ্গে Cohen's kappa আর ক্রম উল্টে পরীক্ষা। ৬. কোনো library-র API নিশ্চিত না হলে বলো; কোনো benchmark সংখ্যা বানিয়ো না।

AI-এর দেওয়া RAG মূল্যায়ন হাতে পেলে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Retrieval মাপার জন্য gold নথি হাতে ঠিক করার এই প্রথা কোথা থেকে এল, আর RAG-এ তা কতটা খাটে?

প্রথাটি তথ্য-খোঁজার (information retrieval) গবেষণার। ১৯৬০-এর দশকে যুক্তরাজ্যের Cranfield-এ Cyril Cleverdon-এর পরীক্ষাগুলো একটি ধাঁচ দাঁড় করায়: একটি নির্দিষ্ট নথি-সংগ্রহ, একটি প্রশ্ন-তালিকা, আর মানুষের ঠিক করা প্রাসঙ্গিকতার রায়। এরপর যেকোনো খোঁজার পদ্ধতি একই মাপকাঠিতে তুলনা করা যায়। ১৯৯২ থেকে যুক্তরাষ্ট্রের NIST-এর TREC এই ধাঁচকে বড় পরিসরে নিয়ে যায়। সেখানে সব নথির রায় দেওয়া অসম্ভব বলে “pooling” চালু হয়: অংশ নেওয়া সিস্টেমগুলোর প্রথম কয়েকটি ফল একসঙ্গে করে শুধু সেগুলো মানুষ বিচার করেন। MRR প্রশ্নোত্তরের TREC ট্র্যাকে (১৯৯৯) পরিচিতি পায় বলে উল্লেখ পাওয়া যায়, আর nDCG আসে Järvelin ও Kekäläinen-এর ২০০২ সালের প্রবন্ধে, যেখানে প্রাসঙ্গিকতার মাত্রা আর অবস্থানের ছাড় দুটোই ধরা হয়। পরে BEIR (২০২১) বহু ধরনের ডেটায় nDCG@১০-কে প্রায় মানদণ্ড বানায়।

RAG-এ এই ধাঁচ খাটে, তবে তিনটি পার্থক্য আছে। এক, পাঠক এখানে মানুষ নয়, LLM. মানুষ প্রথম ফলটাই বেশি পড়েন, তাই ছাড়ের সূত্র অর্থবহ; LLM প্রথম k-টির সবই পড়ে, তাই RAG-এ recall@k প্রায়ই nDCG-র চেয়ে বেশি জরুরি। তবে long context-এ মাঝের তথ্য model কম ব্যবহার করে বলে Liu ও সহকর্মীদের (“Lost in the Middle”, ২০২৩) মতো কাজে রিপোর্ট আছে, তাই ক্রম পুরো অর্থহীনও নয়। দুই, একক নথি নয়, chunk আনা হয়; নথি ঠিক এসেও উত্তরের বাক্যটি বাদ পড়তে পারে, যা আমাদের wrong chunk ঘটনায় দেখেছেন। তিন, নথির সংগ্রহ বদলায়; বাজারঘরের bh40-এর মতো পুরোনো নথি থেকে যায়, আর gold তালিকা পুরোনো হয়ে যায়।

মূল উপলব্ধি: Cranfield-এর ধাঁচ (নির্দিষ্ট সংগ্রহ, প্রশ্ন, মানুষের রায়) আজও ভিত্তি; RAG-এ শুধু মাপার এককটা নথি থেকে chunk আর প্রমাণে নামিয়ে আনতে হয়, আর সংগ্রহ বদলালে gold-ও হালনাগাদ করতে হয়।

প্র ০২Faithful কিন্তু ভুল, আর ঠিক কিন্তু unfaithful — দুটোই কীভাবে সম্ভব, আর কোনটি বেশি বিপজ্জনক?

Faithfulness মাপে উত্তর আর দেওয়া উৎসের সম্পর্ক; সঠিকতা মাপে উত্তর আর বাস্তবের সম্পর্ক। দুটির মাঝে বসে আছে উৎসের মান। উৎস ভুল বা পুরোনো হলে model যত বিশ্বস্তভাবে উৎস মানবে, তত বিশ্বস্তভাবে ভুল বলবে। আমাদের “১,০০০ টাকায় ফ্রি ডেলিভারি” উত্তর ঠিক এটাই: উৎসে হুবহু আছে, কিন্তু নথিটি ২০২৫ সালে বাতিল।

উল্টোটাও ঘটে। LLM প্রশিক্ষণের সময় শেখা জ্ঞান (parametric knowledge) থেকে ঠিক কথা বলতে পারে, যা দেওয়া উৎসে নেই। সাধারণ জ্ঞানের প্রশ্নে এটি সুবিধা মনে হয়। কিন্তু বাজারঘরের মতো নিজস্ব নীতির ক্ষেত্রে model-এর “জানা” মানে অন্য কোনো দোকানের নীতি, বা অনুমান। Lewis ও সহকর্মীদের ২০২০ সালের RAG প্রবন্ধের মূল যুক্তিই ছিল জ্ঞানকে model-এর ওজনের বাইরে রাখা, যাতে হালনাগাদ করা যায় আর উৎস দেখানো যায়। Unfaithful কিন্তু ঠিক উত্তর সেই সুবিধা নষ্ট করে: পাঠক citation খুলে মেলাতে পারেন না, আর আজ ঠিক হলেও কাল নীতি বদলালে model পুরোনো কথাই বলবে।

কোনটি বেশি বিপজ্জনক? প্রেক্ষাপটের ওপর নির্ভর করে। গ্রাহকসেবায় ভুল টাকার অঙ্ক সরাসরি ক্ষতি করে, তাই দুই ভুলই গুরুতর। তবে ধরার দিক থেকে faithful-কিন্তু-ভুল কঠিন: faithfulness metric, citation, এমনকি বিচারক LLM-ও একে পাশ করায়। একে ধরে শুধু gold উত্তরসহ eval set আর নথির metadata (তারিখ, অবস্থা)। এজন্যই শুধু reference-free metric-এর ওপর ভরসা করা ঝুঁকিপূর্ণ।

মূল উপলব্ধি: Faithfulness generator-এর সততা মাপে, সঠিকতা পুরো system-এর। দুটিই মাপুন, আর উৎসের মান (তারিখ, অবস্থা, দায়িত্ব) আলাদা করে দেখুন।

প্র ০৩LLM বিচারক যদি এত পক্ষপাতদুষ্ট হয়, তাহলে বড় প্রতিষ্ঠানগুলো কেন তা ব্যবহার করে? কীভাবে নিরাপদে ব্যবহার করা যায়?

কারণ বিকল্পগুলো আরও খারাপ বা ব্যয়বহুল। মানুষের লেবেল ধীর আর দামি; ROUGE বা BLEU-র মতো শব্দ-মেলানো metric (পর্ব ৪৫) মুক্ত-লেখা উত্তরে অর্থ ধরে না। Zheng ও সহকর্মীদের ২০২৩ সালের MT-Bench আর Chatbot Arena প্রবন্ধ দেখায়, শক্তিশালী LLM বিচারক কিছু কাজে মানুষের সঙ্গে প্রায় মানুষে-মানুষে মিলের মাত্রায় একমত হয়, আবার একই প্রবন্ধে position bias, লম্বা উত্তরের দিকে ঝোঁক আর নিজের উত্তরের প্রতি পক্ষপাতও নথিভুক্ত।

আমাদের run একটি চরম উদাহরণ: 1.5B model নিজের উত্তর বিচার করল, kappa ০.২৪৯, আর শুধু ক্রম বদলে অর্ধেক রায় উল্টাল। এখান থেকে নিরাপদ ব্যবহারের নিয়ম বেরোয়। এক, ক্যালিব্রেশন: প্রথমে ৫০–২০০টি উত্তরে মানুষের লেবেল, তারপর kappa আর ভুলের ধরন। দুই, স্থিরতা: ক্রম উল্টানো, দুবার জিজ্ঞেস করা, ভিন্ন prompt; রায় বদলালে সেই বিচারক অনির্ভরযোগ্য। তিন, সংকীর্ণ কাজ: “উত্তর ভালো কি না” নয়, “এই একটি দাবি এই অনুচ্ছেদে আছে কি না” জিজ্ঞেস করুন; কাজ যত ছোট, বিচার তত স্থির। চার, পরিসংখ্যানে শুধরানো: ARES এই পথে যায়: বিচারকের ভুলের হার মানুষের অল্প লেবেল দিয়ে মেপে prediction-powered inference (Angelopoulos ও সহকর্মী, ২০২৩) দিয়ে চূড়ান্ত হারের ব্যবধান শুধরে নেয়।

আরেকটি কথা: বিচারক আর generator একই পরিবারের হলে self-preference-এর ঝুঁকি থাকে, তাই পারলে ভিন্ন পরিবারের বিচারক নিন, আর যে সিদ্ধান্তের দাম বেশি, সেখানে মানুষ রাখুন।

মূল উপলব্ধি: LLM বিচারক একটি মাপযন্ত্র; যেকোনো মাপযন্ত্রের মতো ব্যবহারের আগে মানদণ্ডের সঙ্গে মিলিয়ে নিতে হয়।

প্র ০৪৪০টি প্রশ্নে দুটি RAG সেটআপের পার্থক্য নিশ্চিত হলো না। তাহলে কতগুলো প্রশ্ন লাগবে?

মোটা হিসাব করা যায়। Paired তুলনায় যা গুরুত্বপূর্ণ তা হলো যেসব প্রশ্নে দুই সেটআপের ফল আলাদা (discordant pair). আমাদের oracle বনাম k৩-এ এমন প্রশ্ন ৭টি (৪টিতে oracle ভালো, ৩টিতে খারাপ)। পার্থক্যের মান d = (৪ − ৩) ÷ ৪০ = ০.০২৫, আর এর standard error মোটামুটি √(৭) ÷ ৪০ ≈ ০.০৬৬; তাই ৯৫% ব্যবধান প্রায় ±০.১৩, যা bootstrap-এর [−০.১০০, +০.১৫০]-এর কাছাকাছি।

এবার উল্টো প্রশ্ন: ৫ শতাংশ বিন্দুর সত্যিকারের উন্নতি ধরতে চাইলে? ধরুন প্রায় ১৫–২০% প্রশ্নে দুই সেটআপের ফল আলাদা হয়। তখন n প্রশ্নে standard error প্রায় √(০.১৮ n) ÷ n. ১.৯৬ × SE < ০.০৫ হতে লাগে প্রায় n > ২৮০, আর স্বাভাবিক power চাইলে আরও বেশি, কয়েকশো থেকে হাজারের কাছাকাছি। শিল্পে RAG eval set তাই প্রায়ই কয়েকশো থেকে কয়েক হাজার প্রশ্নের হয়, আর দ্রুত সিদ্ধান্তের জন্য ছোট “smoke” set রাখা হয়। এই হিসাব মোটা দাগের; মেশিন লার্নিং কোর্সের পর্ব ২২-এ McNemar পরীক্ষা ঠিক এই discordant pair-এর ওপরই দাঁড়ানো।

প্রশ্ন বাড়ানো ছাড়াও পথ আছে: একই প্রশ্নের কয়েকটি রূপ (বাংলা, Banglish, ইংরেজি) লেখা, যাতে ধরনের ফল আলাদা দেখা যায়; বড় পার্থক্যের প্রস্তাবেই আগে মনোযোগ দেওয়া; আর ভুলের শ্রেণি গোনা, যা ছোট n-এও কোথায় খাটবেন বলে দেয়। বিপরীতে যা করবেন না: একই ৪০টি প্রশ্নে ২০টি prompt চেষ্টা করে সেরাটি বেছে সেই সংখ্যাই রিপোর্ট করা; তখন সেরা সংখ্যাটি নিজেই ভাগ্যের দিকে হেলে থাকে (পর্ব ৬৬)।

মূল উপলব্ধি: ছোট পার্থক্য ধরতে বড় নমুনা লাগে; নমুনা ছোট হলে দাবিও ছোট রাখুন, আর ভুলের গুণগত বিশ্লেষণে ভর দিন।

প্র ০৫বাংলা ডকুমেন্টে RAG মূল্যায়নে এমন কী সমস্যা আছে যা ইংরেজি টিউটোরিয়ালে চোখে পড়ে না?

অন্তত পাঁচটি, আর সবই এই পর্বের run-এ দেখা গেছে। এক, অঙ্কের দুই লিপি: একই model একই run-এ “৭ দিন” আর “7 দিন” লিখেছে; normalise না করলে key-মেলানো আর সংখ্যা-পরীক্ষা দুটোই ভুল গোনে। দুই, regex-এর শব্দসীমা: Python-এ বাংলা অক্ষর “word” অক্ষর, তাই \b “৭টি”-র মতো জোড়া লেখায় কাজ করে না; ইংরেজি টিউটোরিয়ালের কোড কপি করলে এই ভুল নীরবে ঢোকে।

তিন, Banglish আর লিপি পেরোনো: “gazipur e delivery charge koto?” প্রশ্নে retriever বাংলা নথি না এনে ইংরেজি আর Banglish নথি আনল; উত্তরের ভাষা কী হবে (বাংলা লিপি, Banglish, না প্রশ্নের লিপি), সেই নীতিও আগে ঠিক করতে হয়। চার, tokenizer: বাংলা লেখা অনেক বেশি token-এ ভাঙে (পর্ব ৬৫), তাই একই k-তে prompt লম্বা হয়, আর ১৬০ token-এর সীমায় বাংলা উত্তর মাঝপথে কাটে, যা আমাদের কয়েকটি “অর্ধেক উত্তর”-এর কারণ। পাঁচ, সাধারণ মানদণ্ডের অভাব: ইংরেজিতে প্রস্তুত প্রশ্নোত্তর benchmark অনেক; বাংলায় নির্দিষ্ট ক্ষেত্রের RAG benchmark খুব কম বলেই আমার ধারণা, তাই প্রায় সবসময় নিজের eval set লিখতে হয়।

আরেকটি সূক্ষ্ম দিক: LLM বিচারক বাংলায় দুর্বল হতে পারে, কিন্তু সেটি ইংরেজি ফল দেখে অনুমান করা যায় না, মাপতে হয়। একইভাবে সংখ্যা কথায় লেখা (“দুটি”, “তিনবারেও”) সংখ্যা-পরীক্ষাকে ফাঁকি দেয়; বাংলা সংখ্যাবাচক শব্দের একটি ছোট তালিকা রাখলে কিছুটা কাজে লাগে।

মূল উপলব্ধি: মূল্যায়নের কোড ভাষা-নিরপেক্ষ মনে হলেও তা নয়; বাংলা লেখার নিজস্ব নিয়ম মাপযন্ত্রে বসাতে হয়, আর প্রতিটি স্বয়ংক্রিয় মাপ কিছু উত্তর হাতে পড়ে যাচাই করতে হয়।

অনুশীলন

কাগজ-কলমে হিসাব, নকশা আর সমালোচনা। প্রতিটির উত্তর দেখার আগে নিজে চেষ্টা করুন।

  1. হাতে হিসাব: একটি প্রশ্নের gold নথি ৩টি। Retriever-এর প্রথম ৫টি ফলের প্রাসঙ্গিকতা [০, ১, ১, ০, ১]. Recall@৩, precision@৫, reciprocal rank আর nDCG@৫ বের করুন (log2৩ ≈ ১.৫৮৫, log2৫ ≈ ২.৩২২, log2৬ ≈ ২.৫৮৫)।

    Recall@৩ = ২ ÷ ৩ ≈ ০.৬৬৭ (প্রথম ৩-এ দুটি gold). Precision@৫ = ৩ ÷ ৫ = ০.৬. প্রথম gold rank ২-এ, তাই RR = ০.৫.

    DCG@৫ = ১/log2৩ + ১/log2৪ + ১/log2৬ = ০.৬৩১ + ০.৫০০ + ০.৩৮৭ = ১.৫১৮. IDCG (তিনটি gold rank ১, ২, ৩-এ) = ১ + ০.৬৩১ + ০.৫০০ = ২.১৩১. nDCG@৫ = ১.৫১৮ ÷ ২.১৩১ ≈ ০.৭১২.

  2. নকশা: বাজারঘর নতুন “কিস্তি আর EMI” নীতির ১০টি নথি যোগ করবে। আগের eval set-এ কোন কোন প্রশ্ন যোগ করবেন, আর পুরোনো নথির ঝুঁকি কীভাবে মাপবেন?

    প্রতিটি নতুন নথির জন্য অন্তত ১–২টি এক-নথির প্রশ্ন (বাংলা, একটি Banglish), একটি হিসাবের প্রশ্ন (যেমন “১২,০০০ টাকার ফোন ৬ মাসে নিলে মাসে কত?”), একটি বহু-নথির প্রশ্ন (EMI আর ফেরত নীতি একসঙ্গে), আর ২–৩টি উত্তর নেই এমন প্রশ্ন (যেমন এমন ব্যাংক যার নাম নথিতে নেই)।

    পুরোনো EMI নথি সরাতে না পারলে একটি বিশেষ পরীক্ষা: যেসব প্রশ্নে পুরোনো আর নতুন নথি ভিন্ন সংখ্যা দেয়, সেখানে উত্তরে পুরোনো সংখ্যা এল কি না আলাদা গুনুন (“stale rate”), আর metadata ফিল্টার চালু ও বন্ধ করে paired তুলনা করুন। প্রশ্নগুলো retriever চালানোর আগেই লিখে রাখুন।

  3. আউটপুট অনুমান: নিচের কোড কী ছাপবে?
    import re
    src = "চার্জ ১২০ টাকা, প্রতি বাড়তি কেজিতে ২০ টাকা।"
    ans = "৩ কেজির চার্জ ১৬০ টাকা: ২টি বাড়তি কেজিতে ৪০ টাকা।"
    d = str.maketrans("০১২৩৪৫৬৭৮৯", "0123456789")
    n = lambda s: set(re.findall(r"\d+", s.translate(d)))
    print(sorted(n(ans) - n(src)), bool(re.search(r"\b2\b", ans.translate(d))))

    ছাপবে ['160', '2', '3', '40'] False (desktop-এ চালিয়ে মিলিয়েছি)। উত্তরের চারটি সংখ্যার কোনোটিই উৎসে নেই, তাই সংখ্যা-পরীক্ষা সবগুলোকে “উৎসের বাইরে” বলবে, অথচ হিসাবটি ঠিক: ১২০ + ২ × ২০ = ১৬০. হিসাবের প্রশ্নে তাই প্রশ্নের সংখ্যা আর অনুমোদিত হিসাবও মেলাতে হয়।

    দ্বিতীয় মান False, কারণ translate-এর পর লেখায় আছে “2টি”. Python-এর regex-এ “ট” একটি word অক্ষর, তাই 2 আর ট-এর মাঝে কোনো শব্দসীমা নেই, আর \b2\b মেলে না। Key-মেলানোয় এভাবেই ঠিক উত্তর ভুল গোনা হয়; অঙ্কের সীমা lookaround দিয়ে লিখুন।

  4. সমালোচনা: একটি দলের রিপোর্ট: “আমাদের RAG-এর faithfulness ৯২%, একটি LLM বিচারক ৫০০টি উত্তরে মেপেছে। তাই উত্তরগুলো নির্ভরযোগ্য।” কী কী জিজ্ঞেস করবেন?
    • বিচারক কোন model, কোন prompt, কোন decoding? মানুষের লেবেলের সঙ্গে kappa কত, কতটি উত্তরে মাপা?
    • ক্রম উল্টে বা দুবার চালালে রায় কতটা বদলায়? বিচারক আর generator কি একই পরিবারের?
    • Faithfulness ৯২% মানেই সঠিকতা নয়: উৎস পুরোনো বা ভুল হলে? Gold উত্তরের সঙ্গে সঠিকতা আর উত্তর নেই এমন প্রশ্নে abstain-এর হার কত?
    • “উৎসে এই তথ্য নেই” বা শুধু citation-এর উত্তর কীভাবে গোনা হয়েছে? অনেক সময় এগুলো faithful গোনা হয়, যা হারকে ফাঁপিয়ে তোলে।
    • ৯২%-এর ব্যবধান কত, আর ৫০০টি প্রশ্ন কোথা থেকে, প্রশ্নের ধরন কী কী?

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি RAG system অংশ ধরে মাপার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রশ্ন, gold নথি আর রেফারেন্স উত্তর লেখা, retriever চালানোর আগেই
  2. Retrieval মাপা: recall@k, MRR, nDCG
  3. সাধারণ pipeline আর oracle context-এ উত্তর তৈরি
  4. স্বয়ংক্রিয় মাপ চালানো আর কিছু উত্তরে নিজের রায়ের সঙ্গে মেলানো
  5. ভুল উত্তরগুলো শ্রেণিতে ভাগ করা আর paired ব্যবধানসহ রিপোর্ট

একটি প্রশ্নের gold নথি ১টি, retriever সেটি rank ৪-এ এনেছে। Reciprocal rank আর recall@৩ কত?

  • RR = ০.২৫, recall@৩ = ১
  • RR = ০.২৫, recall@৩ = ০
  • RR = ৪, recall@৩ = ০
  • RR = ০, recall@৩ = ০

একটি RAG-এর উত্তর হাতে দেওয়া উৎসের সঙ্গে হুবহু মেলে, কিন্তু উৎসটি একটি বাতিল পুরোনো নথি, তাই উত্তর বাস্তবে ভুল। এই উত্তর সম্পর্কে কোনটি ঠিক?

  • Faithfulness কম, সঠিকতা বেশি
  • দুটোই বেশি
  • Faithfulness বেশি, সঠিকতা কম
  • দুটোই কম

একটি RAG-এ ঠিক নথি প্রথম ৩-এ আসে ৯৭% প্রশ্নে, উত্তর ঠিক হয় ৫৩%. ঠিক নথি সরাসরি হাতে দিলে (oracle) উত্তর ঠিক হয় ৫৬%. প্রথমে কোথায় উন্নতির চেষ্টা করবেন?

  • Generator-এ: prompt, model বা হিসাব কোডে সরানো
  • Retriever-এ: আরও বড় embedding model
  • k ৩ থেকে ১০ করা
  • Chunk আরও ছোট করা

দুজন লেবেলার ১০০টি উত্তরে ৯০টিতে একমত। দুজনেই ৯০% উত্তরকে “faithful” বলেন। কাকতালীয় মিল pe আর Cohen's kappa কত?

  • pe = ০.৫০, κ = ০.৯০
  • pe = ০.৮১, κ = ০.৪৭
  • pe = ০.৫০, κ = ০.৮০
  • pe = ০.৮২, κ ≈ ০.৪৪

একটি বাংলা উত্তর “OTP-র মেয়াদ ৫ মিনিট, প্রতি মিনিটে ৩০ সেকেন্ড বাড়ে।” উৎস শুধু বলে “OTP-র মেয়াদ ৫ মিনিট”। Key-মেলানো (“৫” থাকলেই ঠিক) এই উত্তরকে কী বলবে, আর আসলে কী?

  • Key-মেলানো: ভুল; আসলে: ঠিক
  • Key-মেলানো: ঠিক; আসলে: বানানো দাবিসহ, unfaithful
  • দুটোই: ঠিক
  • দুটোই: ভুল

একটি LLM বিচারককে প্রথমে “উৎস, তারপর উত্তর” ক্রমে দিলে ৪৩% উত্তরে YES বলে, আর “উত্তর, তারপর উৎস” ক্রমে দিলে ৫%. এ থেকে সবচেয়ে যুক্তিসংগত সিদ্ধান্ত কোনটি?

  • দ্বিতীয় ক্রমটি বেশি কড়া, তাই সেটিই সঠিক
  • দুই হারের গড় ২৪% রিপোর্ট করাই যথেষ্ট
  • বিচারক ক্রমের ওপর নির্ভরশীল; মানুষের লেবেল দিয়ে যাচাই ছাড়া এর ফল অনির্ভরযোগ্য
  • উত্তরগুলোর faithfulness ক্রমের ওপর নির্ভর করে

RAG-এর context-এ k = ১ থেকে k = ৫ করলে উত্তর নেই এমন প্রশ্নে কোন ঝুঁকি সবচেয়ে বেশি বাড়ে?

  • Model উত্তর না থাকলেও উৎসের কাছাকাছি তথ্য দিয়ে উত্তর বানাতে পারে
  • Recall@k কমে যায়
  • Prompt-এর token কমে যায়
  • Gold নথি হারিয়ে যায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. RAG-এ retrieval আর generation আলাদা মাপুন; oracle context দুই ধাপের ভুল আলাদা করে।
  2. Recall@k প্রথম শর্ত, MRR প্রথম ঠিক নথির অবস্থান, nDCG সব প্রাসঙ্গিক নথির অবস্থান; k সবসময় লিখুন।
  3. সঠিকতা, faithfulness, প্রাসঙ্গিকতা, citation চারটি আলাদা প্রশ্ন; faithful উত্তরও পুরোনো উৎসে ভুল হতে পারে।
  4. Eval set-এ উত্তর নেই, বহু নথি, সংখ্যা আর Banglish প্রশ্ন রাখুন; বাংলা অঙ্ক আর regex-এর সীমা normalise করুন।
  5. স্বয়ংক্রিয় মাপ আর LLM বিচারক মানুষের লেবেলের সঙ্গে kappa দিয়ে যাচাই করুন; পার্থক্যে paired bootstrap.

পরের পর্বে: পর্ব ৭১-এ (Tool use আর function calling — LLM থেকে কাজ) model নিজে হিসাব না করে একটি calculator বা অর্ডার-খোঁজার tool ডাকবে; এই পর্বের হিসাবের ভুলগুলোর একটি বাস্তব সমাধান সেখানে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান