Ultra Techالنسخة الإنجليزية

البرمجياتالدليل 4 من 4

ما هي المخرجات المنظّمة

فُحص آلياً على مصادره في .

تريد من النموذج بيانات، لا نصّاً إنشائيّاً. زبون يكتب إليك، ونظام الدعم عندك يحتاج تذكرة: أيّ طلبيّة، وما الذي حدث، وهل يريد ماله. فتطلب من النموذج أن «يردّ بصيغة JSON»، وفي أغلب الأحيان يفعل — حتى يأتي يوم يسمّي فيه حقلاً باسم آخر، أو يُسقط حقلاً، أو يلفّ الجواب بجملة، فتتوقّف الشيفرة التي تقرؤه عند أوّل سطر. الطلب المهذّب ليس عقداً.

والمخرجات المنظّمة هي العقد. تصفها OpenAI بأنّها تضمن أن تلتزم ردود النموذج النصّيّة بمخطّط JSON تعرّفه أنت. تكتب الشكل الذي تريده — كلّ حقل، ونوعه، والقيم التي يجوز أن يأخذها — وترسل هذا الشكل مع الطلب، فيرجع ردّ النموذج بذلك الشكل بالضبط. وبعبارة OpenAI نفسها، هذه الميزة تضمن أن يولّد النموذج دائماً ردوداً تلتزم بمخطّط JSON الذي قدّمته، فلا تقلق من أن يُسقط مفتاحاً مطلوباً أو يختلق قيمة غير صالحة من قائمة القيم المسموحة.

وهذا الدليل يبني التذكرة التي في المثال، في ملفّ واحد، على ثلاث خطوات. سترسل رسالة زبون مع مخطّط وتقرأ التذكرة التي ترجع؛ ثمّ تكسر قاعدة من قواعد المخطّط عمداً وترى ما تقوله الواجهة؛ ثمّ تقطع ردّاً قبل أن يكتمل لترى الحالة الوحيدة التي لا تصل فيها تذكرة أصلاً. كلّ مخرج في هذه الصفحة هو ما طبعه تشغيل واحد، على نموذج gpt-6-luna من OpenAI.

وكلمات النموذج ستختلف في تشغيلك — فأحد الحقول هنا جملة يكتبها هو. أمّا الشكل فلن يختلف، والشكل هو المقصود.

بنهاية هذا الدليل ستقدر

  • تكتب مخطّط JSON للبيانات التي تحتاجها، وترسله مع الطلب كي يلتزم به الردّ.
  • تقرأ الردّ حقلاً حقلاً، وتعرف أيّ أجزائه يثبّتها المخطّط وأيّها ما زال اختيار النموذج.
  • تعرف الخطأ الذي تعيده الواجهة لمخطّط يخالف قواعدها، وتصلح المخطّط.
  • تتعامل مع الردود التي لا تحمل بيانات — المقطوعة أو المرفوضة — قبل أن تحاول شيفرتك قراءتها.

قبل أن تبدأ

تحتاج أربعة أشياء. تحقّق من كلّ واحد قبل أن تكتب سطراً.

  • Node.js إصدار 22 أو أحدث. شغّل node --version. يطبع إصداراً مثل v24.14.1، وهو الإصدار الذي جرى عليه تشغيل هذه الصفحة. إن طبع رقماً أقلّ أو رسالة خطأ، نزّل Node.js من موقعه الرسميّ.
  • مفتاح API من OpenAI. أنشئه من صفحة مفاتيح API في لوحة تحكّمك واحفظه في مكان آمن. لا يدخل الشيفرة أبداً.
  • مجلّد فيه حزمة SDK. في الطرفيّة: mkdir structured-demo ثمّ cd structured-demo ثمّ npm init -y ثمّ npm install openai@7.15.0. الأمر الأخير يثبّت حزمة openai بالإصدار الذي جرى عليه هذا التشغيل.
  • المفتاح في بيئتك، في نفس نافذة الطرفيّة التي ستشغّل منها الملفّ. على PowerShell في ويندوز: $env:OPENAI_API_KEY = "ضع مفتاحك هنا". على macOS أو لينكس: export OPENAI_API_KEY="ضع مفتاحك هنا". الحزمة تقرأ هذا المتغيّر بنفسها، ولهذا لا تذكر الشيفرة أدناه المفتاح إطلاقاً. وإن فتحت نافذة جديدة، اضبطه من جديد.

كلّ ما في هذه الصفحة جرى على gpt-6-luna، النموذج الذي يوجّه إليه فهرس OpenAI للأحمال الكبيرة الحسّاسة للكلفة — وتحويل الرسائل إلى تذاكر من هذا النوع بالضبط.

الخطوة 1 — أرسل مخطّطاً مع الطلب واقرأ التذكرة

أنشئ ملفّاً اسمه ticket.mjs وضع فيه هذا:

مثالticket
import OpenAI from 'openai';

const client = new OpenAI(); // reads OPENAI_API_KEY from the environment; the key is never in the code

const email =
  'Hi, my order 1042 arrived this morning and the lamp shade is cracked right across. ' +
  'I would rather have my money back than wait for another one. ' +
  'If you need photos you can call me on 555-0142. Thanks, Dana';

// The contract: every field the ticket has, what each may hold, and nothing else.
const ticketSchema = {
  type: 'object',
  properties: {
    order_id: { type: 'string', description: 'The order number as written in the email, digits only.' },
    issue: { type: 'string', enum: ['late', 'damaged', 'wrong_item', 'other'] },
    wants_refund: { type: 'boolean', description: 'True only if the customer asks for their money back.' },
    summary: { type: 'string', description: 'One sentence a support agent can read in five seconds.' },
    contact_phone: { type: ['string', 'null'], description: 'A phone number the customer gave, or null.' },
  },
  required: ['order_id', 'issue', 'wants_refund', 'summary', 'contact_phone'],
  additionalProperties: false,
};

const format = { type: 'json_schema', name: 'support_ticket', schema: ticketSchema, strict: true };
const input = [
  { role: 'system', content: 'Turn the customer email into a support ticket.' },
  { role: 'user', content: email },
];

// A structured reply can end three ways: complete, refused, or cut short. Only the first holds a ticket.
function readTicket(response) {
  if (response.status === 'incomplete') {
    return { ended: `incomplete (${response.incomplete_details?.reason})`, ticket: null };
  }
  const content = response.output.filter((item) => item.type === 'message').flatMap((item) => item.content);
  const refusal = content.find((part) => part.type === 'refusal');
  if (refusal) return { ended: `refused: ${refusal.refusal}`, ticket: null };
  return { ended: 'complete', ticket: JSON.parse(response.output_text) };
}

const response = await client.responses.create({ model: 'gpt-6-luna', input, text: { format } });
const { ended, ticket } = readTicket(response);

console.log(`status: ${ended}`);
for (const [field, value] of Object.entries(ticket)) console.log(`${field}: ${JSON.stringify(value)}`);

المخرجات

status: complete
order_id: "1042"
issue: "damaged"
wants_refund: true
summary: "Order 1042 arrived with a cracked lamp shade, and the customer wants a refund rather than a replacement."
contact_phone: "555-0142"

ثلاثة أجزاء تقوم بالعمل. ticketSchemaهو العقد، مكتوباً بلغة JSON Schema — الطريقة المعياريّة لوصف شكل قيمة JSON. والمخرجات المنظّمة تدعم جزءاً من تلك اللغة لا كلّها ، والأنواع التي تدعمها هي String وNumber وBoolean وInteger وObject وArray وEnum وanyOf. والتذكرة تستعمل أربعة منها:order_id وsummary نصّان، وwants_refund قيمة منطقيّة، وissue نصّ تحصره enum في أربع قيم.

وformat هو الطريقة التي يسافر بها المخطّط مع الطلب. صفحة OpenAI تعطي المفتاح هكذا: text: { format: { type: "json_schema", "strict": true, "schema": ... } }، والشيفرة تفعل ذلك بالضبط، معname للمخطّط. وstrict: true هو ما يحوّل المخطّط من اقتراح إلى عقد.

أمّا readTicket فموجودة لأنّ الردّ قد ينتهي بلا تذكرة فيه. الخطوتان التاليتان تريانك لماذا؛ ولاحظ الآن أنّها لا تستدعي JSON.parse إلّا حين يكون الردّ مكتملاً.

شغّله بالأمر node ticket.mjs. هذا ما طبعه:

  • status: complete — اكتمل الردّ، ووجدت readTicket تذكرة فيه.
  • order_id: "1042" — بين علامتَي تنصيص، لأنّ المخطّط يقول إنّ order_id نصّ. الرسالة كتبت «order 1042»؛ والتذكرة تحمل الرقم وحده، كما طلب وصف الحقل.
  • issue: "damaged" — واحدة من القيم الأربع التي تسمح بها enum. لا يستطيع النموذج أن يجيب «broken» أو «cracked shade»: فهاتان ليستا في القائمة.
  • wants_refund: true — قيمة منطقيّة حقيقيّة، لا كلمة «نعم». فالرسالة قالت إنّ الزبون يفضّل أن يستردّ ماله.
  • summary: "Order 1042 arrived with a cracked lamp shade, and the customer wants a refund rather than a replacement." — الحقل الوحيد الذي هو جملة النموذج نفسه. جملتك ستُصاغ بشكل آخر؛ لكنّها ستبقى نصّاً، في هذا الحقل.
  • contact_phone: "555-0142" — الرقم الذي في الرسالة.

والآن انظر إلى ما لم تحتجه الشيفرة. التعليمة للنموذج جملة قصيرة واحدة، «Turn the customer email into a support ticket.» — لا «ردّ بـJSON فقط»، ولا «لا تضف أيّ نصّ آخر». وهذه واحدة من الفوائد التي تعدّدها OpenAI: التوجيه الأبسط، بلا حاجة إلى تعليمات شديدة اللهجة للحصول على صيغة ثابتة. ومنها أيضاً أمان الأنواع: لا حاجة إلى التحقّق من الردود ذات الصيغة الخاطئة أو إعادة طلبها. فالحلقة في آخر الخطوة تطبع كلّ حقل بـObject.entries ولا تتحقّق أبداً من وجوده، لأنّ المخطّط قال مسبقاً إنّه سيكون هناك.

وتفصيلان في المخطّط قاعدتان لا أسلوب. الأوّل أنّ نوع contact_phone هو ['string', 'null']: فعلى المخطّط أن يجعل كلّ حقل مطلوباً، والحقل الذي قد لا تحتويه الرسالة يُعبَّر عنه بـ«نصّ، أو null». فلاستعمال المخرجات المنظّمة يجب أن تُحدَّد كلّ الحقول أو مُعامِلات الدوالّ مطلوبةً ، ويمكن محاكاة الحقل الاختياريّ بنوع اتّحاد يضمّ null. فرسالة بلا رقم هاتف ترجع وcontact_phone فيها null — موجود، وفارغ. والقاعدة الثانية هي آخر سطر في المخطّط، additionalProperties: false، والخطوة 2 عمّا يحدث بدونه.

وشيء أخير قد تلاحظه إن شغّلت الملفّ مرّتين: قد يكون التشغيل الأوّل أبطأ. فأوّل طلب بأيّ مخطّط يتأخّر قليلاً بينما تعالج الواجهة المخطّط؛ والطلبات التالية بالمخطّط نفسه لا تتأخّر.

الخطوة 2 — اكسر قاعدة، وشاهد الواجهة ترفضها

أضف هذا تحت شيفرة الخطوة 1، في الملفّ نفسه:

مثالbroken-schema
import { APIError } from 'openai';

// The same schema with one rule broken: additionalProperties is no longer false.
const loose = { ...ticketSchema };
delete loose.additionalProperties;

try {
  await client.responses.create({ model: 'gpt-6-luna', input, text: { format: { ...format, schema: loose } } });
  console.log('accepted');
} catch (error) {
  if (!(error instanceof APIError)) throw error;
  console.log(`refused before the model ran: HTTP ${error.status}`);
  console.log(`message: ${error.error?.message ?? error.message}`);
}

المخرجات

refused before the model ran: HTTP 400
message: Invalid schema for response_format 'support_ticket': In context=(), 'additionalProperties' is required to be supplied and to be false.

يرسل الرسالة نفسها بالمخطّط نفسه، إلّا أنّ additionalProperties اختفى منه. شغّل node ticket.mjs مرّة أخرى. تُطبع أسطر الخطوة 1 أوّلاً، ثمّ سطران آخران. هذا ما قالاه:

  • refused before the model ran: HTTP 400 — رفضت الواجهة الطلب نفسه. و400 هي حالة HTTP لطلب لن يعالجه الخادم؛ فالنموذج لم يرَ الرسالة أصلاً.
  • message: Invalid schema for response_format 'support_ticket': In context=(), 'additionalProperties' is required to be supplied and to be false. — السبب، بكلمات الواجهة. 'support_ticket' هو الاسم (name) الذي أعطيته للمخطّط؛ وcontext=() مسار فارغ، والكائن الوحيد في هذا المخطّط هو الكائن الأعلى الذي فقد القاعدة؛ وباقي الرسالة يقول بالضبط ما الذي تعيده.

والقاعدة وراء ذلك: المخرجات المنظّمة لا تولّد إلّا المفاتيح والقيم المحدّدة، ولهذا يجب على المطوّر أن يضبط additionalProperties: falseليفعّلها. فبدون ذلك السطر يسمح المخطّط للنموذج أن يضيف حقولاً لم تسمّها قطّ — والعقد الذي يسمح بأيّ شيء ليس عقداً، فترفضه الواجهة صراحةً بدل أن تخمّن.

وهذا الرفض من النوع المفيد. فهو يصل مع أوّل طلب ترسله بالمخطّط المكسور، في طرفيّتك، والحقل مسمّى — لا بعد أسابيع على هيئة تذكرة فيها مفتاح شارد لا تتوقّعه قاعدة بياناتك. وحين تكتب مخطّطاً خاصّاً بك، توقّع بضع رسائل من هذا النوع قبل أن ترجع أوّل تذكرة، واقرأ كلّاً منها: فهي تسمّي القاعدة.

الخطوة 3 — اقطع ردّاً قبل أن يكتمل، وتعامل مع ما يرجع

أضف هذا تحت شيفرة الخطوة 2:

مثالcut-short
// The same request with room for only 16 output tokens: not enough to finish the ticket.
const short = await client.responses.create({ model: 'gpt-6-luna', input, text: { format }, max_output_tokens: 16 });
const cut = readTicket(short);

console.log(`status: ${cut.ended}`);
console.log(`ticket: ${JSON.stringify(cut.ticket)}`);
console.log(`output_text: ${JSON.stringify(short.output_text)}`);

المخرجات

status: incomplete (max_output_tokens)
ticket: null
output_text: ""

