گاردریل پاسخ استریم LLM: توکنی که فرستاده شد برنمی‌گردد

جلوگیری از توهم در پاسخ استریم LLM: کنترل پس از ارسال، قطعه‌ای یا جمله‌ای؛ بده‌بستان سرعت و ایمنی در NeMo Guardrails، Guardrails AI و Bedrock؛ گیت جمله‌ای برای متن فارسی و نکته‌های SSE.

توکنی که استریم شد پس گرفته نمی‌شود. وقتی متن به خواننده رسید، گاردریلی که آن را رد کند فقط می‌تواند جلوی ادامه پاسخ را بگیرد. پس گاردریل روی پاسخ استریم در اصل یک تصمیم دارد: یا پیش از ارسال بررسی کند و خواننده را منتظر بگذارد، یا ابتدا بفرستد و بپذیرد که پاسخ ردشده پیش‌تر خوانده شده است. در عمل سه راه وجود دارد: بررسی پس از ارسال، بررسی قطعه‌های ثابت پیش از ارسال و بررسی جمله‌به‌جمله. دستیار همین سایت را خودم ساخته‌ام و هر جمله‌اش پیش از ارسال بررسی می‌شود. هزینه این کار انتظاری به اندازه یک جمله است. محدودیتش این است که مستند بودن ادعا را می‌سنجد، نه درست بودن آن را.

این نوشته به آخرین بررسی پیش از ارسال پاسخ مستند به خواننده می‌پردازد. لایه‌های پیش از آن، از رد پرسش‌های خارج از محدوده تا آستانه امتیاز بازیابی، در جلوگیری از توهم هوش مصنوعی در RAG: طراحی پنج‌لایه آمده‌اند.

چرا توکن استریم‌شده را نمی‌توان پس گرفت؟

کنترلی که پس از ارسال اجرا شود، پاسخ کامل را وقتی می‌بیند که خواننده آن را خوانده است. اگر جمله سوم تاریخی ساختگی داشته باشد، حذف آن پس از نمایش فقط متن ثبت‌شده را عوض می‌کند، نه آنچه خوانده شده است. پیش‌چاپی تازه در arXiv به قلم Christopher M. Frost این مرز را دقیق بیان می‌کند: «کنترلی که الگویی ممنوع را پس از ارسال قطعه‌ای که آن الگو را کامل می‌کند تشخیص دهد، نمی‌تواند آن را پس بگیرد».

ارائه‌دهندگان سرویس‌های ابری هم همین محدودیت را مستند کرده‌اند. فیلتر ناهمگام (Asynchronous Filter) در Azure OpenAI، که باید جداگانه فعال شود، متن را بی‌درنگ برمی‌گرداند و فقط تضمین می‌کند که سیگنال نقض سیاست در فاصله‌ای حدود ۱٬۰۰۰ نویسه از محتوای ناقض برسد. بخش عیب‌یابی همان مستندات توصیه می‌کند در برنامه خودتان سازوکاری برای حذف محتوایی بسازید که «پیش‌تر نمایش داده شده است».

مدخل LLM09:2025 در فهرست OWASP هم به‌کارگیری ابزارها و فرایندهایی را توصیه می‌کند که «خروجی‌های کلیدی را به‌طور خودکار اعتبارسنجی کنند». پس پرسش این نیست که اعتبارسنجی لازم است یا نه؛ پرسش این است که اعتبارسنجی پیش از ارسال متن اجرا شود یا پس از آن.

سه راه کنترل پاسخ استریم: پس از ارسال، قطعه‌ای، جمله‌ای

رویکرد

خواننده متن را چه زمانی می‌بیند

پاسخ ردشده

نمونه‌ها

کنترل پس از ارسال (ارسال پیش از بررسی)

پیش از بررسی (در NeMo هر قطعه پس از پر شدن بافر؛ در حالت ناهمگام Azure و Bedrock هم‌زمان با تولید)

بخشی از آن پیش از توقف خوانده شده است

NeMo با stream_first روشن (پیش‌فرض آن)؛ حالت ناهمگام Azure و Bedrock

کنترل قطعه‌ای

پس از تأیید هر قطعه

قطعه ردشده هرگز نمایش داده نمی‌شود

پیش‌فرض Azure و Bedrock؛ NeMo با stream_first خاموش

گیت جمله‌ای

پس از تأیید هر جمله

جمله ردشده هرگز نمایش داده نمی‌شود

دستیار همین سایت

تنظیم stream_first در NeMo Guardrails چه چیزی را فدای سرعت می‌کند؟

در NeMo Guardrails، ریل‌های خروجی (output rails) بررسی‌هایی‌اند که روی متن تولیدشده اجرا می‌شوند، و اجرای آن‌ها روی پاسخ استریم به‌طور پیش‌فرض خاموش است. اگر این قابلیت فعال شود، ریل‌ها با تنظیمات پیش‌فرض روی قطعه‌های ۲۰۰ توکنی اجرا می‌شوند و ۵۰ توکن از هر قطعه برای پیوستگی به قطعه بعد منتقل می‌شود. مقدار پیش‌فرض پرچم stream_first برابر True است و به گفته مستندات NeMo، در این حالت «کلاینت هر قطعه از توکن‌ها را پیش از آنکه ریل‌های خروجی آن قطعه را پردازش کنند دریافت می‌کند». نتیجه‌اش زمان کوتاه‌تر تا نخستین توکن (TTFT) است، «اما اگر ریلی محتوا را مسدود کند، کاربر توکن‌ها را پیش‌تر دریافت کرده است». در این صورت، استریم با یک خطای JSON پایان می‌یابد.

