Gemini در جاوا با Vertex AI و LangChain4j

۱. مقدمه

این آزمایشگاه کد بر روی مدل زبان بزرگ Gemini (LLM) تمرکز دارد که در Vertex AI در Google Cloud میزبانی می‌شود. Vertex AI پلتفرمی است که تمام محصولات، خدمات و مدل‌های یادگیری ماشینی موجود در Google Cloud را در بر می‌گیرد.

شما از جاوا برای تعامل با API Gemini با استفاده از چارچوب LangChain4j استفاده خواهید کرد. با مثال‌های ملموس، از LLM برای پاسخ به سوالات، تولید ایده، استخراج موجودیت و محتوای ساختاریافته، تولید افزوده بازیابی و فراخوانی تابع بهره خواهید برد.

هوش مصنوعی مولد چیست؟

هوش مصنوعی مولد به استفاده از هوش مصنوعی برای ایجاد محتوای جدید مانند متن، تصاویر، موسیقی، صدا و ویدیو اشاره دارد.

هوش مصنوعی مولد (Generative AI) توسط مدل‌های زبان بزرگ (LLM) پشتیبانی می‌شود که می‌توانند چندین کار را همزمان انجام دهند و وظایف آماده‌ای مانند خلاصه‌سازی، پرسش و پاسخ، طبقه‌بندی و موارد دیگر را انجام دهند. با حداقل آموزش، مدل‌های بنیادی را می‌توان برای موارد استفاده هدفمند با داده‌های نمونه بسیار کم تطبیق داد.

هوش مصنوعی مولد چگونه کار می‌کند؟

هوش مصنوعی مولد با استفاده از یک مدل یادگیری ماشینی (ML) برای یادگیری الگوها و روابط موجود در مجموعه داده‌های محتوای ایجاد شده توسط انسان کار می‌کند. سپس از الگوهای آموخته شده برای تولید محتوای جدید استفاده می‌کند.

رایج‌ترین روش برای آموزش یک مدل هوش مصنوعی مولد، استفاده از یادگیری نظارت‌شده است. به این مدل مجموعه‌ای از محتوای ایجاد شده توسط انسان و برچسب‌های مربوطه داده می‌شود. سپس یاد می‌گیرد که محتوایی تولید کند که مشابه محتوای ایجاد شده توسط انسان باشد.

کاربردهای رایج هوش مصنوعی مولد چیست؟

هوش مصنوعی مولد می‌تواند برای موارد زیر استفاده شود:

  • بهبود تعاملات با مشتری از طریق بهبود تجربه چت و جستجو.
  • حجم عظیمی از داده‌های بدون ساختار را از طریق رابط‌های محاوره‌ای و خلاصه‌سازی‌ها کاوش کنید.
  • کمک به انجام کارهای تکراری مانند پاسخ به درخواست‌های پیشنهاد، بومی‌سازی محتوای بازاریابی به زبان‌های مختلف و بررسی انطباق قراردادهای مشتری و موارد دیگر.

گوگل کلود چه پیشنهاداتی در زمینه هوش مصنوعی مولد ارائه می‌دهد؟

با Vertex AI ، می‌توانید با مدل‌های پایه تعامل داشته باشید، آنها را سفارشی کنید و بدون نیاز به تخصص در زمینه یادگیری ماشین، در برنامه‌های خود جاسازی کنید. می‌توانید به مدل‌های پایه در Model Garden دسترسی داشته باشید، مدل‌ها را از طریق یک رابط کاربری ساده در Vertex AI Studio تنظیم کنید، یا از مدل‌ها در یک دفترچه یادداشت علوم داده استفاده کنید.

ابزار Vertex AI Search and Conversation سریع‌ترین راه برای ساخت موتورهای جستجو و چت‌بات‌های مبتنی بر هوش مصنوعی را در اختیار توسعه‌دهندگان قرار می‌دهد.

Gemini برای Google Cloud که توسط Gemini پشتیبانی می‌شود، یک ابزار همکاری مبتنی بر هوش مصنوعی است که در سراسر Google Cloud و IDEها در دسترس است تا به شما در انجام کارهای بیشتر و سریع‌تر کمک کند. Gemini Code Assist تکمیل کد، تولید کد، توضیحات کد را ارائه می‌دهد و به شما امکان می‌دهد با آن چت کنید تا سوالات فنی بپرسید.

جمینی چیست؟

جمینی (Gemini) خانواده‌ای از مدل‌های هوش مصنوعی مولد است که توسط گوگل دیپ‌مایند (Google DeepMind) توسعه داده شده و برای موارد استفاده چندوجهی طراحی شده است. چندوجهی به این معنی است که می‌تواند انواع مختلفی از محتوا مانند متن، کد، تصاویر و صدا را پردازش و تولید کند.

b9913d011999e7c7.png

جمینی در انواع و اندازه‌های مختلفی عرضه می‌شود:

  • فلش Gemini 2.0 : جدیدترین ویژگی‌های نسل بعدی ما و قابلیت‌های بهبود یافته.
  • Gemini 2.0 Flash-Lite : یک مدل Gemini 2.0 Flash که برای بهره‌وری هزینه و تأخیر کم بهینه شده است.
  • Gemini 2.5 Pro : پیشرفته‌ترین مدل استدلال ما تا به امروز.
  • Gemini 2.5 Flash : یک مدل متفکر که قابلیت‌های کاملی را ارائه می‌دهد. این مدل به گونه‌ای طراحی شده است که تعادلی بین قیمت و عملکرد برقرار کند.

