Gemini ב-Java עם Vertex AI ו-LangChain4j

1. מבוא

ה-Codelab הזה מתמקד במודל השפה הגדול (LLM) של Gemini, שמתארח ב-Vertex AI ב-Google Cloud. Vertex AI היא פלטפורמה שכוללת את כל המוצרים, השירותים והמודלים של למידת מכונה ב-Google Cloud.

אתם תשתמשו ב-Java כדי לקיים אינטראקציה עם Gemini API באמצעות ה-framework של LangChain4j. תקבלו דוגמאות קונקרטיות לניצול היתרונות של ה-LLM למענה על שאלות, ליצירת רעיונות, לחילוץ ישויות ותוכן מובנה, לאחזור יצירה משופרת ולשליחת פונקציות.

מהי בינה מלאכותית גנרטיבית?

בינה מלאכותית גנרטיבית היא שימוש בבינה מלאכותית (AI) ליצירת תוכן חדש, כמו טקסטים, תמונות, מוזיקה, אודיו וסרטונים.

בינה מלאכותית גנרטיבית מתבססת על מודלים גדולים של שפה (LLMs) שיכולים לבצע ריבוי משימות ולבצע משימות שונות כמו סיכום, שאלות ותשובות, סיווג ועוד. על ידי אימון מינימלי, ניתן להתאים מודלים בסיסיים לתרחישים לדוגמה ממוקדים, עם מעט מאוד נתונים לדוגמה.

איך עובדת בינה מלאכותית גנרטיבית?

בינה מלאכותית גנרטיבית משתמשת במודל למידת מכונה (ML) כדי ללמוד את הדפוסים ואת הקשרים במערך נתונים של תוכן שנוצר על ידי בני אדם. לאחר מכן היא משתמשת בדפוסים שנלמדו כדי ליצור תוכן חדש.

הדרך הנפוצה ביותר לאמן מודל של בינה מלאכותית גנרטיבית היא להשתמש בלמידה מונחית. נותנים למודל קבוצה של תוכן שנוצר על ידי בני אדם ותוויות תואמות. לאחר מכן, היא לומדת ליצור תוכן שדומה לתוכן שנוצר על ידי בני אדם.

מהן אפליקציות נפוצות של בינה מלאכותית גנרטיבית?

אפשר להשתמש ב-AI גנרטיבי כדי:

  • משפרים את האינטראקציה עם הלקוחות בעזרת חוויות משופרות בצ'אט ובחיפוש.
  • לחקור כמויות עצומות של נתונים לא מובנים באמצעות ממשקים שיחה וסיכומים.
  • סיוע במשימות שחוזרות על עצמן, כמו מענה לבקשות להצעות, לוקליזציה של תוכן שיווקי בשפות שונות, בדיקת חוזים של לקוחות ועוד.

אילו פתרונות של בינה מלאכותית גנרטיבית יש ל-Google Cloud?

בעזרת Vertex AI אתם יכולים לבצע פעולות במודלים בסיסיים, להתאים אותם אישית ולהטמיע אותם באפליקציות שלכם ללא צורך במומחיות בלמידת מכונה או בכלל. אתם יכולים לגשת למודלים בסיסיים ב-Model Garden, לכוונן מודלים דרך ממשק משתמש פשוט ב-Vertex AI Studio או להשתמש במודלים ב-notebook של מדעי נתונים.

חיפוש ושיחות על בסיס Vertex AI הם הדרך המהירה ביותר לפתח מנועי חיפוש וצ'אט בוטים שמבוססים על בינה מלאכותית גנרטיבית.

Gemini ל-Google Cloud, שמבוסס על Gemini, הוא שותף עריכה מבוסס-AI שזמין ב-Google Cloud ובסביבות פיתוח משולבות (IDE) ועוזר לכם להספיק יותר, מהר יותר. Gemini Code Assist מספק השלמת קוד, יצירת קוד, הסברים על הקוד ומאפשר לכם להתכתב בצ'אט כדי לשאול שאלות טכניות.

מה זה Gemini?

Gemini היא קבוצת מודלים של בינה מלאכותית גנרטיבית שפותחה על ידי Google DeepMind, ומיועדת לתרחישים לדוגמה בכמה שיטות. המשמעות של 'ריבוי מצבים' מאפשרת לעבד וליצור סוגים שונים של תוכן, כמו טקסט, קוד, תמונות ואודיו.

b9913d011999e7c7.png

Gemini זמין בכמה וריאציות וגדלים:

  • Gemini Ultra: הגרסה הגדולה והמשופרת של משימות מורכבות.
  • Gemini Flash: הכי מהיר וחסכוני, מותאם למשימות בנפח גבוה.
  • Gemini Pro: גודל בינוני, מותאם להתאמה לעומס (scaling) במשימות שונות.
  • Gemini Nano: המכשיר היעיל ביותר למשימות במכשיר.

תכונות עיקריות:

  • ריבוי מצבים: היכולת של Gemini להבין ולטפל בכמה פורמטים של מידע היא שלב משמעותי מעבר למודלים המסורתיים של טקסט בלבד.
  • ביצועים: הביצועים של Gemini Ultra טובים יותר ביחס למתקדמים שקיימים היום בבנצ'מרקים רבים, והמודל היה המודל הראשון שעבר את הבדיקה של מומחים אנושיים בנקודת ההשוואה מאתגרת של MMLU (Massive Multitask Language Understanding).
  • גמישות: הגדלים השונים של Gemini מאפשרים להתאים אותו לתרחישים לדוגמה שונים, ממחקר בקנה מידה גדול ועד לפריסה במכשירים ניידים.

איך אפשר ליצור אינטראקציה עם Gemini ב-Vertex AI מ-Java?

עומדות לרשותך שתי אפשרויות:

  1. הספרייה הרשמית של Vertex AI Java API ל-Gemini.
  2. מסגרת LangChain4j.

ב-codelab הזה משתמשים ב-framework של LangChain4j.

מהי מסגרת LangChain4j?

