Gemini في Java باستخدام Vertex AI وLangChain4j

1. مقدمة

تركّز ورشة رموز البرامج هذه على Gemini، وهو نموذج لغوي كبير (LLM) مستضاف على Vertex AI على Google Cloud. ‫Vertex AI هي منصة تتضمّن جميع منتجات تعلُّم الآلة وخدماتها ونماذجها على Google Cloud.

ستستخدم Java للتفاعل مع Gemini API باستخدام إطار عمل LangChain4j. ستطّلع على أمثلة ملموسة للاستفادة من النموذج اللغوي الكبير (LLM) في الإجابة عن الأسئلة وإنشاء الأفكار واستخراج الكيانات والمحتوى المنظَّم وإنشاء المحتوى الموسّع النطاق لاسترداد المعلومات وطلب تنفيذ وظائف.

ما هو الذكاء الاصطناعي التوليدي؟

يشير الذكاء الاصطناعي التوليدي إلى استخدام الذكاء الاصطناعي لإنشاء محتوى جديد، مثل النصوص والصور والموسيقى والملفات الصوتية والفيديوهات.

يعتمد الذكاء الاصطناعي التوليدي على النماذج اللغوية الكبيرة (LLM) التي يمكنها تنفيذ مهام متعددة وتنفيذ مهام غير تقليدية، مثل التلخيص والأسئلة والأجوبة والتصنيف وغير ذلك. وباستخدام الحد الأدنى من التدريب، يمكن تعديل النماذج الأساسية لتناسب حالات الاستخدام المستهدَفة باستخدام أمثلة بيانات قليلة جدًا.

كيف يعمل الذكاء الاصطناعي التوليدي؟

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

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

ما هي تطبيقات الذكاء الاصطناعي التوليدي الشائعة؟

يمكن استخدام الذكاء الاصطناعي التوليدي لإجراء ما يلي:

  • تحسين تفاعلات العملاء من خلال تجارب محادثة وبحث محسّنة
  • استكشِف كميات هائلة من البيانات غير المنظَّمة من خلال الواجهات الحوارية والملخّصات.
  • المساعدة في المهام المتكرّرة، مثل الردّ على طلبات العروض، وأتمتة ترجمة المحتوى التسويقي بلغات مختلفة، والتحقّق من عقود العملاء للتأكّد من امتثالها للسياسات، وغير ذلك

ما هي عروض الذكاء الاصطناعي التوليدي التي توفّرها Google Cloud؟

باستخدام Vertex AI، يمكنك التفاعل مع النماذج الأساسية وتخصيصها وتضمينها في تطبيقاتك بدون الحاجة إلى خبرة كبيرة في تعلُّم الآلة أو بدون أي خبرة على الإطلاق. يمكنك الوصول إلى النماذج الأساسية في Model Garden، أو ضبط النماذج من خلال واجهة مستخدم بسيطة في Vertex AI Studio، أو استخدام النماذج في دفتر ملاحظات علم البيانات.

توفّر Vertex AI Search and Conversation للمطوّرين أسرع طريقة لإنشاء محرّكات بحث ومحادثات آلية مستندة إلى الذكاء الاصطناعي التوليدي.

Gemini في Google Cloud هو تطبيق تعاوني مستند إلى الذكاء الاصطناعي (AI) ومستند إلى Gemini، وهو متاح على Google Cloud وأدوات تطوير البرامج (IDE) لمساعدتك في إنجاز المزيد من المهام بشكل أسرع. يوفّر Gemini Code Assist ميزة إكمال الرموز البرمجية وإنشائها وتفسيرها، ويتيح لك الدردشة معه لطرح أسئلة فنية.

ما هو Gemini؟

‫Gemini هي مجموعة من نماذج الذكاء الاصطناعي التوليدي التي طوّرتها Google DeepMind، وهي مصمّمة لحالات الاستخدام المتعدّدة الوسائط. تعني تقنية "الذكاء الاصطناعي المتعدد الوسائط" أنّها يمكنها معالجة أنواع مختلفة من المحتوى وإنشاؤها، مثل النصوص والرموز والصور والمقاطع الصوتية.

b9913d011999e7c7.png

تتوفّر ميزة Gemini بأشكال وأحجام مختلفة:

  • Gemini Ultra: أكبر إصدار وأكثره قدرة على تنفيذ المهام المعقّدة
  • Gemini Flash: أسرع وأرخص نموذج محسَّن للمهام ذات الحجم الكبير.
  • Gemini Pro: حجم متوسط ومحسَّن لأداء مجموعة متنوعة من المهام بكفاءة
  • Gemini Nano: هو الإصدار الأكثر كفاءة، وهو مصمّم للمهام التي يتم تنفيذها على الأجهزة فقط.

الميزات الرئيسية:

  • المعالجة المتعددة الوسائط: تُعدّ قدرة Gemini على فهم تنسيقات المعلومات المتعددة ومعالجتها خطوة مهمة تتجاوز النماذج اللغوية التقليدية التي تتعامل مع النصوص فقط.
  • الأداء: يتفوق Gemini Ultra على أحدث التقنيات الحالية في العديد من مقاييس الأداء، وكان أول نموذج يتفوق على الخبراء البشريين في مقياس الأداء الصعب MMLU (فهم اللغة في المهام المتعدّدة والمكثّفة).
  • مرونة: تسمح أحجام Gemini المختلفة بتعديلها لتلائم حالات الاستخدام المختلفة، بدءًا من الأبحاث على نطاق واسع ووصولاً إلى عمليات النشر على الأجهزة الجوّالة.

كيف يمكنك التفاعل مع Gemini على Vertex AI من Java؟

ثمة خياران:

  1. مكتبة Vertex AI Java API for Gemini الرسمية
  2. إطار عمل LangChain4j

في هذا الدرس البرمجي، ستستخدم إطار عمل LangChain4j.

ما هو إطار عمل LangChain4j؟

إطار عمل LangChain4j هو مكتبة مفتوحة المصدر لدمج النماذج اللغوية الكبيرة في تطبيقات Java، وذلك من خلال تنسيق مكوّنات مختلفة، مثل النموذج اللغوي الكبير نفسه، بالإضافة إلى أدوات أخرى مثل قواعد بيانات المتجهات (للعمليات البحثية الدلالية) وأدوات تحميل المستندات وتقسيمها (لتحليل المستندات والتعلم منها) وأدوات تحليل النتائج وغير ذلك.

استُوحي المشروع من مشروع LangChain في بايثون، ولكن بهدف خدمة مطوّري Java.

bb908ea1e6c96ac2.png

ما ستتعرّف عليه

  • كيفية إعداد مشروع Java لاستخدام Gemini وLangChain4j
  • كيفية إرسال طلبك الأول إلى Gemini آليًا
  • كيفية عرض الردود تدريجيًا من Gemini
  • كيفية إنشاء محادثة بين مستخدم وGemini
  • كيفية استخدام Gemini في سياق متعدد الوسائط من خلال إرسال النصوص والصور معًا
  • كيفية استخراج معلومات مفيدة منظَّمة من المحتوى غير المنظَّم
  • كيفية استخدام نماذج الطلبات
  • كيفية تصنيف النصوص، مثل تحليل المشاعر
  • كيفية إجراء محادثة مع مستنداتك الخاصة (Retrieval Augmented Generation)
  • كيفية توسيع نطاق برامج المحادثة باستخدام استدعاء الدوالّ
  • كيفية استخدام Gemma محليًا مع Ollama وTestContainers

المتطلبات

  • معرفة لغة البرمجة Java
  • مشروع على Google Cloud
  • متصفّح، مثل Chrome أو Firefox

2. الإعداد والمتطلبات

إعداد البيئة حسب السرعة التي تناسبك

  1. سجِّل الدخول إلى Google Cloud Console وأنشِئ مشروعًا جديدًا أو أعِد استخدام مشروع حالي. إذا لم يكن لديك حساب على Gmail أو Google Workspace، عليك إنشاء حساب.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • اسم المشروع هو الاسم المعروض للمشاركين في هذا المشروع. وهي سلسلة أحرف لا تستخدمها واجهات برمجة تطبيقات Google. ويمكنك تعديلها في أي وقت.
  • يكون معرّف المشروع فريدًا في جميع مشاريع Google Cloud وغير قابل للتغيير (لا يمكن تغييره بعد ضبطه). تُنشئ وحدة تحكّم Cloud Console سلسلة فريدة تلقائيًا، ولا يهمّك عادةً معرفة محتواها. في معظم مختبرات الرموز البرمجية، ستحتاج إلى الإشارة إلى معرّف المشروع (يُعرَف عادةً باسم PROJECT_ID). إذا لم يعجبك المعرّف الذي تم إنشاؤه، يمكنك إنشاء معرّف آخر عشوائي. يمكنك بدلاً من ذلك تجربة عنوانك الخاص لمعرفة ما إذا كان متاحًا. ولا يمكن تغييره بعد هذه الخطوة ويبقى ساريًا طوال مدة المشروع.
  • يُرجى العِلم أنّ هناك قيمة ثالثة، وهي رقم المشروع، تستخدمها بعض واجهات برمجة التطبيقات. اطّلِع على مزيد من المعلومات عن كلّ من هذه القيم الثلاث في المستندات.
  1. بعد ذلك، عليك تفعيل الفوترة في Cloud Console لاستخدام موارد/واجهات برمجة تطبيقات Cloud. لن تُكلّفك المشاركة في هذا الدليل التعليمي للترميز الكثير، إن لم يكن أيّ تكلفة على الإطلاق. لإيقاف الموارد لتجنُّب تحصيل رسوم بعد انتهاء هذا الدليل التعليمي، يمكنك حذف الموارد التي أنشأتها أو حذف المشروع. يكون مستخدمو Google Cloud الجدد مؤهّلين للاستفادة من برنامج الفترة التجريبية المجانية التي تقدّم رصيدًا بقيمة 300 دولار أمريكي.