ویژگی‌های کلیدی:

  • چندوجهی بودن : توانایی جمینی در درک و مدیریت قالب‌های اطلاعاتی متعدد، گامی مهم فراتر از مدل‌های سنتی زبان متنی است.
  • عملکرد : Gemini 2.5 Pro در بسیاری از معیارها از پیشرفته‌ترین مدل‌های فعلی پیشی گرفته و اولین مدلی بود که در معیار چالش‌برانگیز MMLU (درک زبان چندوظیفه‌ای گسترده) از متخصصان انسانی پیشی گرفت.
  • انعطاف‌پذیری : اندازه‌های مختلف Gemini آن را برای موارد استفاده مختلف، از تحقیقات در مقیاس بزرگ گرفته تا استقرار در دستگاه‌های تلفن همراه، قابل تنظیم می‌کند.

چگونه می‌توانید با استفاده از جاوا با Gemini در Vertex AI تعامل داشته باشید؟

شما دو گزینه دارید:

  1. API رسمی Vertex AI Java برای کتابخانه Gemini .
  2. چارچوب LangChain4j

در این آزمایشگاه کد، از چارچوب LangChain4j استفاده خواهید کرد.

چارچوب LangChain4j چیست؟

چارچوب LangChain4j یک کتابخانه متن‌باز برای ادغام LLMها در برنامه‌های جاوای شما است که با هماهنگ‌سازی اجزای مختلف، مانند خود LLM، و همچنین ابزارهای دیگری مانند پایگاه‌های داده برداری (برای جستجوهای معنایی)، بارگذاری‌کننده‌ها و تقسیم‌کننده‌های سند (برای تجزیه و تحلیل اسناد و یادگیری از آنها)، تجزیه‌کننده‌های خروجی و موارد دیگر، انجام می‌شود.

این پروژه از پروژه پایتون LangChain الهام گرفته شده است، اما با هدف خدمت به توسعه‌دهندگان جاوا.

bb908ea1e6c96ac2.png

آنچه یاد خواهید گرفت

  • نحوه راه‌اندازی یک پروژه جاوا برای استفاده از Gemini و LangChain4j
  • چگونه اولین اعلان خود را به صورت برنامه‌نویسی شده به Gemini ارسال کنیم؟
  • نحوه پخش پاسخ‌ها از Gemini
  • نحوه ایجاد مکالمه بین کاربر و Gemini
  • نحوه استفاده از Gemini در یک زمینه چندوجهی با ارسال متن و تصویر
  • چگونه اطلاعات ساختار یافته مفید را از محتوای بدون ساختار استخراج کنیم
  • نحوه دستکاری قالب‌های اعلان
  • نحوه انجام طبقه‌بندی متن مانند تحلیل احساسات
  • نحوه چت کردن با اسناد خودتان (بازیابی نسل افزوده)
  • چگونه چت‌بات‌های خود را با فراخوانی تابع گسترش دهیم؟
  • نحوه استفاده از Gemma به صورت محلی با Ollama و TestContainers

آنچه نیاز دارید

  • آشنایی با زبان برنامه نویسی جاوا
  • یک پروژه ابری گوگل
  • یک مرورگر، مانند کروم یا فایرفاکس

۲. تنظیمات و الزامات

تنظیم محیط خودتنظیم

  1. وارد کنسول گوگل کلود شوید و یک پروژه جدید ایجاد کنید یا از یک پروژه موجود دوباره استفاده کنید. اگر از قبل حساب جیمیل یا گوگل ورک اسپیس ندارید، باید یکی ایجاد کنید .

fbef9caa1602edd0.pnga99b7ace416376c4.png5e3ff691252acf41.png

  • نام پروژه، نام نمایشی برای شرکت‌کنندگان این پروژه است. این یک رشته کاراکتری است که توسط APIهای گوگل استفاده نمی‌شود. شما همیشه می‌توانید آن را به‌روزرسانی کنید.
  • شناسه پروژه در تمام پروژه‌های گوگل کلود منحصر به فرد است و تغییرناپذیر است (پس از تنظیم، قابل تغییر نیست). کنسول کلود به طور خودکار یک رشته منحصر به فرد تولید می‌کند؛ معمولاً برای شما مهم نیست که چه باشد. در اکثر آزمایشگاه‌های کد، باید شناسه پروژه خود را (که معمولاً با عنوان PROJECT_ID شناخته می‌شود) ارجاع دهید. اگر شناسه تولید شده را دوست ندارید، می‌توانید یک شناسه تصادفی دیگر ایجاد کنید. به عنوان یک جایگزین، می‌توانید شناسه خودتان را امتحان کنید و ببینید که آیا در دسترس است یا خیر. پس از این مرحله قابل تغییر نیست و در طول پروژه باقی می‌ماند.
  • برای اطلاع شما، یک مقدار سوم، شماره پروژه ، وجود دارد که برخی از APIها از آن استفاده می‌کنند. برای کسب اطلاعات بیشتر در مورد هر سه این مقادیر، به مستندات مراجعه کنید.
  1. در مرحله بعد، برای استفاده از منابع/API های ابری، باید پرداخت صورتحساب را در کنسول ابری فعال کنید . اجرای این آزمایشگاه کد هزینه زیادی نخواهد داشت، اگر اصلاً هزینه‌ای داشته باشد. برای خاموش کردن منابع به منظور جلوگیری از پرداخت صورتحساب پس از این آموزش، می‌توانید منابعی را که ایجاد کرده‌اید یا پروژه را حذف کنید. کاربران جدید Google Cloud واجد شرایط برنامه آزمایشی رایگان ۳۰۰ دلاری هستند.

