Gemini ב-Java עם Vertex AI ו-LangChain4j

1. מבוא

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

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

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

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

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

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

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

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

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

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

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

אילו חבילות שירות של AI גנרטיבי זמינות ב-Google Cloud?

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

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

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

מה זה Gemini?

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

b9913d011999e7c7.png

יש ל-Gemini וריאציות וגדלים שונים:

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

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

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

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

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

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

בשיעור ה-Codelab הזה נשתמש בפלטפורמה LangChain4j.

מהי המסגרת של LangChain4j?

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

הפרויקט נוצר בהשראת פרויקט 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. בשלב הבא, כדי להשתמש במשאבים או ב-API של Cloud, תצטרכו להפעיל את החיוב במסוף Cloud. השלמת הקודלאב הזה לא תעלה הרבה, אם בכלל. כדי להשבית את המשאבים ולמנוע חיובים אחרי סיום המדריך, אפשר למחוק את המשאבים שיצרתם או למחוק את הפרויקט. משתמשים חדשים ב-Google Cloud זכאים להשתתף בתוכנית תקופת ניסיון בחינם בסך 300$.

הפעלת Cloud Shell

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

הפעלת Cloud Shell

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

3c1dabeca90e44e5.png

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

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 ובוחרים את הפרויקט הרצוי.

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

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

451976f1c8652341.png

לוחצים על Enable All Recommended APIs בלוח הבקרה של Vertex AI.

הפעולה הזו תפעיל כמה ממשקי API, אבל החשוב ביותר לקודלאב הוא 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:

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-1.5-flash-002")
            .build();

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

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

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

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

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

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

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

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

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

        interface ConversationService {
            String chat(String message);
        }

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

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

יש כמה אפשרויות ייבוא חדשות ומעניינות בכיתה הזו:

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

בשיטה הראשית תגדירו את המודל, את זיכרון הצ'אט ואת שירות ה-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-002")
            .build();

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

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

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

בפעולות הייבוא, חשוב לשים לב שאנחנו מבחינים בין סוגים שונים של הודעות ותוכן. רכיב UserMessage יכול להכיל גם אובייקט TextContent וגם אובייקט ImageContent. זוהי מודליות מרובת-מודלים: שילוב של טקסט ותמונות. אנחנו לא שולחים רק הנחיה פשוטה של מחרוזת, אלא שולחים אובייקט מובנה יותר שמייצג הודעת משתמש, שמורכב מחלק של תוכן תמונה ומחלק של תוכן טקסט. המודל שולח בחזרה Response שמכיל AiMessage.

לאחר מכן, אפשר לאחזר את AiMessage מהתגובה באמצעות content(), ואז את הטקסט של ההודעה באמצעות text().

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

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

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

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

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

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

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

7. חילוץ מידע מובנה מטקסט לא מובנה

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

נניח שאתם רוצים לחלץ את השם והגיל של אדם מסוים, על סמך ביוגרפיה, קורות חיים או תיאור של אותו אדם. אפשר להנחות את ה-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-1.5-flash-002")
            .responseMimeType("application/json")
            .responseSchema(fromClass(Person.class))
            .build();

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

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

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

