cd ../back to blog
$Guide//June 4, 2026//7 min read

مخرجات منظّمة من Claude و Gemini: وضع JSON و JSON Schema والتحقّق

كيف تُجبر Claude و Gemini على إخراج JSON صحيح ومفيد بشكل OpenAI واحد خلف base_url موحّد: متى تستخدم json_object مقابل json_schema الصارم مقابل استدعاء الأدوات، وكيف تصمّم schema قابلاً للإصابة، ولماذا حلِّ-تحقّق-إصلاح هو ما يجعل الميزة جاهزة للإنتاج.

نماذج المحادثة تريد أن تتكلّم. وخط معالجتك يريد سجلاً. الفجوة بين "إليك فقرة ودودة عن التذكرة" وبين {"category": "billing", "priority": "high"} هي حيث تنكسر معظم تكاملات الـ LLM بصمت — سور markdown شارد، فاصلة زائدة، مفتاح مهلوس، فترمي json.loads في الأسفل عند الثالثة فجراً. هذه التدوينة عن إجبار Claude و Gemini على إخراج JSON صحيح ومفيد، باستخدام شكل طلب OpenAI نفسه خلف base_url واحد، وعن طبقة التحقّق التي تجعله جاهزاً للإنتاج لا مجرد عرض تجريبي.

ثمة ثلاث أدوات لهذه المهمة: response_format مع json_object، و response_format مع json_schema، والاستدعاء الأصلي للأدوات/الدوال. وهي ليست قابلة للتبديل فيما بينها، واختيار الخاطئة منها هو السبب الأكثر شيوعاً لكون ميزة المخرجات المنظّمة تبدو متقلّبة. سنمرّ على كل واحدة، ومتى تلجأ إليها، وكيف تصمّم الـ schema، وكيف تتحقّق وتصلح ما يعود.

وضع JSON: قابل للتحليل بضمان، لا صحيح بضمان