ה-framework של LangChain4j הוא ספריית קוד פתוח לשילוב של מודלים גדולים של שפה באפליקציות Java שלכם, על ידי תזמור רכיבים שונים, כמו ה-LLM עצמו, אלא גם כלים אחרים כמו מסדי נתונים וקטוריים (לחיפושים סמנטיים), מטענים ומפצלי מסמכים (כדי לנתח מסמכים וללמוד מהם), לנתחי פלט ועוד.

הפרויקט נוצר בהשראת פרויקט LangChain של Python, אבל המטרה הייתה לשרת מפתחי Java.

bb908ea1e6c96ac2.png

מה תלמדו

  • איך להגדיר פרויקט Java כדי להשתמש ב-Gemini וב-LangChain4j
  • איך שולחים את ההנחיה הראשונה ל-Gemini באופן פרוגרמטי
  • איך לשדר תשובות מ-Gemini בסטרימינג
  • איך יוצרים שיחה בין משתמש ל-Gemini
  • איך משתמשים ב-Gemini בהקשר רב-אופני על ידי שליחה של טקסט וגם תמונות
  • איך לחלץ מידע מובנה שימושי מתוכן לא מובנה
  • איך משנים תבניות של הנחיות
  • איך לבצע סיווג טקסט כמו ניתוח סנטימנטים
  • איך לשוחח בצ'אט עם המסמכים שלכם (אחזור תכונות מתקדמות
  • איך מרחיבים את הצ'אט בוטים באמצעות הפעלת פונקציות
  • איך משתמשים ב-Gemma באופן מקומי עם Ollama ו-TestContainers

מה צריך להכין

  • ידע בשפת התכנות Java
  • פרויקט ב-Google Cloud
  • דפדפן, כמו Chrome או Firefox

2. הגדרה ודרישות

הגדרת סביבה בקצב עצמאי

  1. נכנסים למסוף Google Cloud ויוצרים פרויקט חדש או עושים שימוש חוזר בפרויקט קיים. אם אין לכם עדיין חשבון Gmail או חשבון Google Workspace, עליכם ליצור חשבון.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Project name הוא השם המוצג של המשתתפים בפרויקט. זו מחרוזת תווים שלא משמשת את Google APIs. תמיד אפשר לעדכן.
  • Project ID הוא ייחודי בכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו (אי אפשר לשנות אותו אחרי שמגדירים אותו). מסוף Cloud יוצר מחרוזת ייחודית באופן אוטומטי; בדרך כלל לא מעניין אותך מה זה. ברוב ה-codelabs תצטרכו להפנות למזהה הפרויקט שלכם (בדרך כלל מזוהה כ-PROJECT_ID). אם המזהה שנוצר לא מוצא חן בעיניכם, אתם יכולים ליצור מזהה אקראי אחר. לחלופין, אפשר לנסות שם משלך ולראות אם הוא זמין. לא ניתן לשנות אותו אחרי השלב הזה, והוא נשאר למשך הפרויקט.
  • לידיעתך, יש ערך שלישי, Project Number, שבו משתמשים בחלק מממשקי ה-API. מידע נוסף על כל שלושת הערכים האלה זמין במסמכי התיעוד.
  1. בשלב הבא צריך להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבים או בממשקי API של Cloud. מעבר ב-Codelab הזה לא יעלה הרבה כסף, אם בכלל. כדי להשבית משאבים ולא לצבור חיובים מעבר למדריך הזה, אתם יכולים למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים להשתתף בתוכנית תקופת ניסיון בחינם בשווי 1,200 ש"ח.

הפעלת Cloud Shell

אומנם אפשר להפעיל את Google Cloud מרחוק מהמחשב הנייד, אבל ב-Codelab הזה משתמשים ב-Cloud Shell, סביבת שורת הפקודה שפועלת ב-Cloud.

הפעלת Cloud Shell

  1. במסוף Cloud, לוחצים על Activate Cloud Shell 853e55310c205094.png.

3c1dabeca90e44e5.png

אם זו הפעם הראשונה שאתם מפעילים את Cloud Shell, יוצג לכם מסך ביניים שמתוארת בו. אם הוצג לכם מסך ביניים, לוחצים על המשך.

9c92662c6a846a5c.png

ההקצאה וההתחברות ל-Cloud Shell נמשכת כמה דקות.

9f0e51b578fecce5.png

במכונה הווירטואלית הזו נמצאים כל כלי הפיתוח הדרושים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר משמעותית את ביצועי הרשת והאימות. אם לא את כולן, ניתן לבצע חלק גדול מהעבודה ב-Codelab הזה באמצעות דפדפן.

אחרי ההתחברות ל-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. הכנת סביבת הפיתוח

ב-Codelab הזה, אתם תשתמשו בטרמינל של Cloud Shell ובעורך Cloud Shell כדי לפתח את תוכנות Java.

הפעלת ממשקי API של Vertex AI

במסוף Google Cloud, מוודאים ששם הפרויקט מוצג בחלק העליון של מסוף Google Cloud. אם לא, לוחצים על Select a project כדי לפתוח את Project Selector ובוחרים את הפרויקט הרצוי.

אפשר להפעיל את Vertex AI APIs מהקטע Vertex AI במסוף Google Cloud או מטרמינל של Cloud Shell.

כדי להפעיל אותו ממסוף Google Cloud, קודם עוברים לקטע Vertex AI בתפריט של מסוף Google Cloud:

451976f1c8652341.png

לוחצים על Enable AllRecommended APIs (הפעלת כל ממשקי ה-API המומלצים) במרכז הבקרה של Vertex AI.

הפעולה הזו תפעיל מספר ממשקי API, אבל המפתח החשוב ביותר ב-Codelab הוא aiplatform.googleapis.com.

לחלופין, אפשר להפעיל את ה-API הזה גם מהטרמינל של Cloud Shell באמצעות הפקודה הבאה:

gcloud services enable aiplatform.googleapis.com

שכפול מאגר GitHub