شروع پوسته ابری

اگرچه می‌توان گوگل کلود را از راه دور و از طریق لپ‌تاپ شما مدیریت کرد، اما در این آزمایشگاه کد از Cloud Shell ، یک محیط خط فرمان که در فضای ابری اجرا می‌شود، استفاده خواهید کرد.

فعال کردن پوسته ابری

  1. از کنسول ابری، روی فعال کردن پوسته ابری کلیک کنید 853e55310c205094.png .

3c1dabeca90e44e5.png

اگر این اولین باری است که Cloud Shell را اجرا می‌کنید، یک صفحه میانی برای توضیح آن به شما نمایش داده می‌شود. اگر با یک صفحه میانی مواجه شدید، روی ادامه کلیک کنید.

9c92662c6a846a5c.png

آماده‌سازی و اتصال به Cloud Shell فقط چند لحظه طول می‌کشد.

9f0e51b578fecce5.png

این ماشین مجازی مجهز به تمام ابزارهای توسعه مورد نیاز است. این ماشین یک دایرکتوری خانگی پایدار ۵ گیگابایتی ارائه می‌دهد و در فضای ابری گوگل اجرا می‌شود که عملکرد شبکه و احراز هویت را تا حد زیادی افزایش می‌دهد. بخش عمده‌ای از کار شما در این آزمایشگاه کد، اگر نگوییم همه، را می‌توان با یک مرورگر انجام داد.

پس از اتصال به 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].

۳. آماده‌سازی محیط توسعه

در این آزمایشگاه کد، شما از ترمینال و ویرایشگر Cloud Shell برای توسعه برنامه‌های جاوا خود استفاده خواهید کرد.

فعال کردن API های هوش مصنوعی Vertex

در کنسول گوگل کلود، مطمئن شوید که نام پروژه شما در بالای کنسول گوگل کلود نمایش داده می‌شود. اگر اینطور نیست، روی «انتخاب یک پروژه» کلیک کنید تا « انتخابگر پروژه» باز شود و پروژه مورد نظر خود را انتخاب کنید.

شما می‌توانید APIهای Vertex AI را یا از بخش Vertex AI کنسول Google Cloud یا از ترمینال Cloud Shell فعال کنید.

برای فعال کردن از طریق کنسول گوگل کلود، ابتدا به بخش Vertex AI در منوی کنسول گوگل کلود بروید:

۴۵۱۹۷۶f1c8652341.png

در داشبورد Vertex AI، روی فعال کردن همه APIهای پیشنهادی کلیک کنید.

این کار چندین API را فعال می‌کند، اما مهم‌ترین آن‌ها برای codelab، aiplatform.googleapis.com است.

همچنین می‌توانید این API را از ترمینال Cloud Shell با دستور زیر فعال کنید:

gcloud services enable aiplatform.googleapis.com

مخزن گیت‌هاب را کلون کنید

در ترمینال 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 Shell باز کنید:

۴۲۹۰۸e۱۱b۲۸f۴۳۸۳.png

در ویرایشگر کد ابری، پوشه منبع codelab را با انتخاب File -> Open Folder باز کنید و به پوشه منبع codelab (مثلاً /home/username/gemini-workshop-for-java-developers/ ) اشاره کنید.

متغیرهای محیطی را تنظیم کنید

با انتخاب Terminal -> New Terminal یک ترمینال جدید در ویرایشگر کد ابری باز کنید. دو متغیر محیطی مورد نیاز برای اجرای مثال‌های کد را تنظیم کنید:

  • PROJECT_ID — شناسه پروژه Google Cloud شما
  • مکان — منطقه‌ای که مدل جمینی در آن مستقر شده است

متغیرها را به صورت زیر اکسپورت کنید:

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

۴. اولین تماس با مدل جمینی

حالا که پروژه به درستی راه‌اندازی شده است، وقت آن رسیده که API مربوط به Gemini را فراخوانی کنیم.

نگاهی به 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 ، مدل زبان چت را با استفاده از سازنده‌ی 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.

تبریک می‌گویم، شما اولین تماس خود را با جمینی برقرار کردید!

پاسخ استریمینگ

آیا متوجه شدید که پاسخ به صورت یکجا و پس از چند ثانیه داده شد؟ همچنین به لطف نوع پاسخ استریمینگ، می‌توان پاسخ را به صورت تدریجی دریافت کرد. در پاسخ استریمینگ، مدل پاسخ را قطعه قطعه و به محض در دسترس قرار گرفتن، برمی‌گرداند.

در این آزمایشگاه کد، ما به پاسخ غیر استریمینگ می‌پردازیم، اما بیایید نگاهی به پاسخ استریمینگ بیندازیم تا ببینیم چگونه می‌توان آن را انجام داد.

در فایل 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));
    }
}

این بار، ما انواع کلاس streaming یعنی VertexAiGeminiStreamingChatModel را که رابط StreamingChatLanguageModel را پیاده‌سازی می‌کند، وارد می‌کنیم. همچنین باید LambdaStreamingResponseHandler.onNext را به صورت استاتیک وارد کنید که یک متد کمکی است که StreamingResponseHandler ها را برای ایجاد یک stream handler با عبارات lambda جاوا فراهم می‌کند.

