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

a99b7ace416376c4.png

5e3ff691252acf41.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);
}

رابط CarExpert یک پاسخ رشته‌ای را که در کلاس Result از LangChain4j قرار داده شده است، برمی‌گرداند. چرا از این پوشش استفاده کنیم؟ زیرا نه تنها پاسخ را به شما می‌دهد، بلکه به شما امکان می‌دهد بخش‌هایی از پایگاه داده را که توسط بازیابی‌کننده محتوا برگردانده شده‌اند، بررسی کنید. به این ترتیب، می‌توانید منابع سند(های) مورد استفاده برای پایه‌گذاری پاسخ نهایی را به کاربر نمایش دهید.

در این مرحله، می‌توانید یک سرویس هوش مصنوعی جدید را پیکربندی کنید:

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

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

  • مدل زبان چت که قبلاً پیکربندی کرده‌اید.
  • حافظه چت برای پیگیری مکالمه.
  • بازیابی‌کننده، یک پرس‌وجوی جاسازی بردار را با بردارهای موجود در پایگاه داده مقایسه می‌کند.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in car automotive, and you answer concisely.

            Here is the question: {{userMessage}}

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

بالاخره آماده‌اید تا سوالاتتان را بپرسید!

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

کد منبع کامل در فایل RAG.java در دایرکتوری app/src/main/java/gemini/workshop قرار دارد.

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

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

در خروجی، باید پاسخ سوالات خود را ببینید:

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

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

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

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

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

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

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

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

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

۱۱. فراخوانی تابع

موقعیت‌هایی وجود دارد که شما می‌خواهید یک LLM به سیستم‌های خارجی دسترسی داشته باشد، مانند یک API وب از راه دور که اطلاعات را بازیابی می‌کند یا عملی انجام می‌دهد، یا سرویس‌هایی که نوعی محاسبه انجام می‌دهند. به عنوان مثال:

API های وب از راه دور:

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

ابزارهای محاسباتی:

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

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

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

چهار مرحله فراخوانی تابع

بیایید نگاهی به مثالی از فراخوانی تابع بیندازیم: دریافت اطلاعات در مورد پیش‌بینی آب و هوا.

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

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

31e0c2aba5e6f21c.png

1️⃣ ابتدا، کاربری در مورد آب و هوای پاریس سوال می‌کند. برنامه چت‌بات (با استفاده از LangChain4j) می‌داند که یک یا چند تابع وجود دارد که برای کمک به LLM در انجام پرس و جو در اختیار دارد. چت‌بات هم درخواست اولیه و هم لیست توابعی را که می‌توان فراخوانی کرد، ارسال می‌کند. در اینجا، تابعی به نام getWeather() وجود دارد که یک پارامتر رشته‌ای برای مکان می‌گیرد.

8863be53a73c4a70.png

از آنجایی که LLM از پیش‌بینی‌های آب و هوا اطلاعی ندارد، به جای پاسخ دادن از طریق متن، یک درخواست اجرای تابع ارسال می‌کند. چت‌بات باید تابع getWeather() را با پارامتر مکان "Paris" فراخوانی کند.

2️⃣ ربات گفتگو آن تابع را از طرف LLM فراخوانی می‌کند و پاسخ تابع را بازیابی می‌کند. در اینجا، ما تصور می‌کنیم که پاسخ {"forecast": "sunny"} است.

d1367cc69c07b14d.png

۳️⃣ برنامه چت‌بات پاسخ JSON را به LLM ارسال می‌کند.

73a5f2ed19f47d8.png

۴️⃣ LLM به پاسخ JSON نگاه می‌کند، آن اطلاعات را تفسیر می‌کند و در نهایت با متن پاسخ می‌دهد که هوا در پاریس آفتابی است.

20832cb1ee6fbfeb.png

هر مرحله به صورت کد

ابتدا، مدل Gemini را طبق معمول پیکربندی خواهید کرد:

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

شما یک مشخصات ابزار تعریف می‌کنید که تابعی را که می‌توان فراخوانی کرد، توصیف می‌کند:

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

نام تابع و همچنین نام و نوع پارامتر تعریف شده است، اما توجه داشته باشید که هم تابع و هم پارامترها توضیحاتی دارند. توضیحات بسیار مهم هستند و به LLM کمک می‌کنند تا واقعاً بفهمد که یک تابع چه کاری می‌تواند انجام دهد و بنابراین قضاوت کند که آیا این تابع باید در متن مکالمه فراخوانی شود یا خیر.

بیایید مرحله ۱ را با ارسال سوال اولیه در مورد آب و هوای پاریس شروع کنیم:

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

در مرحله‌ی دوم، ابزاری را که می‌خواهیم مدل از آن استفاده کند، ارسال می‌کنیم و مدل با یک درخواست اجرای too پاسخ می‌دهد:

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

مرحله ۳. در این مرحله، ما می‌دانیم که 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);

و در مرحله شماره ۴، LLM در مورد نتیجه اجرای تابع اطلاعات کسب می‌کند و سپس می‌تواند یک پاسخ متنی را سنتز کند:

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

کد منبع کامل در فایل FunctionCalling.java در دایرکتوری app/src/main/java/gemini/workshop قرار دارد.

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

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

