
چرا چتباتهای هوش مصنوعی متن فارسی را درست پیدا نمیکنند؟
این روزها خیلی از شرکتها و سازمانها چتباتهایی دارند که از روی سندهای خودشان جواب میدهند: دستیار پشتیبانیای که آییننامهها را میخواند، چتباتی که از دل قراردادها و گزارشها جواب پیدا میکند، یا ابزاری که به سؤال کارمندان دربارهٔ مستندات داخلی پاسخ میدهد. اگر با یکی از اینها به فارسی کار کرده باشید، احتمالاً دیدهاید که گاهی جوابی را که مطمئنید در سندها هست پیدا نمیکند، یا با اطمینان کامل جوابی بیربط میدهد.
خود من وقتی داشتم یک سامانهٔ پرسشوپاسخ روی سندهای فارسی میساختم، دقیقاً به همین وضع خوردم. روی متن انگلیسی همهچیز بیدردسر کار میکرد، اما روی فارسی سندی که دقیقاً جواب سؤال را داشت گاهی اصلاً پیدا نمیشد. بدترین بخش ماجرا این بود که هیچ خطایی دیده نمیشد: نه پیغامی، نه هشداری. سامانه فقط آرام و بیصدا بد کار میکرد. وقتی رد مشکل را گرفتم، فهمیدم ایراد نه از مدل هوش مصنوعی است و نه از پایگاه داده؛ ایراد از خود متن فارسی بود و از اینکه ابزارهای عمومی هیچ درکی از آن ندارند.
در این نوشته سه عامل اصلی این مشکل را توضیح میدهم و بعد سراغ راهحلهایی میروم که در عمل جواب میدهند.
چتباتی که از روی سندها جواب میدهد معمولاً دو مرحله دارد. اول، مثل یک موتور جستوجو، تکههای مرتبط با سؤال را از میان سندها پیدا میکند. بعد، آن تکهها را همراه سؤال به مدل زبانی (همان مغز چتبات) میدهد تا جواب را بنویسد. به این روش در دنیای فنی RAG میگویند، کوتاهشدهٔ Retrieval-Augmented Generation، یعنی «تولید متن با کمک بازیابی».
نکتهٔ کلیدی اینجاست: کیفیت جواب نهایی تقریباً بهطور کامل به مرحلهٔ اول، یعنی جستوجو، بستگی دارد. اگر تکهٔ درست پیدا نشود، بهترین مدل دنیا هم جواب درست نمیدهد. پس هر چیزی که جستوجوی فارسی را خراب کند، کل چتبات را خراب کرده است.
برای ما که فارسی میخوانیم، «کتاب» همیشه «کتاب» است. اما برای کامپیوتر، دو کلمه فقط وقتی یکیاند که حرفبهحرف و کدبهکد یکی باشند. متن فارسی پر از کلمههایی است که ظاهرشان یکی است ولی کدهایشان نه. سه تا از این دشمنها از همه پنهانتر و رایجترند:
ی و ک عربی که روی صفحه با ی و ک فارسی فرقی ندارند؛
نیمفاصله که یک کلمه را به چند شکل مختلف درمیآورد؛
PDF برعکس که «کتاب» را به «باتک» تبدیل میکند.
صفحهکلیدها، سیستمعاملها و سایتهای مختلف گاهی بهجای «ی» و «ک» فارسی، «ي» و «ك» عربی مینویسند. روی صفحه تقریباً هیچ فرقی دیده نمیشود، اما «كتاب» با کاف عربی و «کتاب» با کاف فارسی دو رشتهٔ کاملاً متفاوتاند. اگر سند با یکی نوشته شده باشد و کاربر با دیگری جستوجو کند، جستوجوی کلیدواژهای هیچ تطابقی پیدا نمیکند. همین داستان برای شکلهای مختلف الف (أ، إ، ٱ) هم تکرار میشود.
نیمفاصله (ZWNJ) یکی از ظریفترین بخشهای خط فارسی است. «میروم» را میشود با نیمفاصله، با فاصلهٔ کامل («می روم») یا چسبیده («میروم») نوشت. «کتابها» و «کتاب ها» و «کتابها» هم همینطور. هر کدام از اینها برای ابزاری که متن را به کلمهها میشکند، چیز متفاوتی است. در یکی «می» و «روم» دو کلمهٔ جدا هستند و در دیگری یک کلمه.
و اما عجیبترین دشمن: خیلی از تولیدکنندههای PDF متن فارسی را به «ترتیب نمایشی» ذخیره میکنند، یعنی به همان ترتیبی که حروف از چپبهراست روی صفحه چیده شدهاند. نتیجه این است که ابزارهای استاندارد استخراج متن، بهجای «کتاب» تحویلتان میدهند «باتک». متنی که با این شکل ایندکس شود عملاً برای جستوجو بیمصرف است و هیچ هشداری هم نمیبینید، مگر اینکه خودتان خروجی را با چشم نگاه کنید.
کنار این سه دشمن اصلی، چند مزاحم کوچکتر هم هستند. متن فارسی ممکن است «۱۴۰۳» را با ارقام فارسی، «١٤٠٣» را با ارقام عربی یا «1403» را با ارقام لاتین داشته باشد. ویرگول و علامت سؤال هم گاهی فارسیاند (، ؟) و گاهی لاتین.
بعضی کلمهها در فارسی بیش از یک املای پذیرفته دارند: «اتاق» و «اطاق»، «هیئت» و «هیات»، «مسئله» و «مساله»، «تهران» و «طهران». هیچکدام غلط نیست، ولی اگر سند «اطاق» نوشته باشد و کاربر «اتاق» بپرسد، جستوجوی کلیدواژهای سند را از دست میدهد. این مشکل را با یکسانسازی حروف هم نمیشود حل کرد، چون اینجا خود حروف فرق دارند.
هر کدام از این مشکلها بهتنهایی کوچک به نظر میرسد. اما در یک مجموعهٔ واقعی همه با هم پیش میآیند و نتیجهشان همان چتباتی است که جواب را پیدا نمیکند، بیآنکه بگوید چرا.
خبر خوب این است که هیچکدام از این مشکلها حلنشدنی نیستند. فقط باید پیش از اینکه متن به موتور جستوجو برسد، مرحلههایی مخصوص زبان فارسی در مسیر گذاشته شود. من این راهحلها را در یک کتابخانهٔ متنباز پایتون به اسم persian-rag پیاده کردهام، اما ایدهها عمومیاند و در هر سامانهای قابل پیادهسازیاند.
اولین و مهمترین قدم، یکسانسازی (normalization) است: «ي» و «ى» به «ی»، «ك» به «ک» و شکلهای مختلف الف به «ا» تبدیل شوند، نیمفاصلهٔ پیشوند «می» و پسوندهایی مثل «ها» و «ترین» درست شود، ارقام یکدست شوند و فاصلهگذاری اطراف علائم مرتب شود. نکتهٔ مهم این است که همین کار باید هم روی سندها و هم روی پرسش کاربر انجام شود تا هر دو به یک شکل واحد برسند.
دو اصل را در این مرحله مهم میدانم. اول، هیچ قاعدهای نباید اجباری باشد؛ پروژهای ممکن است ارقام لاتین بخواهد یا نخواهد به نیمفاصلهها دست زده شود. یکسانسازیای که نشود کنترلش کرد، دیر یا زود جایی متن را خراب میکند. دوم، یکسانسازی باید «خودتوان» (idempotent) باشد، یعنی اگر دو بار روی یک متن اجرا شود، نتیجه با یک بار اجرا فرقی نکند.
برای نمونه، در پیادهسازی من خروجی یکسانسازی چنین است:
normalize("كتاب هاي مي روم 123") # کتابهای میروم ۱۲۳
متن برعکس را میشود بهطور خودکار تشخیص داد. دو نشانهٔ ساده کار را انجام میدهد: اینکه کلمهها با کدام شکلهای چسبان حروف شروع و تمام میشوند، و اینکه کلمههای پرتکرار فارسی رو به جلو دیده میشوند یا برعکس. اگر سطری برعکس تشخیص داده شد، برگردانده میشود؛ اما باید حواستان باشد که عددها و متن لاتین جهت چپبهراست خودشان را نگه دارند، وگرنه «2024» به «4202» تبدیل میشود.
دو روش اصلی برای جستوجو داریم. روش کلیدواژهای یا BM25 (الگوریتم کلاسیک رتبهبندی بر اساس تطابق کلمهها) که در پیدا کردن اسمها، اصطلاحها و عبارتهای دقیق عالی است. و روش برداری که با embedding (نمایش عددی معنای متن) کار میکند و میتواند جملههای هممعنا با کلمههای متفاوت را هم پیدا کند.
هیچکدام بهتنهایی کافی نیست. راه رایج این است که هر دو اجرا شوند و رتبهبندیهایشان با Reciprocal Rank Fusion یا RRF (ادغام رتبهها بر اساس جایگاه هر سند در هر فهرست) ترکیب شود. مزیت RRF این است که به مقیاس امتیازها کاری ندارد و فقط به رتبهها نگاه میکند.
برای مشکل «اتاق/اطاق» یکسانسازی جواب نمیدهد، چون هیچکدام غلط نیست. راهحل این است که از روی پرسش کاربر، شکلهای املایی دیگر ساخته شود، با هر کدام جداگانه جستوجو شود و نتیجهها دوباره با RRF ادغام شوند. پرسش اصلی باید وزن بیشتری بگیرد تا همیشه اولویت داشته باشد.
یک خطای بیصدای دیگر: ایندکس را با یک مدل embedding میسازید و بعداً با مدل دیگری جستوجو میکنید. بردارها با هم سازگار نیستند، اما ممکن است ابعادشان یکی باشد و هیچ خطایی نگیرید؛ فقط نتیجهها بیمعنی میشوند. راهحل ساده است: نام مدل را کنار ایندکس ذخیره کنید و اگر مدل دیگری سراغش آمد، خطا بدهید. بهتر است سامانه با صدای بلند شکست بخورد تا اینکه آرام اشتباه کند.
برای اندازهگیری، یک مجموعهٔ آزمون کوچک فارسی ساختم (persian-rag-bench که طراحی آن را در یادداشتی جداگانه توضیح دادهام): ۳۴ متن و ۴۲ پرسش که ۱۲ تایشان عمداً با املای متفاوت از متن نوشته شدهاند.
در جستوجوی کلیدواژهای BM25، جستوجو با املاهای مختلف معیار Hit@1 (سهم پرسشهایی که جواب درست در رتبهٔ اول آمده) را از 0.881 به 0.952 رساند.
انتخاب مدل embedding هم اثر بزرگی دارد. دو مدل چندزبانه را مقایسه کردم؛ معیار nDCG@10 است (کیفیت ترتیب ده نتیجهٔ اول):
مجموعهٔ آزمون خودم: paraphrase-multilingual-MiniLM-L12-v2 برابر 0.8637 و multilingual-e5-small برابر 0.9881
زیرمجموعهٔ فارسی MIRACL (dev): paraphrase-multilingual-MiniLM-L12-v2 برابر 0.4424 و multilingual-e5-small برابر 0.7461
PQuAD (validation): paraphrase-multilingual-MiniLM-L12-v2 برابر 0.6437 و multilingual-e5-small برابر 0.9242
عدد MIRACL روی یک پیکرهٔ محدود از ۶٬۴۰۵ متن داوریشده برای ۶۳۲ پرسش به دست آمده، نه روی کل پیکرهٔ MIRACL، پس با عددهای منتشرشدهٔ آن قابل مقایسه نیست.
این عددها را باید با احتیاط خواند:
مجموعهٔ آزمون را خودم ساختهام و کوچک است. با ۴۲ پرسش، جابهجا شدن فقط یک پرسش Hit@k را حدود ۲٫۴ واحد درصد تغییر میدهد.
امتیازها روی این مجموعه نزدیک سقفاند و برای مقایسهٔ مدلهای قوی چندان تفکیککننده نیست.
ارزیابی املایی فقط بر ۱۲ پرسش دستی تکیه دارد.
در وزندهی شکلهای املایی هنوز یک ایراد باز هست: هر شکل وزن ۱ میگیرد و پرسش اصلی وزن ۲، اما مجموع وزن چند شکل میتواند از پرسش اصلی بیشتر شود.
برای تعمیر متن PDF، رتبهبندی دوباره و دقت ارجاعها هنوز ارزیابی عددی ندارم.
اگر چتبات یا موتور جستوجوی شما روی متن فارسی کار میکند، پیش از عوض کردن مدل، این سه دشمن پنهان را بررسی کنید: حروف عربی، نیمفاصله و PDF برعکس. یکسانسازی دقیق متن در هر دو طرف، تعمیر PDF، جستوجوی ترکیبی و در نظر گرفتن املاهای مختلف، اغلب بیش از هر تغییر دیگری کیفیت جوابها را بالا میبرد.
گزارش فنی persian-rag در Zenodo، DOI: 10.5281/zenodo.23923535
نسخهٔ نرمافزار persian-rag در Zenodo، DOI: 10.5281/zenodo.23944663
کد منبع: github.com/sadrmanesh/persian-rag
بنچمارک persian-rag-bench در Figshare، DOI: 10.6084/m9.figshare.34913889
کد و مستندات پروژه: github.com/sadrmanesh/persian-rag