الطلب هو طلب الخطوة 1 بتغيير واحد: max_output_tokens: 16، أي مكان لـ16 رمزاً فقط من المخرجات — لا يكفي لكتابة التذكرة. شغّل node ticket.mjs مرّة ثالثة. بعد الأسطر السابقة، ثلاثة أسطر أخرى:

  • status: incomplete (max_output_tokens) — توقّف الردّ لأنّه بلغ الحدّ. فقد قرأت readTicket حالة الردّ (status)، ورأت incomplete، وأبلغت عن السبب الذي أعطته الواجهة.
  • ticket: null — لا تذكرة، وreadTicket قالت ذلك بدل أن تخمّن واحدة.
  • output_text: "" — نصّ الردّ فارغ. انتهى الحدّ قبل أن يُكتب حرف واحد من التذكرة.

وهذه هي الحالة الوحيدة التي لا يستطيع المخطّط أن يحميك منها، وصفحة OpenAI تقول ذلك: قد لا يولّد النموذج ردّاً صالحاً يطابق المخطّط في حالة الرفض لأسباب السلامة، أو حين يُبلغ حدّ الرموز الأقصى فيأتي الردّ ناقصاً. ولو استدعت شيفرة الخطوة 1JSON.parse(response.output_text) بلا نظر إلى الحالة أوّلاً، لكانت تحلّل نصّاً فارغاً — وهو ليس JSON، فيرمي الاستدعاء خطأً، ويتوقّف برنامجك عند الردّ الوحيد الذي لم يحمل بيانات.

والحالة الفارغة الأخرى هي الرفض، وreadTicket تتعامل معها أيضاً، مع أنّ هذه الرسالة لا تستدعيه. فلأنّ الرفض لا يتبع المخطّط بالضرورة، يحمل ردّ الواجهة حقلاً اسمه refusalيدلّ على أنّ النموذج رفض الطلب. وهذه هي الفائدة الثانية في قائمة OpenAI: الرفض الصريح، الذي يمكن اكتشافه في الشيفرة. فـreadTicket تبحث عن جزء محتوى من نوع refusal وتعيد نصّه بدل التذكرة. وبين فروعها الثلاثة — مكتمل، ومرفوض، وناقص — كلّ الطرق التي يمكن أن ينتهي بها هذا الطلب.

القاعدة الوحيدة

المخطّط يضمن شكل الجواب، لا صحّته.

فما تعد به المخرجات المنظّمة هو بالضبط ما يقوله تعريفها: كلّ مفتاح مطلوب حاضر، وكلّ قيمة من قائمة القيم المسموحة واحدة منها. ولا تعد بأنّ النموذج اختار القيمة الصحيحة. ففي الخطوة 1،issue: "damaged" صحيحة لأنّ الرسالة تصف غطاء مصباح مشقوقاً؛ أمّا رسالة عن طرد وصل متأخّراً أسبوعاً ومكسوراً فستجبر النموذج على اختيار واحدة من الاثنتين، والمخطّط سيقبل أيّاً منهما.

ما يضمنه المخطّط ما لا يضمنه المخطّط
أنّ order_id حاضر وأنّه نصّ أنّ الطلبيّة 1042 موجودة — ابحث عنها في نظام طلبيّاتك
أنّ issue واحدة من late وdamaged وwrong_item وother أنّ النموذج اختار الصحيحة منها
أنّ wants_refund هي true أو false أنّ الزبون طلب فعلاً استرداد ماله
أنّ contact_phone نصّ أو null أنّ النصّ رقم هاتف يعمل
ألّا يوجد حقل خارج الخمسة أيّ شيء عن ردّ حالته incomplete أو مرفوض — افحص status أوّلاً

فعامل الردّ المنظّم كما تعامل استمارة ملأها غريب: الخانات كلّها موجودة وبالصيغة الصحيحة، ومع ذلك تتحقّق من الأجوبة التي تهمّ. ورقم الطلبيّة يُتحقَّق منه بالبحث — بالدالّة نفسها التي أعطاها دليل ما هو استخدام الأدوات للنموذج أداةً.

متى تستخدمه ومتى لا