با مقدار False، «کاربر هرگز محتوای مسدودشده را نمی‌بیند، اما زمان تا نخستین توکن به اندازه زمان اجرای ریل برای هر قطعه افزایش می‌یابد». پس NeMo، وقتی استریم ریل‌های خروجی فعال باشد، به‌طور پیش‌فرض ابتدا ارسال می‌کند، در حالی که دو سرویس ابری که در ادامه می‌آیند در حالت پیش‌فرض ابتدا بررسی می‌کنند. اگر ریلی را برای جلوگیری از چیزی به کار گرفته‌اید که هرگز نباید دیده شود، این پیش‌فرض را عوض کنید.

اعتبارسنج‌های جمله‌ای در Guardrails AI

Guardrails AI واحد جمله را برگزیده است: «به‌طور پیش‌فرض، اعتبارسنج‌ها صبر می‌کنند تا به اندازه یک جمله محتوا از LLM جمع شود و سپس اعتبارسنجی را اجرا می‌کنند». به گفته همین مستندات، اعتبارسنجی جمله‌های منفرد به‌جای کل پاسخ انباشته «به صرفه‌جویی در هزینه اعتبارسنج‌هایی که به استنتاج پرهزینه نیاز دارند کمک می‌کند».

مستندات دو محدودیت را هم ذکر می‌کند. در حالت استریم، همه انواع on_fail پشتیبانی نمی‌شوند، و وقتی چند اعتبارسنج اصلاح‌کننده دارید، اصلاح‌ها در حالت استریم منتظر می‌مانند «تا همه اعتبارسنج‌ها قطعه‌های کافی جمع کنند» و سپس با یک الگوریتم ادغام ترکیب می‌شوند. مستندات به‌روشنی نمی‌گوید توکن‌های خام پیش از اعتبارسنجی به کاربر می‌رسند یا نه؛ این را در یکپارچه‌سازی خودتان بیازمایید.

بافر کردن پیش‌فرض در Azure OpenAI و Amazon Bedrock

Azure OpenAI در حالت پیش‌فرض متن خروجی مدل را بافر می‌کند، فیلتر محتوا را روی بافر اجرا می‌کند و متن را به‌صورت «قطعه‌های محتوا» برمی‌گرداند، نه توکن‌به‌توکن. گاردریل‌های Amazon Bedrock هم در حالت همگام، که پیش‌فرض است، قطعه‌ها را پیش از ارسال اسکن می‌کنند؛ به گفته AWS این کار «مقداری تأخیر» به پاسخ اضافه می‌کند، اما «دقت بهتری» دارد. در حالت ناهمگام Bedrock، قطعه‌های پاسخ «ممکن است تا پایان اسکن گاردریل محتوای نامناسب داشته باشند»، و پوشاندن اطلاعات حساس در این حالت پشتیبانی نمی‌شود.

گیت جمله‌ای چگونه کار می‌کند؟

دستیار این سایت فقط از محتوای خود سایت پاسخ می‌دهد، و پرسشی که درباره من نیست و هیچ بخشی از متن‌های بازیابی‌شده پشتوانه‌اش نیست، اصلاً به مدل نمی‌رسد. دستیار درباره من به صیغه سوم شخص سخن می‌گوید. در سایتی که نام من را دارد، بدترین خطا سابقه یا مدرکی ساختگی است، و چنین چیزی یک ادعاست. کوچک‌ترین واحدی که یک ادعا را در خود جا می‌دهد جمله است؛ پس گیت جمله‌ای (sentence gate) متن را تا مرز جمله بافر می‌کند، آن را بررسی می‌کند و فقط پس از آن برای خواننده می‌فرستد. دموی RAG در برابر مدل خام در آزمایشگاه هوش مصنوعی سایت هم همان نقطه پایانی (endpoint) استریم را فراخوانی می‌کند.

خط زمانی گیت جمله‌ای: توکن‌ها تا پایان جمله در بافر می‌مانند؛ جمله نخست از چهار بررسی می‌گذرد و بدون نشانگر ارجاع نمایش داده می‌شود؛ جمله دوم مقداری دارد که در منابع نیست، پس نگه داشته می‌شود و استریم متوقف می‌شود.

پایان جمله در متن فارسی و انگلیسی کجاست؟

بخش دشوار کار این است که بدانیم مرز جمله چه زمانی قطعی شده است، آن هم وقتی توکن‌ها هنوز در راه‌اند.

  • نشانه‌های پایان جمله: . و ! و ? و …، به‌علاوه علامت سؤال فارسی ؟ و نقطه پایان جمله در خط عربی ۔، و برای چینی و هندی، 。 و ! و ? و نشانه پایان جمله در خط دیوناگری، یعنی «دَندا» । و «دَندای دوتایی» ॥. دنباله‌ای مانند ?! یک مرز به حساب می‌آید. نقطه‌ویرگول ؛ عمداً کنار گذاشته شده است، چون دو بخش یک جمله را به هم پیوند می‌دهد.

  • پس از نشانه پایان باید فاصله بیاید. همین یک قاعده نمی‌گذارد 3.14 و example.com از وسط به دو جمله تقسیم شوند. نقطه، علامت تعجب و علامت سؤال چینی استثنا هستند، چون در چینی پس از آن‌ها فاصله نمی‌آید. این نشانه‌ها اگر درون نقل‌قول باشند، جمله را جایی تمام می‌کنند که نقل‌قول بسته می‌شود، و اگر پس از آن ویرگول بیاید، اصلاً تمام نمی‌کنند.

  • هر نقطه‌ای جمله را تمام نمی‌کند. نقطه‌ای که پس از آن حرف کوچک لاتین بیاید جمله را تمام نمی‌کند (et al. and)، و نقطه اختصارهای یک فهرست کوتاه، مانند e.g. یا bzw. آلمانی، و نقطه پس از روز در تاریخ آلمانی (am 17. September) هم تمام نمی‌کند.

  • نویسه بعدی تصمیم می‌گیرد. مرز فقط وقتی قطعی می‌شود که نویسه‌ای برسد که نه فاصله است و نه نشانگر ارجاع، چون نشانگرهایی که پس از نقطه می‌آیند به جمله پیش از خود تعلق دارند: همه‌شان، چه [S1][S2] و چه [S1] [S2]، نه فقط اولی.

  • شماره فهرست پس از مرز جمله، جمله نیست. بدون این قاعده، 1. در آغاز پاسخ یا آغاز جمله‌ای تازه ادعایی بدون ارجاع شمرده می‌شد و پاسخی مستند را در همان نخستین بند فهرست متوقف می‌کرد.

  • سقف بافر. اگر تا هزار نویسه مرزی پیدا نشود، بافر در آخرین فاصله بریده و به هر حال بررسی می‌شود.

  • خط خالی حفظ می‌شود. جمله‌ای که پس از یک خط خالی آمده باشد، پس از یک خط خالی هم فرستاده می‌شود؛ پس پاسخی که در دو پاراگراف نوشته شده است، همان دو پاراگراف باقی می‌ماند.

