اذهب إلى المحتوى

تطوير تطبيق 'اختبرني' باستخدام ChatGPT ولغة جافاسكربت مع Node.js


Hassan Hedr

تتميز نماذج اللغة الكبيرة أو Large Language Models بقدرتها على فهم اللغة الطبيعية وتوليد النصوص الصحيحة إملائيًا والصحيحة بمعناها أيضًا، مما يسمح بالتعامل مع نماذج الذكاء الاصناعي تلك بسهولة وبساطة من خلال اللغة الطبيعية، ومن تلك النماذج نموذج ChatGPT الشهير من شركة OpenAI والذي سنستفيد من مزاياه تلك والمعلومات الكامنة ضمنه والتي اكتسبها أثناء مرحلة تدريبه على البيانات النصية المتوفرة على الإنترنت في هذا المقال لتوليد أسئلة للمستخدم عن موضوع معين يطلبه المستخدم، ثم عرض الأجوبة التي يُدخلها المستخدم على النموذج لتحديد مستوى فهمه لذلك الموضوع وحتى تصحيح أجوبته إن كانت خاطئة، سنلاحظ أيضًا الطيف الواسع من الاحتمالات الممكنة عند التعامل مع ذلك النموذج، من توليد المحتوى إلى طلب البيانات بشكل وتنسيق محدد يسهل تكامله مع التطبيق.

تجهيز المشروع وإعداد الصفحات

نبدأ بإنشاء مجلد جديد للمشروع وننفذ الأمر التالي بداخله والذي سيُعرّف المشروع كحزمة وتوليد ملف توصيف المشروع package.json والذي يمكن تعريف بعض الأوامر الخاصة بالمشروع ضمنه وتحديد الاعتماديات التي يريدها:

npm init -y

نثبت الاعتماديات التي نحتاجها لبناء المشروع وهي مكتبة express لإنشاء خادم الويب للتطبيق والمكتبة ejs لبناء قوالب HTML لصفحات المشروع، ومكتبة openai للتعامل مع الواجهة البرمجية API لنموذج ChatGPT ومكتبة dotenv والتي سنحتاجها لتحميل متغيرات البيئة من ملف env. للمشروع، وذلك بتنفيذ الأمر التالي:

npm install express ejs openai dotenv

لنبدأ ببناء خادم الويب وتجهيز الصفحات الأساسية للتطبيق وهي ثلاثة صفحات صفحة يدخل فيها المستخدم موضوع الاختبار، وصفحة لعرض الاختبار وإدخال الأجوبة، وصفحة عرض النتيجة، ونولد أولًا تطبيق جديد باستدعاء تابع مكتبة Express ونضبط إعداداته لاستخدام محرك القوالب ejs كمحرك توليد ملفات العرض وتخديم الملفات الثابتة العامة من المجلد public ضمن المشروع، واستقبال الدخل المرسل من نموذج HTML والتعامل معه، وذلك ضمن الملف bin/www ونضيف له ترويسة تحديد بيئة التشغيل لذلك الملف كالتالي:

#!/usr/bin/env node
const path = require("path");
const express = require("express");

// إنشاء تطبيق جديد
const app = express();
// ضبط محرك توليد ملفات العرض
app.set('view engine', 'ejs');
//إعداد تخديم الملفات العامة
app.use(express.static(path.join(__dirname, '../public')));
// ضبط استقبال المدخلات
app.use(express.urlencoded({ extended: true }))

نُعرّف ضمن متغير التطبيق app ثلاث مسارات وهي المسار الرئيسي / بالطريقة GET لعرض الصفحة الرئيسية، والمسار test/ لعرض الاختبار، والمسار result/ لعرض نتيجة الاختبار، وضمن كل منها نٌصيّر ونعيد صفحة القالب المقابلة لذلك المسار باستخدام التابع render من كائن الطلب req المُمرر لتابع معالجة الطلب لكل مسار، ليكون تعريف المسارات كالتالي مع تمرير بعض البيانات الوهمية لاختبار الصفحات والتي سنستبدلها لاحقًا:

// الصفحة الرئيسية
app.get('/', (req, res) => {
    res.render('index');
})

// صفحة الاختبار
app.post('/test', async (req, res) => {
    res.render('test', { 
        questions: [
            'السؤال الأول',
            'السؤال الثاني',
        ]
    });
})

// صفحة عرض النتيجة
app.post('/result', async (req, res) => {
    res.render('result', {
        results: [
            {
                question: 'السؤال الأول',
                answer: 'جواب السؤال الأول',
                correct: true,
                note: null,
            },
            {
                question: 'السؤال الثاني',
                answer: 'جواب السؤال الثاني',
                correct: false,
                note: 'ملاحظة حول الإجابة الخاطئة',
            }
        ],
    })
})

ملفات قوالب صفحات العرض مكانها الافتراضي ضمن المجلد views، وبما أننا نستخدم محرك ejs يجب أن تنتهي جميع تلك الملفات باللاحقة ejs.، ونربط ضمنها ملف التنسيقات style.css والذي يمكننا إنشاءه في المسار public\style.css، والصفحات هي الصفحة الرئيسية views\index.ejs وتحوي حقل لإدخال موضوع الاختبار:

<html lang="ar">
  <head>
    <link rel="stylesheet" href="style.css" />
    <title>اختبرني</title>
  </head>
  <body>
    <h1>?</h1>
    <form action="test" method="post">
      <input type="text" name="subject" placeholder="أدخل موضوع الاختبار"/>
      <button type="submit">اختبرني</button>
    </form>
  </body>
</html>

والصفحة الثانية لعرض الأسئلة المقترحة، حيث نمرر لقالب العرض القيمة questions والتي تحوي الأسئلة كمصفوفة سلاسل نصية، نمر عليها ضمن القالب ونعرض لكل منها نص السؤال ثم حقل مخفي يحوي نص السؤال لإرساله مع حقل الجواب المُدخل من المستخدم، والبيانات ضمن النموذج المُرسل ستحوي على القيمة answers وهي مصفوفة من الكائنات الذي يُمثل كل منها السؤال question مع الجواب answer المُدخل من قبل المستخدم لتكون من الشكل التالي:

{
  "answers": [
    { "question": "...", "answer": ""},
    
  ]
}

نستعمل صيغة الأقواس المربعة ‎[question][index]answers و ‎[answer][index]answers كأسماء لتلك الحقول ليتولى express ترجمتها إلى الكائن answers وقيمته مصفوفة من كائنات يحوي كل منها الحقل question لنص السؤال والحقل answer للجواب المقابل لذلك السؤال، ليكون ملف قالب صفحة الأسئلة كالتالي:

<html lang="ar">
  <head>
    <link rel="stylesheet" href="style.css" />
    <title>اختبرني | الاختبار</title>
  </head>
  <body>
    <form action="result" method="post">
      <% questions.forEach((question, index) => { %>
      <p dir="auto"><%- question %></p>
      <input type="hidden" name="answers[<%= index %>][question]" value="<%- question %>"
      />
      <input type="text" name="answers[<%= index %>][answer]" />
      <% }) %>
      <button type="submit">عرض النتيجة</button>
    </form>
  </body>
</html>

والصفحة الأخيرة لعرض النتيجة، حيث سنمرر لقالب العرض القيمة results وهي مصفوفة من كائنات نتائج الأسئلة، يحوي كل منها على الحقل question لنص السؤال والحقل answer للجواب المدخل من قبل المستخدم سنضعه كقيمة لحقل input مع الخاصية readonly لمنع تعديله، والحقل المنطقي correct يعبر عما إذا كان الجواب صحيحًا أم لا، وأخيرًا الحقل note والذي سنعرضه في حال كان الجواب خاطئًا ويحوي على ملاحظة من قبل ChatGPT عن الإجابة الصحيحة، وأخيرًا رابط للصفحة الرئيسية لطلب اختبار جديد كالتالي:

