وقتی یک مدل زبانی بزرگ (LLM) — مثل کتابخانهداری که میلیاردها صفحه را خوانده و حالا با همان لحن کتابها جواب میدهد — به جای یک شیء دادهای، یک پاراگراف متن برمیگرداند، رابط کاربری شما میشکند، تستهای نرمافزاری شکست میخورند و API شما به بازی حدس زدن با عبارات منظم (Regex) تبدیل میشود. برای هشت بخش از این سری، عامل هوشمند هر نوبت را به یک شکل به پایان میرساند: چاپ یک جمله برای خواندن توسط انسان. در حالی که این روش برای یک نمایش (Demo) کاربردی است، اما برای یک محصول واقعی یک بنبست است؛ زیرا عاملهای هوشمند قابلاعتماد نمیتوانند تنها با تکیه بر نثر (Prose) بقا یابند. برای حل این مشکل، «بی توکیان» (B Torkian)، قهرمان توسعهدهنده انویدیا در دانشگاه USC، در بخش نهم این سری، مکانیزمی را برای انتقال خروجیهای عامل از متنهای غیرقابل پیشبینی به یک «قرارداد JSON» اعتبارسنجیشده تشریح کرده است.
به گزارش dev.to، اکثر توسعهدهندگان سعی میکنند از طرحوارههای (Schemas) response_format در سمت سرور برای اجبار مدل به رعایت ساختار استفاده کنند. اما این روش در مدلهای وزنباز (Open Weights) — یعنی مدلهایی که دستور پخت آنها علناً منتشر شده و نه فقط غذای آماده — مانند llama-3.3-70b در کاتالوگهای API میزبانیشده، غیرقابلاعتماد است. حالت «طرحواره سختگیرانه» (Strict schema mode) یک ویژگی سمت سرور است که نقاط انتهاییِ مدلهای باز، آن را بهطور ناسازگاری پیادهسازی میکنند و اغلب با فراخوانی ابزارها (Tool Calling) تداخل دارد. وضعیت فعلی در سطح استانداردهای تولیدی (Production) ایجاب میکند که مرز اعتماد از سرور به سمت کلاینت منتقل شود: یعنی درخواست JSON در پرامپت ارسال شود و سپس فرآیند تجزیه، اعتبارسنجی و اصلاح بهصورت دستی انجام گیرد.
خط لوله خروجی ساختاریافته
راهحل پیشنهادی توکیان یک «نردبان» چهارمرحلهای است که خروجی مدل را بهطور پیشفرض «نامعتبر» فرض میکند. به جای امیدواری برای یک پاسخ بینقص، عامل یک حلقه سختگیرانه را پیاده میکند:
- تجزیه (Parsing): تابع
parse_json_objectبا استفاده از متدهای.find()و.rfind()، اولین علامت آکولاد باز{و آخرین علامت آکولاد بسته}را در متن شناسایی میکند. این کار باعث میشود متون اضافی گفتگو یا فنسهای Markdown (مانند ```json) حذف شوند تا فقط شیء JSON ایزوله شود. اگر هیچ آکولادی یافت نشود یا آکولاد بسته قبل از آکولاد باز بیاید، یکValueErrorبا پیام «هیچ شیء JSON یافت نشد» صادر میشود. - اعتبارسنجی (Validation): تابع
validate_answerشیء تجزیهشده را با مجموعهای سختگیرانه از کلیدهای ضروری (REQUIRED_KEYS) بررسی میکند:status،answer،category،items،missingوsources. این تابع تأیید میکند کهstatusوcategoryاعضای لیستهای تعریفشده (Enums) باشند. بهطور مشخص،STATUSESباید یکی از مقادیر{"answered", "not_found", "needs_clarification"}وCATEGORIESباید یکی از مقادیر{"campus_event", "campus_hours", "campus_resource", "comparison", "refusal"}باشد. همچنین بررسی میکند کهanswerیک رشته متنی باشد وitems،missingوsourcesهمگی لیست باشند. بهویژه، چک میکند که هر ورودی در لیستitemsخود یک شیء (Object) باشد. - اصلاح (Repair): اگر اعتبارسنجی شکست بخورد، سیستم دقیقاً یک تلاش برای اصلاح از طریق
repair_answer_jsonانجام میدهد. در این مرحله، لیست خطاهای مشخص به مدل بازگردانده میشود در حالی که دما (Temperature) روی صفر و محدودیت توکنها (max_tokens) روی ۸۰۰ تنظیم شده است. پرامپت سیستمی در این مرحله این است: «تو اصلاحکننده JSONهای معیوب هستی. فقط و فقط یک شیء JSON معتبر برگردان.» پرامپت کاربر نیز از مدل میخواهد تمام حقایق را حفظ کرده و فقط ساختار JSON را اصلاح کند. اگر API خطا دهد یا نتیجه همچنان غیرقابل تجزیه باشد، تابع اصلاح مقدارNoneرا برمیگرداند. - جایگزین (Fallback): اگر اصلاح شکست بخورد یا نتیجه همچنان نامعتبر باشد، عامل یک شیء خطای قطعی (Deterministic) را از طریق
format_errorبرمیگرداند. این کار از کرش کردن برنامه جلوگیری کرده و پاسخی تایپشده برمیگرداند که در آنstatusبرابر باneeds_clarification،answerبرابر با «من نتوانستم یک پاسخ ساختاریافته معتبر تولید کنم. لطفاً دوباره بپرسید.» وcategoryبرابر باrefusalاست. در مخزن کامل کد، این جایگزین میتواند یک پاسخ اختیاری بر اساس علت خطا (مثلاً برای شکست در محدودیت تعداد گامها) بگیرد تا حتی مسیر «تسلیم شدن» نیز قرارداد دادهای را رعایت کند.
تعریف قرارداد دادهها
برای یک عامل دستیار دانشگاهی، توکیان یک قرارداد ششکلیدی تعریف کرده است تا هر نوع تعاملی را مدیریت کند. این قرارداد مانند یک لولا عمل میکند: لاگهای ردیابی (Tracing) این شیء را ثبت میکنند، ارزیابیها (Evaluations) روی فیلدهای آن ادعاهایی را بررسی میکنند و استقرار نهایی (Deployment) آن را به عنوان بدنه پاسخ HTTP برمیگرداند:
status: یک Enum شاملanswered(پاسخ داده شد)،not_found(یافته نشد) یاneeds_clarification(نیاز به شفافسازی) تا کدهای پاییندستی بتوانند بهصورت منطقی تصمیمگیری کنند.answer: یک رشته متنی به زبان طبیعی (مثلاً: «باشگاه هوش مصنوعی USC هر پنجشنبه ساعت ۵ عصر در ساختمان مهندسی، اتاق ۲۰۴ تشکیل جلسه میدهد.»).category: یک Enum متشکل ازcampus_event(رویداد دانشگاهی)،campus_hours(ساعات کاری)،campus_resource(منابع دانشگاهی)،comparison(مقایسه) یاrefusal(رد درخواست).items: یک لیست از اشیاء قابلخوان برای ماشین (Machine-readable payload) که شامل جزئیات ساختاریافته مانندname(نام)،day(روز)،time(ساعت) وlocation(مکان) است (مثلاً:{"name": "USC AI Club meeting", "day": "Thursday", "time": "5 PM", "location": "engineering building, room 204"}).missing: لیستی از موارد مشخصی که کاربر خواسته بود اما عامل نتوانست آنها را بیابد.sources: خطوط دقیقاً استخراجشده از پایگاه دانش برای مبنیسازی (Grounding) پاسخ، تا روحیه فصل «نردههای حفاظتی» (Guardrails) حفظ شود.
یکپارچگی با NVIDIA NIM
در حالی که این راهنما بر اعتبارسنجی سمت کلاینت تأکید دارد، اشاره میکند که نسخههای خودمیزبانیشده NVIDIA NIM 1.x از رمزگشایی محدودشده (Constrained Decoding) در سمت سرور پشتیبانی میکنند. کاربران میتوانند از پارامتر extra_body={"nvext": {"guided_json": schema}} برای رمزگشایی محدود استفاده کنند و آخرین نسخهها در حال حرکت به سمت حالتهای JSON سازگار با OpenAI هستند. صرفنظر از این قابلیتهای سرور، مستندات رسمی انویدیا (در دسترس در https://docs.nvidia.com/nim/large-language-models/latest/structured-generation.html) صراحتاً به توسعهدهندگان توصیه میکند که پاسخ را در سمت کلاینت اعتبارسنجی کنند، که دقیقاً همان «نردبان» است که در این پست ساخته شد.
برای پیادهسازی این سیستم، منطق داخلی عامل بازطراحی شده است. پیش از این، در ورکشاپ ۸، دو حلقه تقریباً یکسان در توابع chat() و stream() وجود داشت. اینها اکنون در یک حلقه مشترک به نام _run_turn(user_message, stream) تجمیع شدهاند. این حلقه مشترک از _complete(stream) برای مدیریت فراخوانی مدل، بازسازی ابزارها و قطعات پیام استفاده میکند. اکنون chat() و stream() تنها به عنوان واسطههای تکخطی برای _run_turn عمل میکنند:
def chat(self, user_message: str) -> dict:
return self._run_turn(user_message, stream=False)
def stream(self, user_message: str) -> dict:
return self._run_turn(user_message, stream=True)
یک نقطه ضعف صادقانه در این معماری این است که نمایش زنده توکنبه-توکن برای پاسخ نهایی حذف شده است. زیرا نمایش یک JSON نیمهکاره برای کاربر بیمعنی است و UI باید منتظر تکمیل کامل شیء بماند. با این حال، استریمینگ در سطح شبکه (Wire-level streaming) و بازسازی قطعات فراخوانی ابزار از بخش ۸ همچنان پابرجاست.
آموزش فرمت به مدل
هدایت ابزارها از ورکشاپهای ۷ و ۸ بدون تغییر باقی مانده است، اما بلوکی با عنوان FINAL ANSWER FORMAT به انتهای پرامپت سیستمی اضافه شده است. این بلوک صراحتاً دستور میدهد که پاسخ نهایی «باید» یک شیء JSON واحد باشد و هیچ چیز دیگری نباشد — نه نثر و نه کد-فنسها. این بخش کلیدهای دقیق و مقادیر مجاز Enum برای status و category را لیست میکند تا قرارداد تثبیت شود:
«فرمت پاسخ نهایی (FINAL ANSWER FORMAT): وقتی استفاده از ابزارها را به پایان رساندی، پاسخ نهایی تو باید یک شیء JSON واحد باشد و هیچ چیز دیگر — نه نثر و نه کد-فنس. دقیقاً از این کلیدها استفاده کن: status (answered|not_found|needs_clarification), answer (string), category (campus_event|campus_hours|campus_resource|comparison|refusal), items (list), missing (list), sources (list).»
به محض اینکه مدل فراخوانی ابزارها را متوقف کند، عامل متد _finalize_json را فراخوانی میکند. این متد سعی میکند متن خام را تجزیه و اعتبارسنجی کند؛ اگر شکست بخورد، تلاش برای اصلاح را فعال میکند. این فرآیند از یک جریان سختگیرانه پیروی میکند: «تجزیه $\rightarrow$ اعتبارسنجی $\rightarrow$ اصلاح (در صورت نیاز) $\rightarrow$ جایگزین نهایی». نکته حیاتی این است که عامل، نسخه استاندارد json.dumps(data) را در تاریخچه گفتگو ذخیره میکند، نه متن خام مدل را. این کار تضمین میکند که حافظه برای نوبت بعدی تمیز و ساختاریافته باقی بماند.
آزمایش پیادهسازی
اثربخشی این رویکرد هنگام اجرای عامل در مواجهه با پرسوجوهای پیچیده با استفاده از ChatSession(verbose=True) مشهود است. برای سوالی مانند «باشگاه هوش مصنوعی USC چه زمانی تشکیل جلسه میدهد؟»، عامل وضعیتی با answered و دستهبندی campus_event برمیگرداند. وقتی پرسیده شود «چند روز تا آن زمان مانده است؟»، عامل از حافظه چند-نوبتی و ابزارها برای تحلیل ضمیر «آن» استفاده میکند. در مورد سوال «کدام زودتر است، آن جلسه یا ساعات اداری AI/ML؟»، عامل برای هر روز یک بار فراخوانی ابزار مقایسه را اجرا میکند تا نتیجه را تعیین کند.
زمانی که عامل به بنبست میرسد — مثلاً وقتی رمز عبور وایفای دانشگاه (که اطلاعاتی محدود است) از او خواسته میشود — او صرفاً در قالب نثر عذرخواهی نمیکند. به جای یک جمله مبهم، یک شیء refusal تایپشده برمیگرداند:
{"status": "not_found", "answer": "من این اطلاعات را ندارم — لطفاً از باشگاه هوش مصنوعی USC بپرسید.", "category": "refusal", "items": [], "missing": ["USC campus wifi password"], "sources": []}.
انتقال از دمو به محصول
این تغییر، عامل را از یک «چتبات» به یک «قطعه نرمافزاری» تبدیل میکند. با تبدیل پاسخ نهایی به داده، عامل قابل یکپارچگی با لاگهای ردیابی، مجموعههای ارزیابی خودکار و پاسخهای استاندارد API HTTP میشود. در بستر کلی این سری، این نقطه چرخش است. در حالی که ورکشاپهای ۱ تا ۸ بر هوشمندتر کردن عامل تمرکز داشت، ورکشاپ ۹ آن را «قابلاتصال» (Integratable) میکند:
- ورکشاپ ۱: مغز (Brain)
- ورکشاپ ۲: حافظه حقایق (Memory of facts)
- ورکشاپ ۳: قضاوت (Judgment)
- ورکشاپ ۴: قابلیت انتقال (Portability)
- ورکشاپ ۵: دستها (Hands / Tool use)
- ورکشاپ ۶: یک برنامه (A plan)
- ورکشاپ ۷: حافظه گفتگو (Conversation memory)
- ورکشاپ ۸: صدای آنی (Real-time voice/streaming)
- ورکشاپ ۹: یک قرارداد (خروجیای که نرمافزارهای دیگر بتوانند مصرف کنند)
این بنیاد ساختاری، مسیر نیمه دوم سری را هموار میکند:
- ردیابیها (Traces): ثبت ابزارها، تأخیر و شیء JSON نهایی هر نوبت به صورت JSONL برای تحلیلهای بازنگرانه و مشاهده دقیق کارهای عامل.
- ارزیابیها (Evals): بازپخش سوالات و بررسی اینکه آیا
statusوcategoryصحیح هستند و آیا فیلدmissingدر سوالاتی مانند رمز وایفای بهدرستی فعال میشود. - استقرار (Deployment): ماندگارسازی تاریخچهها و سرویسدهی بدنه JSON پشت یک API تولیدی.
توسعهدهندگان میتوانند پیادهسازی کامل را در مخزن گیتهاب github.com/torkian/nvidia-nim-workshop بیابند، شامل نوتبوک تک-کلیکی Colab با نام part9_structured_output.ipynb و اسکریپت پایتون part9_structured_output.py. این کد تحت لایسنس MIT است و به توسعهدهندگان اجازه میدهد آن را فورک کرده و پایگاه دانش و قرارداد را با نیازهای پروژه خود جایگزین کنند. این سری کامل از برنامههای پایه NIM و RAG مبتنی بر Embedding (بخش ۲)، به نردههای حفاظتی (بخش ۳)، خودمیزبانی NIM (بخش ۴)، فراخوانی ابزارهای چندمرحلهای (بخش ۵-۶)، حافظه گفتگو (بخش ۷) و استریمینگ (بخش ۸) پیش میرود.
گام بعدی شما
- پیادهسازی حلقه
parse-validate-repairرا در پروژههایی که از مدلهای Llama-3.1 یا 3.3 استفاده میکنند، جایگزین تکیه برresponse_formatدر سمت سرور کنید. - برای کاهش نرخ خطا، در مرحله Repair، مقدار Temperature را روی ۰ تنظیم کنید تا خروجی مدل متمرکزتر و دقیقتر شود.
- ساختار خروجیهای مدل خود را به صورت یک قرارداد (Contract) تعریف کنید تا بتوانید داشبوردهای مانیتورینگ را روی فیلدهای خاص (مانند
statusبرای تحلیل نرخ شکست) متصل کنید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو