Gemini ב-Java עם Vertex AI ו-LangChain4j

1. מבוא

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

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

מה זה AI גנרטיבי?

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

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

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

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

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

מהם יישומים נפוצים של AI גנרטיבי?

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

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

אילו מוצרים מבוססי-AI גנרטיבי זמינים ב-Google Cloud?

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

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

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

מה זה Gemini?

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

b9913d011999e7c7.png

‫Gemini זמין בווריאציות ובגדלים שונים:

  • Gemini 2.0 Flash: התכונות החדשות ביותר מהדור הבא והיכולות המשופרות שלנו.
  • Gemini 2.0 Flash-Lite: מודל Gemini 2.0 Flash שעבר אופטימיזציה ליעילות בעלויות ולזמן אחזור נמוך.
  • Gemini 2.5 Pro: מודל החשיבה הרציונלית המתקדם ביותר שלנו עד כה.
  • Gemini 2.5 Flash: מודל חושב עם יכולות מגוונות. הוא נועד להציע איזון בין מחיר לביצועים.

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

  • מרובה מצבים: היכולת של Gemini להבין ולטפל בפורמטים שונים של מידע היא צעד משמעותי מעבר למודלים מסורתיים של שפה שמבוססים על טקסט בלבד.
  • ביצועים: מודל Gemini 2.5 Pro עולה בביצועים על המודלים המתקדמים ביותר כיום במדדים רבים, והוא היה המודל הראשון שעלה על ביצועי מומחים אנושיים במדד המאתגר MMLU (הבנת שפה מסיבית של משימות מרובות).
  • גמישות: הגדלים השונים של Gemini מאפשרים להתאים אותו לתרחישי שימוש שונים, ממחקר בקנה מידה גדול ועד לפריסה במכשירים ניידים.

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

יש לך שתי אפשרויות:

  1. ספריית Vertex AI Java API for Gemini הרשמית.
  2. LangChain4j framework.

ב-Codelab הזה תשתמשו ב-framework‏ LangChain4j.

מהו ה-framework‏ LangChain4j?

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

הפרויקט קיבל השראה מפרויקט Python של 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 ויוצרים פרויקט חדש או משתמשים בפרויקט קיים. אם עדיין אין לכם חשבון Gmail או Google Workspace, אתם צריכים ליצור חשבון.

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • שם הפרויקט הוא השם המוצג של הפרויקט הזה למשתתפים. זו מחרוזת תווים שלא נמצאת בשימוש ב-Google APIs. תמיד אפשר לעדכן את המיקום.
  • מזהה הפרויקט הוא ייחודי לכל הפרויקטים ב-Google Cloud ואי אפשר לשנות אותו אחרי שהוא מוגדר. מסוף Cloud יוצר באופן אוטומטי מחרוזת ייחודית, ובדרך כלל לא צריך לדעת מה היא. ברוב ה-Codelabs, תצטרכו להפנות למזהה הפרויקט (בדרך כלל מסומן כ-PROJECT_ID). אם אתם לא אוהבים את המזהה שנוצר, אתם יכולים ליצור מזהה אקראי אחר. אפשר גם לנסות שם משתמש משלכם ולבדוק אם הוא זמין. אי אפשר לשנות את ההגדרה הזו אחרי השלב הזה, והיא תישאר לאורך הפרויקט.
  • לידיעתכם, יש ערך שלישי, מספר פרויקט, שחלק מממשקי ה-API משתמשים בו. במאמרי העזרה מפורט מידע נוסף על שלושת הערכים האלה.
  1. בשלב הבא, תצטרכו להפעיל את החיוב במסוף Cloud כדי להשתמש במשאבי Cloud או בממשקי API של Cloud. השלמת ה-codelab הזה לא תעלה לכם הרבה, אם בכלל. כדי להשבית את המשאבים ולמנוע חיובים נוספים אחרי שתסיימו את המדריך הזה, תוכלו למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים לתוכנית תקופת ניסיון בחינם בשווי 300$.

מפעילים את Cloud Shell

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

הפעלת Cloud Shell

  1. ב-Cloud Console, לוחצים על 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 Editor כדי לפתח את תוכניות Java.

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

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

אפשר להפעיל את ממשקי ה-API של Vertex AI דרך הקטע Vertex AI במסוף Google Cloud או דרך מסוף Cloud Shell.

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

451976f1c8652341.png

לוחצים על Enable All Recommended 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/ 

יוצרים את Gradle wrapper:

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/).

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

פותחים טרמינל חדש ב-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-2.0-flash")
            .build();

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

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

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

  • פרויקט
  • מיקום
  • שם המודל (gemini-2.0-flash).

עכשיו מודל השפה מוכן, ואפשר להפעיל את שיטת 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 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-2.0-flash")
            .maxOutputTokens(4000)
            .build();

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

הפעם, אנחנו מייבאים את הווריאציות של המחלקה לסטרימינג VertexAiGeminiStreamingChatModel שמטמיעה את הממשק StreamingChatLanguageModel. תצטרכו גם לייבא באופן סטטי את LambdaStreamingResponseHandler.onNext, שהיא שיטה נוחה שמספקת StreamingResponseHandler כדי ליצור handler לסטרימינג באמצעות ביטויי למדה של Java.

הפעם, החתימה של השיטה generate() קצת שונה. במקום להחזיר מחרוזת, סוג ההחזרה הוא void. בנוסף להנחיה, צריך להעביר handler של תגובה לסטרימינג. כאן, הודות לייבוא הסטטי שציינו למעלה, אפשר להגדיר ביטוי למבדה שמועבר לשיטה 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

בשלב הקודם שאלתם שאלה אחת. עכשיו הגיע הזמן לנהל שיחה אמיתית בין משתמש לבין מודל שפה גדול (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-2.0-flash")
            .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 — class שיעזור לטפל בהיבט של שיחה מרובת תורות, ולשמור בזיכרון המקומי את השאלות והתשובות הקודמות
  • AiServices — מחלקת הפשטה ברמה גבוהה יותר שתקשר בין מודל הצ'אט לבין זיכרון הצ'אט

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

לזיכרון הצ'אט, אנחנו משתמשים בבונה של 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

תמונה של חתול בשלג שצולמה מתוך ויקיפדיה

כדאי לבדוק את 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-2.0-flash")
            .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 מטקסט לא מובנה באמצעות הנחיה מותאמת (זה נקרא בדרך כלל "הנדסת הנחיות").

אבל בדוגמה שלמטה, במקום ליצור הנחיה שמתארת את פלט ה-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-2.0-flash")
            .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.
  • ה-extractPerson() מתויג בהערה @SystemMessage שמשייכת אליו הנחיה. זו ההנחיה שהמודל ישתמש בה כדי להנחות את החילוץ של המידע, ולהחזיר את הפרטים בצורה של מסמך JSON, שינותח עבורכם ויבוטל ה-marshaling שלו למופע 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 הזו אתם פועלים עם אובייקטים עם הקלדה חזקה. אתם לא מקיימים אינטראקציה ישירה עם מודל ה-LLM. במקום זאת, אתם עובדים עם מחלקות קונקרטיות, כמו Person כדי לייצג את המידע האישי שחולץ, ויש לכם אובייקט PersonExtractor עם שיטת extractPerson() שמחזירה מופע Person. מושג ה-LLM מופשט, וכמפתחי Java, אתם רק מפעילים מחלקות ואובייקטים רגילים כשאתם משתמשים בממשק PersonExtractor הזה.

8. איך יוצרים הנחיות באמצעות תבניות להנחיות

כשמבצעים אינטראקציה עם מודל שפה גדול באמצעות קבוצה משותפת של הוראות או שאלות, יש חלק בהנחיה שלא משתנה אף פעם, וחלקים אחרים שמכילים את הנתונים. לדוגמה, אם רוצים ליצור מתכונים, אפשר להשתמש בהנחיה כמו "אתה שף מוכשר, בבקשה תצור מתכון עם המרכיבים הבאים: ...", ואז להוסיף את המרכיבים בסוף הטקסט. לשם כך נועדו תבניות של הנחיות – בדומה למחרוזות עם אינטרפולציה בשפות תכנות. תבנית הנחיה מכילה פלייסהולדרים שאפשר להחליף בנתונים הנכונים לקריאה מסוימת ל-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-2.0-flash")
            .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() שלו, על ידי העברת המחרוזת של ההנחיה שלנו, ומשתמשים במשתני ה-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 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-2.0-flash")
            .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 Chat כרגיל, אבל עם מספר קטן של טוקנים מקסימליים של פלט, כי רוצים לקבל תשובה קצרה: הטקסט הוא POSITIVE,‏ NEGATIVE או NEUTRAL. כדי להגביל את המודל כך שיחזיר רק את הערכים האלה, באופן בלעדי, אפשר להשתמש בתמיכה בפלט מובנה שגיליתם בקטע על חילוץ נתונים. לכן משתמשים בשיטה responseSchema(). הפעם לא משתמשים בשיטה הנוחה מ-SchemaHelper כדי להסיק את הגדרת הסכימה, אלא משתמשים בכלי ליצירת Schema כדי להבין איך נראית הגדרת הסכימה.

אחרי שמגדירים את המודל, יוצרים ממשק SentimentAnalysis ש-LangChain4j AiServices יישם בשבילכם באמצעות ה-LLM. הממשק הזה מכיל שיטה אחת: analyze(). הפונקציה מקבלת כקלט את הטקסט לניתוח ומחזירה ערך enum‏ Sentiment. לכן, אתם רק משנים אובייקט עם הקלדה חזקה שמייצג את סוג הסנטימנט שזוהה.

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

עכשיו נשתמש בשיטה AiServices.builder() כדי לשלב את כל הרכיבים. נעביר את הממשק SentimentAnalysis, את המודל שבו רוצים להשתמש ואת זיכרון הצ'אט עם הדוגמאות של למידה עם הקשר מועט. לבסוף, קוראים לשיטה analyze() עם הטקסט לניתוח.

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

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

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

POSITIVE

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

10. שיפור התשובות בעזרת אחזור מידע (RAG)

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

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

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

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

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

cd07d33d20ffa1c8.png

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

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 ומפצלים אותו לחלקים.
  • יוצרים הטמעות וקטוריות לכל החלקים האלה.
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-2.0-flash")
        .maxOutputTokens(1000)
        .build();

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

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

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

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

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

בשלב הזה, אפשר להגדיר שירות AI חדש:

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

השירות הזה מאגד את:

  • מודל שפת הצ'אט שהגדרתם קודם.
  • זיכרון צ'אט כדי לעקוב אחרי השיחה.
  • רכיב המאחזר משווה שאילתת הטמעת וקטורים לווקטורים במסד הנתונים.
.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}}
            """))
        .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. בקשה להפעלת פונקציה

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

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

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

כלי חישוב:

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

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

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

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

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

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

הנה תרשים לדוגמה:

31e0c2aba5e6f21c.png

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

8863be53a73c4a70.png

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

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

d1367cc69c07b14d.png

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

73a5f2ed19f47d8.png

‫4️⃣ מודל ה-LLM בודק את תגובת ה-JSON, מפרש את המידע הזה ובסופו של דבר משיב עם הטקסט 'מזג האוויר בפריז הוא שמשי'.

20832cb1ee6fbfeb.png

כל שלב כקוד

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

ChatLanguageModel model = VertexAiGeminiChatModel.builder()
    .project(System.getenv("PROJECT_ID"))
    .location(System.getenv("LOCATION"))
    .modelName("gemini-2.0-flash")
    .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();

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

קוד המקור המלא נמצא ב-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, שכולל גם תיאור של הפרמטר. אפשר להוסיף כמה פונקציות שרוצים כדי שהמודל יוכל להשתמש בהן בתרחישים מורכבים יותר.

בשיעור הזה, מחזירים כמה תשובות מוכנות מראש, אבל אם רוצים להתקשר לשירות חיצוני אמיתי של תחזית מזג האוויר, צריך לבצע את הקריאה לשירות הזה בגוף של ה-method.

כפי שראינו כשיוצרים 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-2.0-flash")
        .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?"));
    System.out.println(assistant.chat("Is it warmer in London or in Paris?"));
}

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

עכשיו משתמשים שוב במחלקה AiServices כדי לקשור את מודל הצ'אט, את זיכרון הצ'אט ואת הכלי (כלומר, שירות תחזית מזג האוויר עם הפונקציה שלו). ‫AiServices מחזירה אובייקט שמיישם את הממשק WeatherAssistant שהגדרתם. הדבר היחיד שנותר הוא לקרוא ל-method‏ chat() של העוזר הדיגיטלי. כשמפעילים את התכונה, רואים רק את תשובות הטקסט, אבל הבקשות להפעלת פונקציות והתשובות להפעלת פונקציות לא מוצגות למפתח. הבקשות האלה מטופלות באופן אוטומטי ושקוף. אם Gemini יחשוב שצריך להפעיל פונקציה, הוא ישיב עם בקשה להפעלת הפונקציה, ו-LangChain4j יפעיל את הפונקציה המקומית בשמכם.

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

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

הפלט שיוצג אמור להיות דומה לזה שמופיע כאן:

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

It is warmer in Paris (20 degrees) than in London (15 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 ולשאול שאלות כמו "מהו מחיר המניה של AAPL ב-10% הנחה, אחרי המרה מדולר אמריקאי לאירו?"

public static void main(String[] args) {
    ChatLanguageModel model = VertexAiGeminiChatModel.builder()
        .project(System.getenv("PROJECT_ID"))
        .location(System.getenv("LOCATION"))
        .modelName("gemini-2.0-flash")
        .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. הטכנולוגיה הזו מאפשרת לנו לבנות מערכות מורכבות יותר, שלרוב נקראות 'סוכנים' או 'עוזרים מבוססי-AI'. הסוכנים האלה יכולים ליצור אינטראקציה עם העולם החיצוני באמצעות ממשקי API חיצוניים ועם שירותים שיכולים להשפיע על הסביבה החיצונית (למשל, שליחת אימיילים, יצירת כרטיסים וכו').

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

13. הפעלת Gemma עם Ollama ו-TestContainers

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

Gemma היא משפחה של מודלים קלים ומתקדמים בקוד פתוח, שמבוססים על אותם מחקרים וטכנולוגיות ששימשו ליצירת מודלים של Gemini. מודל Gemma הכי עדכני הוא Gemma3, שזמין בארבעה גדלים: 1B (טקסט בלבד), 4B,‏ 12B ו-27B. המשקלים שלהם זמינים באופן חופשי, והגודל הקטן שלהם מאפשר להריץ אותם בעצמכם, אפילו במחשב הנייד או ב-Cloud Shell.

איך מריצים את Gemma?

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

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

מתקינים את Ollama לפי ההוראות למערכת ההפעלה שלכם.

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

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

אחרי ההתקנה המקומית, אפשר להריץ פקודות כדי לשלוף מודל:

ollama pull gemma3:1b

מחכים עד שהמודל יימשך. פעולה זו עשויה להימשך זמן מה.

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

ollama run gemma3:1b

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

>>> 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_GEMMA3 = "tc-ollama-gemma3-1b";
    public static final String GEMMA_3 = "gemma3:1b";

    // Creating an Ollama container with Gemma 3 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_GEMMA3)
            .exec();

        if (listImagesCmd.isEmpty()) {
            System.out.println("Creating a new Ollama container with Gemma 3 image...");
            OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.7.1");
            System.out.println("Starting Ollama...");
            ollama.start();
            System.out.println("Pulling model...");
            ollama.execInContainer("ollama", "pull", GEMMA_3);
            System.out.println("Committing to image...");
            ollama.commitToImage(TC_OLLAMA_GEMMA3);
            return ollama;
        }

        System.out.println("Ollama image substitution...");
        // Substitute the default Ollama image with our Gemma variant
        return new OllamaContainer(
            DockerImageName.parse(TC_OLLAMA_GEMMA3)
                .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_3)
        .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.7.1 started in PT7.228339916S
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!

קריאה נוספת

מאמרי עזרה