الحالة القرار السبب
تحتاج حقولاً من نصّ حرّ — رسائل، استمارات، ملاحظات استعمل صيغة ردّ بمخطّط JSON يصل الردّ بيانات بشكل تتوقّعه شيفرتك مسبقاً
ردّ النموذج يُعرض في واجهتك أنت، جزءاً جزءاً استعمل صيغة ردّ بمخطّط JSON فهي تناسب مخطّطاً منظّماً لردّ النموذج على المستخدم [claim:response-format-when]
على النموذج أن يشغّل دالّة في تطبيقك استعمل استدعاء الدوالّ مع strict: true بدلاً منها استدعاء الدوالّ لوصل النموذج بوظائف تطبيقك [claim:function-calling-when]؛ والمخرجات المنظّمة تعمل هناك أيضاً [claim:two-forms]
تريد فقرة نصّ يقرؤها إنسان لا المخطّط لا يضيف شيئاً إلى نصّ لا يُقرأ إلّا قراءة
القيمة يجب أن تكون حقيقة مؤكّدة — سعر، أو مخزون، أو حالة طلبيّة الفعليّة لا تأخذها من النموذج أصلاً المخطّط يضمن قيمة سليمة الصيغة، لا قيمة صحيحة؛ ابحث عنها
مخطّطك يحتاج ميزة من JSON Schema لا تقبلها الواجهة بسّط المخطّط الواجهة لا تدعم إلّا جزءاً من JSON Schema [claim:subset]، وترفض الباقي كما فعلت في الخطوة 2

مصطلحات مرّت معنا

  • صيغة JSON — صيغة نصّيّة للبيانات: كائنات بحقول مسمّاة، وقوائم، ونصوص، وأرقام، وtrue وfalse وnull.
  • لغة JSON Schema — طريقة معياريّة لوصف الشكل الذي يجب أن تكون عليه قيمة JSON: حقولها، وأنواعها، والقيم المسموحة لها.
  • المخرجات المنظّمة — ميزة OpenAI التي تجعل ردّ النموذج يلتزم بمخطّط JSON تقدّمه أنت.
  • strict: true — الإعداد الذي يجعل المخطّط ملزماً لا تلميحاً.
  • enum — قائمة القيم الوحيدة التي يجوز أن يأخذها الحقل، مثل القيم الأربع لـissue.
  • الحقل المطلوب — حقل يجب أن يحضر دائماً؛ وفي المخطّط الصارم كلّ حقل كذلك.
  • additionalProperties: false — القاعدة التي تمنع الحقول التي لم يسمّها المخطّط؛ والمخطّط الصارم يجب أن يضبطها.
  • الاتّحاد مع null — نوع مثل ['string', 'null']، وهو طريقة قول «هذا الحقل قد يكون فارغاً».
  • وضع JSON — الإعداد الأقدم الذي يضمن JSON صالحاً لكن لا يضمن شكلاً بعينه.
  • الرفض — ردّ يعتذر فيه النموذج عن الطلب؛ ويحمل حقل refusal بدل بياناتك.
  • incomplete — حالة ردّ توقّف مبكراً، عند max_output_tokens مثلاً.
  • output_text — نصّ الردّ سلسلةً واحدة؛ وفي الردّ المنظّم هو الـJSON الذي تحلّله شيفرتك.

الخلاصة

المخرجات المنظّمة تحوّل «من فضلك ردّ بـJSON» إلى عقد: ترسل مخطّط JSON مع strict: true، فيأتي الردّ المكتمل دائماً بذلك الشكل بالضبط. والواجهة ترفض المخطّط الذي يخالف قواعدها قبل أن يعمل النموذج، ومع ذلك قد ينتهي الردّ ناقصاً أو مرفوضاً، فافحص كيف انتهى قبل أن تحلّله. والمخطّط يضمن صيغة كلّ حقل، لا صحّة قيمته.

ما تغيّر مؤخراً

لم يتغيّر أيّ مصدر من مصادر هذا الدليل منذ آخر فحص.