برای نمونه، جداساز جمله‌ای که از این قواعد پیروی کند، پاسخ زیر را به سه جمله تقسیم می‌کند:

کتابخانه صبح زود باز می‌شود [S1]. آیا کافه باز است؟ بله، در روزهای کاری [S2].

  1. کتابخانه صبح زود باز می‌شود [S1].

  2. آیا کافه باز است؟

  3. بله، در روزهای کاری [S2].

اما «ساعت کاری کوتاه‌تر شد؛ کافه هم زودتر بسته می‌شود [S1].» یک جمله باقی می‌ماند، چون ؛ مرز جمله نیست.

انتظار برای نویسه بعدی معمولاً به اندازه یک نویسه تأخیر می‌آورد، و اگر پس از نقطه نشانگرهای ارجاع بیایند، کمی بیشتر؛ جمله آخر هم وقتی استریم تمام شد بررسی می‌شود. در مقایسه‌ها، ارقام فارسی و جداکننده‌های اعشار و هزارگان به شکل لاتین درمی‌آیند (۳٫۱۴ با 3.14 برابر است)؛ این تبدیل فقط برای مقایسه است و ارقام متن را عوض نمی‌کند. شکل عربی حروفی مانند «ی» و «ک» هم به شکل فارسی‌شان برگردانده می‌شود، اِعراب عربی (مانند فتحه و کسره) حذف می‌شود و نیم‌فاصله (ZWNJ) در تطبیق مانند فاصله شمرده می‌شود. این‌ها همان تفاوت‌هایی‌اند که جستجوی ترکیبی در RAG فارسی در سمت بازیابی با آن‌ها سروکار دارد.

چهار بررسی قطعی روی رشته‌ها

اعتبارسنجی خروجی مدل زبانی در این گیت با چهار بررسی قطعی انجام می‌شود و هیچ‌کدام به مدل دیگری نیاز ندارد:

  1. محدوده ارجاع. نشانگری مانند [S7] وقتی فقط شش منبع در اختیار مدل گذاشته شده است، به منبعی ساختگی اشاره می‌کند.

  2. الزام ارجاع. جمله‌ای که هشت واژه یا بیشتر دارد، یا عدد، URL یا نشانی ایمیلی در آن آمده است، باید نشانگر ارجاع داشته باشد. جمله‌های کوتاه ربط‌دهنده بدون ارجاع از بررسی می‌گذرند.

  3. تطبیق عین‌به‌عین با منابع. هر عدد، URL و نشانی ایمیل باید عیناً در منبعی آمده باشد که جمله به آن ارجاع می‌دهد. تطبیق مرز رقم‌ها را رعایت می‌کند؛ پس سالی که در منبع آمده نمی‌تواند پشتوانه درصدی شود که رقم‌هایش همان رقم‌های پایانی آن سال است، و عددهای اعشاری که فقط در صفرهای پایانی فرق دارند یک مقدارند: 20.00 همان 20 است. ویرگولی که هزارگان را جدا نمی‌کند ممیز اعشار است: 2,5 همان 2.5 است، نه 25.

  4. ممنوعیت اول شخص. هر واژه اول شخص استریم را متوقف می‌کند. فهرست انگلیسی کوتاه و ثابت است: ضمیرهای اول شخص و شکل‌های کوتاه‌شده آن‌ها. فهرست فارسی گزینشی است: ضمیرها به‌علاوه چند فعل پرخطر، مانند «هستم»، «دارم» و «کردم». دلیلش این است که فارسی شخص را با شناسه پایان فعل نشان می‌دهد و قاعده‌ای که همه این شناسه‌ها را بگیرد، اسم‌های معمولی را هم می‌گیرد. فهرست‌های زبان‌های دیگر سایت ضمیرهای شخصی و ملکی را در بر دارند، و در فهرست‌های اسپانیایی، پرتغالی، روسی، عربی و هندی چند فعل هم آمده است، چون این زبان‌ها اول شخص را اغلب تنها با فعل بیان می‌کنند؛ در چینی، وجود 我 در هر جای جمله بررسی می‌شود، مگر در ترکیب‌هایی با 自我 به معنای «خود-»، مانند 自我修正 («خوداصلاحی»). اینکه کدام فهرست‌ها به کار می‌روند به زبانی بستگی دارد که پاسخ به آن درخواست شده است. فهرست‌های انگلیسی، فارسی، عربی، روسی، هندی و چینی در همه پاسخ‌ها به کار می‌روند. انگلیسی زبان دستورهای مدل است و از همین رو پاسخی که از زبان درخواست‌شده دور شود، به احتمال زیاد به انگلیسی می‌لغزد؛ پنج زبان دیگر به خط‌هایی نوشته می‌شوند که هیچ نام یا سرواژه لاتینی نمی‌تواند با واژه‌هایشان یکی شود. تنها استثنا پاسخی است که به عربی درخواست شده باشد: در آن «من» و «ما» جست‌وجو نمی‌شوند، چون در عربی «من» حرف اضافه است و «ما» واژه‌ای رایج. فهرست‌های آلمانی، فرانسوی، اسپانیایی و پرتغالی فقط در پاسخ‌هایی به کار می‌روند که به همان زبان درخواست شده‌اند: واژه‌های کوتاه آن‌ها گاه نام‌اند و گاه واژه‌ای معمولی در زبانی دیگر، مانند «Ma» در «Ma et al.». پیش از خواندن هر فهرستی، سرواژه‌های نوشته‌شده با حروف بزرگ، نام خانوادگی پیش از «et al.» و عدد رومی در «Annex I» کنار گذاشته می‌شوند.