شما باید خروجی مشابه زیر را ببینید:

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

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

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

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.

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

همانطور که در رویکرد قبلی هنگام ایجاد یک 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-2.0-flash")
        .build();

    WeatherForecastService weatherForecastService = new WeatherForecastService();

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

    System.out.println(assistant.chat("What is the weather in Paris?"));
    System.out.println(assistant.chat("Is it warmer in London or in Paris?"));
}

طبق معمول، مدل چت Gemini را پیکربندی می‌کنید. سپس سرویس پیش‌بینی آب و هوای خود را که شامل «تابعی» است که مدل از ما درخواست فراخوانی آن را خواهد کرد، نمونه‌سازی می‌کنید.

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.

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

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

شما باید خروجی مشابه زیر را ببینید:

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

It is warmer in Paris (20 degrees) than in London (15 degrees).

این یک مثال از یک تابع واحد بود.

فراخوانی‌های چندگانه تابع

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

این تابعی برای تبدیل ارزها دارد:

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

    double result = amount;

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

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

    return result;
}

تابع دیگری برای بدست آوردن ارزش سهام:

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

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

    return result;
}

تابع دیگری برای اعمال درصد به مقدار داده شده:

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

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

    return result;
}

سپس می‌توانید همه این توابع و یک کلاس MultiTools را با هم ترکیب کنید و سوالاتی مانند «۱۰٪ از قیمت سهام AAPL که از دلار آمریکا به یورو تبدیل شده است، چقدر می‌شود؟» بپرسید.

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

    MultiTools multiTools = new MultiTools();

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

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

آن را به صورت زیر اجرا کنید:

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

و شما باید چندین تابع به نام‌های زیر را ببینید:

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

به سمت نمایندگان

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

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

۱۳. اجرای Gemma با Ollama و TestContainers

تا اینجا، ما از Gemini استفاده می‌کردیم، اما Gemma هم هست، مدل خواهر کوچک‌ترش.

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.

چطور جما را اداره می‌کنی؟

روش‌های زیادی برای اجرای Gemma وجود دارد: در فضای ابری، از طریق Vertex AI با یک کلیک، یا GKE با برخی از GPUها، اما می‌توانید آن را به صورت محلی نیز اجرا کنید.

یک گزینه خوب برای اجرای Gemma به صورت محلی، استفاده از Ollama است، ابزاری که به شما امکان می‌دهد مدل‌های کوچکی مانند Llama، Mistral و بسیاری دیگر را روی دستگاه محلی خود اجرا کنید. این ابزار شبیه Docker است اما برای LLMها.

Ollama را طبق دستورالعمل مربوط به سیستم عامل خود نصب کنید.

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

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

پس از نصب محلی، می‌توانید دستورات زیر را برای دریافت مدل اجرا کنید:

ollama pull gemma3:1b

صبر کنید تا مدل کشیده شود. این کار ممکن است مدتی طول بکشد.

مدل را اجرا کنید:

ollama run gemma3:1b

حالا می‌توانید با مدل تعامل داشته باشید:

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

برای خروج از اعلان، Ctrl+D را فشار دهید.

اجرای Gemma در Ollama روی TestContainers

به جای اینکه مجبور باشید Ollama را به صورت محلی نصب و اجرا کنید، می‌توانید از Ollama درون یک کانتینر که توسط TestContainers مدیریت می‌شود، استفاده کنید.

TestContainers نه تنها برای تست مفید است، بلکه می‌توانید از آن برای اجرای کانتینرها نیز استفاده کنید. حتی یک OllamaContainer خاص نیز وجود دارد که می‌توانید از آن بهره ببرید!

اینم کل تصویر:

2382c05a48708dfd.png

پیاده‌سازی

بیایید نگاهی به GemmaWithOllamaContainer.java ، به صورت جزئی، بیندازیم.

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

    private static final String TC_OLLAMA_GEMMA3 = "tc-ollama-gemma3-1b";
    public static final String GEMMA_3 = "gemma3:1b";

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

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

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

        System.out.println("Ollama image substitution...");
        // Substitute the default Ollama image with our Gemma variant
        return new OllamaContainer(
            DockerImageName.parse(TC_OLLAMA_GEMMA3)
                .asCompatibleSubstituteFor("ollama/ollama"));
    }

در مرحله بعد، یک کانتینر آزمایشی Ollama ایجاد و اجرا می‌کنید و سپس با اشاره به آدرس و پورت کانتینر حاوی مدلی که می‌خواهید استفاده کنید، یک مدل چت Ollama ایجاد می‌کنید. در نهایت، طبق معمول، model.generate(yourPrompt) را فراخوانی می‌کنید:

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

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

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

    System.out.println(response);
}

آن را به صورت زیر اجرا کنید:

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

اولین اجرا کمی طول می‌کشد تا کانتینر ایجاد و اجرا شود، اما پس از اتمام، باید پاسخ Gemma را ببینید:

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

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

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

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

جما رو داری که تو کلود شل بازی می‌کنه!

۱۴. تبریک

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

بعدش چی؟

نوبت شماست که برنامه‌های خود را با ادغام‌های قدرتمند LLM بهبود بخشید!

مطالعه بیشتر

اسناد مرجع