بدء Cloud Shell

على الرغم من أنّه يمكن تشغيل Google Cloud عن بُعد من الكمبيوتر المحمول، ستستخدم في هذا الإصدار التجريبي من "مختبر رموز Google" Cloud Shell، وهي بيئة سطر أوامر تعمل في السحابة الإلكترونية.

تفعيل Cloud Shell

  1. من Cloud Console، انقر على تفعيل Cloud Shell 853e55310c205094.png.

3c1dabeca90e44e5.png

إذا كانت هذه هي المرة الأولى التي تبدأ فيها Cloud Shell، ستظهر لك شاشة وسيطة توضّح ماهيتها. إذا ظهرت لك شاشة وسيطة، انقر على متابعة.

9c92662c6a846a5c.png

من المفترض ألا يستغرق توفير Cloud Shell والاتصال بها سوى بضع لحظات.

9f0e51b578fecce5.png

تم تحميل هذه الآلة الافتراضية بجميع أدوات التطوير اللازمة. ويقدّم هذا الدليل دليلاً منزليًا دائمًا بسعة 5 غيغابايت ويتم تشغيله في Google Cloud، ما يُحسِّن بشكل كبير أداء الشبكة والمصادقة. يمكن تنفيذ الكثير من عملك في هذا الدليل التعليمي للترميز، إن لم يكن كلّه، باستخدام متصفّح.

بعد الاتصال بخدمة Cloud Shell، من المفترض أن تظهر لك رسالة تفيد بأنّه تم مصادقة حسابك وأنّه تم ضبط المشروع على معرّف مشروعك.

  1. نفِّذ الأمر التالي في Cloud Shell لتأكيد مصادقة حسابك:
gcloud auth list

ناتج الأمر

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. شغِّل الأمر التالي في Cloud Shell للتأكّد من أنّ الأمر gcloud يعرف مشروعك:
gcloud config list project

ناتج الأمر

[core]
project = <PROJECT_ID>

إذا لم يكن كذلك، يمكنك تعيينه من خلال هذا الأمر:

gcloud config set project <PROJECT_ID>

ناتج الأمر

Updated property [core/project].

3- إعداد بيئة التطوير

في هذا الدرس التطبيقي حول الترميز، ستستخدم وحدة الطرفية في Cloud Shell ومحرِّر Cloud Shell لتطوير برامج Java.

تفعيل واجهات برمجة تطبيقات Vertex AI

في Google Cloud Console، تأكَّد من ظهور اسم مشروعك في أعلى Google Cloud Console. إذا لم يكن الأمر كذلك، انقر على اختيار مشروع لفتح أداة اختيار المشاريع واختَر المشروع المقصود.

يمكنك تفعيل واجهات برمجة تطبيقات Vertex AI من قسم Vertex AI في Google Cloud Console أو من وحدة تحكّم Cloud Shell.

لتفعيل هذه الميزة من Google Cloud Console، انتقِل أولاً إلى قسم Vertex AI في قائمة Google Cloud Console:

451976f1c8652341.png

انقر على تفعيل جميع واجهات برمجة التطبيقات المقترَحة في لوحة بيانات Vertex AI.

سيؤدي ذلك إلى تفعيل العديد من واجهات برمجة التطبيقات، ولكن أهمها في الدرس التطبيقي هو aiplatform.googleapis.com.

بدلاً من ذلك، يمكنك أيضًا تفعيل واجهة برمجة التطبيقات هذه من وحدة تحكّم Cloud Shell باستخدام الأمر التالي:

gcloud services enable aiplatform.googleapis.com

استنسِخ مستودع GitHub.

في وحدة Cloud Shell الطرفية، يمكنك استنساخ مستودع هذا الدرس التطبيقي حول الترميز:

git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git

للتأكّد من أنّ المشروع جاهز للتنفيذ، يمكنك تجربة تشغيل برنامج "مرحبًا بك في العالم".

تأكَّد من أنّك في المجلد الأعلى:

cd gemini-workshop-for-java-developers/ 

أنشئ حزمة Gradle:

gradle wrapper

تشغيل gradlew:

./gradlew run

من المفترض أن يظهر لك الناتج التالي:

..
> Task :app:run
Hello World!

فتح Cloud Editor وإعداده

افتح الرمز باستخدام محرِّر Cloud Code من Cloud Shell:

42908e11b28f4383.png

في "محرِّر رموز Cloud"، افتح مجلد مصدر الدرس التطبيقي عن طريق النقر على File -> Open Folder والإشارة إلى مجلد مصدر الدرس التطبيقي (مثل /home/username/gemini-workshop-for-java-developers/).

إعداد المتغيّرات البيئية

افتح وحدة طرفية جديدة في "أداة تعديل الرموز البرمجية في Google Cloud" عن طريق النقر على Terminal -> New Terminal. عليك إعداد متغيّرَين للبيئة مطلوبَين لتشغيل أمثلة الرموز البرمجية:

  • PROJECT_ID: رقم تعريف مشروعك على Google Cloud
  • الموقع الجغرافي: المنطقة التي تم نشر نموذج Gemini فيها

تصدير المتغيّرات على النحو التالي:

export PROJECT_ID=$(gcloud config get-value project)
export LOCATION=us-central1

4. أول طلب إلى نموذج Gemini

بعد إعداد المشروع بشكل صحيح، حان وقت الاتصال بواجهة برمجة التطبيقات Gemini API.

اطّلِع على QA.java في دليل app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

public class QA {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .build();

        System.out.println(model.generate("Why is the sky blue?"));
    }
}

في هذا المثال الأول، عليك استيراد فئة VertexAiGeminiChatModel التي تنفّذ واجهة ChatModel.

في طريقة main، يمكنك ضبط نموذج لغة المحادثة باستخدام أداة إنشاء VertexAiGeminiChatModel وتحديد ما يلي:

  • Project
  • الموقع الجغرافي
  • اسم الطراز (gemini-1.5-flash-002).

بعد أن أصبح نموذج اللغة جاهزًا، يمكنك استدعاء طريقة generate() وضبط الطلب أو السؤال أو التعليمات المطلوب إرسالها إلى نموذج اللغة الكبير. في هذه الحالة، يمكنك طرح سؤال بسيط عن سبب زرقة السماء.

يمكنك تغيير هذا الطلب لتجربة أسئلة أو مهام مختلفة.

شغِّل العيّنة في المجلد الجذر لرمز المصدر:

./gradlew run -q -DjavaMainClass=gemini.workshop.QA

من المفترض أن يظهر لك ناتج مشابه لما يلي:

The sky appears blue because of a phenomenon called Rayleigh scattering.
When sunlight enters the atmosphere, it is made up of a mixture of
different wavelengths of light, each with a different color. The
different wavelengths of light interact with the molecules and particles
in the atmosphere in different ways.

The shorter wavelengths of light, such as those corresponding to blue
and violet light, are more likely to be scattered in all directions by
these particles than the longer wavelengths of light, such as those
corresponding to red and orange light. This is because the shorter
wavelengths of light have a smaller wavelength and are able to bend
around the particles more easily.

As a result of Rayleigh scattering, the blue light from the sun is
scattered in all directions, and it is this scattered blue light that we
see when we look up at the sky. The blue light from the sun is not
actually scattered in a single direction, so the color of the sky can
vary depending on the position of the sun in the sky and the amount of
dust and water droplets in the atmosphere.

تهانينا، لقد أجريت مكالمتك الأولى مع فريق Gemini.

عرض الرد تدريجيًا

هل لاحظت أنّه تم تقديم الردّ دفعة واحدة بعد بضع ثوانٍ؟ من الممكن أيضًا الحصول على الردّ بشكل تدريجي، وذلك بفضل الصيغة المخصّصة للبث. الاستجابة أثناء البث: يعرض النموذج الاستجابة بشكل تدريجي عندما تصبح متاحة.