وقتی جمله‌ای رد می‌شود چه اتفاقی می‌افتد؟

  • همه جمله‌هایی که پیش از جمله ردشده تأیید شده‌اند فرستاده می‌شوند، حتی اگر در همان دسته‌ای رسیده باشند که جمله ردشده در آن بود.

  • گیت متوقف می‌شود و متوقف می‌ماند: یک جمله بی‌پشتوانه اعتبار بقیه پاسخ را هم از بین می‌برد.

  • رویداد guard دلایل رد را همراه با پیامی ثابت و بومی‌سازی‌شده می‌فرستد که می‌گوید ادامه پاسخ نگه داشته شده است، و رویداد پایانی done این توقف را ثبت می‌کند. جمله ردشده هرگز ارسال نمی‌شود و سرور فقط طول آن را در لاگ می‌نویسد.

  • استریم ارائه‌دهنده مدل بسته می‌شود و این طراحی از همین راه درخواست بالادستی را لغو می‌کند. آنچه پیش از توقف تولید شده در صورت‌حساب ارائه‌دهنده می‌آید، پس به حساب سقف هزینه ماهانه هم گذاشته می‌شود. هر دو پروتکل ارائه‌دهنده، مصرف توکن‌های خروجی را در رویدادی پایانی گزارش می‌کنند که استریمِ نیمه‌کاره هرگز به آن نمی‌رسد؛ برای همین سرویس متنی را که دریافت کرده نگه می‌دارد و هر چه را ارائه‌دهنده گزارش نکرده، با نرخ یک توکن برای هر بایت در کدگذاری UTF-8 پر می‌کند؛ این شمارش اگر خطا کند، بیشتر از واقع می‌شمارد. توکن‌هایی که یک مدل استدلالی (reasoning) صرف فکرکردن می‌کند هرگز به‌صورت متن نمی‌رسند، پس این برآورد آن‌ها را در بر نمی‌گیرد. نوشته طراحی پنج‌لایه سقف هزینه را شرح می‌دهد.

پنل گفت‌وگو این نوبت را به‌عنوان پاسخی نگه‌داشته‌شده علامت می‌زند و بخش فرستاده‌شده آن را در تاریخچه گفت‌وگو حفظ می‌کند، چون آن بخش تا نقطه توقف مستند است.

ناتمام ماندن پاسخ (truncation) خطای دیگری است، و نسخه‌ای قدیمی‌تر از همین دستیار این دو را با هم یکی می‌گرفت. وقتی مدل به سقف توکن خروجی می‌رسید، گیت تکه قطع‌شده را جمله‌ای کامل می‌شمرد و آن را به دلیل نداشتن ارجاع نگه می‌داشت؛ پس به خواننده گفته می‌شد پاسخی که فقط نیمه‌کاره مانده بود پشتوانه‌ای در منابع ندارد. وقتی هم هیچ متنی برنمی‌گشت، آن نوبت گفت‌وگو بی هیچ پیامی ناپدید می‌شد. اکنون تکه ناتمام بدون داوری کنار گذاشته می‌شود و اعلانی جداگانه درباره ناتمام ماندن پاسخ نمایش داده می‌شود. جمله‌ای که نیمه‌کاره ماند، جمله‌ای بی‌پشتوانه نیست. اگر هر دو رخ دهند، توقف گیت اولویت دارد.

نشانگرهای ارجاع درونی‌اند

پرامپت از مدل می‌خواهد هر جمله‌ای را که واقعیتی را بیان می‌کند با نشانگر [Sn] تمام کند، و گیت این الزام را روی هر جمله‌ای که مشمول قاعده الزام ارجاع است اعمال می‌کند. وقتی جمله‌ای تأیید شد، نشانگرهایش حذف می‌شوند و فاصله‌گذاری اصلاح می‌شود، از جمله پیش از علامت‌های نگارشی فارسی مانند ؟ و ،؛ پس خواننده متن روان می‌بیند، نه نشانگر. حذف نشانگرها از پرامپت راه‌حل نیست: در آن صورت هر جمله‌ای که مشمول الزام ارجاع است رد می‌شد.

وقتی مدل حرفی برای گفتن ندارد