این بار، امضای متد generate() کمی متفاوت است. به جای برگرداندن یک رشته، نوع بازگشتی void است. علاوه بر اعلان، باید یک کنترل‌کننده پاسخ جریانی نیز ارسال کنید. در اینجا، به لطف import استاتیک که در بالا به آن اشاره کردیم، می‌توانیم یک عبارت لامبدا تعریف کنیم که آن را به متد onNext() ارسال کنید. عبارت لامبدا هر بار که بخش جدیدی از پاسخ در دسترس باشد فراخوانی می‌شود، در حالی که دومی فقط در صورت بروز خطا فراخوانی می‌شود.

اجرا:

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

شما پاسخی مشابه کلاس قبلی دریافت خواهید کرد، اما این بار متوجه خواهید شد که پاسخ به تدریج در پوسته شما ظاهر می‌شود، به جای اینکه منتظر نمایش پاسخ کامل باشید.

پیکربندی اضافی

برای پیکربندی، ما فقط پروژه، مکان و نام مدل را تعریف کردیم، اما پارامترهای دیگری نیز وجود دارند که می‌توانید برای مدل مشخص کنید:

  • temperature(Float temp) - برای تعریف میزان خلاقیت مورد نظر شما برای پاسخ (0 برای خلاقیت کم و اغلب واقعی‌تر، در حالی که 2 برای خروجی‌های خلاقانه‌تر)
  • topP(Float topP) — برای انتخاب کلمات ممکن که مجموع احتمال آنها برابر با آن عدد ممیز شناور (بین ۰ و ۱) باشد.
  • topK(Integer topK) — برای انتخاب تصادفی یک کلمه از حداکثر تعداد کلمات ممکن برای تکمیل متن (از ۱ تا ۴۰)
  • maxOutputTokens(Integer max) — برای مشخص کردن حداکثر طول پاسخ داده شده توسط مدل (به طور کلی، ۴ توکن تقریباً معادل ۳ کلمه هستند)
  • maxRetries(Integer retries) — در صورتی که سهمیه درخواست در هر زمان از حد مجاز بیشتر شده باشد، یا پلتفرم با مشکل فنی مواجه باشد، می‌توانید از مدل بخواهید که فراخوانی را ۳ بار دوباره امتحان کند.

تا اینجا، شما یک سوال از 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 — کلاسی که به مدیریت جنبه چند نوبتی مکالمه کمک می‌کند و سوالات و پاسخ‌های قبلی را در حافظه محلی نگه می‌دارد.
  • AiServices - یک کلاس انتزاعی سطح بالاتر که مدل چت و حافظه چت را به هم پیوند می‌دهد.

در متد اصلی، شما قرار است مدل، حافظه چت و سرویس هوش مصنوعی را تنظیم کنید. مدل طبق معمول با اطلاعات پروژه، مکان و نام مدل پیکربندی شده است.

برای حافظه چت، ما از سازنده MessageWindowChatMemory برای ایجاد حافظه‌ای که 20 پیام آخر رد و بدل شده را نگه می‌دارد، استفاده می‌کنیم. این یک پنجره کشویی روی مکالمه است که زمینه آن به صورت محلی در کلاینت کلاس جاوا ما نگهداری می‌شود.

سپس AI service را ایجاد می‌کنید که مدل چت را به حافظه چت متصل می‌کند.

توجه کنید که سرویس هوش مصنوعی چگونه از رابط کاربری سفارشی ConversationService که ما تعریف کرده‌ایم و LangChain4j آن را پیاده‌سازی کرده است، استفاده می‌کند و این رابط یک کوئری String می‌گیرد و یک پاسخ String برمی‌گرداند.

حالا وقت آن رسیده که با Gemini مکالمه‌ای داشته باشیم. ابتدا یک احوالپرسی ساده ارسال می‌شود، سپس اولین سوال در مورد برج ایفل پرسیده می‌شود تا مشخص شود که در کدام کشور می‌توان آن را پیدا کرد. توجه داشته باشید که جمله آخر مربوط به پاسخ سوال اول است، زیرا شما می‌خواهید بدانید که چند نفر در کشوری که برج ایفل در آن قرار دارد، ساکن هستند، بدون اینکه صریحاً به کشوری که در پاسخ قبلی داده شده بود، اشاره کنید. این نشان می‌دهد که سوالات و پاسخ‌های قبلی با هر سوال ارسال می‌شوند.

نمونه را اجرا کنید:

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

شما باید سه پاسخ مشابه این موارد را ببینید:

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

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

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

شما می‌توانید با Gemini سوالات تک‌نوبتی بپرسید یا مکالمات چندنوبتی داشته باشید، اما تاکنون ورودی فقط متن بوده است. در مورد تصاویر چطور؟ بیایید در مرحله بعدی تصاویر را بررسی کنیم.

۶. چندوجهی بودن با جمینی

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());
    }
}

در بخش importها، توجه داشته باشید که ما بین انواع مختلف پیام‌ها و محتواها تمایز قائل می‌شویم. یک 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.

ترکیب تصاویر و متن‌های راهنما، کاربردهای جالبی را ایجاد می‌کند. می‌توانید برنامه‌هایی بسازید که بتوانند:

  • متن را در تصاویر تشخیص دهید.
  • بررسی کنید که آیا نمایش یک تصویر ایمن است یا خیر.
  • زیرنویس تصاویر ایجاد کنید.
  • جستجو در پایگاه داده‌ای از تصاویر با توضیحات متنی ساده.

علاوه بر استخراج اطلاعات از تصاویر، می‌توانید اطلاعات را از متن بدون ساختار نیز استخراج کنید. این چیزی است که در بخش بعدی یاد خواهید گرفت.

۷. استخراج اطلاعات ساختاریافته از متن بدون ساختار

موقعیت‌های زیادی وجود دارد که اطلاعات مهم در اسناد گزارش، ایمیل‌ها یا سایر متون طولانی به صورت غیرساختاریافته ارائه می‌شوند. در حالت ایده‌آل، شما دوست دارید بتوانید جزئیات کلیدی موجود در متن غیرساختاریافته را به شکل اشیاء ساختاریافته استخراج کنید. بیایید ببینیم چگونه می‌توانید این کار را انجام دهید.

فرض کنید می‌خواهید نام و سن یک شخص را با توجه به بیوگرافی، رزومه یا توضیحات آن شخص استخراج کنید. می‌توانید با یک اعلان هوشمندانه تنظیم‌شده (که معمولاً «مهندسی اعلان» نامیده می‌شود) به LLM دستور دهید تا JSON را از متن بدون ساختار استخراج کند.

اما در مثال زیر، به جای ایجاد یک prompt که خروجی 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 برمی‌گرداند، که برای شما تجزیه و تحلیل شده و در یک نمونه Person دسته‌بندی می‌شود.

حالا بیایید به محتوای متد main() نگاهی بیندازیم:

  • مدل چت پیکربندی و نمونه‌سازی شده است. ما از دو متد جدید از کلاس سازنده مدل استفاده می‌کنیم: responseMimeType() و responseSchema() . متد اول به Gemini می‌گوید که JSON معتبری را در خروجی تولید کند. متد دوم، طرحواره شیء JSON را که باید برگردانده شود، تعریف می‌کند. علاوه بر این، متد دوم به یک متد کمکی واگذار می‌شود که قادر است یک کلاس یا رکورد جاوا را به یک طرحواره 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 انتزاعی شده است و به عنوان یک توسعه‌دهنده جاوا، وقتی از این رابط PersonExtractor استفاده می‌کنید، فقط کلاس‌ها و اشیاء معمولی را دستکاری می‌کنید.

۸. ساختاردهی به سوالات با استفاده از قالب‌های سوالات

وقتی با یک LLM با استفاده از مجموعه‌ای از دستورالعمل‌ها یا سوالات رایج تعامل می‌کنید، بخشی از آن پیام هرگز تغییر نمی‌کند، در حالی که بخش‌های دیگر حاوی داده‌ها هستند. به عنوان مثال، اگر می‌خواهید دستور پخت غذا ایجاد کنید، ممکن است از پیامی مانند "شما یک سرآشپز بااستعداد هستید، لطفاً یک دستور غذا با مواد تشکیل‌دهنده زیر ایجاد کنید: ..." استفاده کنید و سپس مواد تشکیل‌دهنده را به انتهای آن متن اضافه کنید. قالب‌های پیام برای همین کار هستند - مشابه رشته‌های درون‌یابی شده در زبان‌های برنامه‌نویسی. یک قالب پیام شامل متغیرهایی است که می‌توانید آنها را با داده‌های مناسب برای یک فراخوانی خاص به 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 بالا پیکربندی می‌کنید. سپس با ارسال رشته‌ی prompt ما، یک PromptTemplate با متد استاتیک from() آن ایجاد می‌کنید و از متغیرهای درون آکولاد و استفاده می‌کنید.

شما با فراخوانی apply() اعلان نهایی را ایجاد می‌کنید که نقشه‌ای از جفت‌های کلید/مقدار را می‌گیرد که نشان‌دهنده نام متغیر و مقدار رشته‌ای برای جایگزینی آن است.

در نهایت، با ایجاد یک پیام کاربر از آن اعلان، با دستورالعمل prompt.toUserMessage() ، متد generate() از مدل Gemini را فراخوانی می‌کنید.

نمونه را اجرا کنید:

./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 هستند. می‌توانید داده‌ها را ارسال کنید و اعلان‌ها را برای مقادیر مختلف ارائه شده توسط کاربران خود سفارشی کنید.

۹. طبقه‌بندی متن با فراخوانی چند مرحله‌ای

دانشجویان کارشناسی ارشد مدیریت بازرگانی (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 را طبق معمول ایجاد می‌کنید، اما با حداکثر تعداد توکن خروجی کوچک، زیرا فقط یک پاسخ کوتاه می‌خواهید: متن POSITIVE ، NEGATIVE یا NEUTRAL است. و برای محدود کردن مدل به بازگرداندن منحصراً آن مقادیر، می‌توانید از پشتیبانی خروجی ساختاریافته که در بخش استخراج داده‌ها کشف کردید، بهره ببرید. به همین دلیل از متد responseSchema() استفاده می‌شود. این بار، شما از متد مناسب SchemaHelper برای استنباط تعریف طرحواره استفاده نمی‌کنید، بلکه به جای آن از سازنده Schema برای درک اینکه تعریف طرحواره چگونه است، استفاده خواهید کرد.

پس از پیکربندی مدل، یک رابط SentimentAnalysis ایجاد می‌کنید که AiServices از LangChain4j با استفاده از LLM برای شما پیاده‌سازی خواهد کرد. این رابط شامل یک متد است: analyze() . این متد متن را برای تجزیه و تحلیل در ورودی می‌گیرد و یک مقدار شمارشی Sentiment را برمی‌گرداند. بنابراین شما فقط یک شیء با نوع قوی را دستکاری می‌کنید که نشان‌دهنده کلاس احساسی است که تشخیص داده می‌شود.

سپس، برای اینکه «چند مثال اولیه» برای ترغیب مدل به انجام کار طبقه‌بندی ارائه دهید، یک حافظه چت ایجاد می‌کنید تا جفت‌هایی از پیام‌های کاربر و پاسخ‌های هوش مصنوعی را که نمایانگر متن و احساسات مرتبط با آن است، منتقل کنید.

بیایید همه چیز را با متد AiServices.builder() به هم متصل کنیم، با ارسال رابط SentimentAnalysis ، مدلی که باید استفاده شود و حافظه چت به همراه مثال‌های چند قسمتی. در نهایت، متد analyze() را با متنی که باید آنالیز شود، فراخوانی کنید.

نمونه را اجرا کنید:

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

شما باید یک کلمه واحد را ببینید:

POSITIVE

به نظر می‌رسد دوست داشتن توت فرنگی یک حس مثبت است!

۱۰. بازیابی نسل افزوده

LLMها بر اساس حجم زیادی از متن آموزش می‌بینند. با این حال، دانش آنها فقط اطلاعاتی را پوشش می‌دهد که در طول آموزش خود دیده است. اگر اطلاعات جدیدی پس از تاریخ پایان آموزش مدل منتشر شود، آن جزئیات برای مدل در دسترس نخواهد بود. بنابراین، مدل قادر به پاسخ به سوالات مربوط به اطلاعاتی که ندیده است، نخواهد بود.

به همین دلیل است که رویکردهایی مانند بازیابی افزوده نسل (RAG) که در این بخش به آنها پرداخته خواهد شد، به ارائه اطلاعات اضافی که یک LLM ممکن است برای انجام درخواست‌های کاربران خود، پاسخ به اطلاعات به‌روزتر یا اطلاعات خصوصی که در زمان آموزش در دسترس نیست، به دانستن آنها نیاز داشته باشد، کمک می‌کند.

بیایید به مکالمات برگردیم. این بار، شما قادر خواهید بود در مورد اسناد خود سؤال بپرسید. شما یک چت‌بات خواهید ساخت که قادر است اطلاعات مرتبط را از یک پایگاه داده حاوی اسناد شما که به قطعات کوچکتر ("تکه‌ها") تقسیم شده‌اند، بازیابی کند و این اطلاعات توسط مدل برای پاسخ‌های خود استفاده می‌شود، به جای اینکه صرفاً به دانش موجود در آموزش خود تکیه کند.

در RAG، دو مرحله وجود دارد:

  1. مرحله‌ی هضم - اسناد در حافظه بارگذاری می‌شوند، به تکه‌های کوچک‌تر تقسیم می‌شوند و تعبیه‌های برداری (یک نمایش برداری چندبعدی بالا از تکه‌ها) محاسبه و در یک پایگاه داده‌ی برداری که قادر به انجام جستجوهای معنایی است، ذخیره می‌شوند. این مرحله‌ی هضم معمولاً یک بار انجام می‌شود، زمانی که نیاز به اضافه شدن اسناد جدید به مجموعه اسناد باشد.

cd07d33d20ffa1c8.png

  1. مرحله پرس‌وجو - کاربران اکنون می‌توانند در مورد اسناد سؤال بپرسند. سؤال نیز به یک بردار تبدیل شده و با سایر بردارهای موجود در پایگاه داده مقایسه می‌شود. بردارهای مشابه معمولاً از نظر معنایی مرتبط هستند و توسط پایگاه داده بردار بازگردانده می‌شوند. سپس، به LLM زمینه مکالمه، بخش‌هایی از متن که با بردارهای بازگردانده شده توسط پایگاه داده مطابقت دارند، داده می‌شود و از او خواسته می‌شود تا با نگاه کردن به آن بخش‌ها، پاسخ خود را ارائه دهد.

a1d2e2deb83c6d27.png

مدارک خود را آماده کنید

برای این مثال جدید، شما در مورد یک مدل ماشین فرضی از یک سازنده ماشین فرضی دیگر، یعنی ماشین Cymbal Starlight، سوال خواهید پرسید! ایده این است که سندی در مورد یک ماشین فرضی نباید بخشی از دانش مدل باشد. بنابراین اگر Gemini بتواند به سوالات مربوط به این ماشین به درستی پاسخ دهد، به این معنی است که رویکرد RAG کار می‌کند: این رویکرد قادر به جستجو در سند شما است.

پیاده سازی چت بات

بیایید بررسی کنیم که چگونه می‌توان رویکرد دو مرحله‌ای را ایجاد کرد: ابتدا با دریافت سند، و سپس زمان پرس‌وجو (که "مرحله بازیابی" نیز نامیده می‌شود) زمانی که کاربران در مورد سند سؤال می‌پرسند.

در این مثال، هر دو مرحله در یک کلاس پیاده‌سازی شده‌اند. به طور معمول، شما یک برنامه دارید که وظیفه دریافت (ingestion) را بر عهده دارد و برنامه دیگری که رابط کاربری ربات چت را به کاربران شما ارائه می‌دهد.

همچنین، در این مثال ما از یک پایگاه داده برداری درون حافظه‌ای استفاده خواهیم کرد. در یک سناریوی تولید واقعی، مراحل دریافت و پرس‌وجو در دو برنامه مجزا از هم جدا می‌شوند و بردارها در یک پایگاه داده مستقل ذخیره می‌شوند.

بلعیدن سند

اولین قدم در فاز دریافت سند، یافتن فایل 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 را به قطعات ۵۰۰ کاراکتری تقسیم می‌کند که ۱۰۰ کاراکتر آن با هم همپوشانی دارند (با قطعه بعدی، برای جلوگیری از بریدن کلمات یا جملات، به صورت تکه تکه).

ورودی ذخیره، تقسیم‌کننده سند، مدل جاسازی برای محاسبه بردارها و پایگاه داده بردار درون حافظه را به هم پیوند می‌دهد. سپس، متد 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);
}

The CarExpert interface returns a string response wrapped in LangChain4j's Result class. Why use this wrapper? Because not only it will give you the answer, but it will also let you examine the chunks from the database that have been returned by the content retriever. That way, you can display the sources of the document(s) that are used to ground the final answer to the user.

At this point, you can configure a new AI service:

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

This service binds together:

  • The chat language model that you configured earlier.
  • A chat memory to keep track of the conversation.
  • The retriever compares a vector embedding query to the vectors in the database.
.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())

You're finally ready to ask your questions!

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());
});

The full source code is in RAG.java in app/src/main/java/gemini/workshop directory.

Run the sample:

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

In the output, you should see answers to your questions:

=== 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. Function calling

There are situations where you would like an LLM to have access to external systems, like a remote web API that retrieves information or have an action, or services that perform some kind of computation. For example:

Remote web APIs:

  • Track and update customer orders.
  • Find or create a ticket in an issue tracker.
  • Fetch real time data like stock quotes or IoT sensor measurements.
  • ارسال ایمیل.

Computation tools:

  • A calculator for more advanced math problems.
  • Code interpretation for running code when LLMs need reasoning logic.
  • Convert natural language requests into SQL queries so that an LLM can query a database.

Function calling (sometimes called tools, or tool use) is the ability for the model to request one or more function calls to be made on its behalf, so it can properly answer a user's prompt with fresher data.

Given a particular prompt from a user, and the knowledge of existing functions that can be relevant to that context, an LLM can reply with a function call request. The application integrating the LLM can then call the function on its behalf, and then reply back to the LLM with a response, and the LLM then interprets back by replying with a textual answer.

Four steps of function calling

Let's have a look at an example of function calling: getting information about the weather forecast.

If you ask Gemini or any other LLM about the weather in Paris, they would reply by saying that it has no information about the current weather forecast. If you want the LLM to have real time access to the weather data, you need to define some functions it can request to be used.

Take a look at the following diagram:

31e0c2aba5e6f21c.png

1️⃣ First, a user asks about the weather in Paris. The chatbot app (using LangChain4j) knows there are one or more functions that are at its disposal to help the LLM fulfill the query. The chatbot both sends the initial prompt, as well as the list of functions that can be called. Here, a function called getWeather() which takes a string parameter for the location.

8863be53a73c4a70.png

As the LLM doesn't know about weather forecasts, instead of replying via text, it sends back a function execution request. The chatbot must call the getWeather() function with "Paris" as location parameter.

2️⃣ The chatbot invokes that function on behalf of the LLM, retrieves the function response. Here, we imagine that the response is {"forecast": "sunny"} .

d1367cc69c07b14d.png

3️⃣ The chatbot app sends the JSON response back to the LLM.

73a5f2ed19f47d8.png

4️⃣ The LLM looks at the JSON response, interprets that information, and eventually replies back with the text that the weather is sunny in Paris.

20832cb1ee6fbfeb.png

Each step as code

First, you'll configure the Gemini model as usual:

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

You define a tool specification that describes the function that can be called:

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();

The name of the function is defined, as well as the name and type of the parameter, but notice that both the function and the parameters are given descriptions. Descriptions are very important and help the LLM really understand what a function can do, and thus judge whether this function needs to be called in the context of the conversation.

Let's start step #1, by sending the initial question about the weather in Paris:

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);

In step #2, we pass the tool we'd like the model to use, and the model replies with a too execution request:

// 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());

Step #3. At this point, we know what function the LLM would like us to call. In the code, we're not making a real call to an external API, we just return an hypothetical weather forecast directly:

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

And in step #4, the LLM learns about the function execution result, and can then synthesize a textual response:

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

The full source code is in FunctionCalling.java in app/src/main/java/gemini/workshop directory.

Run the sample:

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

You should see an output similar to the following:

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.

You can see in the output above the tool execution request, as well as the answer.

12. LangChain4j handles function calling

In the previous step, you saw how the normal text question/answer and function request/response interactions are interleaved, and in between, you provided the requested function response directly, without calling a real function.

However, LangChain4j also offers a higher-level abstraction that can handle the function calls transparently for you, while handling the conversation as usual.

Single function call

Let's have a look at FunctionCallingAssistant.java , piece by piece.

First, you create a record that will represent the function's response data structure:

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

The response contains information about the location, the forecast, and the temperature.

Then you create a class that contains the actual function you want to make available to the model:

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);
        }
    }
}

Note that this class contains a single function, but it is annotated with the @Tool annotation which corresponds to the description of the function the model can request to call.

The parameters of the function (a single one here) is also annotated, but with this short @P annotation, which also gives a description of the parameter. You could add as many functions as you wanted, to make them available to the model, for more complex scenarios.

In this class, you return some canned responses, but if you wanted to call a real external weather forecast service, this is in the body of that method that you would make the call to that service.

As we saw when you created a ToolSpecification in the previous approach, it's important to document what a function does, and describe what the parameters correspond to. This helps the model understand how and when this function can be used.

Next, LangChain4j lets you provide an interface that corresponds to the contract you want to use to interact with the model. Here, it's a simple interface that takes in a string representing the user message, and returns a string corresponding to the model's response:

interface WeatherAssistant {
    String chat(String userMessage);
}

It is also possible to use more complex signatures that involve LangChain4j's UserMessage (for a user message) or AiMessage (for a model response), or even a TokenStream , if you want to handle more advanced situations, as those more complicated objects also contain extra information such as the number of tokens consumed, etc. But for simplicity sake, we'll just take string in input, and string in output.

Let's finish with the main() method that ties all the pieces together:

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?"));
}

As usual, you configure the Gemini chat model. Then you instantiate your weather forecast service that contains the "function" that the model will request us to call.

Now, you use the AiServices class again to bind the chat model, the chat memory, and the tool (ie. the weather forecast service with its function). AiServices returns an object that implements your WeatherAssistant interface you defined. The only thing left is to call the chat() method of that assistant. When invoking it, you will only see the text responses, but the function call requests and the function call responses will not be visible from the developer, and those requests will be handled automatically and transparently. If Gemini thinks a function should be called, it'll reply with the function call request, and LangChain4j will take care of calling the local function on your behalf.

Run the sample:

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

You should see an output similar to the following:

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

This was an example of a single function.

Multiple function calls

You can also have multiple functions and let LangChain4j handle multiple function calls on your behalf. Take a look at MultiFunctionCallingAssistant.java for a multiple function example.

It has a function to convert currencies:

@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;
}

Another function to get the value of a stock:

@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;
}

Another function to apply a percentage to a given amount:

@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;
}

You can then combine all these functions and a MultiTools class and ask questions like "What is 10% of AAPL stock price converted from USD to EUR?""

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?"));
}

Run it as follows:

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

And you should see the multiple functions called:

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.

Towards Agents

Function calling is a great extension mechanism for large language models like Gemini. It enables us to build more complex systems often called "agents" or "AI assistants". These agents can interact with the external world via external APIs and with services that can have side effects on the external environment (like sending emails, creating tickets, etc.)

When creating such powerful agents, you should do so responsibly. You should consider a human-in-the-loop before making automatic actions. It's important to keep safety in mind when designing LLM-powered agents that interact with the external world.

13. Running Gemma with Ollama and TestContainers

So far, we've been using Gemini but there's also Gemma , its little sister model.

Gemma is a family of lightweight, state-of-the-art open models built from the same research and technology used to create the Gemini models. The latest Gemma model is Gemma3 available in four sizes: 1B ( text-only ), 4B, 12B and 27B. Their weights are freely available, and their small sizes means you can run it on your own, even on your laptop or in Cloud Shell.

How do you run Gemma?

There are many ways to run Gemma: in the cloud, via Vertex AI with a click of a button, or GKE with some GPUs, but you can also run it locally.

One good option to run Gemma locally is with Ollama , a tool that lets you run small models, like Llama, Mistral, and many others on your local machine. It's similar to Docker but for LLMs.

Install Ollama following the instruction for your Operating System.

If you are using a Linux environment you will need to enable Ollama first after installing it.

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

Once installed locally, you can run commands to pull a model:

ollama pull gemma3:1b

Wait for the model to be pulled. This can take some time.

Run the model:

ollama run gemma3:1b

Now, you can interact with the model:

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

To exit the prompt press Ctrl+D

Running Gemma in Ollama on TestContainers

Instead of having to install and run Ollama locally, you can use Ollama within a container, handled by TestContainers .

TestContainers is not only useful for testing, but you can also use it for executing containers. There's even a specific OllamaContainer you can take advantage of!

Here's the whole picture:

2382c05a48708dfd.png

پیاده‌سازی

Let's have a look at GemmaWithOllamaContainer.java , piece by piece.

First, you need to create a derived Ollama container that pulls in the Gemma model. This image either already exists from a previous run or it will be created. If the image already exists, you're just going to tell TestContainers that you want to substitute the default Ollama image with your Gemma-powered variant:

    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"));
    }

Next, you create and start an Ollama test container and then create an Ollama chat model, by pointing at the address and port of the container with the model you want to use. Finally, you just invoke model.generate(yourPrompt) as usual:

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);
}

Run it as follows:

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

The first run will take a while to create and run the container but once done, you should see Gemma responding:

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.

You have Gemma running in Cloud Shell!

۱۴. تبریک

Congratulations, you've successfully built your first Generative AI chat application in Java using LangChain4j and the Gemini API! You discovered along the way that multimodal large language models are pretty powerful and capable of handling various tasks like question/answering, even on your own documentation, data extraction, interacting with external APIs, and more.

بعدش چی؟

It's your turn to enhance your applications with powerful LLM integrations!

مطالعه بیشتر

اسناد مرجع