في هذا الدرس التطبيقي حول الرموز البرمجية، سنكتفي بالاستجابة غير المستندة إلى البث المباشر، ولكن لنلقِ نظرة على الاستجابة المستندة إلى البث المباشر لمعرفة كيفية تنفيذها.

في StreamQA.java في دليل app/src/main/java/gemini/workshop، يمكنك الاطّلاع على استجابة البث المباشر:

package gemini.workshop;

import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;

import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;

public class StreamQA {
    public static void main(String[] args) {
        StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(4000)
            .build();

        model.generate("Why is the sky blue?", onNext(System.out::println));
    }
}

هذه المرة، سنستورد أنواع فئة البث VertexAiGeminiStreamingChatModel التي تنفّذ واجهة StreamingChatLanguageModel. ستحتاج أيضًا إلى الاستيراد الثابت LambdaStreamingResponseHandler.onNext، وهي طريقة سهلة توفّر StreamingResponseHandler لإنشاء معالِج بث باستخدام تعبيرات lambda في Java.

هذه المرة، يختلف توقيع طريقة generate() قليلاً. بدلاً من عرض سلسلة، يكون نوع العرض فارغًا. بالإضافة إلى الطلب، عليك تمرير معالِج استجابة البث. هنا، بفضل الاستيراد الثابت الذي ذكرناه أعلاه، يمكننا تحديد تعبير لامبادا يتم تمريره إلى طريقة onNext(). يتمّ استدعاء تعبير lambda في كلّ مرّة يتوفّر فيها جزء جديد من الردّ، في حين لا يتمّ استدعاء الأخير إلا في حال حدوث خطأ.

التنفيذ:

./gradlew run -q -DjavaMainClass=gemini.workshop.StreamQA

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

الإعدادات الإضافية

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

  • temperature(Float temp) - لتحديد مدى إبداعية الردّ (يكون القيمة 0 للردّ غير الإبداعي وغالبًا ما يكون أكثر واقعية، في حين تكون القيمة 2 للردّ الإبداعي)
  • topP(Float topP) - لاختيار الكلمات المحتمَلة التي يضيف احتمال ظهورها إلى بعضها هذا الرقم الفاصل (بين 0 و1)
  • topK(Integer topK) - لاختيار كلمة عشوائيًا من الحد الأقصى لعدد الكلمات المحتمَلة لإكمال النص (من 1 إلى 40)
  • maxOutputTokens(Integer max) - لتحديد الحد الأقصى لطول الإجابة التي يقدّمها النموذج (بشكل عام، تمثّل 4 وحدات ترميز 3 كلمات تقريبًا)
  • maxRetries(Integer retries) - في حال تجاوزت الحصة المحدّدة للطلبات في كل مرة أو إذا كانت المنصة تواجه مشكلة فنية، يمكنك أن تطلب من النموذج إعادة إجراء المكالمة 3 مرات.

لقد طرحت حتى الآن سؤالاً واحدًا على Gemini، ولكن يمكنك أيضًا إجراء محادثة متعددة المقاطع. سنتناول هذا الموضوع في القسم التالي.

5- التحدّث إلى Gemini

في الخطوة السابقة، طرحت سؤالاً واحدًا. لقد حان الوقت الآن لإجراء محادثة حقيقية بين مستخدم ونموذج اللغة الكبيرة. يمكن أن يستند كل سؤال وإجابة إلى الأسئلة والأجوبة السابقة لتشكيل مناقشة حقيقية.

اطّلِع على Conversation.java في مجلد app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;

import java.util.List;

public class Conversation {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .build();

        MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
            .maxMessages(20)
            .build();

        interface ConversationService {
            String chat(String message);
        }

        ConversationService conversation =
            AiServices.builder(ConversationService.class)
                .chatLanguageModel(model)
                .chatMemory(chatMemory)
                .build();

        List.of(
            "Hello!",
            "What is the country where the Eiffel tower is situated?",
            "How many inhabitants are there in that country?"
        ).forEach( message -> {
            System.out.println("\nUser: " + message);
            System.out.println("Gemini: " + conversation.chat(message));
        });
    }
}

في ما يلي نوعان جديدان من عمليات الاستيراد المثيرة للاهتمام في هذه الفئة:

  • MessageWindowChatMemory: فئة ستساعد في التعامل مع الجوانب المتعدّدة المحادثات، والاحتفاظ بالأسئلة والأجوبة السابقة في الذاكرة المحلية
  • AiServices: فئة مجردة من المستوى الأعلى ستربط بين نموذج المحادثة وذاكرة المحادثة

في الطريقة الرئيسية، عليك إعداد النموذج وذاكرة المحادثة وخدمة الذكاء الاصطناعي. يتم ضبط النموذج كالمعتاد مع معلومات المشروع والموقع الجغرافي واسم النموذج.

بالنسبة إلى ذاكرة المحادثات، نستخدم أداة إنشاء MessageWindowChatMemory لإنشاء ذاكرة تحتفظ بآخر 20 رسالة تم تبادلها. وهي نافذة متحركة على المحادثة التي يتم الاحتفاظ بسياقها محليًا في برنامج Java Class Client.

بعد ذلك، يمكنك إنشاء AI service الذي يربط نموذج المحادثة بذاكرة المحادثة.

يُرجى ملاحظة كيف تستخدِم خدمة الذكاء الاصطناعي واجهة ConversationService مخصّصة حدّدناها، والتي تنفِّذها LangChain4j، وتستخدِم طلب بحث String وتُعرِض استجابة String.

حان الآن وقت إجراء محادثة مع Gemini. أولاً، يتم إرسال رسالة ترحيب بسيطة، ثمّ سؤال أوّل عن برج إيفل لمعرفة البلد الذي يمكن العثور عليه فيه. يُرجى العِلم أنّ الجملة الأخيرة مرتبطة بإجابة السؤال الأول، لأنّك تتساءل عن عدد السكان في البلد الذي يقع فيه برج إيفل، بدون ذكر البلد الذي تم ذكره في الإجابة السابقة. ويُظهر ذلك أنّه يتم إرسال الأسئلة والإجابات السابقة مع كل طلب.

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation

من المفترض أن تظهر لك ثلاث إجابات مشابهة لما يلي:

User: Hello!
Gemini: Hi there! How can I assist you today?

User: What is the country where the Eiffel tower is situated?
Gemini: France

User: How many inhabitants are there in that country?
Gemini: As of 2023, the population of France is estimated to be around 67.8 million.

يمكنك طرح أسئلة ذات خطوة واحدة أو إجراء محادثات متعددة الخطوات مع Gemini، ولكن حتى الآن، كانت الإدخالات نصية فقط. ماذا عن الصور؟ لنستكشف الصور في الخطوة التالية.

6- تعدد الوسائط باستخدام Gemini

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

هل تعتقد أنّ Gemini سيتعرّف على هذه القطة؟

af00516493ec9ade.png

صورة قطة في الثلج مأخوذة من Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

اطّلِع على Multimodal.java في الدليل app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;

public class Multimodal {

    static final String CAT_IMAGE_URL =
        "https://upload.wikimedia.org/wikipedia/" +
        "commons/b/b6/Felis_catus-cat_on_snow.jpg";


    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .build();

        UserMessage userMessage = UserMessage.from(
            ImageContent.from(CAT_IMAGE_URL),
            TextContent.from("Describe the picture")
        );

        Response<AiMessage> response = model.generate(userMessage);

        System.out.println(response.content().text());
    }
}

في عمليات الاستيراد، نميز بين أنواع مختلفة من الرسائل والمحتوى. يمكن أن يحتوي UserMessage على عنصرَي TextContent وImageContent. هذا هو تعدُّد الوسائط: مزج النصوص والصور. لا نرسل فقط طلبًا بسيطًا في سلسلة، بل نرسل عنصرًا أكثر تنظيمًا يمثّل رسالة مستخدم، ويتألف من قطعة محتوى صورة وقطعة محتوى نصي. يُرسِل النموذج Response يحتوي على AiMessage.

يمكنك بعد ذلك استرداد AiMessage من الردّ من خلال content()، ثم نص الرسالة بفضل text().

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal

من المؤكد أنّ اسم الصورة قدّم لك لمحة عن ما تحتويه الصورة، ولكن نتيجة Gemini تشبه ما يلي:

A cat with brown fur is walking in the snow. The cat has a white patch of fur on its chest and white paws. The cat is looking at the camera.

يؤدي الجمع بين الصور والطلبات النصية إلى توفير حالات استخدام مثيرة للاهتمام. يمكنك إنشاء تطبيقات يمكنها تنفيذ ما يلي:

  • التعرّف على النص في الصور
  • التحقّق مما إذا كانت الصورة آمنة للعرض
  • أنشئ شرحًا للصور.
  • البحث في قاعدة بيانات من الصور باستخدام أوصاف نصية عادية