בטרמינל של Cloud Shell, משכפלים את המאגר של ה-Codelab הזה:

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

כדי לבדוק שהפרויקט מוכן להרצה, אפשר לנסות להריץ את הפקודה 'Hello World' בתוכנית.

צריך לוודא שאתם נמצאים בתיקייה ברמה העליונה:

cd gemini-workshop-for-java-developers/ 

יוצרים את ה-wrapper של Gradle:

gradle wrapper

הפעלה עם gradlew:

./gradlew run

הפלט הבא אמור להתקבל:

..
> Task :app:run
Hello World!

פתיחה והגדרה של Cloud Editor

פותחים את הקוד באמצעות Cloud Code Editor מ-Cloud Shell:

42908e11b28f4383.png

ב-Cloud Code Editor, פותחים את תיקיית המקור של Codelab על ידי בחירה באפשרות File -> Open Folder ומצביעים על תיקיית המקור של Codelab (למשל, /home/username/gemini-workshop-for-java-developers/).

התקנת Gradle ל-Java

כדי שעורך הקוד בענן יפעל כמו שצריך עם Gradle, צריך להתקין את התוסף Gradle ל-Java.

קודם עוברים לקטע 'פרויקטים של Java' ומקישים על סימן הפלוס:

84d15639ac61c197.png

בוחרים Gradle for Java:

34d6c4136a3cc9ff.png

צריך לבחור את הגרסה: Install Pre-Release

3b044fb450cccb7.png

לאחר ההתקנה, אמורים להופיע הלחצנים Disable והUninstall:

46410fe86d777f9c.png

לסיום, מנקים את סביבת העבודה כדי להחיל את ההגדרות החדשות:

31e27e9bb61d975d.png

תתבקשו לטעון מחדש את הסדנה ולמחוק אותה. עכשיו צריך לבחור Reload and delete:

d6303bc49e391dc.png

אם אתם פותחים אחד מהקבצים, לדוגמה App.java, אתם אמורים לראות שהעורך פועל כראוי עם הדגשת תחביר:

fed1b1b5de0dff58.png

עכשיו הכול מוכן להרצת כמה דוגמאות מול Gemini!

הגדרה של משתני סביבה

כדי לפתוח טרמינל חדש ב-Cloud Code Editor, בוחרים באפשרות Terminal -> New Terminal. צריך להגדיר שני משתני סביבה שנדרשים להרצת דוגמאות הקוד:

  • PROJECT_ID – מזהה הפרויקט ב-Google Cloud
  • LOCATION – האזור שבו מודל 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-001")
            .build();

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

בדוגמה הראשונה הזו צריך לייבא את המחלקה VertexAiGeminiChatModel, שמטמיעה את הממשק ChatModel.

בשיטה main, מגדירים את מודל שפת הצ'אט באמצעות ה-builder של VertexAiGeminiChatModel ומציינים:

  • פרויקט
  • מיקום
  • שם הדגם (gemini-1.5-flash-001).

עכשיו, כשמודל השפה מוכן, אפשר להפעיל את השיטה generate() ולהעביר את ההנחיה, את השאלה או את ההוראות שצריך לשלוח ל-LLM. כאן אפשר לשאול שאלה פשוטה מה הופך את השמיים לכחולים.

אפשר לשנות את ההנחיה הזו כדי לנסות שאלות או משימות אחרות.

מריצים את הדוגמה בתיקיית הבסיס של קוד המקור:

./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 בפעם הראשונה.

שידור התשובה

האם הבחנת שהתשובה נמסרה פעם אחת, לאחר מספר שניות? ניתן גם לקבל את התשובה בהדרגה, הודות לווריאציה של התגובה בסטרימינג. אחרי שהמודל יפנה את התשובה באופן שוטף, המודל מחזיר חלק מהתשובה לחלקים, ברגע שהוא הופך לזמין.

ב-Codelab הזה נישאר עם התשובה שאינה סטרימינג, אבל בואו נסתכל על התשובה בסטרימינג כדי לראות איך אפשר לעשות זאת.

בתיקייה StreamQA.java בספרייה app/src/main/java/gemini/workshop אפשר לראות את התגובה בסטרימינג בפעולה:

package gemini.workshop;

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

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-001")
            .build();
        
        model.generate("Why is the sky blue?", new StreamingResponseHandler<>() {
            @Override
            public void onNext(String text) {
                System.out.println(text);
            }

            @Override
            public void onError(Throwable error) {
                error.printStackTrace();
            }
        });
    }
}

הפעם אנחנו מייבאים את הווריאציות של סוג הסטרימינג VertexAiGeminiStreamingChatModel שמטמיעות את הממשק של StreamingChatLanguageModel. צריך גם StreamingResponseHandler.

הפעם, החתימה של method generate() היא קצת שונה. במקום להחזיר מחרוזת, סוג ההחזרה הוא מבוטל. בנוסף להנחיה, צריך להעביר handler של תגובות בסטרימינג. כאן מטמיעים את הממשק על ידי יצירת מחלקה פנימית אנונימית, עם שתי שיטות onNext(String text) ו-onError(Throwable error). המערכת מפעילה את התגובה הראשונה בכל פעם שקטע חדש זמין בתשובה, והשנייה נשלחת רק אם מתרחשת שגיאה כלשהי.

הפעלה:

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

תקבלו תשובה דומה לזו של הכיתה הקודמת, אבל הפעם תבחינו שהתשובה מופיעה בהדרגה במעטפת, במקום להמתין להצגת התשובה המלאה.

הגדרות נוספות