وقتی منابع به پرسش پاسخ نمی‌دهند، پرامپت از مدل یک توکن ثابت می‌خواهد، نه یک جمله. امتناعی که خود مدل بنویسد هم مانند هر ادعای دیگری یک ادعاست، آن هم ادعایی بدون ارجاع؛ گیت آن را متوقف می‌کرد و پیامی نشان می‌داد که تقصیر را به گردن منابع می‌انداخت، آن هم برای پرسشی که اصلاً قرار نبود منابع به آن پاسخ دهند.

گیت این توکن را هرگز نمی‌فرستد. تا وقتی متنی که تا این لحظه رسیده ممکن است همان توکن باشد، نگه داشته می‌شود؛ نخستین نویسه‌ای که این احتمال را کنار بگذارد، متن را به مسیر معمول برمی‌گرداند، پس پاسخ عادی هیچ انتظار اضافه‌ای ندارد. اگر پاسخ با این توکن شروع شود، سرویس به‌جای آن پیام ثابت رد پرسش را نشان می‌دهد، همان متنی که در هر رد دیگری هم دیده می‌شود. اگر توکن پس از جمله‌هایی بیاید که پیش‌تر فرستاده شده‌اند، آن جمله‌ها می‌مانند و پاسخ همان‌جا تمام می‌شود.

ادعای مستند لزوماً درست نیست

در پژوهشی به رهبری دانشگاه استنفورد، Magesh و همکارانش ابزارهای پژوهش حقوقی مبتنی بر هوش مصنوعی را ارزیابی کردند و کار خود را «نخستین ارزیابی تجربی پیش‌ثبت‌شده» این ابزارها می‌نامند. ابزارهای هوش مصنوعی LexisNexis و Thomson Reuters هر کدام در ۱۷ تا ۳۳ درصد موارد دچار توهم شدند. در این پژوهش، پاسخی دچار توهم (hallucination یا «هالوسینیشن») شمرده می‌شد که «یا نادرست باشد یا استناد نادرست (misgrounded) داشته باشد»، و استناد نادرست یعنی «گزاره‌های کلیدی همراه با ارجاع آمده‌اند، اما منبع را نادرست تفسیر می‌کنند یا به منبعی ارجاع می‌دهند که در این مورد قابل استناد نیست». پس پاسخی که استناد نادرست دارد، ارجاع هم دارد. پیامد این یافته برای محصولات حقوقی در نوشته‌ای درباره آنچه در هوش مصنوعی حقوقی باید ساخت آمده است.

گیتی که رشته‌ها را می‌سنجد استناد نادرست را نمی‌بیند، و این گیت نقطه‌های کور شناخته‌شده‌ای دارد:

  • جمله‌ای که به دو منبع ارجاع می‌دهد، می‌تواند مقدار را از هر یک از آن دو بگیرد. بررسی در سطح جمله انجام می‌شود، نه در سطح هر بخش از جمله.

  • در تطبیق اعداد، نقطه اعشار مرز به حساب می‌آید. ۱۴٫۵۲ در منبع می‌تواند پشتوانه ۵۲٪ یا ۱۴ شود و جداکننده هزارگان هم همین اثر را دارد: ۱٬۲۰۰ پشتوانه ۲۰۰ می‌شود.

  • واژه‌ها بررسی نمی‌شوند. جمله‌ای بدون عدد که نقشی یا سازمانی را جابه‌جا کند، با هر نشانگر ارجاعی که در محدوده باشد از بررسی می‌گذرد.

  • جمله‌های کوتاه بدون ارجاع از بررسی می‌گذرند. جمله‌ای کمتر از هشت واژه که عدد، URL یا نشانی ایمیل ندارد، به نشانگر ارجاع نیاز ندارد. در چینی میان واژه‌ها فاصله‌ای نیست، پس در آن نویسه‌ها شمرده می‌شوند و هر سه نویسه دو واژه به حساب می‌آید: جمله‌ای که فقط از نویسه‌های چینی ساخته شده باشد، تا وقتی کمتر از دوازده نویسه دارد به نشانگر نیاز ندارد.

  • فهرست‌های ثابت کامل نیستند. در انگلیسی we'd و let's از بررسی می‌گذرند، و ضمیری هم که یکسره با حروف بزرگ نوشته شود، مانند WE یا US، می‌گذرد، چون سرواژه خوانده می‌شود. پوشش اول شخص در فارسی بیشتر به پرامپت تکیه دارد: فعل اول شخصی که در فهرست گزینشی نیامده باشد از بررسی می‌گذرد. فهرست‌های زبان‌های دیگر هم گزینشی‌اند، و اول شخصِ آلمانی، فرانسوی، اسپانیایی یا پرتغالی در پاسخی که به زبان دیگری درخواست شده باشد، به پرامپت سپرده شده است.

  • گیومه یا پرانتز بسته پس از نشانه پایان ممکن است مرز جمله را پنهان کند. وقتی گیومه پس از نشانه پایان بسته شود، فقط حرف بزرگ یا سطر تازه جمله را تمام می‌کند: end." Next در انگلیسی دو جمله است. پس از پرانتز بسته، مانند .)، فقط سطر تازه یا نشانگر ارجاع جمله را تمام می‌کند، پس end.) Next یک جمله است. فارسی، عربی و هندی حرف بزرگ ندارند، پس در است؟» او دو جمله یک واحد به حساب می‌آیند و یکجا داوری می‌شوند، و گیومه‌های آلمانی „…?“ و »…?« هم اصلاً بسته خوانده نمی‌شوند. نشانه‌های پایان چینی قاعده خودشان را دارند که بالاتر آمد.