بالإضافة إلى استخراج المعلومات من الصور، يمكنك أيضًا استخراج المعلومات من النصوص غير المنظَّمة. وهذا ما ستتعرّف عليه في القسم التالي.

7- استخراج معلومات منظَّمة من نص غير منظَّم

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

لنفترض أنّك تريد استخراج اسم شخص وعمره، استنادًا إلى سيرة ذاتية أو سيرة أكاديمية أو وصف لهذا الشخص. يمكنك توجيه النموذج اللغوي الكبير لاستخراج تنسيق JSON من نص غير منظَّم باستخدام طلب معدَّل بذكاء (يُعرف هذا الإجراء عادةً باسم "هندسة الطلب").

في المثال أدناه، بدلاً من إنشاء طلب يصف إخراج JSON، سنستخدم ميزة فعّالة في Gemini تُعرف باسم الإخراج المنظَّم، أو الترميز المحدود في بعض الأحيان، ما يجبر النموذج على عرض محتوى JSON صالح فقط، وفقًا لنموذج JSON محدّد.

اطّلِع على ExtractData.java في app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;

public class ExtractData {

    record Person(String name, int age) { }

    interface PersonExtractor {
        @SystemMessage("""
            Your role is to extract the name and age 
            of the person described in the biography.
            """)
        Person extractPerson(String biography);
    }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .responseMimeType("application/json")
            .responseSchema(fromClass(Person.class))
            .build();

        PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);

        String bio = """
            Anna is a 23 year old artist based in Brooklyn, New York. She was born and 
            raised in the suburbs of Chicago, where she developed a love for art at a 
            young age. She attended the School of the Art Institute of Chicago, where 
            she studied painting and drawing. After graduating, she moved to New York 
            City to pursue her art career. Anna's work is inspired by her personal 
            experiences and observations of the world around her. She often uses bright 
            colors and bold lines to create vibrant and energetic paintings. Her work 
            has been exhibited in galleries and museums in New York City and Chicago.    
            """;
        Person person = extractor.extractPerson(bio);

        System.out.println(person.name());  // Anna
        System.out.println(person.age());   // 23
    }
}

لنلقِ نظرة على الخطوات المختلفة في هذا الملف:

  • يتم تعريف سجلّ Person لتمثيل التفاصيل التي تصف شخصًا (الاسم والعمر).
  • يتم تعريف واجهة PersonExtractor باستخدام طريقة تُعرِض مثيل Person استنادًا إلى سلسلة نصية غير منظَّمة.
  • تمّت إضافة تعليق توضيحي @SystemMessage إلى extractPerson() يربطه بطلب تعليمات. هذا هو الطلب الذي سيستخدمه النموذج لتوجيه استخراج المعلومات، وعرض التفاصيل في شكل مستند JSON، وسيتم تحليله إليك وتحويله إلى مثيل Person.

لنلقِ الآن نظرة على محتوى الطريقة main():

  • يتم ضبط نموذج المحادثة وإنشاء مثيل له. نحن نستخدم طريقتَين جديدتَين من فئة "أداة إنشاء النماذج": responseMimeType() وresponseSchema(). يطلب الإجراء الأول من Gemini إنشاء ملف JSON صالح في الإخراج. تحدِّد الطريقة الثانية مخطّط كائن JSON الذي يجب إرجاعه. بالإضافة إلى ذلك، يفوّض الأخير إلى طريقة مساعدة يمكنها تحويل فئة Java أو سجلّ إلى مخطّط JSON مناسب.
  • يتم إنشاء عنصر PersonExtractor بفضل فئة AiServices في LangChain4j.
  • بعد ذلك، يمكنك ببساطة استدعاء Person person = extractor.extractPerson(...) لاستخراج تفاصيل الشخص من النص غير المنظَّم، واسترداد مثيل Person يتضمّن الاسم والعمر.

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData

من المفترض أن يظهر لك الناتج التالي:

Anna
23

نعم، اسمي "آنا" وعمري 23 عامًا.

باستخدام أسلوب AiServices هذا، يمكنك التعامل مع عناصر ذات أنواع محدّدة. لا تتفاعل مباشرةً مع نموذج اللغة الكبير. بدلاً من ذلك، يتم التعامل مع فئات محدّدة، مثل سجلّ Person لتمثيل المعلومات الشخصية المستخرَجة، ويكون لديك عنصر PersonExtractor يحتوي على طريقة extractPerson() تُعرِض مثيل Person. يتمّ تجريد مفهوم LLM، وبصفتك مطوّر Java، ما عليك سوى التحكّم في الفئات والعناصر العادية عند استخدام واجهة PersonExtractor هذه.

8. تنظيم الطلبات باستخدام نماذج الطلبات

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

على وجه التحديد، لندرس TemplatePrompt.java في دليل app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;

import java.util.HashMap;
import java.util.Map;

public class TemplatePrompt {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(500)
            .temperature(1.0f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

        PromptTemplate promptTemplate = PromptTemplate.from("""
            You're a friendly chef with a lot of cooking experience.
            Create a recipe for a {{dish}} with the following ingredients: \
            {{ingredients}}, and give it a name.
            """
        );

        Map<String, Object> variables = new HashMap<>();
        variables.put("dish", "dessert");
        variables.put("ingredients", "strawberries, chocolate, and whipped cream");

        Prompt prompt = promptTemplate.apply(variables);

        Response<AiMessage> response = model.generate(prompt.toUserMessage());

        System.out.println(response.content().text());
    }
}

كالعادة، يمكنك ضبط نموذج VertexAiGeminiChatModel، مع مستوى عالٍ من الإبداع مع درجة حرارة عالية وقيم عالية أيضًا لكل من topP وtopK. بعد ذلك، يمكنك إنشاء PromptTemplate باستخدام الطريقة الثابتة from()، وذلك من خلال تمرير سلسلة الطلب، واستخدام متغيّري العنصر النائب للقوسين المزدوجين: {{dish}} و{{ingredients}}.

يمكنك إنشاء الطلب النهائي من خلال استدعاء apply() الذي يأخذ خريطة من أزواج المفتاح/القيمة التي تمثّل اسم العنصر النائب وقيمة السلسلة التي سيتم استبداله بها.

أخيرًا، يمكنك استدعاء طريقة generate() لنموذج Gemini من خلال إنشاء رسالة مستخدم من هذا الطلب، مع التعليمات prompt.toUserMessage().

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.TemplatePrompt

من المفترض أن يظهر لك ناتج تم إنشاؤه يشبه هذا الناتج:

**Strawberry Shortcake**

Ingredients:

* 1 pint strawberries, hulled and sliced
* 1/2 cup sugar
* 1/4 cup cornstarch
* 1/4 cup water
* 1 tablespoon lemon juice
* 1/2 cup heavy cream, whipped
* 1/4 cup confectioners' sugar
* 1/4 teaspoon vanilla extract
* 6 graham cracker squares, crushed

Instructions:

1. In a medium saucepan, combine the strawberries, sugar, cornstarch, 
water, and lemon juice. Bring to a boil over medium heat, stirring 
constantly. Reduce heat and simmer for 5 minutes, or until the sauce has 
thickened.
2. Remove from heat and let cool slightly.
3. In a large bowl, combine the whipped cream, confectioners' sugar, and 
vanilla extract. Beat until soft peaks form.
4. To assemble the shortcakes, place a graham cracker square on each of 
6 dessert plates. Top with a scoop of whipped cream, then a spoonful of 
strawberry sauce. Repeat layers, ending with a graham cracker square.
5. Serve immediately.

**Tips:**

* For a more elegant presentation, you can use fresh strawberries 
instead of sliced strawberries.
* If you don't have time to make your own whipped cream, you can use 
store-bought whipped cream.

يمكنك تغيير قيم dish وingredients في الخريطة وتعديل درجة الحرارة وtopK وtokP وإعادة تشغيل الرمز. سيتيح لك ذلك ملاحظة تأثير تغيير هذه المَعلمات في نموذج المعالجة المحدودة للغة.

تُعدّ نماذج الطلبات طريقة جيدة للحصول على تعليمات قابلة لإعادة الاستخدام وقابلة للضبط للمكالمات مع النماذج اللغوية الكبيرة. يمكنك تمرير البيانات وتخصيص طلبات للحصول على قيم مختلفة يقدّمها المستخدمون.

9. تصنيف النصوص باستخدام طلبات مكوّنة من بضع أمثلة

تُجيد النماذج اللغوية الكبيرة تصنيف النصوص ضمن فئات مختلفة. يمكنك مساعدة نموذج اللغة الضخم في هذه المهمة من خلال تقديم بعض الأمثلة على النصوص والفئات المرتبطة بها. يُطلق على هذا الأسلوب غالبًا اسم طلب عدد قليل من اللقطات.