בתצורה, הגדרנו רק את הפרויקט, את המיקום ואת שם המודל, אבל יש פרמטרים נוספים שאפשר לציין בשביל המודל:

  • temperature(Float temp) — כדי להגדיר את מידת היצירתיות הרצויה לתשובה (0 אם הקריאייטיב שלך נמוך ולרוב תהיה עובדתית יותר, והערך 1 מייצג פלט של יותר קריאייטיב).
  • topP(Float topP) - כדי לבחור את המילים האפשריות שההסתברות הכוללת שלהן מסתכמת במספר הנקודה הצפה (בין 0 ל-1)
  • topK(Integer topK) — בחירה אקראית של מילה מתוך מספר מקסימלי של מילים סבירות להשלמת הטקסט (מ-1 עד 40)
  • maxOutputTokens(Integer max) – כדי לציין את האורך המקסימלי של התשובה שהמודל נותן (בדרך כלל, 4 אסימונים מייצגים כ-3 מילים)
  • maxRetries(Integer retries) – במקרה שחרגתם ממכסת הזמן של הבקשה, או אם נתקלתם בבעיה טכנית בפלטפורמה, אפשר לבקש מהמודל לנסות לבצע שוב את הקריאה 3 פעמים

עד עכשיו, שאלת את Gemini שאלה אחת, אבל אפשר גם לנהל שיחה של מספר תורות. את זה תגלו בקטע הבא.

5. שיחה עם Gemini

בשלב הקודם שאלת שאלה אחת. עכשיו הגיע הזמן לנהל שיחה אמיתית בין המשתמש לבין ה-LLM. כל שאלה ותשובה יכולה להתבסס על השאלות הקודמות כדי ליצור דיון אמיתי.

כדאי לעיין ב-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-001")
            .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 – כיתה שתחבר בין מודל הצ'אט לזיכרון הצ'אט

בשיטה הראשית, מגדירים את המודל, את זיכרון הצ'אט ואת שירות ה-AI. המודל מוגדר כרגיל עם פרטי הפרויקט, המיקום ושם הדגם.

כדי לשמור את זיכרון הצ'אט, אנחנו משתמשים ב-builder של MessageWindowChatMemory כדי ליצור זיכרון ששומר את 20 ההודעות האחרונות שנשלחו. זהו חלון הזזה מעל השיחה, שההקשר שלה נשמר באופן מקומי בלקוח השיעור של Java.

אחר כך יוצרים את ה-AI service שמקשר בין מודל הצ'אט לזיכרון הצ'אט.

שימו לב איך שירות ה-AI משתמש בממשק 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

תמונה של חתול בשלג, נלקחה מוויקיפדיהhttps://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-001")
            .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. חילוץ מידע מובנה מטקסט לא מובנה

יש מצבים רבים שבהם מידע חשוב מסופק באופן לא מובנה במסמכי דוחות, בהודעות אימייל או בטקסטים ארוכים אחרים. באופן אידיאלי, תרצו לחלץ את פרטי המפתח שנכללים בטקסט הלא מובנה, בצורת אובייקטים מובנים. בואו נראה איך אפשר לעשות את זה.

נניח שאתם רוצים לחלץ את השם והגיל של אדם כלשהו, על סמך הביוגרפיה או התיאור שלו. אפשר להורות ל-LLM לחלץ 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.UserMessage;

public class ExtractData {

    static record Person(String name, int age) {}

    interface PersonExtractor {
        @UserMessage("""
            Extract the name and age of the person described below.
            Return a JSON document with a "name" and an "age" property, \
            following this structure: {"name": "John Doe", "age": 34}
            Return only JSON, without any markdown markup surrounding it.
            Here is the document describing the person:
            ---
            {{it}}
            ---
            JSON:
            """)
        Person extractPerson(String text);
    }

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

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

        Person person = extractor.extractPerson("""
            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.    
            """
        );

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

בואו נסתכל על השלבים השונים בקובץ:

  • רשומת Person מוגדרת לייצוג הפרטים שמתארים אדם ( שם וגיל).
  • הממשק PersonExtractor מוגדר באמצעות שיטה שסיפקה מחרוזת טקסט לא מובנית, ומחזירה מכונה של Person.
  • נוספה ההערה extractPerson() עם הערת @UserMessage שמשייכת אליה הנחיה. זו ההנחיה שהמודל ישתמש בו כדי לחלץ את המידע ולהחזיר את הפרטים בפורמט של מסמך JSON, שינתח בשבילכם את הנתונים ולא ישוחזרו למכונה של Person.

עכשיו נבחן את התוכן של השיטה main():