خطا در جهت مخالف هم رخ می‌دهد. نقطه اختصار در «ه.ش.»، یا در اختصار لاتینی که در فهرست نیست و پس از آن حرف بزرگ می‌آید، مانند Mr. پیش از یک نام، اگر پس از آن فاصله بیاید، «جمله» را زودتر تمام می‌کند، و پرسشی در گیومه هم که پس از آن نامی لاتین با حرف بزرگ، یا در آلمانی هر اسمی، بیاید همین کار را می‌کند؛ اگر تکه جداشده هشت واژه یا بیشتر داشته باشد و نشانگر ارجاع نداشته باشد، پاسخی مستند نگه داشته می‌شود. فهرستی هم که سطر پیش از آن به دونقطه ختم می‌شود، پیش از 1. مرز جمله‌ای ندارد؛ پس جداساز تکه را درست پس از این شماره جدا می‌کند و آن تکه در بررسی الزام ارجاع رد می‌شود. در هر دو حالت، گیت بسته عمل می‌کند (fail closed): پاسخ را نگه می‌دارد، نه اینکه متن بررسی‌نشده را بفرستد.

پیکره متنی هم ثابت نمی‌ماند. مقاله‌های منتشرشده برای دستیار نمایه می‌شوند، پس رقم‌هایشان، از جمله همان ۱۷ تا ۳۳ درصد همین نوشته، به مقدارهایی تبدیل می‌شوند که گیت مستند می‌شمارد. مستند در این گیت یعنی «جایی در منبعی آمده است که جمله به آن ارجاع می‌دهد»، نه «درباره همین موضوع است».

گیت آخرین بررسی پیش از ارسال پاسخ است، اما تنها بررسی نیست. Joren و همکارانش نشان دادند مدل‌های بزرگ‌تر وقتی زمینه کافی نیست «اغلب به‌جای امتناع، پاسخ نادرست می‌دهند»، و Google Research می‌نویسد با آنکه RAG در مجموع عملکرد را بهتر می‌کند، برخلاف انتظار «توانایی مدل را برای خودداری از پاسخ، در جایی که باید، کاهش می‌دهد». به همین دلیل رد پرسش‌های خارج از محدوده و آستانه امتیاز بازیابی پیش از گیت می‌آیند؛ این لایه‌ها در طراحی پنج‌لایه برای جلوگیری از توهم هوش مصنوعی در RAG شرح داده شده‌اند. پیش‌چاپ Frost محدودیت نتیجه خودش را با جمله‌ای بیان می‌کند که درباره این گیت هم صدق می‌کند: نتیجه «انطباق با سیاست برای یک لایه پشتیبان قطعی است، نه شاهدی بر ایمنی معنایی یا کامل بودن سیاست».

استریم پاسخ چت‌بات با SSE

فناوری Server-Sent Events یا SSE بخشی از استاندارد HTML است: سرور روی یک پاسخ HTTP باز، رویدادهای متنی را یکی پس از دیگری به مرورگر می‌فرستد. دستیار این سایت هم پاسخ را به همین شکل می‌فرستد. گیت جمله‌ای فقط وقتی سودمند است که مسیر انتقال هم هر جمله تأییدشده را بی‌درنگ به خواننده برساند، و چهار نکته زیر به همین مربوط‌اند.

چرا fetch و ReadableStream، نه EventSource؟

در مشخصات WHATWG، سازنده EventSource یک URL و فقط یک گزینه می‌گیرد: withCredentials. نه می‌توان متد درخواست را تعیین کرد و نه بدنه‌ای فرستاد، در حالی که پرسش کاربر، همراه با زبان و تاریخچه گفت‌وگو، باید در بدنه درخواست فرستاده شود. پس نقطه پایانی دستیار درخواست POST می‌پذیرد و پاسخ را به‌صورت Server-Sent Events برمی‌گرداند، و مرورگر آن را با fetch، یک reader از ReadableStream و AbortController می‌خواند. به گفته MDN، متد abort() «می‌تواند درخواست‌های fetch، مصرف هر بدنه پاسخ و استریم‌ها را لغو کند».

تجزیه‌گری که خودتان می‌نویسید باید این قاعده‌ها را رعایت کند:

  • میان خواندن‌ها بافر داشته باشد. هر قطعه دریافتی یک پیام کامل نیست.

  • با رسیدن خط خالی، رویداد را تحویل دهد. تا آن لحظه رویداد کامل نیست.

  • خط‌های توضیح را نادیده بگیرد، یعنی خط‌هایی که با : شروع می‌شوند.

  • داده چندخطی را یکی کند: مقدار همه خط‌های data: یک رویداد را با نویسه line feed به هم وصل کند.

  • پایان خط را در هر سه شکل CRLF و LF و CR بشناسد.

  • پس از دونقطه فقط یک فاصله را حذف کند، نه بیشتر.

برای کدگشایی از TextDecoder با {stream: true} استفاده کنید؛ MDN این گزینه را برای زمانی توصیه می‌کند که «داده در قطعه‌ها پردازش می‌شود». اگر چت‌بات فارسی می‌سازید، این گزینه اختیاری نیست: حروف فارسی در UTF-8 چندبایتی‌اند، و اگر بایت‌های یک حرف میان دو قطعه تقسیم شوند، بدون این گزینه به نویسه جایگزین تبدیل می‌شوند.

بافر شدن پاسخ در پراکسی (X-Accel-Buffering)

nginx پاسخ‌های پراکسی‌شده را به‌طور پیش‌فرض بافر می‌کند و سرآیند پاسخ X-Accel-Buffering می‌تواند این رفتار را خاموش کند. استریم دستیار این سایت سرآیندهای X-Accel-Buffering: no و Cache-Control: no-cache, no-store, no-transform را می‌فرستد؛ no-transform از آن رو آمده است که پراکسی‌ای که بدنه را دوباره فشرده می‌کند ممکن است قطعه‌ها را یکجا کند. Caddy پاسخ‌های text/event-stream را بی‌درنگ به کلاینت می‌فرستد، و پیکربندی Caddy این سایت روی مسیر چت flush_interval -1 را هم تنظیم کرده است. در service worker سایت هیچ مسیری (route) برای استریم ثبت نشده است، چون هر راهبرد کش، پاسخ را تا پایانش نگه می‌داشت.