בואו נבחן את השלבים השונים בקובץ הזה:

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

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

  • מודל הצ'אט מוגדר ונוצר. אנחנו משתמשים בשתי שיטות חדשות של מחלקת ה-Model Builder: responseMimeType() ו-responseSchema(). ההגדרה הראשונה מורה ל-Gemini ליצור JSON תקין בתוצר. השיטה השנייה מגדירה את הסכימה של אובייקט ה-JSON שצריך להחזיר. בנוסף, ה-method האחרון מעביר את הבעלות ל-method נוח שיכול להמיר כיתה או רשומה של 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 באמצעות קבוצה משותפת של הוראות או שאלות, יש חלק בהנחיה הזו שלא משתנה אף פעם, ואילו חלקים אחרים מכילים את הנתונים. לדוגמה, אם אתם רוצים ליצור מתכונים, תוכלו להשתמש בהנחיה כמו "יש לך כישרון בישול, עליך ליצור מתכון עם המרכיבים הבאים: …", ואז לצרף את המרכיבים בסוף הטקסט הזה. לשם כך משמשות תבניות של הנחיות – בדומה למחרוזות מופרדות בשפות תכנות. תבנית הנחיה מכילה placeholder שאפשר להחליף בנתונים המתאימים לקריאה מסוימת ל-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-002")
            .maxOutputTokens(500)
            .temperature(1.0f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

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

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

        Prompt prompt = promptTemplate.apply(variables);

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

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

כרגיל, מגדירים את המודל VertexAiGeminiChatModel עם רמה גבוהה של קריאייטיב עם טמפרטורה גבוהה וגם ערכים גבוהים של topP ו-topK. לאחר מכן יוצרים PromptTemplate באמצעות השיטה הסטטית from() שלו, על ידי העברת המחרוזת של ההנחיה שלנו, ומשתמשים במשתני placeholder עם סוגריים מסולסלים כפולים: {{dish}} ו-{{ingredients}}.

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

לבסוף, קוראים ל-method‏ 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-1.5-flash-002")
            .maxOutputTokens(10)
            .maxRetries(3)
            .responseSchema(Schema.newBuilder()
                .setType(Type.STRING)
                .addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
                .build())
            .build();


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

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

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

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

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

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

ב-enum של Sentiment מפורטים הערכים השונים של סנטימנט: שלילי, ניטרלי או חיובי.

בשיטה main(), יוצרים את מודל הצ'אט של Gemini כרגיל, אבל עם מספר קטן של אסימוני פלט מקסימליים, כי רוצים לקבל רק תשובה קצרה: הטקסט הוא POSITIVE,‏ NEGATIVE או NEUTRAL. כדי להגביל את המודל להחזרת הערכים האלה בלבד, אפשר להיעזר בתמיכה בפלט מובנה שגיליתם בקטע 'חילוץ נתונים'. לכן נעשה שימוש בשיטה responseSchema(). הפעם לא משתמשים בשיטה הנוחה של SchemaHelper כדי להסיק את הגדרת הסכימה, אלא משתמשים ב-builder של Schema כדי להבין איך נראית הגדרת הסכימה.

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

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

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

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

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

אמורה להופיע מילה אחת:

POSITIVE

נראה ש'אהבה לתותים' היא סנטימנט חיובי.

10. יצירת מודלים משופרים לאחזור

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

לכן, גישות כמו יצירה משופרת של אחזור (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-1.5-flash-002")
        .maxOutputTokens(1000)
        .build();

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

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

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

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

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

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

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

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

  • מודל שפת הצ'אט שהגדרתם קודם.
  • זיכרון צ'אט כדי לעקוב אחרי השיחה.
  • ה-retriever משווה בין שאילתה להטמעת וקטור לבין הווקטורים במסד הנתונים.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in car automotive, and you answer concisely.

            Here is the question: {{userMessage}}

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

עכשיו אתם מוכנים לשאול את השאלות שלכם.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

11. קריאה לפונקציה

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

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

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

כלים לחישוב:

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

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

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

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

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

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

כדאי לעיין בתרשים הבא:

31e0c2aba5e6f21c.png

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

8863be53a73c4a70.png

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

d1367cc69c07b14d.png

2️⃣ ה-chatbot מפעיל את הפונקציה הזו בשם ה-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-002")
    .maxOutputTokens(100)
    .build();

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

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

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

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

כפי שראינו כשיצרנו פונקציית 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-pro-002")
        .build();

    WeatherForecastService weatherForecastService = new WeatherForecastService();

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

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

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

עכשיו משתמשים שוב בכיתה 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.

זו הייתה דוגמה לפונקציה אחת.

קריאות מרובות לפונקציות

אפשר גם ליצור כמה פונקציות ולאפשר ל-LangChain4j לטפל בכמה קריאות לפונקציות בשמכם. דוגמה לשימוש בכמה פונקציות מופיעה ב-MultiFunctionCallingAssistant.java.

יש לו פונקציה להמרת מטבעות:

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

    double result = amount;

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

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

    return result;
}

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

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

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

    return result;
}

פונקציה אחרת להחיל אחוז על סכום נתון:

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

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

    return result;
}

לאחר מכן תוכלו לשלב את כל הפונקציות האלה עם כיתה של MultiTools ולשאול שאלות כמו "מהו 10% ממחיר המניה של AAPL לאחר המרה מ-USD ל-EUR?"

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

    MultiTools multiTools = new MultiTools();

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

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

מריצים אותו באופן הבא:

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

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

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

לעבר סוכני תמיכה

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

במקום להתקין ולהפעיל את Ollama באופן מקומי, אפשר להשתמש ב-Ollama בתוך קונטיינר שמנוהל על ידי TestContainers.

TestContainers שימושי לא רק לבדיקה, אלא גם להרצת קונטיינרים. יש אפילו OllamaContainer ספציפי שתוכלו להשתמש בו.

זו התמונה המלאה:

2382c05a48708dfd.png

הטמעה

נבחן את GemmaWithOllamaContainer.java, חלק אחרי חלק.

קודם כול, צריך ליצור מאגר Ollama נגזר שמושך את מודל Gemma. התמונה הזו כבר קיימת מהרצה קודמת או שהיא תיווצר. אם התמונה כבר קיימת, פשוט תצטרכו להודיע ל-TestContainers שאתם רוצים להחליף את קובץ האימג' שמוגדר כברירת מחדל ב-Ollama בגרסה שלכם שמבוססת על Gemma:

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

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

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

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

בשלב הבא, יוצרים ומפעילים קונטיינר בדיקה של Ollama, ולאחר מכן יוצרים מודל צ'אט של Ollama על ידי הצבעה על הכתובת והיציאה של הקונטיינר עם המודל שבו רוצים להשתמש. בסיום, פשוט מפעילים את model.generate(yourPrompt) כרגיל:

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

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

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

    System.out.println(response);
}

מריצים אותו באופן הבא:

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

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

מקורות מידע נוספים

מסמכי עזר