  • מודל הצ'אט נוצר. שימו לב: אנחנו משתמשים ב-temperature נמוך מאוד מתוך אפס, וב-topK של רק אחד, כדי להבטיח תשובה דטרמיניסטית מאוד. זה גם עוזר למודל לבצע את ההוראות בצורה טובה יותר. אנחנו לא רוצים ש-Gemini יעטף את התשובה JSON עם תגי עיצוב נוספים של Markdown.
  • אובייקט PersonExtractor נוצר באמצעות המחלקה AiServices של LangChain4j.
  • אחר כך אפשר פשוט לקרוא ל-Person person = extractor.extractPerson(...) כדי לחלץ את פרטי האדם מהטקסט הלא מובנה, ולקבל חזרה מופע Person עם השם והגיל.

מריצים את הדוגמה:

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

הפלט הבא אמור להתקבל:

Anna
23

כן, זו אנה ובגיל 23!

בגישה הזו של AiServices, אתם מפעילים אובייקטים עם הקלדה חזקה. אין לכם אינטראקציה ישירה עם ה-LLM. במקום זאת, אתם עובדים עם מחלקות קונקרטיות, כמו רשומת Person, שמייצגת את המידע האישי שחולץ, ויש אובייקט PersonExtractor עם method extractPerson() שמחזיר מופע Person. המושג LLM מופשט, וכמפתח Java, אתה פשוט עושה מניפולציה על מחלקות ואובייקטים רגילים.

8. שינוי המבנה של הנחיות באמצעות תבניות של הנחיות

כשמבצעים אינטראקציה עם LLM באמצעות קבוצה משותפת של הוראות או שאלות, יש חלק מההנחיה הזו שלא משתנה אף פעם, בעוד שחלקים אחרים מכילים את הנתונים. לדוגמה, אם ברצונך ליצור מתכונים, עליך להשתמש בהנחיה כגון "אתה שף מוכשר, צור מתכון מהמרכיבים הבאים: ...", ולאחר מכן הוסף את המרכיבים לסוף הטקסט. זאת המטרה של תבניות הנחיות – בדומה למחרוזות אינטרפולציה בשפות תכנות. תבנית של הנחיות מכילה placeholders שאפשר להחליף בנתונים המתאימים לקריאה מסוימת ל-LLM.

באופן ספציפי יותר, עכשיו נלמד על 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-001")
            .maxOutputTokens(500)
            .temperature(0.8f)
            .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 עם טמפרטורה גבוהה של יצירתיות, טמפרטורה גבוהה וערכי 'עליון' ו-'topK' גבוה. אחר כך יוצרים PromptTemplate עם השיטה הסטטית from() על ידי העברת המחרוזת של ההנחיה שלנו, ומשתמשים במשתני ה-placeholder של סוגר מסולסל כפול: {{dish}} ו-{{ingredients}}.

כדי ליצור את ההנחיה הסופית, שולחים קריאה ל-apply() שמקבלת מפה של צמדי מפתח/ערך שמייצגים את שם ה-placeholder ואת ערך המחרוזת שבו הוא יוחלף.

לסיום, מפעילים את השיטה 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 ולהריץ את הקוד מחדש. כך תוכלו לראות את ההשפעה של שינוי הפרמטרים האלה על ה-LLM.

תבניות של הנחיות הן דרך טובה להשתמש בהוראות לשימוש חוזר ובהתאמה לפרמטרים לקריאות ל-LLM. אתם יכולים להעביר נתונים ולהתאים אישית את ההנחיות לערכים שונים שהמשתמשים סיפקו.

9. סיווג טקסטים בעזרת הנחיות מכמה דוגמאות

מודלים גדולים של שפה (LLM) די טובים בסיווג טקסט לקטגוריות שונות. כדי לעזור ל-LLM במשימה הזאת, אתם יכולים לספק דוגמאות לטקסטים ולקטגוריות שמשויכות אליהם. הגישה הזו נקראת בדרך כלל יצירת הנחיות מכמה דוגמאות.

כדי לבצע סוג ספציפי של סיווג טקסט: ניתוח סנטימנטים, כדאי לבדוק את TextClassification.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;

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.Map;

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

        PromptTemplate promptTemplate = PromptTemplate.from("""
            Analyze the sentiment of the text below. Respond only with one word to describe the sentiment.

            INPUT: This is fantastic news!
            OUTPUT: POSITIVE

            INPUT: Pi is roughly equal to 3.14
            OUTPUT: NEUTRAL

            INPUT: I really disliked the pizza. Who would use pineapples as a pizza topping?
            OUTPUT: NEGATIVE

            INPUT: {{text}}
            OUTPUT: 
            """);

        Prompt prompt = promptTemplate.apply(
            Map.of("text", "I love strawberries!"));

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

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

בשיטה main() יוצרים את מודל הצ'אט של Gemini כרגיל, אבל עם מספר אסימון פלט מקסימלי קטן, כי רוצים רק תשובה קצרה: הטקסט הוא POSITIVE, NEGATIVE או NEUTRAL.

לאחר מכן, אתם יוצרים תבנית של הנחיות לשימוש חוזר בשיטת יצירת הנחיות עם כמה דוגמאות, ומנחים את המודל על מספר דוגמאות של קלט ופלט. זה גם עוזר למודל לעקוב אחר הפלט בפועל. במקום זאת, Gemini לא נותן תשובה עם משפט שלם, אלא לתת לו הוראה להשיב רק במילה אחת.

מחילים את המשתנים באמצעות השיטה apply() כדי להחליף את ה-placeholder הזה {{text}} בפרמטר האמיתי ("I love strawberries"), ולהפוך את התבנית הזו להודעת משתמש באמצעות toUserMessage().

מריצים את הדוגמה:

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

אתם אמורים לראות מילה אחת:

POSITIVE

נראה לאהוב תותים זה סנטימנט חיובי!

10. יצירה מוגדלת של אחזור

מודלי שפה גדולים מאומנים לפי כמות גדולה של טקסט. עם זאת, הידע שלהם כולל רק מידע שהוא ראה במהלך ההכשרה. אם יתפרסם מידע חדש אחרי המועד האחרון לאימון המודל, הפרטים האלה לא יהיו זמינים למודל. כך, המודל לא יוכל להשיב על שאלות לגבי מידע שהוא לא ראה.

לכן, גישות כמו אחזור Augmented Generation (RAG) עוזרות לספק את המידע הנוסף ש-LLM צריך לדעת כדי למלא בקשות של המשתמשים שלו, כדי לתת תשובה עם מידע שעשוי להיות עדכני יותר או לגבי מידע פרטי שלא נגיש בזמן האימון.

נחזור לשיחות. הפעם תוכלו לשאול שאלות לגבי המסמכים. נפתח צ'אט בוט שיכול לאחזר מידע רלוונטי ממסד נתונים שמכיל את המסמכים שלך מחולקים לחלקים קטנים יותר ("מקטעים"), והמידע הזה ישמש את המודל לבסיס התשובות שלו, במקום להסתמך רק על הידע שכלול באימון שלו.

ב-RAG יש שני שלבים:

  1. שלב הטמעת נתונים — המסמכים נטענים בזיכרון ומפוצלים למקטעים קטנים יותר, והטמעות וקטוריות (ייצוג רב-מימדי בווקטור של המקטעים) מחושבות ומאוחסנות במסד נתונים וקטורי שיכול לבצע חיפושים סמנטיים. שלב הטמעת הנתונים הזה מתבצע בדרך כלל פעם אחת, כשצריך להוסיף מסמכים חדשים לקורפוס המסמך.

cd07d33d20ffa1c8.png