أبسط رافعة هي response_format={"type": "json_object"}. يُقيِّد النموذج بإصدار JSON صحيح نحوياً — لا تمهيد نثري، ولا سور ```json، ولا اعتذار. أما ما لا يفعله فهو فرض شكلك أنت. يبقى عليك وصف الحقول في الـ prompt، ويبقى بمقدور النموذج إغفال مفتاح، أو اختلاق آخر، أو وضع نص مكان قيمة منطقية أردتها.

json_object.py
# response_format=json_object: يُقيَّد النموذج بإصدار JSON صحيح نحوياً.
# لكنه لا يفرض الشكل الذي تريده أنت — يبقى عليك وصف الحقول في الـ prompt.
# النداء نفسه يعمل مع Claude و Gemini خلف base_url واحد.
from openai import OpenAI
import json

client = OpenAI(
    api_key="sk-brievio-...",
    base_url="https://api.brievio.com/v1",
)

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",          # أو "gemini-2.5-flash" — الكود نفسه
    response_format={"type": "json_object"},
    messages=[
        {
            "role": "system",
            "content": (
                "Extract the support ticket fields. Reply ONLY with a JSON object "
                "with keys: category (one of billing|bug|feature|other), "
                "priority (low|medium|high), summary (string), "
                "needs_human (boolean)."
            ),
        },
        {"role": "user", "content": "I was charged twice this month, please refund."},
    ],
)

data = json.loads(resp.choices[0].message.content)   # قابل للتحليل بضمان
print(data["category"], data["priority"])            # "billing" "high"

# json_object يضمن أن النص يُحلَّل. لكنه لا يضمن وجود مفاتيحك، ولا صحة قيم
# الـ enums، ولا سلامة الأنواع. ذلك تحديداً هو دور التحقّق.

هذه هي الأداة الصحيحة حين يكون الشكل بسيطاً، أو حين تتحكّم في الـ prompt بإحكام، أو حين تنوي التحقّق على أي حال (وأنت ستفعل). النموذج الذهني: json_object يشتري لك ضماناً بأن json.loads لن ترمي. لكنه لا يشتري لك ضماناً بأن الكائن يعني ما تظنّه. تعامل مع هذا الفرق بوصفه جوهر اللعبة كله.

وضع JSON Schema: قيِّد الشكل، لا النحو فقط

حين تريد فرض أسماء الحقول وأنواعها وقيم الـ enums — لا مجرد طلبها — فالجأ إلى json_schema. يسافر الـ schema مع الطلب، ومع strict: true (حيث تدعمه عائلة النموذج) يُقيَّد المخرَج ليطابقه. والحقلان اللذان يجعلان "strict" يعني شيئاً فعلاً هما additionalProperties: false (لا مفاتيح مفاجئة) ومصفوفة required كاملة (لا مفاتيح ناقصة).

json_schema.py
# response_format=json_schema: يُرسَل الـ schema إلى النموذج ويُقيَّد المخرَج
# ليطابقه. اضبط strict=True للحصول على الضمان الصارم حيثما يُدعَم. وجود
# additionalProperties=False مع مفاتيح required هو ما يجعل "strict" ذا معنى.
schema = {
    "name": "support_ticket",
    "strict": True,
    "schema": {
        "type": "object",
        "additionalProperties": False,
        "properties": {
            "category": {"type": "string", "enum": ["billing", "bug", "feature", "other"]},
            "priority": {"type": "string", "enum": ["low", "medium", "high"]},
            "summary": {"type": "string"},
            "needs_human": {"type": "boolean"},
        },
        "required": ["category", "priority", "summary", "needs_human"],
    },
}

resp = client.chat.completions.create(
    model="claude-sonnet-4-6",
    response_format={"type": "json_schema", "json_schema": schema},
    messages=[
        {"role": "system", "content": "Extract the support ticket fields."},
        {"role": "user", "content": "I was charged twice this month, please refund."},
    ],
)

ticket = json.loads(resp.choices[0].message.content)
# مع json_schema الصارم، يكون category قابلاً للإثبات أنه أحد الأربعة في
# الـ enum — دون حاجة إلى "if category not in (...)" دفاعية في المسار السعيد.

وإليك التحفّظ الصادق، وهو مهم: دعم json_schema الصارم يتفاوت بحسب عائلة النموذج. بعض النماذج تحترم كل قيد بما في ذلك additionalProperties: false المتشعّب؛ وأخرى تعامل الـ schema كتلميح قوي لا كقواعد صارمة، خصوصاً على الكائنات العميقة التشعّب، والاتحادات (anyOf)، أو البنى التكرارية. يمرّر Brievio response_format الخاص بك مباشرة إلى النموذج الأصلي من الطرف الأول، فما تحصل عليه هو السلوك الحقيقي للنموذج الحقيقي — لا محاكاة مخفّفة. لكن ذلك يعني أيضاً أن الحدود الأصلية للنموذج هي حدودك. والقاعدة العملية: اطلب الـ schema، ثم تحقّق على أي حال. لا تدع "strict" يقنعك بالتخلّي عن خطوة التحقّق.

وضع JSON مقابل استدعاء الأدوات: متى تستخدم أيّاً

استدعاء الأدوات/الدوال يعيد أيضاً JSON منظّماً — تعود الوسائط كنص JSON مرتبط باسم دالة. فأيّهما تستخدم؟ التمييز يدور حول القصد، لا التنسيق:

  • استخدم وضع JSON حين يكون الـ JSON هو الجواب. أنت تستخرج حقولاً، أو تصنّف، أو تلخّص في سجل، أو تولّد كائن إعداد. ثمة شكل واحد بالضبط تريد عودته، في كل مرة. response_format هو الأنسب وأنظفها — مخرَج واحد، دون مراسم استدعاء الدوال، ودون سباكة tool_choice.
  • استخدم استدعاء الأدوات حين يختار النموذج إجراءً. قد يستدعي get_weather، أو search_db، أو يجيب نثراً — وتريد أن يقرّر النموذج أيّها، وربما يستدعي عدة دوال. استدعاء الدوال مبني لأجل التوزيع: أشكال مرشّحة كثيرة، والنموذج ينتقي. وإجبار ذلك عبر كائن JSON واحد أمر أخرق.
  • المنطقة الرمادية: استدعاء أداة واحدة مفروض كمخرَج منظّم. ضبط tool_choice ليُلزِم دالة بعينها هو وسيلة عريقة للحصول على مخرَج منظّم في النماذج التي تسبق json_schema. ولا يزال يعمل وهو بديل جيد. لكن إن كان النموذج يدعم json_schema، فذلك المسار أكثر مباشرة وأقل عبئاً على التفكير.

إن كان عبء عملك يدور فعلاً حول الإجراءات والتوزيع لا حول سجل ثابت، فالآليات ومطبّات اختلاف النماذج تجدها في استخدام الأدوات عبر Claude و Gemini. أما لكل ما هو "أعطني هذا الكائن" فابقَ مع response_format.

تصميم schema يستطيع النموذج إصابته فعلاً

الـ schema هو prompt. وطريقة تشكيله تغيّر معدّل الإصابة بقدر ما يفعل اختيار النموذج. وإليك بضع قواعد تؤتي ثمارها عبر العائلتين:

  • فضّل المسطّح على عميق التشعّب. ثلاثة مستويات من التشعّب مع كائنات اختيارية هي حيث يتذبذب الوضع الصارم. إن استطعت تسطيح address.city إلى city، فافعل، ثم أعد التشكيل بعد التحقّق.
  • استخدم enums لأي مجموعة مغلقة. "priority": {"enum": ["low","medium","high"]} أكثر موثوقية بكثير من string حرّ تعالجه لاحقاً. الـ enums هي أعلى ميزات الـ schema رافعةً.
  • سمِّ الحقول كما يسمّيها إنسان. needs_human_review أفضل من nh_flag. يملأ النموذج الحقول حسنة التسمية بدقة أعلى لأن الاسم يحمل التعليمة.
  • ضع description على الحقول الملتبسة. سطر واحد لكل حقل داخل الـ schema يحلّ معظم حالات "خمّن النموذج خطأ" دون إعادة كتابة الـ prompt.
  • اجعل الاختيارية صريحة. إن كان حقل قد يغيب، فإمّا أن تتركه خارج required أو تنمذجه كاتحاد قابل للقيمة الفارغة — ولا تتوقّع من النموذج اختلاق قيمة حارسة. قرِّر من يملك حالة "الغياب"، أنت أم النموذج.
  • تجنّب الأرقام الحرّة حين يفي نوع محدود بالغرض. تقييم بعدد صحيح من 1 إلى 5 كـ enum من [1,2,3,4,5] يتفوّق على "رقم من 1 إلى 5" في الـ prompt.

التحقّق والإصلاح: الطبقة التي تجعله جاهزاً للإنتاج

أكبر ترقية مفردة في الموثوقية هي معاملة مخرَج النموذج كطلب عميل غير موثوق: حلِّله، وتحقّق منه مقابل schema الحقيقي لديك، وعند الفشل أعد المحاولة مرة واحدة مع إعادة تغذية الخطأ. نموذج Pydantic (أو zod، أو تحقّق JSON Schema بلغتك) يصطاد الحالات التي تتسلّل حتى من الوضع الصارم — ودورة الإصلاح تصحّح معظمها، لأن النموذج بارع في تصحيح خطأ تشير إليه مباشرة.

validate.py
# لا تثق أبداً بمخرَج لم تتحقّق منه. عامل النموذج كأنه عميل غير موثوق:
# حلِّل -> تحقّق مقابل الـ schema الخاص بك -> أعد المحاولة مرة واحدة مع
# إعادة تغذية الخطأ. هذه هي الطبقة التي تحوّل "يعمل عادة" إلى "جاهز للإنتاج".
from pydantic import BaseModel, ValidationError
from typing import Literal

class Ticket(BaseModel):
    category: Literal["billing", "bug", "feature", "other"]
    priority: Literal["low", "medium", "high"]
    summary: str
    needs_human: bool

def extract(text: str, model: str, retries: int = 1) -> Ticket:
    messages = [
        {"role": "system", "content": "Extract the support ticket fields as JSON."},
        {"role": "user", "content": text},
    ]
    for attempt in range(retries + 1):
        resp = client.chat.completions.create(
            model=model,
            response_format={"type": "json_object"},
            messages=messages,
        )
        raw = resp.choices[0].message.content
        try:
            return Ticket.model_validate_json(raw)      # تحليل + تحقّق في خطوة واحدة
        except ValidationError as e:
            if attempt == retries:
                raise
            # دورة إصلاح: أرِ النموذج بالضبط ما كان خاطئاً.
            messages += [
                {"role": "assistant", "content": raw},
                {"role": "user", "content": f"That failed validation: {e}. Re-emit valid JSON only."},
            ]
    raise RuntimeError("unreachable")

لاحظ ما تفعله دورة الإصلاح: تُري النموذج مخرَجه السيّئ نفسه مع خطأ التحقّق بالضبط، ثم تطلب إعادة الإصدار. محاولة واحدة تحلّ الغالبية الساحقة من الإخفاقات؛ وإن ظلّ يفشل فأنت تريد أن تعلم، فدعه يرمي الخطأ. لا تدُر إلى الأبد محرقاً التوكنات — قيِّد المحاولات، وسجِّل الحمولة الخام، ونبِّه على الإخفاقات الصعبة. فشل التحقّق المستمر يعني عادة أن الـ schema يطلب شيئاً لا يدعمه المدخل، لا أن النموذج معطّل.

ملاحظتان للإنتاج. أولاً، اضبط max_tokens سخياً: فالـ JSON المبتور في منتصف الكائن هو JSON غير صحيح، وسقف التوكنات الضيّق هو سبب رئيسي لإخفاقات التحليل على السجلات الكبيرة. ثانياً، أبقِ temperature منخفضة (من 0 إلى 0.3) للاستخراج والتصنيف — فأنت تريد أن يُنتج المدخل نفسه السجل نفسه، والإبداع ليس فضيلة حين تملأ بنية.

شكل واحد، كلتا عائلتي النماذج

كل مقطع أعلاه يعمل مع Claude Sonnet 4.6 و Gemini 2.5 Flash بتغيير نص واحد — حقل model. وهذا هو جوهر توجيه المخرجات المنظّمة عبر Brievio: عقد response_format ذو شكل OpenAI متطابق، فيمكنك إجراء اختبار A/B لنموذج أرخص على مهمة استخراج، أو الرجوع عبر العائلات أثناء عطل، دون إعادة كتابة التحليل أو التحقّق لديك. الطلب الذي ترسله هو الطلب الذي يستلمه النموذج الأصلي من الطرف الأول — إليك بالضبط ما يتطابق وما ينبغي الانتباه إليه حين تعتمد على توافق OpenAI.

تدفّق عمل عملي: ابدأ النموذج الأولي بـ json_schema صارم على Sonnet، وتأكّد من أن أداة التحقّق لديك تنجح على مجموعة محجوزة، ثم جرّب الـ schema نفسه على Flash. إن اجتاز النموذج الأرخص معدّل التحقّق لديك، فقد خفّضت التكلفة بصفر تغيير في الكود — ولأن Brievio يبلّغ عن أعداد توكنات صادقة ويحتسب النداءات الفاشلة 4xx/5xx بصفر، فإن إعادات محاولتك ودورات إصلاحك لا تخفي مفاجأة في القياس. قارن النماذج في صفحة النماذج، وعقد الطلب/الاستجابة الكامل للمحادثة تجده في وثائق المحادثة.

الخلاصة

الجأ إلى json_object حين يكون الشكل بسيطاً وتملك الـ prompt؛ والجأ إلى json_schema مع strict: true و additionalProperties: false ومصفوفة required كاملة حين تريد فرض البنية؛ والجأ إلى استدعاء الأدوات حين يختار النموذج إجراءً لا حين يُنتج سجلاً ثابتاً واحداً. وأيّاً اخترت، صمّم الـ schema مسطّحاً مكثّف الـ enums، ثم احرص دائماً على حلِّ-تحقّق-إصلاح — لأن الدعم الصارم يتفاوت بحسب عائلة النموذج، وطبقة التحقّق هي الفارق بين ميزة مخرجات منظّمة تَعرِض وأخرى تصمد أمام حركة حقيقية. الكود نفسه، والعقد نفسه، والنموذج الأصلي — عبر Claude و Gemini، خلف عنوان base واحد.