پیام keep-alive و خطا در دل استریم باز

مشخصات SSE برای محافظت در برابر پراکسی‌های قدیمی که اتصال HTTP را پس از مهلتی کوتاه قطع می‌کنند، توصیه می‌کند یک خط توضیح (comment) «هر ۱۵ ثانیه یا در همین حدود» فرستاده شود؛ سرور این سایت با همین فاصله ping می‌فرستد. وقتی نخستین بایت فرستاده می‌شود، کد وضعیت HTTP، یعنی 200، پیش‌تر ارسال شده است؛ پس خطاها به‌صورت رویداد error در دل همان استریم فرستاده می‌شوند.

لغو درخواست و توقف هزینه

توقف پاسخ در مرورگر، درخواست fetch را لغو می‌کند. سرور با رسیدن هر توکن بررسی می‌کند که اتصال قطع شده است یا نه؛ اگر قطع شده باشد، از حلقه بیرون می‌آید و استریم ارائه‌دهنده را می‌بندد، که در این طراحی درخواست بالادستی را لغو می‌کند. دو چیز را باید آزمود، نه فرض کرد. نخست اینکه همه پراکسی‌ها و CDNهای میان مرورگر و API شما قطع اتصال کلاینت را منتقل کنند؛ این مسیر را بیازمایید و به پیکربندی تکیه نکنید. دوم اینکه ارائه‌دهنده مدل میزان مصرف را چه زمانی گزارش می‌کند: اگر گزارش مصرف توکن‌های خروجی فقط همراه آخرین قطعه برسد، استریمی که زودتر می‌بندید هرگز آن را دریافت نمی‌کند، و سقف هزینه‌ای که مصرف گزارش‌شده را می‌شمارد کمتر از آنچه واقعاً تولید شده حساب می‌کند.

چگونه گاردریل پاسخ استریم را بیازماییم؟

آزمون‌ها رفتار گیت را در سه سطح پوشش می‌دهند:

  • آزمون‌های واحد گیت روی منابع ثابت. مقدار ساختگی، نشانگر خارج از محدوده، ادعای بدون ارجاع و واژه اول شخص (در هر یک از زبان‌های سایت) استریم را متوقف می‌کنند؛ مقدار مستند و جمله کوتاه ربط‌دهنده از بررسی می‌گذرند؛ و جمله‌های تأییدشده پیشین با رد شدن جمله‌ای در ادامه از بین نمی‌روند.

  • آزمون‌های جداساز جمله. اعداد اعشاری و نام دامنه‌ها مرز نیستند، نشانگر ارجاع با جمله خودش می‌ماند و ؟ جمله را تمام می‌کند. متن چینی هم بدون فاصله به جمله‌ها تقسیم می‌شود. آزمونی که ارزش دارد در پروژه خودتان هم بنویسید این است: متنی که در تکه‌های سه‌نویسه‌ای وارد می‌شود باید دقیقاً مانند کل همان متن به جمله تقسیم شود.

  • آزمون‌های استریم با ارائه‌دهنده‌ای شبیه‌سازی‌شده که پاسخ را در تکه‌های کوچک بازپخش می‌کند: مقدار ساختگی هرگز روی شبکه فرستاده نمی‌شود، توقف گیت یا قطع اتصال، استریم بالادستی را می‌بندد و پاسخ ناتمام به‌عنوان پاسخ ناتمام گزارش می‌شود.

در ادامه طرحی از دو آزمون اصلی آمده است که برای همین مقاله نوشته شده و از کد سایت برداشته نشده است. رشته‌های نمونه انگلیسی‌اند، چون در نمایش چپ‌به‌راست کد، متن فارسی کنار نشانگرهایی مانند [S1] جابه‌جا دیده می‌شود؛ همین آزمون‌ها را با پاسخ‌های فارسی هم اجرا کنید:

# Sketch only: SentenceSplitter and SentenceGate stand for your own classes.
import pytest

ANSWER = "The reading room opens early [S1]. Is the café open? Yes, on weekdays [S2]."

def pieces(text, size):
    return [text[i:i + size] for i in range(0, len(text), size)]

def split_streamed(text, size):
    splitter = SentenceSplitter()
    sentences = [s for piece in pieces(text, size) for s in splitter.feed(piece)]
    tail = splitter.flush()
    return sentences + ([tail] if tail else [])

@pytest.mark.parametrize("size", [1, 2, 3, 7])
def test_piece_size_does_not_change_sentences(size):
    assert split_streamed(ANSWER, size) == split_streamed(ANSWER, len(ANSWER))

def test_unsupported_value_never_reaches_the_reader():
    gate = SentenceGate(sources=["The library opens at nine."])
    released = ""
    for piece in pieces("It opens at nine [S1]. Late fees ended in 2019 [S1]. More", 7):
        outcome = gate.feed(piece)
        released += outcome.text
        if outcome.stopped:
            break
    assert "2019" not in released and gate.stopped