  1. שלב השאילתה — עכשיו המשתמשים יכולים לשאול שאלות לגבי המסמכים. השאלה תומר גם לווקטור ותשוווה לכל הווקטורים האחרים במסד הנתונים. הווקטורים הדומים ביותר בדרך כלל קשורים סמנטיים ומוחזרים על ידי מסד הנתונים הווקטורי. לאחר מכן מקבלים ל-LLM את ההקשר של השיחה, את קטעי הטקסט שתואמים לווקטורים שמסד הנתונים מחזיר, ומתבקשים להבין את התשובה שלו על ידי בחינת מקטעי הטקסט האלה.

a1d2e2deb83c6d27.png

הכנת המסמכים

בהדגמה החדשה הזו, נשאל שאלות לגבי "Attention is all you need" במאמר הזה. הוא מתאר את ארכיטקטורת רשת הטרנספורמר של טרנספורמר, שעליה עמדה בראש שיטת Google, שעליה מיישמים בימינו את כל מודלי השפה הגדולים (LLM) המודרניים.

הסקירה כבר ירדה אל attention-is-all-you-need.pdf במאגר.

הטמעת הצ'אט בוט

בואו נראה איך לפתח את הגישה הדו-שלבית: קודם בהטמעת הנתונים של המסמך ולאחר מכן בזמן השאילתה, כשמשתמשים שואלים שאלות לגבי המסמך.

בדוגמה הזו, שני השלבים מוטמעים באותה מחלקה. בדרך כלל יש אפליקציה אחת שמטפלת בהטמעת הנתונים ואפליקציה נוספת שמציעה את ממשק הצ'אט בוט למשתמשים.

בנוסף, בדוגמה הזו נשתמש במסד נתונים וקטורי בזיכרון. בתרחיש ייצור אמיתי, הטמעת הנתונים ושלבי השאילתות יהיו מופרדים בשתי אפליקציות נפרדות, והווקטורים יישמרו במסד נתונים עצמאי.

הטמעת נתונים של מסמכים

השלב הראשון בשלב הטמעת המסמך הוא לאתר את קובץ ה-PDF שכבר הורדנו, ולהכין PdfParser כדי לקרוא אותו:

URL url = new URI("https://github.com/glaforge/gemini-workshop-for-java-developers/raw/main/attention-is-all-you-need.pdf").toURL();
ApachePdfBoxDocumentParser pdfParser = new ApachePdfBoxDocumentParser();
Document document = pdfParser.parse(url.openStream());

במקום ליצור את מודל השפה הרגיל של הצ'אט, אתם יוצרים מופע של מודל הטמעה. יש מודל ספציפי שתפקידו ליצור ייצוגים בווקטור של קטעי טקסט (מילים, משפטים ואפילו פסקאות). היא מחזירה וקטורים של מספרים בנקודה צפה (floating-point), ולא מחזירה תגובות טקסט.

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

בשלב הבא, צריך כמה כיתות כדי לשתף פעולה יחד כדי:

  • טוענים ומפצלים את מסמך ה-PDF במקטעים.
  • ליצור הטמעות וקטורים לכל המקטעים האלה.
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 תווים (עם המקטע הבא, כדי למנוע חיתוך של מילים או משפטים, בביטים או בחלקים).

מטמיע ה-Store מקשר בין מפצל המסמך, מודל ההטמעה לחישוב הווקטורים ומסד הנתונים הווקטורי בזיכרון. לאחר מכן, השיטה ingest() תבצע את הטמעת הנתונים.

השלב הראשון הסתיים. המסמך הועבר למקטעי טקסט עם הטמעות וקטורים המשויכות אליהם ונשמר במסד נתונים וקטורי.

איך שואלים שאלות

הגיע הזמן לשאול שאלות! יוצרים מודל של צ'אט כדי להתחיל את השיחה:

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

נדרשת גם מחלקה רטריבר כדי לקשר בין מסד הנתונים של הווקטור (במשתנה embeddingStore) למודל ההטמעה. המשימה שלו היא לשלוח שאילתה למסד הנתונים הווקטורי באמצעות חישוב של הטמעה וקטורית לשאילתה של המשתמש, כדי למצוא וקטורים דומים במסד הנתונים:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

מחוץ לשיטה הראשית, צריך ליצור ממשק שמייצג עוזר מומחה LLM. זה ממשק שכיתת AiServices תטמיע כדי שתוכלו לקיים בו אינטראקציה עם המודל:

interface LlmExpert {
    String ask(String question);
}

בשלב הזה ניתן להגדיר שירות AI חדש:

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

השירות הזה מקושר:

  • מודל שפת הצ'אט שהגדרתם קודם.
  • זיכרון צ'אט לצורך מעקב אחר השיחה.
  • האחזור משווה בין שאילתה של הטמעת וקטורים לבין הווקטורים במסד הנתונים.
  • בתבנית הנחיה כתוב במפורש שמודל הצ'אט צריך להשיב על ידי ביסוס התגובה שלו על המידע שסופק (כלומר, הקטעים הרלוונטיים במסמך שבו הטמעת הווקטור דומה לווקטור השאלה של המשתמש).
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in large language models,\s
            you excel at explaining simply and clearly questions about LLMs.

            Here is the question: {{userMessage}}

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

סוף סוף אתם יכולים לשאול את השאלות שלכם!

List.of(
    "What neural network architecture can be used for language models?",
    "What are the different components of a transformer neural network?",
    "What is attention in large language models?",
    "What is the name of the process that transforms text into vectors?"
).forEach(query ->
    System.out.printf("%n=== %s === %n%n %s %n%n", query, expert.ask(query)));
);

קוד המקור המלא נמצא ב-RAG.java בספרייה app/src/main/java/gemini/workshop:

מריצים את הדוגמה:

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

בפלט אמורות להופיע תשובות לשאלות:

=== What neural network architecture can be used for language models? === 

 Transformer architecture 


=== What are the different components of a transformer neural network? === 

 The different components of a transformer neural network are:

1. Encoder: The encoder takes the input sequence and converts it into a 
sequence of hidden states. Each hidden state represents the context of 
the corresponding input token.
2. Decoder: The decoder takes the hidden states from the encoder and 
uses them to generate the output sequence. Each output token is 
generated by attending to the hidden states and then using a 
feed-forward network to predict the token's probability distribution.
3. Attention mechanism: The attention mechanism allows the decoder to 
attend to the hidden states from the encoder when generating each output 
token. This allows the decoder to take into account the context of the 
input sequence when generating the output sequence.
4. Positional encoding: Positional encoding is a technique used to 
inject positional information into the input sequence. This is important 
because the transformer neural network does not have any inherent sense 
of the order of the tokens in the input sequence.
5. Feed-forward network: The feed-forward network is a type of neural 
network that is used to predict the probability distribution of each 
output token. The feed-forward network takes the hidden state from the 
decoder as input and outputs a vector of probabilities. 


=== What is attention in large language models? === 

Attention in large language models is a mechanism that allows the model 
to focus on specific parts of the input sequence when generating the 
output sequence. This is important because it allows the model to take 
into account the context of the input sequence when generating each output token.

Attention is implemented using a function that takes two sequences as 
input: a query sequence and a key-value sequence. The query sequence is 
typically the hidden state from the previous decoder layer, and the 
key-value sequence is typically the sequence of hidden states from the 
encoder. The attention function computes a weighted sum of the values in 
the key-value sequence, where the weights are determined by the 
similarity between the query and the keys.

The output of the attention function is a vector of context vectors, 
which are then used as input to the feed-forward network in the decoder. 
The feed-forward network then predicts the probability distribution of 
the next output token.

Attention is a powerful mechanism that allows large language models to 
generate text that is both coherent and informative. It is one of the 
key factors that has contributed to the recent success of large language 
models in a wide range of natural language processing tasks. 


=== What is the name of the process that transforms text into vectors? === 

The process of transforming text into vectors is called **word embedding**.

Word embedding is a technique used in natural language processing (NLP) 
to represent words as vectors of real numbers. Each word is assigned a 
unique vector, which captures its meaning and semantic relationships 
with other words. Word embeddings are used in a variety of NLP tasks, 
such as machine translation, text classification, and question 
answering.

There are a number of different word embedding techniques, but one of 
the most common is the **skip-gram** model. The skip-gram model is a 
neural network that is trained to predict the surrounding words of a 
given word. By learning to predict the surrounding words, the skip-gram 
model learns to capture the meaning and semantic relationships of words.

Once a word embedding model has been trained, it can be used to 
transform text into vectors. To do this, each word in the text is 
converted to its corresponding vector. The vectors for all of the words 
in the text are then concatenated to form a single vector, which 
represents the entire text.

Text vectors can be used in a variety of NLP tasks. For example, text 
vectors can be used to train machine translation models, text 
classification models, and question answering models. Text vectors can 
also be used to perform tasks such as text summarization and text 
clustering. 

11. שליחת פונקציות

יש גם מצבים שבהם רוצים של-LLM תהיה גישה למערכות חיצוניות, כמו ממשק API מרחוק לאינטרנט שמאחזר מידע או מבצע פעולה, או שירותים שמבצעים חישוב כלשהו. לדוגמה:

ממשקי API מרחוק לאינטרנט:

  • לעקוב אחרי הזמנות של לקוחות ולעדכן אותן.
  • אפשר לחפש פנייה או ליצור פנייה בכלי למעקב אחר בעיות.
  • אחזור נתונים בזמן אמת, כמו מחירי מניות או מדידות של חיישן IoT.
  • לשלוח אימייל.

כלי מחשוב:

  • מחשבון לבעיות מתמטיות מתקדמות יותר.
  • פרשנות של קוד להרצת קוד כשמודלים מסוג LLM צריכים לוגיקת הסקת מסקנות.
  • להמיר בקשות משפה טבעית לשאילתות SQL, כדי ש-LLM יוכל לשלוח שאילתות למסד נתונים.

שליחת פונקציות היא היכולת של המודל לבקש לבצע הפעלה אחת או יותר של פונקציה מטעמו, כדי שהמודל יוכל לענות בצורה תקינה להנחיות של המשתמש עם נתונים עדכניים יותר.

בהתחשב בהנחיה מסוימת מהמשתמש, והידע על פונקציות קיימות שיכולות להיות רלוונטיות להקשר הזה, מודל שפה גדול (LLM) יכול לתת תשובה לבקשה לקריאה לפונקציה. האפליקציה שמשלבת את ה-LLM יכולה לקרוא לפונקציה ואז להשיב ל-LLM באמצעות תשובה. ה-LLM מפרש את התשובה באמצעות תשובה כתובה.

ארבעת השלבים של הפעלת פונקציות

נבחן דוגמה להפעלה של פונקציה: קבלת מידע על תחזית מזג האוויר.

אם שואלים את Gemini או כל LLM אחר על מזג האוויר בפריז, הם אומרים שאין להם מידע על תחזית מזג האוויר. אם רוצים של-LLM תהיה גישה בזמן אמת לנתוני מזג האוויר, צריך להגדיר כמה פונקציות שהוא יכול להשתמש בו.

נסתכל על התרשים הבא:

31e0c2aba5e6f21c.png

1️⃣ קודם כל משתמש שואל על מזג האוויר בפריז. אפליקציית הצ'אט בוט יודעת שיש פונקציה אחת לפחות שעומדת לרשותה כדי לעזור ל-LLM למלא את השאילתה. הצ'אט בוט שולח את ההנחיה הראשונית וגם את רשימת הפונקציות שאפשר לקרוא להן. בדוגמה הזו, פונקציה בשם getWeather() שמחזיקה פרמטר מחרוזת עבור המיקום.

8863be53a73c4a70.png

ה-LLM לא יודע על תחזיות מזג אוויר, לכן הוא שולח חזרה בקשה לביצוע פונקציה במקום להשיב לו בהודעת טקסט. הצ'אט בוט חייב להפעיל את הפונקציה getWeather() עם "Paris" כפרמטר של מיקום.