لنفتح TextClassification.java في دليل app/src/main/java/gemini/workshop لإجراء نوع معيّن من تصنيف النصوص: تحليل المشاعر.

package gemini.workshop;

import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import java.util.List;

public class TextClassification {

    enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(10)
            .maxRetries(3)
            .responseSchema(Schema.newBuilder()
                .setType(Type.STRING)
                .addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
                .build())
            .build();


        interface SentimentAnalysis {
            @SystemMessage("""
                Analyze the sentiment of the text below.
                Respond only with one word to describe the sentiment.
                """)
            Sentiment analyze(String text);
        }

        MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
        memory.add(UserMessage.from("This is fantastic news!"));
        memory.add(AiMessage.from(Sentiment.POSITIVE.name()));

        memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
        memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));

        memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
        memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));

        SentimentAnalysis sentimentAnalysis =
            AiServices.builder(SentimentAnalysis.class)
                .chatLanguageModel(model)
                .chatMemory(memory)
                .build();

        System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
    }
}

يسرد Sentiment enum القيم المختلفة للعاطفة: سلبية أو محايدة أو إيجابية.

في الطريقة main()، يمكنك إنشاء نموذج محادثة Gemini كالمعتاد، ولكن مع الحد الأقصى لعدد الرموز المميّزة للإخراج، لأنّك تريد فقط الحصول على ردّ قصير: النص هو POSITIVE أو NEGATIVE أو NEUTRAL. ولحصر النموذج على عرض هذه القيم فقط، يمكنك الاستفادة من ميزة الإخراج المنظَّم التي اكتشفتها في قسم استخراج البيانات. لهذا السبب، يتم استخدام الطريقة responseSchema(). هذه المرة، لن تستخدم الطريقة المُريحة من SchemaHelper لاستنتاج تعريف المخطّط، ولكنك ستستخدم أداة إنشاء Schema بدلاً من ذلك، لفهم شكل تعريف المخطّط.

بعد ضبط النموذج، يمكنك إنشاء واجهة SentimentAnalysis ستنفذها AiServices في LangChain4j باستخدام نموذج اللغة الكبير. تحتوي هذه الواجهة على طريقة واحدة: analyze(). تأخذ هذه الدالة النص المطلوب تحليله في الإدخال، وتعرض قيمة Sentiment enum. وبالتالي، لن تتعامل إلا مع عنصر من النوع المحدد بدقة الذي يمثّل فئة المشاعر التي يتم التعرّف عليها.

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

لنربط كل شيء معًا باستخدام طريقة AiServices.builder()، من خلال تمرير واجهة SentimentAnalysis والنموذج المطلوب استخدامه وذاكرة المحادثة مع أمثلة البيانات القليلة. أخيرًا، استخدِم الطريقة analyze() مع النص المطلوب تحليله.

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification

من المفترض أن تظهر لك كلمة واحدة:

POSITIVE

يبدو أنّ حبّ الفراولة يعكس شعورًا إيجابيًا.

10. الجيل المعزّز للاسترداد

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

لهذا السبب، تساعد الأساليب، مثل الإنشاء المعزّز لاسترداد المعلومات (RAG) التي سيتم تناولها في هذا القسم، في تقديم المعلومات الإضافية التي قد يحتاج إليها النموذج اللغوي الكبير لتلبية طلبات المستخدمين، وذلك من خلال الردّ بمعلومات قد تكون أكثر حداثة أو بمعلومات خاصة لا يمكن الوصول إليها في وقت التدريب.

لنعود إلى المحادثات. هذه المرة، ستتمكّن من طرح أسئلة حول مستنداتك. ستنشئ برنامجًا محادثيًا قادرًا على استرداد المعلومات ذات الصلة من قاعدة بيانات تحتوي على مستنداتك مقسّمة إلى أجزاء أصغر ("أجزاء")، وسيستخدم النموذج هذه المعلومات لصياغة إجاباته، بدلاً من الاعتماد فقط على المعرفة الواردة في عملية التدريب.

في مقياس RAG، هناك مرحلتان:

  1. مرحلة نقل البيانات: يتم تحميل المستندات في الذاكرة وتقسيمها إلى أجزاء أصغر، ويتم احتساب عمليات إدراج المتجهات (تمثيل متّجه عالي الأبعاد للمقاطع) وتخزينها في قاعدة بيانات متّجهات قادرة على إجراء عمليات بحث دلالية. تتم عادةً مرحلة نقل البيانات هذه مرة واحدة، عندما تكون هناك حاجة إلى إضافة مستندات جديدة إلى مجموعة النصوص.

cd07d33d20ffa1c8.png

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

a1d2e2deb83c6d27.png

تجهيز المستندات

في هذا المثال الجديد، ستطرح أسئلة عن طراز سيارة خيالي من شركة خيالية أيضًا: سيارة Cymbal Starlight. والفكرة هي أنّه يجب ألّا يكون المستند الذي يتناول سيارة خيالية جزءًا من المعلومات المتعلّقة بالنموذج. إذا كان بإمكان Gemini الإجابة عن الأسئلة بشكل صحيح عن هذه السيارة، يعني ذلك أنّ نهج RAG ناجح: يمكنه البحث في مستندك.

تنفيذ برنامج المحادثة

لنطّلِع على كيفية إنشاء منهج المرحلتَين: أولاً من خلال نقل المستندات، ثمّ وقت طلب البحث (المعروف أيضًا باسم "مرحلة الاسترجاع") عندما يطرح المستخدمون أسئلة عن المستند.

في هذا المثال، يتم تنفيذ كلتا المرحلتَين في الفئة نفسها. عادةً ما يكون لديك تطبيق واحد يتولّى نقل البيانات وتطبيق آخر يقدّم واجهة المحادثة الآلية للمستخدمين.

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

نقل المستندات

الخطوة الأولى في مرحلة نقل المستندات هي تحديد موقع ملف PDF الخاص بالسيارة الخيالية وإعداد PdfParser لقراءته:

URL url = new URI("https://raw.githubusercontent.com/meteatamel/genai-beyond-basics/main/samples/grounding/vertexai-search/cymbal-starlight-2024.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());

بدلاً من إنشاء نموذج لغة المحادثة المعتاد أولاً، يمكنك إنشاء مثيل لنموذج التضمين. هذا نموذج معيّن دوره هو إنشاء تمثيلات متّجهة لقطع النص (الكلمات أو الجمل أو حتى الفقرات). ويعرِض المتجهات الخاصة بأرقام الكسور العشرية بدلاً من عرض الردود النصية.

VertexAiEmbeddingModel embeddingModel = VertexAiEmbeddingModel.builder()
    .endpoint(System.getenv("LOCATION") + "-aiplatform.googleapis.com:443")
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .publisher("google")
    .modelName("text-embedding-005")
    .maxRetries(3)
    .build();

بعد ذلك، ستحتاج إلى بعض الصفوف للتعاون معًا من أجل:

  • تحميل ملف PDF وتقسيمه إلى أجزاء
  • أنشئ embeddings متّجهًا لكل هذه الأجزاء.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
    .documentSplitter(DocumentSplitters.recursive(500, 100))
    .embeddingModel(embeddingModel)
    .embeddingStore(embeddingStore)
    .build();
storeIngestor.ingest(document);

يتم إنشاء مثيل من InMemoryEmbeddingStore، وهي قاعدة بيانات للمتجهات في الذاكرة، لتخزين عمليات تضمين المتجهات.

يتم تقسيم المستند إلى أجزاء بفضل فئة DocumentSplitters. سيتم تقسيم نص ملف PDF إلى مقتطفات من 500 حرف، مع تداخل 100 حرف (مع الجزء التالي، لتجنُّب قطع الكلمات أو الجمل إلى أجزاء صغيرة).

يربط أداة نقل البيانات إلى المتجر أداة تقسيم المستندات ونموذج التضمين لاحتساب المتجهات وقاعدة بيانات المتجهات في الذاكرة. بعد ذلك، ستتولى طريقة ingest() عملية نقل البيانات.

الآن، انتهت المرحلة الأولى، وتم تحويل المستند إلى أجزاء نصية مع إدراجات المتجهات المرتبطة بها، وتم تخزينها في قاعدة بيانات المتجهات.

طرح الأسئلة

حان وقت الاستعداد لطرح الأسئلة. أنشئ نموذج محادثة لبدء المحادثة:

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-1.5-flash-002")
        .maxOutputTokens(1000)
        .build();

ستحتاج أيضًا إلى فئة مسترجع لربط قاعدة بيانات المتجهات (في المتغيّر embeddingStore) بنموذج التضمين. وتتمثل مهمتها في طلب البحث من قاعدة بيانات المتجهات من خلال احتساب عملية إدراج متجه لطلب بحث المستخدم، للعثور على متجهات مشابهة في قاعدة البيانات:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

أنشئ واجهة تمثّل مساعدًا خبيرًا في السيارات، وهي واجهة ستنفّذها فئة AiServices للتفاعل مع النموذج:

interface CarExpert {
    Result<String> ask(String question);
}

تعرض واجهة CarExpert استجابة سلسلة ملفوفة في فئة Result من LangChain4j. لماذا يجب استخدام هذا الغلاف؟ لأنّه لن يمنحك الإجابة فحسب، بل سيتيح لك أيضًا فحص الأجزاء من قاعدة البيانات التي أرجعها مسترجع المحتوى. بهذه الطريقة، يمكنك عرض مصادر المستندات المستخدَمة لتأسيس الإجابة النهائية للمستخدم.

في هذه المرحلة، يمكنك ضبط خدمة جديدة للذكاء الاصطناعي:

CarExpert expert = AiServices.builder(CarExpert.class)
    .chatLanguageModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .contentRetriever(retriever)
    .build();

تربط هذه الخدمة ما يلي معًا:

  • نموذج لغة المحادثة الذي أعددته سابقًا.
  • ذاكرة المحادثة لتتبُّع المحادثة
  • يقارن retriever طلب إدراج متجه بالمتجهات في قاعدة البيانات.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in car automotive, and you answer concisely.

            Here is the question: {{userMessage}}

            Answer using the following information:
            {{contents}}
the following information:
            {{contents}}
            """))
        .build())
    .contentRetriever(retriever)
    .build())

لقد حان الوقت لطرح أسئلتك.

List.of(
    "What is the cargo capacity of Cymbal Starlight?",
    "What's the emergency roadside assistance phone number?",
    "Are there some special kits available on that car?"
).forEach(query -> {
    Result<String> response = expert.ask(query);
    System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
    System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});

يمكن العثور على رمز المصدر الكامل في RAG.java في الدليل app/src/main/java/gemini/workshop.

شغِّل العيّنة:

./gradlew -q run -DjavaMainClass=gemini.workshop.RAG

من المفترض أن تظهر لك في الإخراج إجابات عن أسئلتك:

=== What is the cargo capacity of Cymbal Starlight? === 

 The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet.
 

SOURCE: Cargo
The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of
the vehicle.
To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell.
When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this
could affect the vehicle's handling and stability.
Luggage

=== What's the emergency roadside assistance phone number? === 

The emergency roadside assistance phone number is 1-800-555-1212.
 

SOURCE: Chapter 18: Emergencies
Roadside Assistance
If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside
assistance for help. Roadside assistance is available 24 hours a day, 7 days a week.
To call roadside assistance, dial the following number:
1-800-555-1212
When you call roadside assistance, be prepared to provide the following information:
Your name and contact information
Your vehicle's make, model, and year
Your vehicle's location

=== Are there some special kits available on that car? === 

 Yes, the Cymbal Starlight comes with a tire repair kit.
 

SOURCE: Lane keeping assist:  This feature helps to keep you in your lane by gently steering the vehicle back
into the lane if you start to drift.
Adaptive cruise control:  This feature automatically adjusts your speed to maintain a safe following
distance from the vehicle in front of you.
Forward collision warning:  This feature warns you if you are approaching another vehicle too
quickly.
Automatic emergency braking:  This feature can automatically apply the brakes to avoid a collision.

11. استدعاء الدوالّ

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

واجهات برمجة التطبيقات البعيدة للويب:

  • تتبُّع طلبات العملاء وتعديلها
  • ابحث عن طلب دعم أو أنشئ طلبًا في أداة تتبُّع المشاكل.
  • جلب البيانات في الوقت الفعلي، مثل أسعار الأسهم أو قياسات أجهزة استشعار إنترنت الأشياء
  • إرسال رسالة إلكترونية

أدوات الحساب:

  • آلة حاسبة لحلّ المسائل الحسابية الأكثر تقدمًا
  • تفسير الرموز البرمجية لتشغيل الرموز البرمجية عندما تحتاج النماذج اللغوية الكبيرة إلى منطق الاستدلال
  • تحويل طلبات اللغة الطبيعية إلى طلبات بحث SQL ليتمكّن نموذج اللغة المحوسبة من إجراء طلب بحث في قاعدة بيانات

تعني "طلبات تنفيذ الدوالّ" (التي تُعرف أحيانًا باسم "الأدوات" أو "استخدام الأدوات") قدرة النموذج على طلب إجراء طلب تنفيذ دالة واحدة أو أكثر نيابةً عنه، حتى يتمكّن من الإجابة بشكل صحيح عن طلب المستخدم باستخدام بيانات أحدث.

استنادًا إلى طلب معيّن من المستخدِم ومعرفة الدوالّ الحالية التي يمكن أن تكون ذات صلة بهذا السياق، يمكن للنموذج اللغوي الكبير الردّ بطلب استدعاء دالة. يمكن للتطبيق الذي يُدمج النموذج اللغوي الكبير (LLM) بعد ذلك استدعاء الدالة نيابةً عنه، ثم الردّ على النموذج اللغوي الكبير (LLM) بتقديم ردّ، ثم يُعيد النموذج اللغوي الكبير (LLM) التفسير من خلال الردّ بإجابة نصية.

الخطوات الأربع لاستدعاء الدالة

لنلقِ نظرة على مثال على استدعاء الدالة: الحصول على معلومات عن توقّعات الطقس.

إذا سألت Gemini أو أي نموذج لغوي كبير آخر عن حالة الطقس في باريس، سيجيب بأنّه لا يملك معلومات عن توقّعات الطقس الحالية. إذا كنت تريد أن يتمكّن "النموذج اللغوي الكبير" من الوصول إلى بيانات الطقس في الوقت الفعلي، عليك تحديد بعض الدوال التي يمكنه طلب استخدامها.

اطّلِع على الرسم البياني التالي:

31e0c2aba5e6f21c.png

1️⃣ أولاً، يسأل أحد المستخدمين عن حالة الطقس في باريس. يعلم تطبيق محادثة البودكاست (الذي يستخدم LangChain4j) أنّ هناك دالة واحدة أو أكثر متاحة له لمساعدة نموذج اللغة الكبير على تنفيذ الطلب. يرسل chatbot الطلب الأوّلي، بالإضافة إلى قائمة الدوال التي يمكن استدعاؤها. في ما يلي دالة باسم getWeather() تأخذ مَعلمة سلسلة للموضع الجغرافي.

8863be53a73c4a70.png

بما أنّ نموذج اللغة الكبيرة لا يعرف توقّعات الطقس، بدلاً من الردّ عبر النص، يُرسِل طلب تنفيذ الدالة. يجب أن يستدعي تطبيق الدردشة الذكية دالة getWeather() مع "Paris" كمعلّمة الموقع الجغرافي.

d1367cc69c07b14d.png

2️⃣ يستدعي تطبيق الدردشة هذه الدالة نيابةً عن النموذج اللغوي الكبير، ويسترجع ردّ الدالة. نفترض هنا أنّ الردّ هو {"forecast": "sunny"}.

73a5f2ed19f47d8.png

3️⃣ يُرسِل تطبيق المحادثة الآلية استجابة JSON مرة أخرى إلى نموذج اللغة الكبيرة.

20832cb1ee6fbfeb.png

4️⃣ يفحص النموذج اللغوي الكبير استجابة JSON ويفسّر هذه المعلومات، ثم يردّ في النهاية بالنص الذي يفيد بأنّ الطقس مشمس في باريس.

كل خطوة على شكل رمز برمجي

أولاً، عليك ضبط نموذج Gemini كالمعتاد:

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .modelName("gemini-1.5-flash-002")
    .maxOutputTokens(100)
    .build();

يمكنك تحديد مواصفات الأداة التي تصف الدالة التي يمكن استدعاؤها:

ToolSpecification weatherToolSpec = ToolSpecification.builder()
    .name("getWeather")
    .description("Get the weather forecast for a given location or city")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty(
            "location", 
            "the location or city to get the weather forecast for")
        .build())
    .build();

تم تحديد اسم الدالة، بالإضافة إلى اسم المَعلمة ونوعها، ولكن يُرجى ملاحظة أنّه تم تقديم أوصاف لكل من الدالة والمَعلمات. إنّ الأوصاف مهمة جدًا وتساعد النموذج اللغوي الكبير على فهم ما يمكن أن تفعله الوظيفة، وبالتالي الحكم على ما إذا كان يجب استدعاء هذه الوظيفة في سياق المحادثة.

لنبدأ الخطوة الأولى بإرسال السؤال الأول عن حالة الطقس في باريس:

List<ChatMessage> allMessages = new ArrayList<>();

// 1) Ask the question about the weather
UserMessage weatherQuestion = UserMessage.from("What is the weather in Paris?");
allMessages.add(weatherQuestion);

في الخطوة 2، نرسل الأداة التي نريد أن يستخدمها النموذج، ويردّ النموذج بطلب تنفيذ الأداة:

// 2) The model replies with a function call request
Response<AiMessage> messageResponse = model.generate(allMessages, weatherToolSpec);
ToolExecutionRequest toolExecutionRequest = messageResponse.content().toolExecutionRequests().getFirst();
System.out.println("Tool execution request: " + toolExecutionRequest);
allMessages.add(messageResponse.content());

الخطوة 3. في هذه المرحلة، نعرف الدالة التي يريد منّا نموذج اللغة القليلة الكلمات استدعاؤها. في الرمز البرمجي، لا نُجري طلبًا حقيقيًا لواجهة برمجة تطبيقات خارجية، بل نعرض توقّعات جوية افتراضية مباشرةً:

// 3) We send back the result of the function call
ToolExecutionResultMessage toolExecResMsg = ToolExecutionResultMessage.from(toolExecutionRequest,
    "{\"location\":\"Paris\",\"forecast\":\"sunny\", \"temperature\": 20}");
allMessages.add(toolExecResMsg);

وفي الخطوة 4، يتعرّف النموذج اللغوي الكبير (LLM) على نتيجة تنفيذ الدالة، ويمكنه بعد ذلك تجميع ردّ نصي:

// 4) The model answers with a sentence describing the weather
Response<AiMessage> weatherResponse = model.generate(allMessages);
System.out.println("Answer: " + weatherResponse.content().text());

الناتج هو:

Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer:  The weather in Paris is sunny with a temperature of 20 degrees Celsius.

يمكنك الاطّلاع في الإخراج أعلاه على طلب تنفيذ الأداة، بالإضافة إلى الإجابة.

يمكن العثور على رمز المصدر الكامل في FunctionCalling.java في الدليل app/src/main/java/gemini/workshop:

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCalling

من المفترض أن يظهر لك ناتج مماثل لما يلي:

Tool execution request: ToolExecutionRequest { id = null, name = "getWeatherForecast", arguments = "{"location":"Paris"}" }
Answer:  The weather in Paris is sunny with a temperature of 20 degrees Celsius.

12. يعالج LangChain4j استدعاء الدوالّ.

في الخطوة السابقة، اطّلعت على كيفية تداخل تفاعلات السؤال/الإجابة النصية العادية وطلبات/ردود الدوالّ، وفي ما بينهما، قدّمت ردّ الدالة المطلوب مباشرةً بدون استدعاء دالة حقيقية.

ومع ذلك، يوفّر LangChain4j أيضًا مستوى أعلى من التجريد يمكنه التعامل مع طلبات الدالة بشكل شفاف نيابةً عنك، مع التعامل مع المحادثة كالمعتاد.

طلب دالة واحد

لنلقِ نظرة على FunctionCallingAssistant.java، خطوة بخطوة.

أولاً، عليك إنشاء سجلّ يمثّل بنية بيانات استجابة الدالة:

record WeatherForecast(String location, String forecast, int temperature) {}

يحتوي الردّ على معلومات عن الموقع الجغرافي والتوقّعات الجوية ودرجة الحرارة.

بعد ذلك، يمكنك إنشاء فئة تحتوي على الدالة الفعلية التي تريد إتاحتها للنموذج:

static class WeatherForecastService {
    @Tool("Get the weather forecast for a location")
    WeatherForecast getForecast(@P("Location to get the forecast for") String location) {
        if (location.equals("Paris")) {
            return new WeatherForecast("Paris", "Sunny", 20);
        } else if (location.equals("London")) {
            return new WeatherForecast("London", "Rainy", 15);
        } else {
            return new WeatherForecast("Unknown", "Unknown", 0);
        }
    }
}

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

تم أيضًا وضع تعليق توضيحي على مَعلمات الدالة (مَعلمة واحدة هنا)، ولكن باستخدام تعليق توضيحي @P قصير يقدّم أيضًا وصفًا للمَعلمة. يمكنك إضافة أي عدد تريده من الدوالّ لإتاحتها للنموذج، وذلك لسيناريوهات أكثر تعقيدًا.

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

كما رأينا عند إنشاء ToolSpecification في المنهج السابق، من المهم توثيق ما تفعله الدالة ووصف ما تتوافق معه المَعلمات. يساعد ذلك النموذج في فهم كيفية استخدام هذه الدالة وحالات استخدامها.

بعد ذلك، يتيح لك LangChain4j توفير واجهة تتوافق مع العقد الذي تريد استخدامه للتفاعل مع النموذج. في ما يلي واجهة بسيطة تتلقّى سلسلة تمثّل رسالة المستخدم، وتُعيد سلسلة تتوافق مع ردّ النموذج:

interface WeatherAssistant {
    String chat(String userMessage);
}

من الممكن أيضًا استخدام توقيعات أكثر تعقيدًا تتضمّن UserMessage (لرسالة مستخدم) أو AiMessage (لردّ نموذج) في LangChain4j، أو حتى TokenStream، إذا كنت تريد التعامل مع مواقف أكثر تقدمًا، لأنّ هذه العناصر الأكثر تعقيدًا تحتوي أيضًا على معلومات إضافية، مثل عدد الرموز المستهلكة وما إلى ذلك. ولكن من أجل البساطة، سنأخذ سلسلة في الإدخال وسلسلة في الإخراج.

لننهي هذه المقالة بطريقة main() التي تربط كل القطع معًا:

public static void main(String[] args) {
    ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-1.5-pro-002")
        .build();

    WeatherForecastService weatherForecastService = new WeatherForecastService();

    WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
        .chatLanguageModel(model)
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .tools(weatherForecastService)
        .build();

    System.out.println(assistant.chat("What is the weather in Paris?"));
}

وكما هو معتاد، يمكنك ضبط نموذج Gemini Chat. بعد ذلك، يمكنك إنشاء مثيل لخدمة توقّعات الطقس التي تحتوي على "الدالة" التي سيطلب منّا النموذج استدعاؤها.

الآن، يمكنك استخدام فئة AiServices مرة أخرى لربط نموذج المحادثة وذاكرة المحادثة والأداة (أي خدمة توقّعات الطقس مع وظيفتها). تُعرِض AiServices عنصرًا ينفِّذ واجهة WeatherAssistant التي حدّدتها. والخطوة المتبقية الوحيدة هي استدعاء طريقة chat() لهذا المساعد. عند استدعائه، ستظهر لك الردود النصية فقط، ولكن لن تظهر طلبات استدعاء الدوالّ والردود على استدعاء الدوالّ للمطوّر، وسيتم التعامل مع هذه الطلبات تلقائيًا وشفافًا. إذا اعتقد Gemini أنّه يجب استدعاء دالة، سيردّ بطلب استدعاء الدالة، وسيتولى LangChain4j استدعاء الدالة المحلية نيابةً عنك.

شغِّل العيّنة:

./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant

من المفترض أن يظهر لك ناتج مماثل لما يلي:

OK. The weather in Paris is sunny with a temperature of 20 degrees.

كان هذا مثالاً على دالة واحدة.

طلبات متعددة للدوالّ

يمكنك أيضًا استخدام عدة دوال والسماح لواجهة LangChain4j بمعالجة طلبات الدوال المتعدّدة نيابةً عنك. اطّلِع على MultiFunctionCallingAssistant.java للاطّلاع على مثال لدوالّ متعددة.

تتضمّن هذه الدالة وظيفة لتحويل العملات:

@Tool("Convert amounts between two currencies")
double convertCurrency(
    @P("Currency to convert from") String fromCurrency,
    @P("Currency to convert to") String toCurrency,
    @P("Amount to convert") double amount) {

    double result = amount;

    if (fromCurrency.equals("USD") && toCurrency.equals("EUR")) {
        result = amount * 0.93;
    } else if (fromCurrency.equals("USD") && toCurrency.equals("GBP")) {
        result = amount * 0.79;
    }

    System.out.println(
        "convertCurrency(fromCurrency = " + fromCurrency +
            ", toCurrency = " + toCurrency +
            ", amount = " + amount + ") == " + result);

    return result;
}

دالة أخرى للحصول على قيمة سهم:

@Tool("Get the current value of a stock in US dollars")
double getStockPrice(@P("Stock symbol") String symbol) {
    double result = 170.0 + 10 * new Random().nextDouble();

    System.out.println("getStockPrice(symbol = " + symbol + ") == " + result);

    return result;
}

دالة أخرى لتطبيق نسبة مئوية على مبلغ معيّن:

@Tool("Apply a percentage to a given amount")
double applyPercentage(@P("Initial amount") double amount, @P("Percentage between 0-100 to apply") double percentage) {
    double result = amount * (percentage / 100);

    System.out.println("applyPercentage(amount = " + amount + ", percentage = " + percentage + ") == " + result);

    return result;
}

يمكنك بعد ذلك دمج كل هذه الدوالّ ومجموعة MultiTools وطرح أسئلة مثل "ما هي نسبة% 10 من سعر سهم AAPL التي تم تحويلها من دولار أمريكي إلى يورو؟"

public static void main(String[] args) {
    ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-1.5-flash-002")
        .maxOutputTokens(100)
        .build();

    MultiTools multiTools = new MultiTools();

    MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
        .chatLanguageModel(model)
        .chatMemory(withMaxMessages(10))
        .tools(multiTools)
        .build();

    System.out.println(assistant.chat(
        "What is 10% of the AAPL stock price converted from USD to EUR?"));
}

يمكنك تنفيذه على النحو التالي:

./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant

من المفترض أن تظهر لك الدوالّ المتعددة التي تمّت تسميتها:

getStockPrice(symbol = AAPL) == 172.8022224055534
convertCurrency(fromCurrency = USD, toCurrency = EUR, amount = 172.8022224055534) == 160.70606683716468
applyPercentage(amount = 160.70606683716468, percentage = 10.0) == 16.07060668371647
10% of the AAPL stock price converted from USD to EUR is 16.07060668371647 EUR.

التوجيه نحو موظّفي الدعم

يُعدّ استدعاء الدوالّ آلية رائعة لتوسيع النماذج اللغوية الكبيرة، مثل Gemini. ويتيح لنا ذلك إنشاء أنظمة أكثر تعقيدًا يُطلق عليها غالبًا اسم "موظّفو الدعم" أو "مساعِدون من الذكاء الاصطناعي". يمكن لموظّفي الدعم هؤلاء التفاعل مع العالم الخارجي من خلال واجهات برمجة التطبيقات الخارجية والخدمات التي يمكن أن يكون لها تأثيرات جانبية على البيئة الخارجية (مثل إرسال الرسائل الإلكترونية وإنشاء طلبات الدعم وما إلى ذلك).

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

13. تشغيل Gemma باستخدام Ollama وTestContainers

حتى الآن، كنا نستخدم نموذج Gemini، ولكن هناك أيضًا Gemma، وهو نموذج أصغر.

Gemma هي مجموعة من أحدث النماذج المتطوّرة والخفيفة المتاحة للجميع، والتي تم إنشاؤها بناءً على الأبحاث والتكنولوجيا نفسها المستخدَمة في إنشاء نماذج Gemini. تتوفّر Gemma بطريقتَين، Gemma1 وGemma2، وكلتاهما بمقاسات مختلفة. تتوفّر Gemma1 بحجمَين: 2B و7B. تتوفر Gemma2 بحجمَين: 9B و27B. وتتوفّر هذه الأوزان مجانًا، كما أنّ أحجامها الصغيرة تعني أنّه يمكنك تشغيلها بنفسك، حتى على الكمبيوتر المحمول أو في Cloud Shell.

كيف يتم تشغيل Gemma؟

هناك العديد من الطرق لتشغيل Gemma: في السحابة الإلكترونية، أو من خلال Vertex AI بنقرة زر، أو GKE مع بعض وحدات معالجة الرسومات، ولكن يمكنك أيضًا تشغيلها على الجهاز.

من الخيارات الجيدة لتشغيل Gemma على الجهاز المحلي استخدام Ollama، وهي أداة تتيح لك تشغيل نماذج صغيرة، مثل Llama 2 وMistral وغيرها الكثير على جهازك المحلي. وهو مشابه لخدمة Docker ولكن مخصّص للنماذج اللغوية الكبيرة.

ثبِّت Ollama باتّباع التعليمات الخاصة بنظام التشغيل.

إذا كنت تستخدم بيئة Linux، عليك تفعيل Ollama أولاً بعد تثبيته.

ollama serve > /dev/null 2>&1 & 

بعد التثبيت على الجهاز، يمكنك تنفيذ أوامر لسحب نموذج:

ollama pull gemma:2b

انتظِر حتى يتم سحب النموذج. يمكن أن تستغرق هذه العملية بعض الوقت.

شغِّل النموذج:

ollama run gemma:2b

يمكنك الآن التفاعل مع النموذج:

>>> Hello!
Hello! It's nice to hear from you. What can I do for you today?

للخروج من الطلب، اضغط على Ctrl+D.

تشغيل Gemma في Ollama على TestContainers

بدلاً من الحاجة إلى تثبيت Ollama وتشغيله محليًا، يمكنك استخدام Ollama داخل حاوية تتعامل معها TestContainers.

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

في ما يلي الصورة الكاملة:

2382c05a48708dfd.png

التنفيذ

لنلقِ نظرة على GemmaWithOllamaContainer.java، خطوة بخطوة.

أولاً، عليك إنشاء حاوية Ollama مشتقة تسحب نموذج Gemma. هذه الصورة متوفّرة من عملية سابقة أو سيتم إنشاؤها. إذا كانت الصورة متوفّرة، ما عليك سوى إخبار TestContainers بأنّك تريد استبدال صورة Ollama التلقائية بنسخة Gemma:

private static final String TC_OLLAMA_GEMMA_2_B = "tc-ollama-gemma-2b";

// Creating an Ollama container with Gemma 2B if it doesn't exist.
private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {

    // Check if the custom Gemma Ollama image exists already
    List<Image> listImagesCmd = DockerClientFactory.lazyClient()
        .listImagesCmd()
        .withImageNameFilter(TC_OLLAMA_GEMMA_2_B)
        .exec();

    if (listImagesCmd.isEmpty()) {
        System.out.println("Creating a new Ollama container with Gemma 2B image...");
        OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.1.26");
        ollama.start();
        ollama.execInContainer("ollama", "pull", "gemma:2b");
        ollama.commitToImage(TC_OLLAMA_GEMMA_2_B);
        return ollama;
    } else {
        System.out.println("Using existing Ollama container with Gemma 2B image...");
        // Substitute the default Ollama image with our Gemma variant
        return new OllamaContainer(
            DockerImageName.parse(TC_OLLAMA_GEMMA_2_B)
                .asCompatibleSubstituteFor("ollama/ollama"));
    }
}

بعد ذلك، يمكنك إنشاء حاوية اختبار Ollama وبدءها، ثم إنشاء نموذج محادثة Ollama، وذلك من خلال الإشارة إلى عنوان الحاوية ومنفذها باستخدام النموذج الذي تريد استخدامه. أخيرًا، ما عليك سوى استدعاء model.generate(yourPrompt) كالمعتاد:

public static void main(String[] args) throws IOException, InterruptedException {
    OllamaContainer ollama = createGemmaOllamaContainer();
    ollama.start();

    ChatLanguageModel model = OllamaChatModel.builder()
        .baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
        .modelName("gemma:2b")
        .build();

    String response = model.generate("Why is the sky blue?");

    System.out.println(response);
}

يمكنك تنفيذه على النحو التالي:

./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer

ستستغرق عملية التشغيل الأولى بعض الوقت لإنشاء الحاوية وتشغيلها، ولكن بعد الانتهاء، من المفترض أن تظهر لك ردّة فعل من "جيما":

INFO: Container ollama/ollama:0.1.26 started in PT2.827064047S
The sky appears blue due to Rayleigh scattering. Rayleigh scattering is a phenomenon that occurs when sunlight interacts with molecules in the Earth's atmosphere.

* **Scattering particles:** The main scattering particles in the atmosphere are molecules of nitrogen (N2) and oxygen (O2).
* **Wavelength of light:** Blue light has a shorter wavelength than other colors of light, such as red and yellow.
* **Scattering process:** When blue light interacts with these molecules, it is scattered in all directions.
* **Human eyes:** Our eyes are more sensitive to blue light than other colors, so we perceive the sky as blue.

This scattering process results in a blue appearance for the sky, even though the sun is actually emitting light of all colors.

In addition to Rayleigh scattering, other atmospheric factors can also influence the color of the sky, such as dust particles, aerosols, and clouds.

تم تشغيل Gemma في Cloud Shell.

14. تهانينا

مبروك، لقد نجحت في إنشاء أوّل تطبيق محادثة بالذكاء الاصطناعي التوليدي في Java باستخدام LangChain4j وGemini API. لقد اكتشفت على طول الطريق أنّ النماذج اللغوية الكبيرة متعددة الوسائط فعّالة جدًا وقادرة على معالجة مهام مختلفة، مثل طرح الأسئلة والإجابة عنها، حتى في مستنداتك الخاصة، واستخراج البيانات، والتفاعل مع واجهات برمجة التطبيقات الخارجية، وغير ذلك.

الخطوة التالية

حان دورك لتحسين تطبيقاتك من خلال عمليات دمج فعّالة للنماذج اللغوية الكبيرة.

مراجع إضافية

المستندات المرجعية