
در یادداشت قبلیام دربارهی «ی عربی، نیمفاصله و PDF برعکس» توضیح دادم که چرا جستوجوی فارسی در سامانههای پرسشوپاسخ مبتنی بر بازیابی (RAG) بیصدا خراب میشود. این یادداشت دربارهی روی دیگر همان سکه است: چطور بفهمیم یک خط لولهی بازیابی فارسی در برابر این مشکلات مقاوم است، و وقتی تغییرش میدهیم بدتر نشده است؟
پاسخ من یک بنچمارک کوچک و تکرارپذیر به نام persian-rag-bench است که آن را بهصورت عمومی در Figshare (با DOI) و Hugging Face منتشر کردهام. در ادامه طراحی آن، دادهها، معیارها، نحوهی استفاده و محدودیتهایش را شرح میدهم.
بنچمارکهای بزرگ فارسی وجود دارند؛ مثلاً زیرمجموعهی فارسی MIRACL یا PQuAD. اینها برای مقایسهی کلی مدلها ارزشمندند، اما برای یک کار روزمرهی مهندسی مناسب نیستند: آزمون پسرفت (regression check). وقتی یک قاعدهی یکسانسازی را عوض میکنید یا اندازهی تکهها را تغییر میدهید، میخواهید در چند ثانیه و بدون GPU بفهمید چیزی خراب شده یا نه.
بنچمارکهای بزرگ همچنین یک ضعف خاص دارند: معمولاً پرسشهایی که عمداً با املای متفاوت از متن نوشته شده باشند را بهطور جداگانه علامت نمیزنند. پس نمیتوان مستقیماً پرسید «سامانهی من با املاهای چندگانه چطور کنار میآید؟».
بنابراین سه هدف طراحی داشتم:
سرعت و تکرارپذیری: اجرای کامل روی یک لپتاپ معمولی، حتی با یک embedder آفلاین.
برچسب صریح برای املای چندگانه: پرسشهای املایی قابل گزارش جداگانه باشند.
سادگی قالب: دو فایل JSONL که با هر ابزاری خوانده شوند.
و یک «غیرهدف» صریح: این بنچمارک برای رتبهبندی کلی مدلها ساخته نشده و برای این کار بیش از حد کوچک است.
بنچمارک دو بخش دارد: پیکره (corpus) با ۳۴ ردیف و فیلدهای id، title و text، و پرسشها (qa) با ۴۲ ردیف و فیلدهای id، question، relevant_ids و tags. جدول زیر همین را خلاصه میکند:
corpus: تعداد ردیف: 34؛ فیلدها: id، title، text
qa: تعداد ردیف: 42؛ فیلدها: id، question، relevant_ids، tags
پیکره ۳۴ متن کوتاه دانشنامهای است دربارهی شاعران فارسیزبان، شهرها، بناهای تاریخی، جشنها، خوراکها و موضوعات روزمره مثل اتاق، هیئت مدیره و بورس. طول متنها بین ۱۸ تا ۳۵ کلمه است (میانگین حدود ۲۷ کلمه، با شمارش سادهی فاصلهها). یک نمونه:
{"id": "hafez", "title": "حافظ", "text": "حافظ شیرازی شاعر قرن هشتم هجری است. ..."}
کوتاه بودن متنها عمدی است: هر متن تقریباً یک واقعیت اصلی دارد و ابهام دربارهی اینکه کدام متن جواب پرسش است کم میشود.
۴۲ پرسش طبیعی یا عبارت کوتاه جستوجو، هر کدام با شناسهی متنهای مرتبط. بر اساس شمارش خودم روی فایل:
۴۱ پرسش دقیقاً یک متن مرتبط دارند و یک پرسش دو متن مرتبط.
۲۶ متن از ۳۴ متن پیکره دستکم یک بار هدف یک پرسشاند؛ ۸ متن باقیمانده فقط نقش متن رقیب (distractor) را دارند.
۱۲ پرسش برچسب spelling دارند.
پرسشها هم به شکل پرسش کاملاند («آرامگاه حافظ در کدام شهر است؟») و هم به شکل عبارت کوتاه جستوجو («قیمت بلیط»)، چون کاربران واقعی هر دو را مینویسند.
قلب این بنچمارک ۱۲ پرسش املایی است. هر کدام یک کلمه را با املایی متفاوت اما رایج نسبت به متن نوشتهاند. هیچکدام از این املاها «غلط» نیستند؛ هر دو شکل در متنهای فارسی واقعی دیده میشوند. الگوهای پوشش دادهشده:
ت/ط و تفاوت حروف همآوا: طهران بهجای تهران، اطاق بهجای اتاق، بلیط و باطری.
همزه و یای میانی: هیات بهجای هیئت، رییس، مساله بهجای مسئله.
همزه روی کرسی یا ی: پائیز، آئین.
برخی کلمهها عمداً بیش از یک بار آمدهاند، یک بار به شکل عبارت کوتاه («طهران») و یک بار درون جملهای بلندتر («پرجمعیتترین شهر طهران»)، تا بتوان دید سامانه فقط وقتی کلمه تنهاست موفق میشود یا در بافت هم.
نکتهی مهم این است که این مشکل با یکسانسازی حروف (مثل تبدیل ی عربی به فارسی) حل نمیشود، چون اینجا خود حروف فرق دارند. بنابراین بنچمارک دقیقاً چیزی را میسنجد که یکسانسازی ساده از آن غافل است.
بنچمارک با معیارهای استاندارد بازیابی گزارش میشود:
Hit@k: سهم پرسشهایی که دستکم یک متن مرتبط در k نتیجهی اول آمده است. Hit@1 سختگیرانهترین شکل آن است.
MRR@10: میانگین معکوس رتبهی اولین متن مرتبط در ده نتیجهی اول.
nDCG@10: کیفیت ترتیب ده نتیجهی اول با در نظر گرفتن جایگاه.
Hit@1 (spelling): همان Hit@1 فقط روی ۱۲ پرسش املایی.
چون تقریباً هر پرسش فقط یک متن مرتبط دارد، MRR و nDCG رفتار نزدیکی دارند. معیار تفکیککنندهی اصلی، شکاف میان Hit@1 کل و Hit@1 املایی است: اگر سامانه روی کل پرسشها خوب است اما روی پرسشهای املایی افت شدید دارد، مشکل دقیقاً در مقاومت املایی است.
با ۴۲ پرسش، جابهجا شدن فقط یک پرسش Hit@k را حدود ۰٫۰۲۴ تغییر میدهد (۱ تقسیم بر ۴۲). روی زیرمجموعهی ۱۲ پرسشی املایی، هر پرسش حدود ۰٫۰۸۳ اثر دارد. پس اختلافهای کوچک را نباید بیش از حد تفسیر کرد. این بنچمارک برای دیدن پسرفتهای واضح ساخته شده، نه اختلافهای ظریف.
نتیجههای زیر از persian-rag نسخهی 0.3.1 با embedder آفلاین مبتنی بر hashing (بُعد ۵۱۲) و اندازهی تکهی ۴۰۰ به دست آمدهاند، همانطور که در مستندات بنچمارک پروژه منتشر شده است. بهطور خلاصه: BM25 ساده به Hit@1 برابر 0.881 و Hit@1 املایی 0.583 میرسد، و هر دو پیکربندی «BM25 با بازنویسی پرسش» و «ترکیبی» به Hit@1 برابر 0.952 و Hit@1 املایی 0.833 میرسند. جزئیات در جدول زیر است:
sparse (BM25): Hit@1: 0.881؛ Hit@5: 0.881؛ MRR@10: 0.881؛ nDCG@10: 0.879؛ Hit@1 (املایی): 0.583
sparse + rewrite: Hit@1: 0.952؛ Hit@5: 0.976؛ MRR@10: 0.964؛ nDCG@10: 0.967؛ Hit@1 (املایی): 0.833
hybrid: Hit@1: 0.952؛ Hit@5: 0.976؛ MRR@10: 0.964؛ nDCG@10: 0.967؛ Hit@1 (املایی): 0.833
خوانش این جدول آموزنده است. BM25 ساده روی کل پرسشها 0.881 میگیرد که بد به نظر نمیرسد، اما روی پرسشهای املایی به 0.583 میرسد؛ یعنی تقریباً در ۵ پرسش از ۱۲ پرسش املایی جواب درست در رتبهی اول نیست. افزودن بازنویسی پرسش (ساختن شکلهای املایی دیگر و ادغام نتیجهها) Hit@1 املایی را به 0.833 میرساند. بدون برچسب spelling، این تفاوت در میانگین کل گم میشد.
from datasets import load_dataset corpus = load_dataset("sadrmanesh/persian-rag-bench", "corpus", split="test") qa = load_dataset("sadrmanesh/persian-rag-bench", "qa", split="test")
git clone github.com/sadrmanesh/persian-rag && cd persian-rag pip install -e . python benchmarks/run_benchmark.py
این اسکریپت همهی حالتهای بازیابی را با و بدون بازنویسی پرسش اجرا میکند.
اگر خط لولهی دیگری دارید، کافی است:
متنهای corpus را با همان یکسانسازیای که در سامانهی اصلیتان دارید ایندکس کنید.
هر پرسش qa را با همان یکسانسازی جستوجو کنید.
شناسههای بازیابیشده را با relevant_ids مقایسه و معیارها را محاسبه کنید.
نتیجه را یک بار برای همهی پرسشها و یک بار فقط برای پرسشهایی که tags آنها شامل spelling است گزارش کنید.
بهترین کاربرد آن قرار دادنش در CI است: هر تغییر در یکسانسازی، تکهبندی یا مدل embedding باید Hit@1 املایی را حفظ کند.
کوچک و دستنویس: با یک یا چند متن مرتبط برای هر پرسش، امتیازها سریع به سقف میرسند و برای مقایسهی مدلهای قوی تفکیککننده نیستند.
دامنهی محدود: متنها کوتاهاند و بیشتر دانش عمومی دربارهی ایران و فرهنگ فارسیاند؛ متنهای حقوقی، فنی یا فروشگاهی را نمایندگی نمیکنند.
سازنده و ارزیاب یکی است: بنچمارک را خودم ساختهام و همان پروژهای که آن را ساخته، روی آن ارزیابی شده است. این خطر بیشبرازش به بنچمارک را دارد.
تنها ۱۲ پرسش املایی: پوشش الگوهای املایی کامل نیست.
متن پیشیکسانسازی نشده: داده عمداً یکسانسازی نشده است. هر دو طرف را به یک روش یکسانسازی کنید.
یک بنچمارک لازم نیست بزرگ باشد تا مفید باشد؛ باید پرسش درستی بپرسد. persian-rag-bench یک پرسش مشخص میپرسد: آیا خط لولهی بازیابی فارسی شما وقتی کاربر «طهران» یا «اطاق» مینویسد هم متن درست را پیدا میکند؟ با برچسب جداگانه برای پرسشهای املایی، این پرسش در هر اجرای CI در چند ثانیه پاسخ داده میشود. پیشنهاد و مشارکت برای گسترش پرسشها، بهخصوص در حوزههای تخصصیتر، را با کمال میل میپذیرم.
دیتاست در Figshare، DOI: 10.6084/m9.figshare.34913889
دیتاست در Hugging Face: huggingface.co/datasets/sadrmanesh/persian-rag-bench
گزارش فنی persian-rag در Zenodo، DOI: 10.5281/zenodo.23923535
کد منبع persian-rag: github.com/sadrmanesh/persian-rag
مستندات بنچمارک: sadrmanesh.github.io/persian-rag/benchmarks
کد و مستندات پروژه: huggingface.co/datasets/sadrmanesh/persian-rag-bench