فاز 11 · درس 3

خروجی‌های ساختاریافته در مدل‌های زبانی: از JSON تا دیکود مقید

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
یک قیف فلزی دقیق با شبکه‌های فیلتر هندسی که رشته‌های متنی را دریافت کرده و به مکعب‌های منظم، یکدست و نوع‌دار تبدیل می‌کند.
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. طیف خروجی‌های ساختاریافته
  5. زبان قرارداد: JSON Schema
  6. الگوی Pydantic
  7. فراخوانی تابع / استفاده از ابزار
  8. حالت‌های شکست رایج
  9. پیاده‌سازی از صفر
  10. گام 1: اعتبارسنج اسکیماهای JSON
  11. گام 2: تبدیل مدل شبیه Pydantic به اسکیما
  12. گام 3: فیلتر کردن توکن‌ها با دیکود مقید
  13. گام 4: پایپ‌لاین استخراج داده
  14. گام 5: اجرای پایپ‌لاین کامل
  15. استفاده در عمل
  16. خروجی‌های ساختاریافته در OpenAI
  17. فراخوانی ابزار در Anthropic
  18. کتابخانه‌ی Instructor
  19. خروجی این درس
  20. تمرین‌ها
  21. اصطلاحات کلیدی
  22. برای مطالعه‌ی بیشتر

بزرگ‌ترین چالش اتصال مدل‌های زبانی به نرم‌افزارهای تجاری، دریافت متنی آزاد به‌جای داده‌های ساخت‌یافته و معتبر است. اتکا به پرامپت‌هایی مثل «پاسخ را در قالب JSON بده» معمولاً پایپ‌لاین‌های پروداکشن را به علت خطاهای نحوی، کدهای مارک‌داون اضافی یا مقادیر نامعتبر متوقف می‌کند. در این درس یاد می‌گیرید چگونه به‌جای سر‌وکله‌زدن با عبارات باقاعده (Regex)، داده‌های خروجی را مستقیماً در سطح دیکودینگ مقید و نوع‌دار کنید.

اگر برای ارسال درخواست به ارائه‌دهندگان خارجی مانند OpenAI یا Anthropic با چالش پرداخت‌های ارزی یا محدودیت دسترسی بر اساس نشانی اینترنتی (IP) مواجه هستید، پیاده‌سازی این الگوها تفاوتی نخواهد داشت؛ چرا که کتابخانه‌هایی مثل Instructor و پروتکل‌های دیکود مقید روی تمام گیت‌وی‌های واسط داخلی که با فرمت استاندارد OpenAI سازگارند، تنها با تغییر آدرس پایه (Base URL) و کلید دسترسی کار می‌کنند.

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

مدل زبانی شما یک رشته برمی‌گرداند؛ اما برنامه‌ی شما به JSON نیاز دارد. همین فاصله‌ی به‌ظاهر ساده، بیشتر از هر توهمی در مدل‌ها، سیستم‌های پروداکشن را زمین زده است. خروجی ساختاریافته (Structured output) پلی میان زبان طبیعی و داده‌های نوع‌دار (Typed data) است. اگر آن را درست پیاده کنید، مدل زبانی شما به یک API مطمئن تبدیل می‌شود؛ وگرنه ساعت 3 نصف‌شب باید مشغول سر‌و‌کله زدن با Regex برای بیرون کشیدن داده از دل متن آزاد باشید.

مرتبط: بخش 5 · 20 (خروجی‌های ساختاریافته و دیکود مقید) مبانی نظری سطح دیکودر (پردازنده‌های لاجیت مبتنی بر FSM/CFG، کتابخانه‌ی Outlines و XGrammar) را پوشش می‌دهد. تمرکز این درس روی پوسته‌ی SDKهای پروداکشن (قابلیت response_format در OpenAI، فراخوانی ابزار در Anthropic و کتابخانه‌ی Instructor) است — اگر می‌خواهید بدانید در لایه‌های زیرین API چه می‌گذرد، ابتدا بخش 5 · 20 را بخوانید.

