یک پاسخ اشتباه که لباس سفید پزشک به تن دارد؛ این ترسناکترین کابوس هر توسعهدهندهی هوش مصنوعی در حوزه سلامت است. در ژوئیه ۲۰۲۶، توسعهدهندهی ClinicaQuery-AI با حقیقتی تلخ روبرو شد: دستیار پژوهشی پزشکی او در حالی که با اعتمادبهنفس کامل به مقالات مربوط به ورزشهای قلبی ارجاع میداد، دوزهای آنتیبیوتیک را از خودش میساخت و ابداع میکرد.
این سیستم خراب نبود یا دچار باگ برنامهنویسی نشده بود، بلکه دقیقاً طبق استاندارد تولید بازیابیافزا (RAG) عمل میکرد. این معماری در اساس خود برای حذف توهمات مدلهای زبانی طراحی شده است، اما در اینجا سیستم صرفاً پنج پاراگراف «کمارتباطتر» را بازمیگرداند و سپس یک مدل زبانی بزرگ (LLM) — شبیه کتابخانهداری که میلیاردها صفحه را خوانده و حالا با همان لحن متقاعدکننده پاسخ میدهد — یک دروغ fluent و روان از دل آنها میبافت. در چنین سناریویی، یک نشان سبز «اعتماد بالا» و ارجاعات کاملاً مرتب و فرمت شده، خطرناکترین بخش پاسخ هستند؛ زیرا هیچ خطا یا خط لاگی وجود نداشت که سیگنالی از شکست سیستم ارسال کند.
اکثر توسعهدهندگان RAG را به عنوان یک فرآیند ساده سه-مرحلهای میسازند: ابتدا پرسش را به بردار معنایی (Embedding) تبدیل میکنند، سپس ۵ تکه متن (chunk) نزدیک را مییابند و در نهایت پاسخ را تولید میکنند. این مدل پایه برای یک نمایش یا دموی ساده کافی است، اما در محیطهای با ریسک بالا (high-stakes) شکست میخورد؛ زیرا سیستم نمیتواند تشخیص دهد که تفاوت بین «من پاسخ را پیدا کردم» و «من پنج پاراگراف پیدا کردم» چیست. برای حل این مشکل، ClinicaQuery-AI از یک خطلوله سه-مرحلهای به یک معماری هشت-مرحلهای منتقل شد که هدف آن «شکست ایمن» (fail safely) بود. این گذار نیازمند دو هفته اصلاحات مداوم بود، از جمله مدیریت تداخلات وابستگیها و خطاهای DLL که در نهایت باعث شد کل معماری سیستم بازطراحی شود.
همانطور که در تحلیلهای قبلی ما دربارهی امنیت مدلهای بازمتن اشاره کردیم، اتکای مطلق به خروجی مدل بدون لایهی بازبینی، ریسک توهم را افزایش میدهد. در این پروژه، تغییر معماری با چالشهای شدیدی همراه بود تا شکافهای بازیابی پر شود.
رفع شکاف بازیابی
اولین نقطه شکست، وجود «تکههای بدون زمینه» (contextless chunks) است. برای مثال، تکه متنی مانند «کاهش ۶.۹ میلیمتر جیوه بود (۹۵٪ CI: ۵.۲-۸.۶, p<0.001)» بدون دانستن اینکه چه چیزی کاهش یافته، در چه گروهی از بیماران رخ داده یا بعد از کدام مداخله بوده است، کاملاً بیفایده است. نه کاربر و نه مدل برداری (Embedding Model) نمیتوانند زمینه این تکه را تشخیص دهند. در نتیجه، این تکه به برداری تبدیل میشود که تقریباً یعنی «یک عدد کمی پایین رفت»، و بازیابی آن برای پرسشی که واقعاً پاسخ میدهد، تقریباً غیرممکن میشود.
با پیروی از پژوهشهای شرکت Anthropic در زمینه بازیابی زمینهای (Contextual Retrieval)، این سیستم اکنون از یک LLM استفاده میکند تا پیش از تبدیل به بردار، یک جمله تکخطی را به ابتدای هر تکه اضافه کند تا جایگاه آن تکه در کل سند مشخص شود. با استفاده از یک پرامپت خاص — که از مدل میخواهد تنها یک جمله با حداکثر ۲۵ کلمه برای تبیین جایگاه تکه در سند بنویسد — این تکنیک به تنهایی نرخ شکست در بازیابی را ۴۹٪ کاهش داد. برای مقالهای با ۱۴ تکه متن، این فرآیند با استفاده از یک thread pool حدود ۶ ثانیه زمان میبرد. برای اینکه کاربر همچنان دادههای اصلی را ببیند، سیستم متن اصلی را به صورت جداگانه در node.metadata["raw_text"] ذخیره میکند و از جمله زمینهای فقط برای بازیابی ماشینی استفاده میکند.
علاوه بر این، جستوجوی برداری با اصطلاحات دقیق پزشکی مانند نمادهای ژنی، نام داروها، کدهای ICD یا مقادیر دقیق p-value مشکل دارد، زیرا فضای برداری ذاتاً برای «مبهم بودن» (fuzziness) طراحی شده است. برای مثال، جستوجو برای «فسفوریلاسیون eNOS در Ser1177» ممکن است پاساژی درباره سیگنالینگ اکسید نیتریک برگرداند که هرگز نام Ser1177 را نمیآورد. در پزشکی، این «مبهم بودن» یک نقطه ضعف و ریسک است. این محدودیتها دلیل اصلی این است که چرا جستوجوی برداری خالص در محیطهای صنعتی اغلب با شکست مواجه میشود.
برای رفع این مشکل، اپلیکیشن یک جستوجوی ترکیبی (Hybrid Search) را پیادهسازی کرد که در آن از BM25 برای تطبیق کلیدواژهها در کنار جستوجوی برداری استفاده میشود. سیستم از یک عبارت منظم (regex) خاص به شکل re.compile(r"[a-z0-9]+(?:[-_][a-z0-9]+)*") استفاده میکند تا اطمینان حاصل شود توکنهایی مانند ser1177 ،covid-19 و il-6 به جای تکهتکه شدن، به صورت یکپارچه باقی بمانند. این قابلیت از طریق rank_bm25 پیاده شده و برای جلوگیری از سربار استفاده از کانتینرهای داکر یا کلاسترهای Elasticsearch، در یک فایل pickle ذخیره میشود.
علم ادغام نتایج
ادغام لیستهای رتبهبندی شده حاصل از جستوجوی برداری و کلیدواژهای، یکی از رایجترین نقاط شکست است. بسیاری از توسعهدهندگان سعی میکنند امتیازها را به بازه ۰ تا ۱ نرمالسازی کرده و با هم جمع کنند. اما مشکل اینجاست که شباهت کسینوسی (Cosine Similarity) دارای محدوده مشخص و توزیع نرمال است، در حالی که امتیاز BM25 نامحدود است و توسط طول متن دچار تغییر شکل (skewed) میشود. در نتیجه، نرمالسازی min-max اغلب باعث میشود یک امتیاز پرت (outlier) در BM25، سایر نتایج را خرد کند و رتبهبندی را به نویز تبدیل نماید.
به جای این روش، سیستم از الگوریتم Reciprocal Rank Fusion (RRF) استفاده میکند که در یک مقاله سال ۲۰۰۹ در SIGIR معرفی شد. RRF امتیازها را کاملاً دور میاندازد و فقط رتبهها (Ranks) را نگه میدارد. با استفاده از فرمول 1.0 / (k + rank) و ثابت میراکننده k=60 ،تضمین میشود که هیچ یک از لیستها بر دیگری تسلط مطلق پیدا نکند. در این روش، یک سند برای برنده شدن باید در چندین لیست مختلف رتبه خوبی کسب کند. این رویکرد توسط رهبرانی در صنعت مانند Azure AI Search و Weaviate به کار گرفته شده است.
به عنوان یک قابلیت افزوده، سیستم «توافق بازیاب» (retriever agreement) را ردیابی میکند. اگر یک تکه متن هم در نتایج برداری و هم در نتایج کلیدواژهای ظاهر شود، رابط کاربری (UI) آن را با یک نشان «هر دو» (both) نمایش میدهد که سیگنالی از یک تطبیق بسیار قوی است.
بازرتبهبندی پیشرفته و درجهبندی
برای پالایش بیشتر دقت، خطلوله یک بازرتبهبند (Reranker) از نوع cross-encoder را معرفی میکند. پایگاههای داده برداری استاندارد از bi-encoderها استفاده میکنند که یک تکه متن را بدون دانستن پرسش آینده، به یک خلاصه فشرده تبدیل میکنند. در مقابل، یک cross-encoder پرسش و تکه متن را به طور همزمان در یک پاس میخواند. این مدل میتواند به طور دقیق تشخیص دهد که عبارت «۱۵۰ دقیقه» در یک متن، دقیقاً پاسخ پرسش «چند دقیقه» را میدهد.
از آنجایی که cross-encoderها از نظر محاسباتی گران هستند و نمیتوان آنها را پیشمحاسبه کرد، سیستم یک الگوی دقیق را دنبال میکند: ابتدا ۳۰ کاندید را به صورت ارزان (با روش برداری) بازیابی میکند، سپس آن ۳۰ مورد را با دقت بازرتبهبندی کرده و در نهایت ۵ مورد برتر را نگه میدارد. در رابط کاربری، منابعی که توسط بازرتبهبند ارتقا یافتهاند، با یک نشان سبز «+4» علامت میخورند که نشاندهنده جهش آنها از یک رتبه پایین برداری (مثلاً رتبه ۷) به جایگاههای برتر است.
برای مدیریت حالت «ترسناک» — یعنی زمانی که پاسخ اصلاً در اسناد موجود نیست — توسعهدهنده سیستم Corrective RAG (CRAG) را پیاده کرد. این سیستم یک «درجهبند» (Grader) بین مرحله بازیابی و تولید قرار میدهد. این درجهبند به جای میانگین گرفتن، بهترین سند را امتیازدهی میکند تا بازیابیهای خوبی که چند تکه مربوطنشده را همراه خود کشاندند، جریمه نشوند. درجهبند یکی از سه اقدام زیر را تعیین میکند:
- درست (max_score >= 0.70): ادامه فرآیند و رفتن به مرحله تولید پاسخ.
- مبهم (max_score >= 0.35): بازنویسی پرسش (Query Rewrite)، بازیابی مجدد و ادغام نتایج.
- نادرست (max_score < 0.35): توقف فوری خطلوله. در این حالت مولد (Generator) هرگز اجرا نمیشود و سیستم پاسخ
NO_ANSWER_RESPONSEرا برمیگرداند.
عبور از جهنم وابستگیها و بدهی فنی
فرآیند ساخت، بدهی فنی (Technical Debt) قابل توجهی را در مورد وابستگیهای نرمافزاری آشکار کرد. توسعهدهنده با خطای ImportError: DLL load failed در مربوط به onnxruntime_pybind11_state مواجه شد؛ زیرا ChromaDB به طور پیشفرض onnxruntime را وارد میکند، حتی اگر از Gemini برای Embeddings استفاده شود. تثبیت نسخه روی onnxruntime<1.20 کرش را متوقف کرد اما باعث تداخل در protobuf شد: بسته opentelemetry-proto 1.42.1 نیازمند protobuf<7.0 بود، در حالی که سیستم protobuf 7.35.1 را داشت.
یک «جهنم وابستگی» (dependency hell) شدیدتر با llama-index رخ داد. بسته A (llama-index 0.14.23) نیازمند llama-index-llms-openai>=0.7.0 بود، در حالی که بسته B (llama-index-llms-openai-like 0.5.3) همان بسته را در نسخه <0.6 میخواست. از آنجایی که هر دو به دلیل وجود llama-index-llms-groq در درخت وابستگی بودند، pip وارد یک حلقه تکرار از نصبهای متضاد شد.
راه حل نهایی این بود که تلاش برای حل تداخل نسخهها متوقف شود و در عوض، خودِ وابستگی به کلی حذف گردد. توسعهدهنده متوجه شد که این چارچوب تنها یک فراخوانی تابع را فراهم میکرد: Settings.llm.stream_complete(prompt). با جایگزینی این تابع با یک درخواست HTTP مستقیم از طریق SDK شرکت Groq ،کل درخت وابستگیهای مشکلساز حذف شد.
تابآوری معماری و مدیریت خطاهای پلتفرم
خطاهای خاص پلتفرم نیز منجر به بهبودهای معماری شد. یک خطای DLL در PyTorch (Error loading torch\lib\c10.dll) در سیستمعامل ویندوز، بازرتبهبند محلی را از کار انداخت. توسعهدهنده به جای جنگ با سیستمعامل، یک سیستم پشتیبان (fallback) سهلایه برای بازرتبهبندی پیاده کرد:
۱. Cohere rerank-v3.5: مبتنی بر API که بهترین دقت موجود را ارائه میدهد.
۲. BGE cross-encoder: نسخه محلی و آفلاین از طریق sentence-transformers.
۳. Groq LLM-as-reranker: کاملاً مبتنی بر HTTP، با استفاده از ThreadPoolExecutor و ۸ Worker برای امتیازدهی موازی کاندیدها جهت به حداقل رساندن تأخیر شبکه.
این ساختار تضمین میکند که سیستم حتی در صورت شکست یک وابستگی نیتیو (native)، همچنان فعال بماند. سیستمهای سازمانی نه با بهترین جزء خود، بلکه با اتفاقاتی که هنگام عدم دسترسی به آن جزء میافتند، تعریف میشوند.
عملکرد، شفافیت و HyDE
برای اعتبارسنجی این انتخابها، توسعهدهنده یک داشبورد ردیابی (tracing dashboard) سفارشی با استفاده از ring buffer و فایلهای JSONL ساخت. این داشبورد یک خط زمانی برای هر پرسش با تأخیرهای (latencies) معمول زیر را آشکار کرد:
- BM25: زیر ۱۰ میلیثانیه (تقریباً رایگان).
- RRF Fusion: زیر ۵ میلیثانیه.
- جستوجوی برداری: ۲۰۰ تا ۴۰۰ میلیثانیه.
- بازرتبهبندی (Rerank): ۱۵۰ تا ۴۰۰ میلیثانیه.
- درجهبندی CRAG: ۳۰۰ تا ۶۰۰ میلیثانیه.
- HyDE: ۴۰۰ تا ۸۰۰ میلیثانیه.
- تولید پاسخ (Generation): ۱ تا ۳ ثانیه.
مشخص شد که HyDE (Hypothetical Document Embeddings) دومین مرحله پرهزینه است. HyDE شکاف واژگانی بین یک پرسش (مثلاً «ورزش فشار خون را چقدر کم میکند؟») و یک پاسخ (مثلاً «فشار سیستولیک به طور میانگین ۶.۹ میلیمتر جیوه کاهش یافت») را پر میکند. این سیستم از LLM میخواهد ابتدا یک پاسخ محتمل — هرچند احتمالاً توهمآمیز — بنویسد. این پاساژ «جعلی» سپس برداری میشود تا اسناد واقعی با «شکل» مشابه در فضای برداری پیدا شوند. توهم اولیه بلافاصله پس از بازیابی دور ریخته میشود.
محصول نهایی شامل دکمههای فعال/غیرفعال (toggles) در رابط کاربری برای HyDE، جستوجوی ترکیبی، بازرتبهبندی و CRAG است. این امر به کاربران اجازه میدهد مطالعات ابلاسیون (ablation studies) را در لحظه انجام دهند، یک ویژگی را خاموش کنند و مشاهده کنند که چگونه ترتیب منابع تخریب میشود تا تأثیر معماری را به چشم ببینند.
خطلوله نهایی هشت-مرحلهای
سیستم از یک فرآیند ساده سه-مرحلهای به یک توالی سختگیرانه هشت-مرحلهای تکامل یافت:
۱. HyDE: تولید و برداری کردن یک پاسخ فرضی.
۲. بازیابی متراکم (Dense): جستوجوی برداری برای یافتن ۲۰ کاندید اول.
۳. بازیابی پراکنده (Sparse): جستوجوی کلیدواژهای BM25 برای ۲۰ کاندید اول.
۴. ادغام RRF: ترکیب لیستها بر اساس رتبه، نه امتیاز.
۵. Cross-encoder: بازرتبهبندی ۳۰ مورد برتر و نگه داشتن ۵ مورد اول.
۶. درجهبندی CRAG: امتیازدهی ارتباط؛ بازنویسی، تلاش مجدد یا رد درخواست.
۷. تولید: استریم کردن پاسخ نهایی.
۸. حفاظها (Guardrails): اعمال امتیاز اعتماد با استفاده از همپوشانی لغتنامهای (lexical overlap) و بررسی LLM.
اگرچه تأخیر هر پرسش از ۱-۲ ثانیه به ۳-۵ ثانیه افزایش یافت، اما این trade-off ارزش هر میلیثانیه آن را داشت؛ زیرا سیستم اکنون میتواند مهمترین کار ممکن را انجام دهد: تشخیص این موضوع که «پاسخ را نمیداند». در متون پزشکی، سیستمی که بیصدا شکست میخورد، بسیار خطرناکتر از سیستمی است که نادانی خود را اعتراف کند.
گام بعدی شما
- اگر از RAG استفاده میکنید، به جای اعتماد به امتیاز Cosine Similarity، الگوریتم RRF را برای ادغام نتایج امتحان کنید.
- برای کاهش توهمات، یک لایه درجهبند (Grader) قبل از مرحله تولید پاسخ اضافه کنید تا در صورت نبود داده، مدل پاسخ نسازد.
- وابستگیهای سنگین را با درخواستهای HTTP مستقیم به APIهای سبک (مثل Groq) جایگزین کنید تا از «جهنم نسخهها» رهایی یابید.
این معماری تنها بخشی از بهینهسازیهای سطح بالا است؛ برای درک اینکه چگونه لایههای حفاظتی (Guardrails) در مقیاس صنعتی عمل میکنند، تحلیل ما دربارهی سیستمهای نظارتی LLM را بخوانید.




گفتگو