d1367cc69c07b14d.png

2️⃣ הצ'אט בוט מפעיל את הפונקציה בשם ה-LLM ומאחזר את התשובה של הפונקציה. כאן אנחנו מניחים שהתגובה היא {"forecast": "sunny"}.

73a5f2ed19f47d8.png

3️⃣ אפליקציית הצ'אט בוט שולחת את התשובה ב-JSON חזרה ל-LLM.

20832cb1ee6fbfeb.png

4️⃣ ה-LLM בוחן את התשובה של JSON, מפרש את המידע הזה ולבסוף משיב על כך שמזג האוויר בפריז בהיר.

כל שלב בתור קוד

קודם כול, מגדירים את מודל Gemini כרגיל:

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

מציינים מפרט כלי שמתאר את הפונקציה שאפשר לקרוא לה:

ToolSpecification weatherToolSpec = ToolSpecification.builder()
    .name("getWeatherForecast")
    .description("Get the weather forecast for a location")
    .addParameter("location", JsonSchemaProperty.STRING,
        JsonSchemaProperty.description("the location to get the weather forecast for"))
    .build();

שם הפונקציה מוגדר, וגם השם והסוג של הפרמטר, אבל שימו לב שגם הפונקציה וגם הפרמטרים מקבלים תיאורים. התיאורים הם חשובים מאוד ועוזרים ל-LLM להבין באמת מה פונקציה יכולה לעשות, וכך לקבוע אם צריך לקרוא לפונקציה הזו בהקשר של השיחה.

נתחיל בשלב 1, על ידי שליחת השאלה הראשונית לגבי מזג האוויר בפריז:

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. בשלב הזה, אנחנו יודעים לאיזו פונקציה ה-LLM רוצה שנפנה אליו. בקוד, אנחנו לא מבצעים קריאה אמיתית ל-API חיצוני, אלא מחזירים תחזית מזג אוויר היפותטית באופן ישיר:

// 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 של LangChain4j (להודעת משתמש) או AiMessage (לתגובה של מודל), או אפילו 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-flash-001")
        .maxOutputTokens(100)
        .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. לאחר מכן יוצרים מופע של שירות תחזית מזג האוויר שמכיל את ה'פונקציה' שהמודל יבקש מאיתנו לקרוא לו.

עכשיו תשתמשו שוב במחלקה 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 ולשאול שאלות כמו "What is 10% of AAPL לצמצם price (מחיר המניה ב-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-001")
        .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.

לנציגים

שליחת פונקציות היא מנגנון תוסף מצוין למודלים גדולים של שפה (LLM), כמו Gemini. הוא מאפשר לנו ליצור מערכות מורכבות יותר שנקראות לעיתים קרובות 'סוכנים' או 'עוזרים מבוססי-AI'. הנציגים האלה יכולים לנהל אינטראקציה עם העולם החיצוני דרך ממשקי API חיצוניים ועם שירותים שעשויים להיות להם תופעות לוואי בסביבה החיצונית (למשל, שליחת אימיילים, יצירת כרטיסים וכו').

כשיוצרים נציגים חזקים כאלה, צריך לעשות זאת בצורה אחראית. לפני שמבצעים פעולות אוטומטיות, כדאי להביא בחשבון את האדם שעומד מאחוריהם. חשוב לשמור על הבטיחות כשמתכננים סוכנים מבוססי-LLM שיוצרים אינטראקציה עם העולם החיצוני.

13. הרצת Gemma עם Ollama ו-TestContainers

עד עכשיו השתמשנו ב-Gemini, אבל יש גם את Gemma, מודל אחותה הקטנה.

Gemma היא משפחה של מודלים חד-פעמיים קלילים ופתוחים שנוצרו על ידי אותו מחקר ואותה טכנולוגיה ששימשו ליצירת המודלים של Gemini. Gemma זמינה בשתי וריאציות של Gemma1 ו-Gemma2, כל אחת בגדלים שונים. Gemma1 זמין בשני גדלים: 2B ו-7B. Gemma2 זמין בשני גדלים: 9B ו-27B. המשקולות שלהם זמינות בחינם, והמשקל הקטן שלהם מאפשר לכם להריץ אותו לבד, גם במחשב הנייד או ב-Cloud Shell.

איך אתם משתמשים ב-Gemma?

יש הרבה דרכים להריץ Gemma: בענן, דרך Vertex AI בלחיצת כפתור, או GKE עם כמה מעבדי GPU, אבל אפשר גם להריץ אותו באופן מקומי.

אחת האפשרויות שכדאי להריץ את Gemma באופן מקומי היא להשתמש ב-Ollama, כלי שמאפשר להריץ מודלים קטנים כמו Llama 2, Mistral ועוד רבים אחרים במחשב המקומי שלכם. הוא דומה ל-Docker, אלא ל-LLM.

מתקינים את 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 ב-Olama ב-TestContainers

במקום שתצטרכו להתקין ולהפעיל את Ollama באופן מקומי, תוכלו להשתמש ב-Olama בתוך קונטיינר שמטופל על ידי 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

ההפעלה הראשונה תימשך זמן מה עד ליצירה ולהפעלה של מאגר התגים, אבל בסיום התהליך אמור להופיע תגובה של Gemma:

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. מזל טוב

כל הכבוד, פיתחת בהצלחה את אפליקציית הצ'אט הראשונה שלך מ-AI גנרטיבי ב-Java באמצעות LangChain4j ו-Gemini API! לאורך הדרך גיליתם שמודלים רב-אופניים של שפה גדולים (LLM) הם די עוצמתיים ומסוגלים לטפל במשימות שונות כמו שאלות/תשובות, גם במסמכים שלכם, חילוץ נתונים, אינטראקציה עם ממשקי API חיצוניים ועוד.

מה השלב הבא?

עכשיו תורכם לשפר את האפליקציות שלכם באמצעות שילובים חזקים של מודלים גדולים של שפה (LLM).

קריאה נוספת

מסמכי עזר