هیچ‌یک از این آزمون‌ها روی مدلی زنده اجرا نمی‌شود و جای red-teaming را نمی‌گیرد. آنچه سنجیده‌ام خطاهای خود گیت است: پاسخ‌هایی که در محیط اصلی ضبط و سپس آفلاین دوباره از گیت گذرانده شدند، تا معلوم شود کجا پاسخی مستند را متوقف کرده است. این شمار توقف‌های نابه‌جا با یک مدل و یک مجموعه پرسش است، نه اینکه یک مدل واقعی هر چند وقت یک بار چیزی از خودش می‌سازد؛ پس آن را به‌صورت نرخ منتشر نمی‌کنم.

چک‌لیست راه‌اندازی گاردریل روی پاسخ استریم

  • روشن کنید چه چیزی هرگز نباید دیده شود. پاسخ همین پرسش تعیین می‌کند که ابتدا بررسی کنید یا ابتدا بفرستید، فارغ از پیش‌فرض فریم‌ورکی که به کار می‌برید.

  • واحد بررسی را انتخاب کنید و روشن بگویید خواننده منتظر چه می‌ماند: یک قطعه ثابت یا یک جمله.

  • هر جا بررسی رشته‌ای کافی است، بررسی قطعی به کار ببرید و پیش از مقایسه، ارقام و حروف را یکسان کنید: ارقام فارسی و لاتین، شکل‌های عربی و فارسی «ی» و «ک»، و نیم‌فاصله. ریلی که خودش مدل را فراخوانی می‌کند، زمان اجرایش را به هر واحد اضافه می‌کند.

  • مرز جمله را فقط وقتی قطعی بدانید که نویسه بعدی رسیده باشد، و برای بافر سقف بگذارید.

  • هنگام رد یک جمله، آنچه را تأیید شده است بفرستید، استریم را متوقف کنید، پیامی ثابت نمایش دهید، متن ردشده را هرگز بازنگردانید و استریم بالادستی را ببندید.

  • ناتمام ماندن پاسخ را از نقض قاعده جدا کنید و مصرف را بر پایه آنچه تولید شده بشمارید، نه آنچه نمایش داده شده است.

  • انتقال: درخواست POST با fetch، reader استریم و AbortController؛ کدگشای استریمی؛ ping با خط توضیح؛ خطا به‌صورت رویداد.

  • پراکسی‌ها: X-Accel-Buffering: no، no-transform، ارسال فوری text/event-stream، مسیر قطع اتصالی که آزموده شده باشد، و هیچ service worker روی مسیر استریم.

  • آزمون‌ها: پاسخ‌ها را در تکه‌های کوچک بازپخش کنید و مطمئن شوید مقدار ردشده هرگز روی شبکه فرستاده نمی‌شود.

اگر دستیاری مبتنی بر منابع طراحی می‌کنید و می‌خواهید کس دیگری هم مسیر ارسال پاسخ آن را بازبینی کند، صفحه سنجش و ارزیابی فنی پروژه جای شروع این گفت‌وگوست.

پرسش‌های متداول

آیا می‌توان خروجی LLM را بدون کنار گذاشتن استریم اعتبارسنجی کرد؟

بله. متن را تا واحدی که قابل بررسی است بافر کنید و هر واحد را پس از تأیید بفرستید. در این صورت خواننده متن را جمله‌به‌جمله یا قطعه‌به‌قطعه می‌بیند، نه توکن‌به‌توکن.

آیا بافر کردن تا پایان جمله زمان تا نخستین توکن را بیشتر می‌کند؟

تولید نخستین توکن را نه، اما نمایش متن را به اندازه یک جمله عقب می‌اندازد: متن وقتی ظاهر می‌شود که نخستین جمله کامل و بررسی شده باشد. بررسی‌های رشته‌ای در مقایسه با تولید متن سبک‌اند، اما این تفاوت را در این سایت اندازه نگرفته‌ام، پس عددی نمی‌دهم.

چرا EventSource برای استریم پاسخ LLM مناسب نیست؟

EventSource فقط یک URL و یک پرچم برای اعتبارنامه‌ها (credentials) می‌گیرد و نمی‌تواند بدنه درخواست بفرستد. درخواست چت، که تاریخچه گفت‌وگو را هم دارد، باید با POST فرستاده شود؛ پس از fetch و یک reader استریم استفاده کنید. این روش لغو درخواست با AbortController را هم ممکن می‌کند.

آیا بررسی ارجاع جلوی توهم هوش مصنوعی را می‌گیرد؟

خیر. جلوی سه چیز را می‌گیرد: شماره منبع ساختگی، جمله‌ای که باید ارجاع داشته باشد و ندارد، و مقداری که در هیچ‌یک از منابعی که جمله به آن‌ها ارجاع می‌دهد نیامده است. اما نمی‌تواند تشخیص دهد منبعی که به آن ارجاع شده واقعاً پشتوانه آن ادعاست یا نه؛ این همان خطای استناد نادرست است. آن را لایه‌ای پشتیبان بدانید که پشت رد پرسش و آستانه بازیابی قرار می‌گیرد.

مرز جمله در متن فارسی چگونه تشخیص داده می‌شود؟

در گیت این سایت، نقطه، علامت سؤال و تعجب، سه‌نقطه، علامت سؤال فارسی ؟ و ۔ جمله را تمام می‌کنند، به شرط آنکه پس از آن‌ها فاصله بیاید؛ نقطه‌ویرگول ؛ جمله را تمام نمی‌کند، چون بخش‌های یک جمله را به هم پیوند می‌دهد. مرز فقط وقتی قطعی است که نویسه بعدی برسد، تا نشانگرهای ارجاعی که پس از نقطه می‌آیند با جمله خودشان بمانند. اما اگر گیومه یا پرانتز بی‌فاصله پس از نشانه پایان بسته شود، مانند است؟» او، مرز دیده نمی‌شود، و نقطه اختصار در «ه.ش.» جمله را زودتر تمام می‌کند.

منابع