اهداف یادگیری

  • پیاده‌سازی حالت JSON و خروجی‌های مقید به اسکیما با استفاده از پارامترهای API در OpenAI و Anthropic
  • ساخت لایه‌ی اعتبارسنجی با Pydantic برای رد کردن خروجی‌های بدشکل مدل و تلاش مجدد همراه با بازخورد خطا
  • توضیح نحوه‌ی تحمیل خروجی معتبر JSON در سطح توکن به‌کمک دیکود مقید (Constrained decoding) بدون نیاز به پس‌پردازش
  • طراحی پرامپت‌های استخراج داده‌ی مستحکم برای تبدیل مطمئن متن ساختارنیافته به ساختارهای داده‌ی نوع‌دار

صورت مسئله

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

The product is the Sony WH-1000XM5 headphones, which cost $348.00 and are currently in stock.

این پاسخ کاملاً درست است؛ اما برای برنامه‌ی شما کمترین فایده‌ای ندارد. سیستم انبارداری شما به داده‌ای مثل {"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true} نیاز دارد. شما یک شیء JSON با کلیدهای مشخص، انواع داده‌ای دقیق و قیدهای معین روی مقادیر می‌خواهید، نه یک جمله.

راه‌حل ساده‌لوحانه این است: عبارت «در قالب JSON پاسخ بده» را به پرامپت اضافه کنید. این روش در 90% مواقع جواب می‌دهد. در 10% باقی‌مانده، مدل خروجی JSON را داخل بلوک‌های کد مارک‌داون می‌پیچد، یا متنی مقدماتی مثل «این هم JSON شما:» می‌آورد، یا با بستن زودهنگام یک براکت، خروجی JSON با ساختار نحوی نامعتبر تحویل می‌دهد. در نتیجه، پارسر JSON کرش می‌کند و پایپ‌لاین از کار می‌افتد. برای حل این مشکل، بلوک try/except و حلقه‌ی تلاش مجدد اضافه می‌کنید؛ اما تلاش مجدد گاهی داده‌های متفاوتی برمی‌گرداند. حالا علاوه بر مشکل پارس کردن، با مشکل ناسازگاری داده‌ها هم روبه‌رو هستید.

این مسئله ربطی به مهندسی پرامپت ندارد؛ این یک مشکل در فرآیند دیکود است. مدل توکن‌ها را از چپ به راست تولید می‌کند. در هر جایگاه، محتمل‌ترین توکن بعدی را از بین دایره‌ی واژگانی با بیش از 100 هزار گزینه برمی‌گزیند. در هر جایگاه مشخص، بیشتر این گزینه‌ها خروجی JSON را نامعتبر می‌کنند. اگر مدل به‌تازگی توکن {"price": را تولید کرده باشد، توکن بعدی حتماً باید یک رقم، علامت نقل‌قول (برای رشته)، null، true، false یا علامت منفی باشد. هر چیز دیگری ساختار نحوی JSON را خراب می‌کند. بدون قید، ممکن است مدل یک کلمه‌ی انگلیسی کاملاً منطقی را انتخاب کند که از نظر ساختار گرامری، خروجی را به فاجعه بکشاند.

مفاهیم پایه

طیف خروجی‌های ساختاریافته

برای کنترل خروجی ساختاریافته چهار سطح وجود دارد که قابلیت اطمینان هر یک از سطح قبلی بیشتر است.

طیف خروجی ساختاریافتهمبتنی بر پرامپت'Return JSON'حدود 90% معتبرحالت JSON ModeJSON تضمین‌شده معتبربدون تضمین تطابق اسکیماحالت اسکیماJSON + مطابق با اسکیماتضمین انطباقدیکود مقیداعمال در سطح توکنانطباق 100%

مبتنی بر پرامپت («در قالب JSON معتبر پاسخ بده»): هیچ اجباری در کار نیست. مدل معمولاً رعایت می‌کند، اما گاهی هم نه. قابلیت اطمینان: حدود 90%. حالت‌های شکست: بلوک‌های مارک‌داون، متن مقدماتی، خروجی نصفه‌کاره، ساختار اشتباه.

حالت JSON: در این حالت، API تضمین می‌کند که خروجی حتماً یک JSON معتبر است. پارامتر response_format: { type: "json_object" } در OpenAI این قابلیت را فعال می‌کند. خروجی بدون خطا پارس می‌شود؛ اما شاید با اسکیمای مورد انتظار شما همخوانی نداشته باشد — کلیدهای اضافی داشته باشد، نوع داده‌ها اشتباه باشد یا فیلدهایی جا بیفتند.

حالت اسکیما: در این حالت، API یک JSON Schema دریافت می‌کند و تضمین می‌دهد خروجی دقیقاً با آن مطابقت دارد. در سال 2026 همه‌ی ارائه‌دهندگان مطرح به‌طور پیش‌فرض از این قابلیت پشتیبانی می‌کنند: قابلیت response_format: { type: "json_schema", json_schema: {...} } (یا با tool_choice="required") در OpenAI، فراخوانی ابزار همراه با input_schema در Anthropic، و response_schema در کنار response_mime_type: "application/json" در Gemini. خروجی دقیقاً با همان کلیدها، انواع داده و قیدهای تعیین‌شده به دست می‌آید.

دیکود مقید: هنگام تولید هر توکن در فرآیند دیکود، تمام توکن‌هایی که باعث نامعتبر شدن خروجی می‌شوند ماسک (احتمال‌شان صفر) می‌شوند. اگر اسکیما عدد بخواهد و مدل در آستانه‌ی تولید یک حرف باشد، احتمال آن توکن صفر می‌شود. مدل تنها مجاز به تولید توکن‌هایی است که به یک خروجی معتبر ختم شوند. این همان مکانیزمی است که در حالت خروجی ساختاریافته‌ی OpenAI و کتابخانه‌هایی مثل Outlines و Guidance در لایه‌های زیرین پیاده شده است.

زبان قرارداد: JSON Schema

با استفاده از JSON Schema به مدل (یا لایه‌ی اعتبارسنجی) اعلام می‌کنید که خروجی نهایی باید چه ساختاری داشته باشد. همه‌ی سیستم‌های مطرح خروجی ساختاریافته از این استاندارد استفاده می‌کنند.

{
  "type": "object",
  "properties": {
    "product": { "type": "string" },
    "price": { "type": "number", "minimum": 0 },
    "in_stock": { "type": "boolean" },
    "categories": {
      "type": "array",
      "items": { "type": "string" }
    }
  },
  "required": ["product", "price", "in_stock"]
}

این اسکیما تعیین می‌کند: خروجی باید شیئی شامل فیلد رشته‌ای product، فیلد عددی غیرمنفی price، فیلد بولی in_stock و آرایه‌ای اختیاری از رشته‌ها برای categories باشد. هر خروجی مغایر با این مشخصات رد می‌شود.

اسکیماها حالت‌های دشوار را مدیریت می‌کنند: اشیای تو‌در‌تو، آرایه‌هایی با اعضای نوع‌دار، مقادیر گزینشی یا اِنام (محدود کردن رشته به مقادیری خاص)، انطباق الگو (Regex روی رشته‌ها) و ترکیب‌کننده‌ها (oneOf، anyOf، allOf برای خروجی‌های چندشکلی).

الگوی Pydantic

در پایتون معمولاً JSON Schema را دستی نمی‌نویسید. کافی است یک مدل Pydantic تعریف کنید تا اسکیما را برایتان بسازد.

from pydantic import BaseModel

class Product(BaseModel):
    product: str
    price: float
    in_stock: bool
    categories: list[str] = []

این کد دقیقاً همان JSON Schema بالا را می‌سازد. کتابخانه‌ی Instructor (و SDK شرکت OpenAI) مدل‌های Pydantic را مستقیم دریافت می‌کنند: کلاس مدل را به متد می‌دهید و یک نمونه‌ی معتبر و نوع‌دار تحویل می‌گیرید. اگر خروجی مدل زبانی تطابق نداشته باشد، Instructor به‌طور خودکار درخواست را تکرار می‌کند.

فراخوانی تابع / استفاده از ابزار

رویکرد دیگری برای حل همین مسئله است. به‌جای این‌که مستقیماً از مدل JSON بخواهید، «ابزارها» (توابع) را با پارامترهای نوع‌دار برای آن تعریف می‌کنید. مدل یک فراخوانی ابزار (Tool call) شامل آرگومان‌های ساختاریافته تحویل می‌دهد. OpenAI به این سازوکار Function Calling می‌گوید و Anthropic از اصطلاح Tool Use استفاده می‌کند. نتیجه در هر دو حالت یکسان است: داده‌ی ساختاریافته.

جریان استفاده از ابزارکاربر: استخراج اطلاعات محصولاز متن این نقدپردازش ورودی توسط مدلفراخوانی ابزار:extract_product(product='Sony WH-1000XM5',price=348.00,in_stock=true)اعتبارسنجی در برابراسکیمای تابعنتیجه ساختاریافته:{product, price, in_stock}

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

حالت‌های شکست رایج

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

تولید مقادیر خیالی (توهم): خروجی کاملاً با اسکیما مطابقت دارد، اما داده‌های من‌درآوردی دارد. مثلاً در متن قیمت 348 دلار ذکر شده، اما مدل خروجی {"price": 299.99} را تحویل می‌دهد. اعتبارسنجی اسکیما متوجه این موضوع نمی‌شود؛ چون نوع داده درست است ولی مقدار آن غلط است.

سردرگمی در Enum: اگر یک فیلد را به مقادیر ["in_stock", "out_of_stock", "preorder"] محدود کرده باشید، ممکن است مدل مقداری مثل "available" برگرداند؛ مقداری که از نظر معنایی درست است اما در مجموعه‌ی مقادیر مجاز نیست. دیکود مقید مناسب جلوی این خطا را می‌گیرد، در حالی که روش‌های مبتنی بر پرامپت از پس آن برنمی‌آیند.

عمق اشیای تو‌در‌تو: اسکیماهای عمیق (4 لایه یا بیشتر) خطاهای بیشتری تولید می‌کنند. در هر لایه‌ی تودرتو، احتمال این‌که مدل رشته‌ی ساختار را گم کند بیشتر می‌شود.

طول آرایه‌ها: مدل ممکن است تعداد عناصر آرایه را کمتر یا بیشتر از حد مجاز تولید کند. اسکیماها از محدودیت‌های minItems و maxItems پشتیبانی می‌کنند، اما همه‌ی ارائه‌دهنده‌ها این قیدها را در سطح دیکود اعمال نمی‌کنند.

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

قیف خروجی ساختاریافتهمتن آزاد از طریق یک گرامر به JSON نوع‌دار تبدیل می‌شود
STRUCTURED OUTPUT FUNNELAn LLM returns a string; your application needs typed JSON. Adding "respond in JSON" to the prompt works about 90% of the time and crashes on the rest. Constrained decoding closes the gap at the token level: a finite-state machine or grammar masks the logits so any token that would break the schema is forbidden before it is sampled. The output is valid JSON by construction, not by post-processing, with a Pydantic layer validating types and retrying on the rare miss.متن آزاددروازه گرامرFSM / CFGماسک لاجیت‌هاJSON نوع‌دار{ name, price, in_stock }توکن نامعتبر در دروازه ماسک شددیکود مقید هر توکنی را که اسکیما را بشکند ممنوع می‌کند
یک LLM یک رشته متنی برمی‌گرداند، اما برنامه شما به JSON نوع‌دار نیاز دارد. افزودن عبارت «در قالب JSON پاسخ بده» به پرامپت در حدود 90٪ مواقع کار می‌کند و در بقیه موارد با خطا مواجه می‌شود. دیکود مقید این شکاف را در سطح توکن برطرف می‌کند: یک ماشین حالت متناهی یا گرامر، لاجیت‌ها را ماسک می‌کند تا هر توکنی که اسکیمای مورد نظر را بشکند پیش از نمونه‌برداری ممنوع شود. در نتیجه خروجی به شکل ذاتی (نه با پس‌پردازش) یک JSON معتبر است و یک لایه Pydantic انواع داده را اعتبارسنجی کرده و خطاهای نادر را دوباره تلاش می‌کند.

پیاده‌سازی از صفر

گام 1: اعتبارسنج اسکیماهای JSON

اعتبارسنجی از پایه بسازید که بررسی کند آیا یک شیء پایتونی با یک JSON Schema مطابقت دارد یا نه. این همان کدی است که در سمت خروجی اجرا می‌شود تا مطابقت را تأیید کند.

import json

def validate_schema(data, schema):
    errors = []
    _validate(data, schema, "", errors)
    return errors

def _validate(data, schema, path, errors):
    schema_type = schema.get("type")

    if schema_type == "object":
        if not isinstance(data, dict):
            errors.append(f"{path}: expected object, got {type(data).__name__}")
            return
        for key in schema.get("required", []):
            if key not in data:
                errors.append(f"{path}.{key}: required field missing")
        properties = schema.get("properties", {})
        for key, value in data.items():
            if key in properties:
                _validate(value, properties[key], f"{path}.{key}", errors)

    elif schema_type == "array":
        if not isinstance(data, list):
            errors.append(f"{path}: expected array, got {type(data).__name__}")
            return
        min_items = schema.get("minItems", 0)
        max_items = schema.get("maxItems", float("inf"))
        if len(data) < min_items:
            errors.append(f"{path}: array has {len(data)} items, minimum is {min_items}")
        if len(data) > max_items:
            errors.append(f"{path}: array has {len(data)} items, maximum is {max_items}")
        items_schema = schema.get("items", {})
        for i, item in enumerate(data):
            _validate(item, items_schema, f"{path}[{i}]", errors)

    elif schema_type == "string":
        if not isinstance(data, str):
            errors.append(f"{path}: expected string, got {type(data).__name__}")
            return
        enum_values = schema.get("enum")
        if enum_values and data not in enum_values:
            errors.append(f"{path}: '{data}' not in allowed values {enum_values}")

    elif schema_type == "number":
        if not isinstance(data, (int, float)):
            errors.append(f"{path}: expected number, got {type(data).__name__}")
            return
        minimum = schema.get("minimum")
        maximum = schema.get("maximum")
        if minimum is not None and data < minimum:
            errors.append(f"{path}: {data} is less than minimum {minimum}")
        if maximum is not None and data > maximum:
            errors.append(f"{path}: {data} is greater than maximum {maximum}")

    elif schema_type == "boolean":
        if not isinstance(data, bool):
            errors.append(f"{path}: expected boolean, got {type(data).__name__}")

    elif schema_type == "integer":
        if not isinstance(data, int) or isinstance(data, bool):
            errors.append(f"{path}: expected integer, got {type(data).__name__}")

گام 2: تبدیل مدل شبیه Pydantic به اسکیما

مبدل ساده‌ای برای تبدیل کلاس به اسکیما بسازید تا با تعریف یک کلاس پایتون، JSON Schema آن به‌طور خودکار ساخته شود.

class SchemaField:
    def __init__(self, field_type, required=True, default=None, enum=None, minimum=None, maximum=None):
        self.field_type = field_type
        self.required = required
        self.default = default
        self.enum = enum
        self.minimum = minimum
        self.maximum = maximum

def python_type_to_schema(field):
    type_map = {
        str: "string",
        int: "integer",
        float: "number",
        bool: "boolean",
    }

    schema = {}

    if field.field_type in type_map:
        schema["type"] = type_map[field.field_type]
    elif field.field_type == list:
        schema["type"] = "array"
        schema["items"] = {"type": "string"}
    elif isinstance(field.field_type, dict):
        schema = field.field_type

    if field.enum:
        schema["enum"] = field.enum
    if field.minimum is not None:
        schema["minimum"] = field.minimum
    if field.maximum is not None:
        schema["maximum"] = field.maximum

    return schema

def model_to_schema(name, fields):
    properties = {}
    required = []

    for field_name, field in fields.items():
        properties[field_name] = python_type_to_schema(field)
        if field.required:
            required.append(field_name)

    return {
        "type": "object",
        "properties": properties,
        "required": required,
    }

گام 3: فیلتر کردن توکن‌ها با دیکود مقید

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

def next_valid_tokens(partial_json, schema):
    stripped = partial_json.strip()

    if not stripped:
        return ["{"]

    try:
        json.loads(stripped)
        return ["<EOS>"]
    except json.JSONDecodeError:
        pass

    last_char = stripped[-1] if stripped else ""

    if last_char == "{":
        return ['"', "}"]
    elif last_char == '"':
        if stripped.endswith('":'):
            return ['"', "0-9", "true", "false", "null", "[", "{"]
        return ["a-z", '"']
    elif last_char == ":":
        return [" ", '"', "0-9", "true", "false", "null", "[", "{"]
    elif last_char == ",":
        return [" ", '"', "{", "["]
    elif last_char in "0123456789":
        return ["0-9", ".", ",", "}", "]"]
    elif last_char == "}":
        return [",", "}", "]", "<EOS>"]
    elif last_char == "]":
        return [",", "}", "<EOS>"]
    elif last_char == "[":
        return ['"', "0-9", "true", "false", "null", "{", "[", "]"]
    else:
        return ["any"]

def demonstrate_constrained_decoding():
    partial_states = [
        '',
        '{',
        '{"product"',
        '{"product":',
        '{"product": "Sony"',
        '{"product": "Sony",',
        '{"product": "Sony", "price":',
        '{"product": "Sony", "price": 348',
        '{"product": "Sony", "price": 348}',
    ]

    print(f"{'Partial JSON':<45} {'Valid Next Tokens'}")
    print("-" * 80)
    for state in partial_states:
        valid = next_valid_tokens(state, {})
        display = state if state else "(empty)"
        print(f"{display:<45} {valid}")

گام 4: پایپ‌لاین استخراج داده

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

def simulate_llm_extraction(text, schema, attempt=0):
    if "headphones" in text.lower() or "sony" in text.lower():
        if attempt == 0:
            return '{"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true, "categories": ["audio", "headphones"]}'
        return '{"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true}'

    if "laptop" in text.lower():
        return '{"product": "MacBook Pro 16", "price": 2499.00, "in_stock": false, "categories": ["computers"]}'

    return '{"product": "Unknown", "price": 0, "in_stock": false}'

def extract_with_retry(text, schema, max_retries=3):
    for attempt in range(max_retries):
        raw = simulate_llm_extraction(text, schema, attempt)

        try:
            data = json.loads(raw)
        except json.JSONDecodeError as e:
            print(f"  Attempt {attempt + 1}: JSON parse error -- {e}")
            continue

        errors = validate_schema(data, schema)
        if not errors:
            return data

        print(f"  Attempt {attempt + 1}: Schema validation errors -- {errors}")

    return None

product_schema = {
    "type": "object",
    "properties": {
        "product": {"type": "string"},
        "price": {"type": "number", "minimum": 0},
        "in_stock": {"type": "boolean"},
        "categories": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["product", "price", "in_stock"],
}

گام 5: اجرای پایپ‌لاین کامل

def run_demo():
    print("=" * 60)
    print("  Structured Output Pipeline Demo")
    print("=" * 60)

    print("\n--- Schema Definition ---")
    product_fields = {
        "product": SchemaField(str),
        "price": SchemaField(float, minimum=0),
        "in_stock": SchemaField(bool),
        "categories": SchemaField(list, required=False),
    }
    generated_schema = model_to_schema("Product", product_fields)
    print(json.dumps(generated_schema, indent=2))

    print("\n--- Schema Validation ---")
    test_cases = [
        ({"product": "Test", "price": 10.0, "in_stock": True}, "Valid object"),
        ({"product": "Test", "price": -5.0, "in_stock": True}, "Negative price"),
        ({"product": "Test", "in_stock": True}, "Missing price"),
        ({"product": "Test", "price": "ten", "in_stock": True}, "String as price"),
        ("not an object", "String instead of object"),
    ]

    for data, label in test_cases:
        errors = validate_schema(data, product_schema)
        status = "PASS" if not errors else f"FAIL: {errors}"
        print(f"  {label}: {status}")

    print("\n--- Constrained Decoding Simulation ---")
    demonstrate_constrained_decoding()

    print("\n--- Extraction Pipeline ---")
    texts = [
        "The Sony WH-1000XM5 headphones are priced at $348 and currently available.",
        "The new MacBook Pro 16-inch laptop costs $2499 but is sold out.",
        "This is a random sentence with no product info.",
    ]

    for text in texts:
        print(f"\n  Input: {text[:60]}...")
        result = extract_with_retry(text, product_schema)
        if result:
            print(f"  Output: {json.dumps(result)}")
        else:
            print(f"  Output: FAILED after retries")

استفاده در عمل

خروجی‌های ساختاریافته در OpenAI

# from openai import OpenAI
# from pydantic import BaseModel
#
# client = OpenAI()
#
# class Product(BaseModel):
#     product: str
#     price: float
#     in_stock: bool
#
# response = client.beta.chat.completions.parse(
#     model="gpt-5-mini",
#     messages=[
#         {"role": "system", "content": "Extract product information."},
#         {"role": "user", "content": "Sony WH-1000XM5, $348, in stock"},
#     ],
#     response_format=Product,
# )
#
# product = response.choices[0].message.parsed
# print(product.product, product.price, product.in_stock)

قابلیت خروجی‌های ساختاریافته در OpenAI در لایه‌های درونی خود از دیکود مقید بهره می‌برد. تولید تک‌تک توکن‌ها توسط مدل تضمین می‌کند که خروجی نهایی حتماً با اسکیمای Pydantic تطابق دارد. نه نیازی به تلاش مجدد است و نه نیازی به اعتبارسنجی؛ قیدها در تاروپود فرایند دیکود جا گرفته‌اند.

فراخوانی ابزار در Anthropic

# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
#     model="claude-opus-4-7",
#     max_tokens=1024,
#     tools=[{
#         "name": "extract_product",
# "description": "استخراج اطلاعات محصول از متن",
#         "input_schema": {
#             "type": "object",
#             "properties": {
#                 "product": {"type": "string"},
#                 "price": {"type": "number"},
#                 "in_stock": {"type": "boolean"},
#             },
#             "required": ["product", "price", "in_stock"],
#         },
#     }],
#     messages=[{"role": "user", "content": "Extract: Sony WH-1000XM5, $348, in stock"}],
# )

سرویس Anthropic خروجی ساختاریافته را از طریق فراخوانی ابزار فراهم می‌کند. مدل یک فراخوانی ابزار با آرگومان‌های ساختاریافته ارسال می‌کند که دقیقاً با input_schema همخوانی دارد. نتیجه یکی است، فقط ظاهر کار با API تفاوت دارد.

کتابخانه‌ی Instructor

# pip install instructor
# import instructor
# from openai import OpenAI
# from pydantic import BaseModel
#
# client = instructor.from_openai(OpenAI())
#
# class Product(BaseModel):
#     product: str
#     price: float
#     in_stock: bool
#
# product = client.chat.completions.create(
#     model="gpt-5-mini",
#     response_model=Product,
#     messages=[{"role": "user", "content": "Sony WH-1000XM5, $348, in stock"}],
# )

کتابخانه‌ی Instructor کلاینت مدل‌های زبانی را در بر می‌گیرد و قابلیت تلاش مجدد خودکار همراه با اعتبارسنجی را به آن‌ها اضافه می‌کند. اگر اولین تلاش در اعتبارسنجی رد شود، خطاها را به‌عنوان بافت کمکی برای مدل می‌فرستد تا خروجی را اصلاح کند. این کتابخانه محدود به OpenAI نیست و با هر ارائه‌دهنده‌ای کار می‌کند.

خروجی این درس

از این درس فایل outputs/prompt-structured-extractor.md حاصل می‌شود؛ یک قالب پرامپت (Prompt template) با قابلیت استفاده‌ی مجدد که با دریافت هر متن و ساختار اسکیما، داده‌های ساختاریافته را استخراج می‌کند. کافی است یک JSON Schema و متن ساختارنیافته به آن بدهید تا JSON اعتبارسنجی‌شده تحویل بگیرید.

همچنین فایل outputs/skill-structured-outputs.md را خواهید داشت؛ یک چارچوب تصمیم‌گیری برای انتخاب راهبرد مناسب خروجی ساختاریافته متناسب با ارائه‌دهنده، سطح اطمینان مورد نیاز و پیچیدگی اسکیما.

تمرین‌ها

  1. اعتبارسنج اسکیما را گسترش دهید تا از oneOf هم پشتیبانی کند (داده باید دقیقاً با یکی از چند اسکیما همخوانی داشته باشد). با این قابلیت می‌توانید خروجی‌های چندشکلی را مدیریت کنید؛ مثلاً فیلدی که می‌تواند شیئی از نوع Product یا Service با ساختارهای متفاوت باشد.

  2. ابزاری برای مقایسه‌ی اسکیماها («Schema diff») بسازید که دو اسکیما را مقایسه کند و تغییرات شکننده (مثل حذف فیلدهای اجباری، تغییر نوع داده‌ها) را از تغییرات غیرشکننده (مثل اضافه کردن فیلدهای اختیاری، سبک‌تر کردن قیدها) تشخیص دهد. این ابزار برای نسخه‌بندی اسکیماهای استخراج در محیط پروداکشن حیاتی است.

  3. شبیه‌ساز واقعی‌تری برای دیکود مقید بنویسید. با داشتن یک JSON Schema و دایره‌ی واژگانی با 100 توکن (حروف، ارقام، نشانه‌گذاری، کلمات کلیدی)، مراحل تولید را گام‌به‌گام پیش ببرید و توکن‌های نامعتبر را در هر جایگاه ماسک کنید. محاسبه کنید در هر گام چند درصد از دایره‌ی واژگان مجاز به انتخاب هستند.

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

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

اصطلاحات کلیدی

اصطلاح در گفت‌وگوهای روزمره معنای واقعی و فنی
حالت JSON (JSON mode) «JSON برمی‌گرداند» فلگی در API که معتبر بودن ساختار نحوی خروجی JSON را تضمین می‌کند، اما هیچ اسکیمای خاصی را اعمال نمی‌کند
خروجی ساختاریافته (Structured output) «JSON نوع‌دار» خروجی داده‌ای که دقیقاً با یک JSON Schema مشخص با کلیدها، انواع داده و قیدهای معین مطابقت دارد
دیکود مقید (Constrained decoding) «تولید هدایت‌شده» ماسک کردن توکن‌هایی که در هر جایگاه تولید باعث نامعتبر شدن خروجی می‌شوند — تضمین تطابق 100 درصدی با اسکیما
اسکیما JSON (JSON Schema) «یک قالب برای JSON» زبانی توصیفی برای مشخص کردن ساختار، انواع داده و قیدهای داده‌های JSON (مورد استفاده در OpenAPI، فرم‌های JSON و غیره)
کتابخانه‌ی Pydantic «دیتایکلاس‌های پیشرفته در پایتون» کتابخانه‌ای پایتونی برای تعریف مدل‌های داده همراه با اعتبارسنجی نوع، مورد استفاده در FastAPI و Instructor برای ساخت خودکار JSON Schema
فراخوانی تابع (Function calling) «استفاده از ابزار» امکانی که مدل زبانی به‌جای متن آزاد، یک فراخوانی ساختاریافته‌ی تابع (نام به همراه آرگومان‌های نوع‌دار) تولید می‌کند — OpenAI و Anthropic هر دو از آن پشتیبانی می‌کنند
کتابخانه‌ی Instructor «Pydantic برای مدل‌های زبانی» کتابخانه‌ای در پایتون که کلاینت‌های مدل زبانی را پوشش می‌دهد تا نمونه‌های اعتبارسنجی‌شده‌ی Pydantic تحویل دهند و در صورت شکست اعتبارسنجی، فرآیند را به‌طور خودکار تکرار می‌کند
ماسک کردن توکن (Token masking) «فیلتر کردن دایره واژگان» صفر کردن احتمال توکن‌های نامناسب در جریان تولید تا مدل نتواند آن‌ها را انتخاب کند
تطابق با اسکیما (Schema compliance) «درست بودن ساختار داده» یعنی خروجی همه‌ی فیلدهای اجباری را دارد، نوع داده‌ها درست است، مقادیر در چارچوب قیدها هستند و هیچ فیلد غیرمجازی وجود ندارد
حلقه‌ی تلاش مجدد (Retry loop) «تکرار کن تا درست شود» فرستادن خطاهای اعتبارسنجی به مدل برای اصلاح خروجی — Instructor این فرآیند را خودکار و تا سقفی معین انجام می‌دهد

برای مطالعه‌ی بیشتر

سؤالات متداول

تفاوت حالت JSON (JSON mode) با خروجی مقید به اسکیما (Schema mode) چیست؟

حالت JSON تنها معتبر بودن ساختار گرامری JSON را تضمین می‌کند، اما تضمینی برای وجود فیلدهای خاص یا نوع درست داده‌ها ندارد؛ در حالی که حالت اسکیما خروجی را دقیقاً به کلیدها، انواع و قیود تعیین‌شده در یک JSON Schema مقید می‌سازد.

دیکود مقید (Constrained Decoding) چگونه کار می‌کند؟

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

آیا اعتبارسنجی با Pydantic مانع توهم (Hallucination) مدل می‌شود؟

خیر؛ اسکیما فقط ساختار و نوع داده (مثلاً عددی بودن قیمت) را بررسی می‌کند و نمی‌تواند متوجه شود که مقدار عددی یا متنی تولیدشده از روی متن درست استخراج شده یا ساختگی است.

ما رو توی گوگل بیشتر ببین

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