<html lang="ar">
  <head>
    <link rel="stylesheet" href="style.css" />
    <title>اختبرني | النتيجة</title>
  </head>
  <body>
    <% results.forEach((result, index) => { %>
    <div class="result">
      <p dir="auto"><%- result.question %></p>
      <input type="text" value="<%= result.answer %>" readonly class="<% if(result.correct){ %> correct <% } else { %> wrong <% } %>"/>
      <% if(!result.correct) { %>
        <div class="wrong"><%= result.note %></div>
      <% } %>
    </div>
    <% }) %>

    <a href="/">اختبار جديد</a>
  </body>
</html>

وقبل أن نعاين الصفحات يجب أن نضيف تعليمة تشغيل خادم الويب بنهاية الملف bin/www على منفذ محدد نستخرجه من متغيرات البيئة كالتالي:

// رقم المنفذ من متغيرات البيئة
const port = process.env.PORT;

// تشغيل خادم الويب
app.listen(port, () => console.log(`Listening on http://localhost:${port}`))

ولتحميل متغيرات البيئة من ملف env. نضعه ضمن مجلد المشروع مباشرةً نستخدم المكتبة dotenv بإضافة التعليمة التالية في بداية نفس الملف لتحميل القيم من env. وتعيينها كمتغيرات بيئة مباشرةً كالتالي:

#!/usr/bin/env node
// تحميل متغيرات البيئة
require('dotenv').config()
...

وننشئ الملف env. ونعرف ضمنه القيمة PORT بأي رقم منفذ نريده وليكن 3000:

PORT=3000

ونُعرّف ضمن ملف تعريف الحزمة package.json النص البرمجي start لتشغيل الخادم ضمن القيمة scripts كالتالي:

"scripts": {
  "start": "node bin/www"
},

لتنسيق جميع الصفحات يمكن التعديل على الملف public/style.css ولن نعرض محتواه اختصارًا، وبعد التعديل عليه لتنسيق الصفحات يمكن معاينتها بتشغيل الخادم بتنفيذ التعليمة التالية بطرفية ضمن مجلد المشروع:

npm start

نزور الصفحة الرئيسية على الرابط المعروض بعد تشغيل الخادم:

index.png

ندخل أي قيمة ضمن الصفحة السابقة لنعاين صفحة عرض الأسئلة لنشاهد التالي:

questions.png

ونضغط عرض النتيجة لنعاين صفحة عرض النتيجة النهائية كالتالي:

result.png

بعد أن جهزنا ملفات المشروع بالكامل والصفحات المطلوبة أصبح جاهزًا الآن لربطه مع ChatGPT لتوليد المحتوى المطلوب وعرضه وهو ما سنبدأ به في الفقرات التالية.

تحضير الربط مع ChatGPT

قبل أن نتمكن من التعامل مع الواجهة البرمجية لنموذج ChatGPT علينا التحضير بعدة خطوات، أولًا عبر تسجيل حساب مدفوع ضمن موقع شركة OpenAI، ومنه توليد مفتاح واجهة برمجية جديد ثم نسخه إلى ملف env. ضمن المشروع وتحديده كقيمة للمفتاح OPENAI_API_KEY كالتالي:

...
OPENAI_API_KEY=<قيمة المفتاح>

وضمن الملف نستخدم المكتبة openai التي ثبتناها في بداية المقال ونُنشئ كائن جديد من الصنف الذي توفره OpenAIApi ونمرر له الإعدادات المناسبة مع قيمة المفتاح السابق من متغيرات البيئة في بداية الملف كالتالي:

...
const { OpenAIApi, Configuration } = require("openai");

const openai = new OpenAIApi(
    new Configuration({ apiKey: process.env.OPENAI_API_KEY })
)

يمكننا الآن استخدام الثابت openai للتعامل مع الواجهات البرمجية التي توفرها OpenAI وتحديدًا نموذج ChatGPT وهو ما سنبدأ بالتعامل معه في الفقرة التالية.

اقتباس

ChatGPT المُطلق للعموم والمجاني محصور بواجهة الويب التي توفرها الشركة وهو مجاني أصلًا كي يُدربه المستخدمون، ولا يملك واجهة برمجية سوى المدفوعة حاليًا.

توليد أسئلة الاختبار

كل ما علينا الآن هو تعديل توابع معالجة الطلبات المرسلة والطلب من نموذج ChatGPT توليد بعض الأسئلة عن الموضوع الذي أرسله المستخدم من الصفحة الرئيسية إلى المسار test/ باستخراج قيمة موضوع الاختبار ضمن تابع معالجة الطلب كالتالي:

app.post('/test', async (req, res) => {
    // استخراج موضوع الاختبار
    const { subject } = req.body
    ...
})

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

اقتباس

أكتب أربعة أسئلة يمكن الاجابة عليها بجواب قصير لاختبار مستوايي في <الموضوع>، الأسئلة كل سؤال بسطر:

نلاحظ كيف حددنا شكل الخرج الذي نريده وهو أن يكتب كل سؤال ضمن سطر لنتمكن من استخراجه، ويمكننا تحديد شكل الخرج بطريقة متقدمة سنتعرف عليها لاحقًا أما حاليًا يمكننا فصل الجواب الناتج من النموذج إلى الأسطر المكونة له واعتبار كل سطر هو سؤال، ونبني التعليمة السابقة وندرج ضمنها قيمة الموضوع المرسل ونستخدم الثابت openai لإرسال رسالة إلى النموذج gpt-3.5-turbo وهو النسخة المتاحة دون اشتراك شهري وقت كتابة هذا المقال، حيث تحوي الرسالة على التعليمة السابقة، ثم نستخرج من الجواب قيمة الرسالة المُرسلة من النموذج ليصبح تابع معالجة طلب توليد أسئلة عن موضوع معين كالتالي:

app.post('/test', async (req, res) => {
    // استخراج موضوع الاختبار
    const { subject } = req.body

    // بناء التعليمة
    const prompt = `أكتب أربعة أسئلة يمكن الاجابة عليها بجواب قصير لاختبار مستوايي في ${subject الأسئلة كل سؤال بسطر:`;

    // إرسال طلب إلى النموذج
    const response = await openai.createChatCompletion({
        model: 'gpt-3.5-turbo',
        messages: [
            { role: 'user', content: prompt }
        ]
    })

    // استخراج الجواب
    const message = response.data.choices[0].message.content

    // استخراج الأسئلة المولدة من الجواب
    const questions = message.split('\n')

    // تمرير الأسئلة المقترحة لصفحة الاختبار
    res.render('test', { questions });
})

وفي الفقرة التالية سنستقبل الأجوبة من المستخدم ونُقيمها باستخدام ChatGPT مجددًا ونعرض النتيجة للمستخدم.

إرسال الإجابات للتقييم

بعد إدخال الإجابات في صفحة الاختبار وإرسالها إلى مسار عرض النتيجة result/ يمكننا ضمن ذلك المسار استقبال الإجابات من الكائن answers من جسم الطلب:

app.post('/result', async (req, res) => {
    // استخراج الإجابات
    const { answers } = req.body
    ...
})

قيمة ذلك المتغير ستكون مصفوفة من كائنات يحوي كل منها على نص السؤال ضمن الحقل question والجواب المُدخل من المستخدم answer، والتي سنضمنها ضمن التعليمة المُرسلة إلى ChatGPT لتقييمها، وبما أن التقييمات لكل سؤال نريدها أن تحوي معلومات عن صحة الجواب وملاحظة في حال كان خاطئًا، فيمكننا الطلب من ChatGPT إعادة الجواب بصيغة JSON ليسهل علينا استخراج المعلومات منه وعرضها للمستخدم ضمن التطبيق، وذلك بإعطاء مثال عن شكل الخرج في نهاية التعليمة ليكون شكل التعليمة النهائي كالتالي:

اقتباس

السؤال: … الجواب: …

السؤال: … الجواب: …

قيم الإجابات السابقة مع الشرح بصيغة JSON التالية:

[{correct: false, note:"تعليل أو ملاحظة حول الإجابة...." ...]

نبني تلك التعليمة من الأجوبة المرسلة كالتالي:

const { answers } = req.body

// الأسئلة مع الأجوبة
const questionsAndAnswers = answers
        .map(({ question, answer }) => `السؤال: ${question}\nالجواب: ${answer}`)
        .join('\n\n')

// بناء التعليمة
const prompt = `${questionsAndAnswers}\n\nقيم الإجابات السابقة مع الشرح بصيغة JSON التالية:
    [{correct: false, note:"تعليل أو ملاحظة حول الإجابة...." ...]`;

ثم نرسلها إلى ChatGPT كما فعلنا سابقًا والفرق هذه المرة في معالجة الجواب حيث سنترجمه من صيغة JSON إلى مصفوفة في جافاسكريبت باستخدام التابع JSON.parse

// إرسال التعليمة
const response = await openai.createChatCompletion({
        model: 'gpt-3.5-turbo',
        messages: [
            { role: 'user', content: prompt }
        ]
    })

// استخراج الجواب
const message = response.data.choices[0].message?.content

// معالجة الجواب
const results = JSON.parse(message)

والآن نعرضها ضمن قالب العرض result.ejs باستخدام التابع renderمع تمرير نص السؤال والجواب المدخل من المستخدم وتقييم الجواب والملاحظة حوله كالتالي:

// عرض النتيجة
res.render('result', {
    results: results.map((result, index) => ({
        question: answers[index].question,
        answer: answers[index].answer,
        note: result.note,
        correct: result.correct,
    })),
})

وبذلك يصبح التطبيق جاهزًا للاختبار سنستعرضه ضمن الفقرة التالية.

اختبار التطبيق

لنحاول الإجابة على بعض الأسئلة المتعلقة بالبرمجة في نود لاختبار المستوى:

test-index.png

نلاحظ اقتراح أسئلة متنوعة عن ذلك الموضوع، لنحاول الإجابة عليها باللغة العربية وترك الجواب الأخير خاطئًا:

test-questions.png

عاين ChatGPT تلك الأجوبة وحدد الصحيح والخاطئ منها بفهم النص المكتوب وربطه بسؤاله بدقة وحدد الجواب الأخير بأنه خاطئ وأعطى ملاحظة عن سبب الخطأ:

test-result.png

خاتمة

كما رأينا سهولة التعامل مع ChatGPT والاحتمالات الواسعة الممكنة ضمنه، من توليد محتوى محدد إلى قراء وفهم مدخلات المستخدم إلى إمكانية تحديد شكل الخرج المطلوب لتسهيل التعامل معه برمجيًا، ويمكن التطوير على التطبيق الذي طورناه في هذا المقال بعدة أفكار، فمثلًا يمكن تحديد عدد الأسئلة من قبل المستخدم والطلب من ChatGPT ذلك العدد من الأسئلة، أو يمكن حفظ سجل الأسئلة المجاب عنها من قبل المستخدم وإرسالها مع طلب توليد الأسئلة الجديدة والطلب منه أن تكون الأسئلة مختلفة في مواضيعها عن السابقة ما يغني تجربة المستخدم، أو إذا كان موضوع الأسئلة محددًا يمكن توليد العديد من أسئلة اختيار من متعدد عن ذلك الموضوع دفعة واحدة مع تحديد الإجابة الصحيحة لكل سؤال ثم تخزينها ضمن قاعدة بيانات أسئلة كبيرة وتُعرض للمستخدم عشوائيًا كي يتمرن عليها ونحدد مستواه وبذلك لا داعي للاتصال مع النموذج خلال مرحلة الإنتاج ونستفيد منه لتوليد محتوى الأسئلة فقط.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...