Gemini ใน Java พร้อม Vertex AI และ LangChain4j

1. เกริ่นนำ

Codelab นี้มุ่งเน้นไปที่โมเดลภาษาขนาดใหญ่ (LLM) ของ Gemini ซึ่งโฮสต์บน Vertex AI ใน Google Cloud Vertex AI เป็นแพลตฟอร์มที่รวมผลิตภัณฑ์ บริการ และโมเดลแมชชีนเลิร์นนิงทั้งหมดใน Google Cloud

คุณจะใช้ Java เพื่อโต้ตอบกับ Gemini API โดยใช้เฟรมเวิร์ก LangChain4j คุณจะได้ยกตัวอย่างที่เป็นรูปธรรมเพื่อใช้ประโยชน์จาก LLM ในการตอบคำถาม การสร้างแนวคิด การดึงเอนทิตีและเนื้อหาที่มีโครงสร้าง การดึงข้อมูลการสร้างแบบเสริม (AR) และการเรียกใช้ฟังก์ชัน

Generative AI คืออะไร

Generative AI หมายถึงการใช้ปัญญาประดิษฐ์ (AI) เพื่อสร้างเนื้อหาใหม่ เช่น ข้อความ รูปภาพ เพลง เสียง และวิดีโอ

Generative AI ขับเคลื่อนโดยโมเดลภาษาขนาดใหญ่ (LLM) ที่สามารถทํางานหลายอย่างพร้อมกันและทํางานได้ทันที เช่น การสรุป ถามและตอบ การแยกประเภท และอีกมากมาย เมื่อใช้การฝึกเพียงเล็กน้อย โมเดลพื้นฐานจะนำไปปรับให้เข้ากับกรณีการใช้งานเป้าหมายที่มีข้อมูลตัวอย่างน้อยมากได้

Generative AI ทำงานอย่างไร

Generative AI ทำงานโดยใช้โมเดลแมชชีนเลิร์นนิง (ML) เพื่อเรียนรู้รูปแบบและความสัมพันธ์ในชุดข้อมูลที่มนุษย์สร้างขึ้น จากนั้นใช้รูปแบบที่เรียนรู้เพื่อสร้างเนื้อหาใหม่

วิธีที่พบบ่อยที่สุดในการฝึกโมเดล Generative AI คือการใช้การเรียนรู้ภายใต้การควบคุมดูแล โมเดลจะได้รับชุดเนื้อหาที่มนุษย์สร้างขึ้นและป้ายกำกับที่เกี่ยวข้อง จากนั้นจะเรียนรู้การสร้างเนื้อหาที่คล้ายกับเนื้อหาที่มนุษย์สร้างขึ้น

แอปพลิเคชัน Generative AI ทั่วไปคืออะไร

Generative AI สามารถใช้เพื่อวัตถุประสงค์ต่อไปนี้

  • ปรับปรุงการโต้ตอบของลูกค้าผ่านการแชทและการค้นหาที่ดีขึ้น
  • สำรวจข้อมูลจำนวนมากที่ไม่มีโครงสร้างผ่านอินเทอร์เฟซการสนทนาและการสรุปข้อมูล
  • ช่วยเหลือการทำงานที่ซ้ำๆ เช่น การตอบกลับคำขอข้อเสนอ การแปลเนื้อหาการตลาดเป็นภาษาต่างๆ และตรวจสอบสัญญาของลูกค้าว่าสอดคล้องกับนโยบายหรือไม่ และอีกมากมาย

Google Cloud มีข้อเสนอ Generative AI ใดบ้าง

Vertex AI ช่วยให้คุณโต้ตอบ ปรับแต่ง และฝังโมเดลพื้นฐานลงในแอปพลิเคชันได้โดยที่มีความเชี่ยวชาญเกี่ยวกับ ML เพียงเล็กน้อยหรือไม่มีเลย โดยคุณสามารถเข้าถึงโมเดลพื้นฐานใน Model Garden ปรับแต่งโมเดลผ่าน UI แบบง่ายใน Vertex AI Studio หรือใช้โมเดลในสมุดบันทึกเกี่ยวกับวิทยาศาสตร์ข้อมูล

Vertex AI Search and Conversation ช่วยให้นักพัฒนาแอปสร้างเครื่องมือค้นหาและแชทบ็อตที่ทำงานด้วย Generative AI ได้อย่างรวดเร็วที่สุด

Gemini สำหรับ Google Cloud ซึ่งขับเคลื่อนโดย Gemini เป็นผู้ช่วยที่ทำงานด้วยระบบ AI ซึ่งมีให้บริการใน Google Cloud และ IDE เพื่อช่วยให้คุณทำสิ่งต่างๆ ได้มากขึ้นและรวดเร็วยิ่งขึ้น Gemini Code Assist ช่วยเขียนโค้ด สร้างโค้ด อธิบายโค้ด และให้คุณแชทกับ Gemini Code เพื่อถามคำถามทางเทคนิค

Gemini คืออะไร

Gemini คือตระกูลโมเดล Generative AI ที่พัฒนาโดย Google DeepMind ซึ่งออกแบบมาสำหรับกรณีการใช้งานหลายรูปแบบ หลายรูปแบบหมายถึงความสามารถในการประมวลผลและสร้างเนื้อหาประเภทต่างๆ เช่น ข้อความ โค้ด รูปภาพ และเสียง

b9913d011999e7c7.png

Gemini มีรูปแบบและขนาดที่หลากหลาย ดังนี้

  • Gemini Ultra: เวอร์ชันที่ใหญ่ที่สุดและมีความสามารถมากที่สุดสำหรับงานที่ซับซ้อน
  • Gemini Flash: รวดเร็วและประหยัดที่สุด พร้อมเพิ่มประสิทธิภาพเพื่องานที่มีปริมาณมาก
  • Gemini Pro: ขนาดกลาง ได้รับการเพิ่มประสิทธิภาพสำหรับการปรับขนาดในงานต่างๆ
  • Gemini Nano: มีประสิทธิภาพมากที่สุด และออกแบบมาสำหรับงานในอุปกรณ์

ฟีเจอร์หลัก

  • สื่อหลากรูปแบบ: ความสามารถของ Gemini ในการทำความเข้าใจและจัดการรูปแบบข้อมูลหลายรูปแบบเป็นขั้นตอนสำคัญที่เหนือกว่าโมเดลภาษารูปแบบข้อความอย่างเดียวแบบเดิม
  • ประสิทธิภาพ: Gemini Ultra มีประสิทธิภาพสูงกว่าเทคโนโลยีที่ทันสมัยกว่าหลายเกณฑ์ และเป็นโมเดลแรกที่แซงหน้าผู้เชี่ยวชาญคนอื่นๆ ในด้านการเปรียบเทียบ MMLU (Massive Multitask Language Understanding) ที่ท้าทาย
  • ความยืดหยุ่น: ขนาดต่างๆ ของ Gemini ทำให้ Gemini สามารถปรับให้เข้ากับการใช้งานที่หลากหลาย ตั้งแต่การวิจัยขนาดใหญ่ไปจนถึงการติดตั้งใช้งานบนอุปกรณ์เคลื่อนที่

คุณจะโต้ตอบกับ Gemini ใน Vertex AI จาก Java ได้อย่างไร

คุณมี 2 ตัวเลือกต่อไปนี้

  1. ไลบรารี Vertex AI Java API สำหรับ Gemini อย่างเป็นทางการ
  2. เฟรมเวิร์ก LangChain4j

คุณจะใช้เฟรมเวิร์ก LangChain4j ใน Codelab นี้

เฟรมเวิร์ก LangChain4j คืออะไร

เฟรมเวิร์ก LangChain4j เป็นไลบรารีโอเพนซอร์สสำหรับผสานรวม LLM ในแอปพลิเคชัน Java โดยจัดการคอมโพเนนต์ต่างๆ เช่น LLM เอง และยังมีเครื่องมืออื่นๆ เช่น ฐานข้อมูลเวกเตอร์ (สำหรับการค้นหาความหมาย) ตัวโหลดและตัวแยกเอกสาร (เพื่อวิเคราะห์และเรียนรู้จากเอกสารเหล่านั้น) โปรแกรมแยกวิเคราะห์เอาต์พุต และอื่นๆ

โปรเจ็กต์นี้ได้รับแรงบันดาลใจมาจากโปรเจ็กต์ Python LangChain แต่มีเป้าหมายในการให้บริการนักพัฒนา Java

bb908ea1e6c96ac2.png

สิ่งที่คุณจะได้เรียนรู้

  • วิธีตั้งค่าโปรเจ็กต์ Java เพื่อใช้ Gemini และ LangChain4j
  • วิธีส่งพรอมต์แรกไปยัง Gemini แบบเป็นโปรแกรม
  • วิธีสตรีมคำตอบจาก Gemini
  • วิธีสร้างการสนทนาระหว่างผู้ใช้และ Gemini
  • วิธีใช้ Gemini ในบริบทที่มีหลายโมดัลด้วยการส่งทั้งข้อความและรูปภาพ
  • วิธีดึงข้อมูลที่มีโครงสร้างที่มีประโยชน์จากเนื้อหาที่ไม่มีโครงสร้าง
  • วิธีจัดการเทมเพลตพรอมต์
  • วิธีจำแนกข้อความ เช่น การวิเคราะห์ความเห็น
  • วิธีแชทกับเอกสารของคุณเอง (Retrieval Augmented Generation)
  • วิธีขยายการใช้งานแชทบ็อตด้วยการเรียกใช้ฟังก์ชัน
  • วิธีใช้ Gemma ในเครื่องด้วย Ollama และ TestContainers

สิ่งที่คุณต้องมี

  • ความรู้เกี่ยวกับภาษาโปรแกรม Java
  • โปรเจ็กต์ Google Cloud
  • เบราว์เซอร์ เช่น Chrome หรือ Firefox

2. การตั้งค่าและข้อกำหนด

การตั้งค่าสภาพแวดล้อมตามเวลาที่สะดวก

  1. ลงชื่อเข้าใช้ Google Cloud Console และสร้างโปรเจ็กต์ใหม่หรือใช้โปรเจ็กต์ที่มีอยู่อีกครั้ง หากยังไม่มีบัญชี Gmail หรือ Google Workspace คุณต้องสร้างบัญชี

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • ชื่อโครงการคือชื่อที่แสดงของผู้เข้าร่วมโปรเจ็กต์นี้ เป็นสตริงอักขระที่ Google APIs ไม่ได้ใช้ โดยคุณจะอัปเดตวิธีการชำระเงินได้ทุกเมื่อ
  • รหัสโปรเจ็กต์จะไม่ซ้ำกันในโปรเจ็กต์ Google Cloud ทั้งหมดและจะเปลี่ยนแปลงไม่ได้ (เปลี่ยนแปลงไม่ได้หลังจากตั้งค่าแล้ว) Cloud Console จะสร้างสตริงที่ไม่ซ้ำกันโดยอัตโนมัติ ซึ่งโดยปกติแล้วคุณไม่สนใจว่านี่คืออะไร ใน Codelab ส่วนใหญ่ คุณจะต้องอ้างอิงรหัสโปรเจ็กต์ของคุณ (โดยปกติจะระบุเป็น PROJECT_ID) หากไม่ชอบรหัสที่สร้างขึ้นนี้ คุณสามารถสร้างรหัสแบบสุ่มขึ้นมาอีกรหัส หรือคุณจะลองดำเนินการเองแล้วดูว่าพร้อมให้ใช้งานหรือไม่ คุณจะเปลี่ยนแปลงหลังจากขั้นตอนนี้ไม่ได้และจะยังคงอยู่ตลอดระยะเวลาของโปรเจ็กต์
  • สำหรับข้อมูลของคุณ ค่าที่ 3 คือหมายเลขโปรเจ็กต์ ซึ่ง API บางตัวใช้ ดูข้อมูลเพิ่มเติมเกี่ยวกับค่าทั้ง 3 ค่าได้ในเอกสารประกอบ
  1. ถัดไป คุณจะต้องเปิดใช้การเรียกเก็บเงินใน Cloud Console เพื่อใช้ทรัพยากร/API ของระบบคลาวด์ การใช้งาน Codelab นี้จะไม่มีค่าใช้จ่ายใดๆ หากมี หากต้องการปิดทรัพยากรเพื่อหลีกเลี่ยงการเรียกเก็บเงินที่นอกเหนือจากบทแนะนำนี้ คุณสามารถลบทรัพยากรที่คุณสร้างหรือลบโปรเจ็กต์ได้ ผู้ใช้ Google Cloud ใหม่มีสิทธิ์เข้าร่วมโปรแกรมช่วงทดลองใช้ฟรี$300 USD

เริ่มต้น Cloud Shell

แม้ว่าคุณจะดำเนินการ Google Cloud จากระยะไกลได้จากแล็ปท็อป แต่คุณจะใช้ Cloud Shell ใน Codelab ซึ่งเป็นสภาพแวดล้อมบรรทัดคำสั่งที่ทำงานในระบบคลาวด์

เปิดใช้งาน Cloud Shell

  1. คลิกเปิดใช้งาน Cloud Shell 853e55310c205094.png จาก Cloud Console

3c1dabeca90e44e5.png

หากเริ่มต้นใช้งาน Cloud Shell เป็นครั้งแรก คุณจะเห็นหน้าจอตรงกลางที่อธิบายว่านี่คืออะไร หากเห็นหน้าจอตรงกลาง ให้คลิกต่อไป

9c92662c6a846a5c.png

การจัดสรรและเชื่อมต่อกับ Cloud Shell ใช้เวลาเพียงไม่กี่นาที

9f0e51b578fecce5.png

เครื่องเสมือนนี้โหลดด้วยเครื่องมือการพัฒนาทั้งหมดที่จำเป็น โดยมีไดเรกทอรีหลักขนาด 5 GB ถาวรและทำงานใน Google Cloud ซึ่งช่วยเพิ่มประสิทธิภาพของเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก งานส่วนใหญ่ใน Codelab นี้สามารถทำได้โดยใช้เบราว์เซอร์

เมื่อเชื่อมต่อกับ Cloud Shell แล้ว คุณควรเห็นข้อความตรวจสอบสิทธิ์และโปรเจ็กต์ได้รับการตั้งค่าเป็นรหัสโปรเจ็กต์แล้ว

  1. เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell เพื่อยืนยันว่าคุณได้รับการตรวจสอบสิทธิ์แล้ว
gcloud auth list

เอาต์พุตจากคำสั่ง

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. เรียกใช้คำสั่งต่อไปนี้ใน Cloud Shell เพื่อยืนยันว่าคำสั่ง gcloud รู้เกี่ยวกับโปรเจ็กต์ของคุณ
gcloud config list project

เอาต์พุตจากคำสั่ง

[core]
project = <PROJECT_ID>

หากไม่ใช่ ให้ตั้งคำสั่งด้วยคำสั่งนี้

gcloud config set project <PROJECT_ID>

เอาต์พุตจากคำสั่ง

Updated property [core/project].

3. การเตรียมความพร้อมสภาพแวดล้อมในการพัฒนา

ใน Codelab นี้ คุณจะใช้เทอร์มินัล Cloud Shell และตัวแก้ไข Cloud Shell เพื่อพัฒนาโปรแกรม Java

เปิดใช้ Vertex AI API

ในคอนโซล Google Cloud ให้ตรวจสอบว่าชื่อโปรเจ็กต์ปรากฏที่ด้านบนของคอนโซล Google Cloud หากไม่มี ให้คลิกเลือกโปรเจ็กต์เพื่อเปิดตัวเลือกโปรเจ็กต์ แล้วเลือกโปรเจ็กต์ที่ต้องการ

คุณเปิดใช้ Vertex AI API ได้จากส่วน Vertex AI ของคอนโซล Google Cloud หรือจากเทอร์มินัล Cloud Shell

หากต้องการเปิดใช้จากคอนโซล Google Cloud ให้ไปที่ส่วน Vertex AI ของเมนูคอนโซล Google Cloud ก่อน โดยทำตามขั้นตอนต่อไปนี้

451976f1c8652341.png

คลิก เปิดใช้ API ที่แนะนำทั้งหมดในแดชบอร์ด Vertex AI

การดำเนินการนี้จะเปิดใช้ API หลายรายการ แต่ API ที่สำคัญที่สุดสำหรับ Codelab คือ aiplatform.googleapis.com

นอกจากนี้คุณยังเปิดใช้ API นี้จากเทอร์มินัล Cloud Shell ด้วยคำสั่งต่อไปนี้ได้ด้วย

gcloud services enable aiplatform.googleapis.com

โคลน ที่เก็บ GitHub

โคลนที่เก็บสำหรับ Codelab นี้ในเทอร์มินัล Cloud Shell

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

หากต้องการตรวจสอบว่าโปรเจ็กต์พร้อมทำงานแล้ว ให้ลองเรียกใช้โปรแกรม " Hello World"

ตรวจสอบว่าคุณอยู่ที่โฟลเดอร์ระดับบนสุด

cd gemini-workshop-for-java-developers/ 

สร้าง Wrapper ของ Gradle ดังนี้

gradle wrapper

เรียกใช้ด้วย gradlew:

./gradlew run

คุณควรจะเห็นผลลัพธ์ต่อไปนี้

..
> Task :app:run
Hello World!

เปิดและตั้งค่า Cloud Editor

เปิดโค้ดด้วยตัวแก้ไข Cloud Code จาก Cloud Shell ดังนี้

42908e11b28f4383.png

ในเครื่องมือแก้ไข Cloud Code ให้เปิดโฟลเดอร์ต้นทางของ Codelab โดยเลือก File -> Open Folder แล้วชี้ไปที่โฟลเดอร์ต้นทางของ Codelab (เช่น /home/username/gemini-workshop-for-java-developers/).

ติดตั้ง Gradle สำหรับ Java

ติดตั้งส่วนขยาย Gradle สำหรับ Java เพื่อให้ตัวแก้ไขโค้ดของระบบคลาวด์ทำงานอย่างถูกต้องกับ Gradle

ขั้นแรก ไปที่ส่วนโปรเจ็กต์ Java แล้วกดเครื่องหมายบวก:

84d15639ac61c197.png

เลือก Gradle for Java:

34d6c4136a3cc9ff.png

เลือกเวอร์ชัน Install Pre-Release:

3b044fb450cccb7.png

เมื่อติดตั้งแล้ว คุณควรเห็นปุ่ม Disable และ Uninstall:

46410fe86d777f9c.png

ขั้นตอนสุดท้าย ให้ล้างพื้นที่ทำงานเพื่อใช้การตั้งค่าใหม่

31e27e9bb61d975d.png

ระบบจะขอให้คุณโหลดซ้ำและลบเวิร์กช็อป ดำเนินการต่อแล้วเลือก Reload and delete:

d6303bc49e391dc.png

หากคุณเปิดไฟล์ใดไฟล์หนึ่ง เช่น App.java คุณควรจะเห็นว่าตัวแก้ไขทำงานได้อย่างถูกต้องด้วยการไฮไลต์ไวยากรณ์

fed1b1b5de0dff58.png

คุณพร้อมจะทดสอบตัวอย่างกับ Gemini แล้ว

ตั้งค่าตัวแปรสภาพแวดล้อม

เปิดเทอร์มินัลใหม่ใน Cloud Code Editor โดยเลือก Terminal -> New Terminal ตั้งค่าตัวแปรสภาพแวดล้อม 2 รายการที่จำเป็นสำหรับการเรียกใช้ตัวอย่างโค้ด ดังนี้

  • PROJECT_ID — รหัสโปรเจ็กต์ Google Cloud ของคุณ
  • LOCATION — ภูมิภาคที่ทำให้โมเดล Gemini ใช้งานได้

ส่งออกตัวแปรดังนี้

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

4. การเรียกโมเดล Gemini ครั้งแรก

เมื่อตั้งค่าโปรเจ็กต์อย่างถูกต้องแล้ว ก็ถึงเวลาเรียกใช้ Gemini API

ลองดู QA.java ในไดเรกทอรี app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;

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

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

ในตัวอย่างแรกนี้ คุณต้องนำเข้าคลาส VertexAiGeminiChatModel ซึ่งใช้อินเทอร์เฟซ ChatModel

ในเมธอด main คุณสามารถกำหนดค่าโมเดลภาษาแชทได้โดยใช้เครื่องมือสร้าง VertexAiGeminiChatModel พร้อมทั้งระบุข้อมูลต่อไปนี้

  • โปรเจ็กต์
  • ตำแหน่ง
  • ชื่อโมเดล (gemini-1.5-flash-001)

เมื่อโมเดลภาษาพร้อมใช้งานแล้ว คุณสามารถเรียกใช้เมธอด generate() และส่งพรอมต์ คำถาม หรือวิธีการเพื่อส่งไปยัง LLM ได้ คราวนี้ คุณถามคำถามง่ายๆ เกี่ยวกับสิ่งที่ทำให้ท้องฟ้าเป็นสีฟ้า

เปลี่ยนพรอมต์ได้ตามต้องการเพื่อลองคำถามหรืองานอื่นๆ

เรียกใช้ตัวอย่างในโฟลเดอร์รูทของซอร์สโค้ด

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

คุณควรจะเห็นผลลัพธ์ที่คล้ายกับวิดีโอนี้

The sky appears blue because of a phenomenon called Rayleigh scattering.
When sunlight enters the atmosphere, it is made up of a mixture of
different wavelengths of light, each with a different color. The
different wavelengths of light interact with the molecules and particles
in the atmosphere in different ways.

The shorter wavelengths of light, such as those corresponding to blue
and violet light, are more likely to be scattered in all directions by
these particles than the longer wavelengths of light, such as those
corresponding to red and orange light. This is because the shorter
wavelengths of light have a smaller wavelength and are able to bend
around the particles more easily.

As a result of Rayleigh scattering, the blue light from the sun is
scattered in all directions, and it is this scattered blue light that we
see when we look up at the sky. The blue light from the sun is not
actually scattered in a single direction, so the color of the sky can
vary depending on the position of the sun in the sky and the amount of
dust and water droplets in the atmosphere.

ยินดีด้วย คุณโทรหา Gemini ครั้งแรกแล้ว

คำตอบสตรีมมิง

คุณสังเกตเห็นว่าคำตอบได้รับเพียงครั้งเดียวหรือ 2-3 วินาทีหรือไม่ นอกจากนี้ คุณอาจได้รับการตอบกลับอย่างต่อเนื่องด้วยตัวแปรการตอบกลับสตรีมมิง การตอบกลับสตรีมมิง โมเดลจะแสดงผลการตอบกลับทีละส่วนเมื่อพร้อมใช้งาน

ใน Codelab นี้ เราจะใช้การตอบกลับที่ไม่ใช่สตรีมมิงต่อไป แต่มาดูวิธีการตอบกลับสตรีมมิงกัน

ใน StreamQA.java ในไดเรกทอรี app/src/main/java/gemini/workshop คุณจะเห็นการทำงานของสตรีมตอบกลับดังนี้

package gemini.workshop;

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

public class StreamQA {
    public static void main(String[] args) {
        StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-001")
            .build();
        
        model.generate("Why is the sky blue?", new StreamingResponseHandler<>() {
            @Override
            public void onNext(String text) {
                System.out.println(text);
            }

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

ครั้งนี้เรานำเข้าตัวแปรคลาสสตรีมมิง VertexAiGeminiStreamingChatModel ซึ่งใช้อินเทอร์เฟซ StreamingChatLanguageModel คุณจะต้องมี StreamingResponseHandler ด้วย

ครั้งนี้ลายเซ็นของเมธอด generate() จะแตกต่างออกไปเล็กน้อย แทนที่จะส่งคืนสตริง ประเภทผลลัพธ์จะเป็นโมฆะ นอกเหนือจากข้อความแจ้ง คุณต้องส่งเครื่องจัดการการตอบกลับสตรีมมิงด้วย ในหน้านี้ คุณจะใช้อินเทอร์เฟซได้โดยการสร้างคลาสภายในแบบไม่ระบุชื่อ โดยมี 2 วิธีคือ onNext(String text) และ onError(Throwable error) โดยอย่างแรกจะมีการเรียกทุกครั้งที่มีคำตอบใหม่พร้อมใช้งาน และในระบบหลังจะมีการเรียกเมื่อเกิดข้อผิดพลาดขึ้นเท่านั้น

เรียกใช้:

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

คุณจะได้รับคำตอบที่คล้ายๆ กันกับคลาสก่อนหน้า แต่ครั้งนี้คุณจะเห็นว่าคำตอบค่อยๆ ปรากฏขึ้นในเชลล์ แทนที่จะต้องรอการแสดงคำตอบแบบเต็ม

การกำหนดค่าเพิ่มเติม

เราจะกำหนดเฉพาะโปรเจ็กต์ ตำแหน่ง และชื่อโมเดลสำหรับการกำหนดค่าเท่านั้น แต่ก็มีพารามิเตอร์อื่นๆ ที่คุณระบุสำหรับโมเดลได้ ดังนี้

  • temperature(Float temp) — เพื่อกำหนดความคิดสร้างสรรค์ที่คุณต้องการคำตอบ (0 หมายถึงความคิดสร้างสรรค์ต่ำและมักเป็นข้อเท็จจริงมากกว่า ส่วน 1 คือสำหรับเอาต์พุตครีเอทีฟโฆษณามากกว่า)
  • topP(Float topP) — เพื่อเลือกคำที่เป็นไปได้ซึ่งความน่าจะเป็นทั้งหมดรวมกันได้จำนวนทศนิยมดังกล่าว (ระหว่าง 0 ถึง 1)
  • topK(Integer topK) — เพื่อสุ่มเลือกคำจากจำนวนคำที่เป็นไปได้มากที่สุดสำหรับข้อความสำเร็จ (ตั้งแต่ 1 ถึง 40 คำ)
  • maxOutputTokens(Integer max) — เพื่อระบุความยาวสูงสุดของคำตอบจากโมเดล (โดยทั่วไป โทเค็น 4 รายการจะแสดงประมาณ 3 คำ)
  • maxRetries(Integer retries) - ในกรณีที่คุณเรียกใช้คำขอเกินโควต้าต่อโควต้าหรือแพลตฟอร์มพบปัญหาทางเทคนิค คุณสามารถให้โมเดลลองเรียกใช้อีกครั้ง 3 ครั้ง

ถึงตอนนี้ คุณถามคำถามกับ Gemini เพียงคำถามเดียว แต่ก็ยังสนทนาแบบหลายรอบได้ คือสิ่งที่คุณจะสำรวจในส่วนถัดไป

5. แชทกับ Gemini

ในขั้นตอนก่อนหน้า คุณถามคำถามเดียว ถึงเวลาสนทนาจริงระหว่างผู้ใช้และ LLM แล้ว คำถามและคำตอบแต่ละข้อสามารถต่อยอดจากคำถามก่อนหน้านี้ เพื่อสร้างการสนทนาที่แท้จริง

ดูที่ Conversation.java ในโฟลเดอร์ app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;

import java.util.List;

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

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

        interface ConversationService {
            String chat(String message);
        }

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

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

การนำเข้าใหม่ที่น่าสนใจในชั้นเรียนนี้มีดังนี้

  • MessageWindowChatMemory — ชั้นเรียนที่จะช่วยจัดการการสนทนาในหลายๆ ด้าน รวมถึงเก็บคำถามและคำตอบก่อนหน้าไว้ในความทรงจำ
  • AiServices — ชั้นเรียนที่จะเชื่อมโยงโมเดลแชทและความทรงจำของแชทเข้าด้วยกัน

ในเมธอดหลัก คุณจะต้องตั้งค่าโมเดล หน่วยความจำแชท และบริการ AI ระบบจะกำหนดค่าโมเดลตามปกติด้วยข้อมูลโปรเจ็กต์ ตำแหน่ง และชื่อโมเดล

สำหรับความทรงจำของแชท เราใช้เครื่องมือสร้างของ MessageWindowChatMemory เพื่อสร้างความทรงจำที่เก็บรักษาข้อความ 20 รายการล่าสุดที่มีการแลกเปลี่ยนกัน ซึ่งเป็นหน้าต่างเลื่อนเหนือการสนทนาที่มีการเก็บบริบทไว้ในไคลเอ็นต์ของคลาส Java

จากนั้นสร้าง AI service ที่จะเชื่อมโยงโมเดลการแชทกับหน่วยความจำแชท

สังเกตวิธีที่บริการ AI ใช้ประโยชน์จากอินเทอร์เฟซ ConversationService ที่กำหนดเองซึ่งเราได้กำหนดไว้ ที่ LangChain4j นำไปใช้ และที่นำคำค้นหา String ไปใช้และแสดงผลคำตอบ String

ได้เวลาพูดคุยกับ Gemini แล้ว ขั้นแรก ระบบจะส่งคำทักทายง่ายๆ ตามด้วยคำถามแรกเกี่ยวกับหอไอเฟลเพื่อให้ทราบว่าหอไอเฟลอยู่ที่ใด โปรดสังเกตว่าประโยคสุดท้ายเกี่ยวข้องกับคำตอบของคำถามแรก เนื่องจากคุณสงสัยว่ามีผู้อยู่อาศัยกี่คนในประเทศที่ตั้งของหอไอเฟล โดยไม่กล่าวถึงประเทศที่ระบุในคำตอบก่อนหน้าอย่างชัดเจน โดยแสดงให้เห็นว่าระบบส่งคำถามและคำตอบในอดีตพร้อมพรอมต์ทุกครั้ง

เรียกใช้ตัวอย่าง

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

คุณจะเห็นคำตอบ 3 ประการที่คล้ายกับคำตอบต่อไปนี้

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

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

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

คุณสามารถถามคำถามแบบผลัดกันเล่นหรือสนทนาหลายคำถามกับ Gemini ได้ แต่จนถึงตอนนี้ข้อมูลที่ป้อนมีเพียงข้อความเท่านั้น แล้วรูปภาพล่ะ มาดูรูปภาพในขั้นตอนถัดไปกัน

6. สื่อหลากรูปแบบด้วย Gemini

Gemini เป็นโมเดลสื่อหลากรูปแบบ ระบบไม่เพียงยอมรับข้อความเป็นอินพุตเท่านั้น แต่ยังยอมรับรูปภาพหรือแม้กระทั่งวิดีโอเป็นอินพุตด้วย ในส่วนนี้ คุณจะเห็นกรณีการใช้งานสำหรับการผสมข้อความและรูปภาพ

คุณคิดว่า Gemini จะจำแมวตัวนี้ได้หรือเปล่า

af00516493ec9ade.png

รูปภาพแมวท่ามกลางหิมะซึ่งถ่ายจาก Wikipediahttps://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

ลองดู Multimodal.java ในไดเรกทอรี app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;

public class Multimodal {

    static final String CAT_IMAGE_URL =
        "https://upload.wikimedia.org/wikipedia/" +
        "commons/b/b6/Felis_catus-cat_on_snow.jpg";


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

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

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

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

ในการนำเข้า เราจะแยกความแตกต่างระหว่างข้อความและเนื้อหาประเภทต่างๆ UserMessage จะมีได้ทั้งออบเจ็กต์ TextContent และ ImageContent รูปแบบนี้เป็นวิธีการหลายรูปแบบ ไม่ว่าจะเป็นการผสมข้อความและรูปภาพ โมเดลจะส่ง Response ที่มี AiMessage กลับมา

จากนั้นคุณก็ดึงข้อมูล AiMessage จากการตอบกลับผ่าน content() แล้วเรียกดูข้อความของเนื้อหาด้วย text()

เรียกใช้ตัวอย่าง

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

ชื่อรูปภาพก็บอกใบ้ให้คุณรู้ถึงรูปภาพที่มีอยู่ แต่เอาต์พุตของ Gemini มีความคล้ายคลึงกันดังต่อไปนี้

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

การผสมรูปภาพและพรอมต์ข้อความจะเปิดกรณีการใช้งานที่น่าสนใจ คุณสร้างแอปพลิเคชันที่ดำเนินการต่อไปนี้ได้

  • การรู้จำข้อความในรูปภาพ
  • ตรวจสอบว่ารูปภาพนั้นปลอดภัยที่จะแสดงหรือไม่
  • สร้างคำบรรยายภาพ
  • ค้นหาผ่านฐานข้อมูลของรูปภาพด้วยคำอธิบายที่เป็นข้อความธรรมดา

นอกจากการดึงข้อมูลจากรูปภาพแล้ว คุณยังแยกข้อมูลจากข้อความที่ไม่มีโครงสร้างได้ด้วย สิ่งที่คุณจะได้เรียนรู้ในส่วนถัดไป

7. ดึงข้อมูลที่มีโครงสร้างออกจากข้อความที่ไม่มีโครงสร้าง

มีหลายกรณีที่การให้ข้อมูลสำคัญในเอกสารรายงาน ในอีเมล หรือข้อความแบบยาวอื่นๆ อย่างไม่มีโครงสร้าง โดยหลักการแล้ว คุณควรจะดึงรายละเอียดสำคัญที่อยู่ในข้อความที่ไม่มีโครงสร้างได้ในรูปแบบของออบเจ็กต์ที่มีโครงสร้าง มาดูวิธีทำกันเลย

สมมติว่าคุณต้องการแยกชื่อและอายุของบุคคลหนึ่งจากประวัติหรือคำอธิบายของบุคคลนั้น คุณสามารถสั่งให้ LLM แยก JSON จากข้อความที่ไม่มีโครงสร้างด้วยพรอมต์ที่ปรับแต่งอย่างชาญฉลาด (โดยทั่วไปเรียกว่า "prompt Engineering")

ลองดู ExtractData.java ใน app/src/main/java/gemini/workshop:

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.UserMessage;

public class ExtractData {

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

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

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

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

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

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

มาดูขั้นตอนต่างๆ ในไฟล์นี้กัน

  • ระเบียน Person ได้รับการกำหนดเพื่อแสดงรายละเอียดที่อธิบายบุคคล ( ชื่อและอายุ)
  • อินเทอร์เฟซ PersonExtractor กำหนดด้วยเมธอดที่ให้สตริงข้อความที่ไม่มีโครงสร้างและแสดงอินสแตนซ์ Person
  • extractPerson() มีคำอธิบายประกอบด้วยคำอธิบายประกอบ @UserMessage ที่เชื่อมโยงพรอมต์เข้ากับคำอธิบายประกอบ นั่นคือพรอมต์ที่โมเดลจะใช้เพื่อดึงข้อมูลและแสดงผลรายละเอียดในรูปแบบของเอกสาร JSON ซึ่งจะมีการแยกวิเคราะห์ให้คุณและยกเลิกการกำหนดค่าเป็นอินสแตนซ์ Person

คราวนี้มาดูเนื้อหาของเมธอด main() กัน

  • ระบบจะสร้างอินสแตนซ์โมเดลแชท โปรดสังเกตว่าเราใช้ temperature ที่ต่ำมาก ของ 0 และ topK ของเพียงรายการเดียว เพื่อให้ได้คำตอบที่แน่นอน ซึ่งจะช่วยให้โมเดลทำตามวิธีการได้ดียิ่งขึ้น โดยเฉพาะอย่างยิ่ง เราไม่ต้องการให้ Gemini รวมการตอบกลับ JSON ด้วยมาร์กอัปมาร์กดาวน์เพิ่มเติม
  • ระบบสร้างออบเจ็กต์ PersonExtractor ด้วยคลาส AiServices ของ LangChain4j
  • จากนั้นคุณจะเรียกใช้ Person person = extractor.extractPerson(...) เพื่อดึงรายละเอียดของบุคคลดังกล่าวจากข้อความที่ไม่มีโครงสร้างได้ และเรียกอินสแตนซ์ Person ที่มีชื่อและอายุกลับมา

เรียกใช้ตัวอย่าง

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

คุณควรจะเห็นผลลัพธ์ต่อไปนี้

Anna
23

ใช่แล้ว ฉันชื่อ Anna อายุ 23 ปี

เมื่อใช้วิธี AiServices นี้ คุณจะดำเนินการกับออบเจ็กต์ที่มีการพิมพ์ได้อย่างแน่นหนา คุณไม่ได้โต้ตอบกับ LLM โดยตรง แต่คุณจะทำงานกับคลาสที่เป็นรูปธรรม เช่น ระเบียน Person เพื่อแสดงข้อมูลส่วนบุคคลที่ดึงมา และคุณมีออบเจ็กต์ PersonExtractor ที่มีเมธอด extractPerson() ซึ่งแสดงผลอินสแตนซ์ Person แนวคิดของ LLM หายไป และในฐานะนักพัฒนาซอฟต์แวร์ Java คุณเพียงแค่ต้องควบคุมคลาสและออบเจ็กต์ปกติ

8. จัดโครงสร้างพรอมต์ด้วยเทมเพลตพรอมต์

เมื่อโต้ตอบกับ 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-1.5-flash-001")
            .maxOutputTokens(500)
            .temperature(0.8f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

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

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

        Prompt prompt = promptTemplate.apply(variables);

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

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

คุณกำหนดค่าโมเดล VertexAiGeminiChatModel ด้วยความคิดสร้างสรรค์ระดับสูง มีอุณหภูมิสูง รวมถึงมีค่า TopP และ TopK สูง จากนั้นให้สร้าง PromptTemplate ด้วยเมธอด from() แบบคงที่ โดยการส่งสตริงของพรอมต์และใช้ตัวแปรตัวยึดตำแหน่งวงเล็บปีกกา 2 ตัว ได้แก่ {{dish}} และ {{ingredients}}

คุณสร้างพรอมต์สุดท้ายได้ด้วยการเรียกใช้ apply() ซึ่งจะใช้การแมปคู่คีย์/ค่าที่แทนชื่อของตัวยึดตำแหน่งและค่าสตริงที่จะแทนที่

สุดท้าย คุณเรียกใช้เมธอด generate() ของโมเดล Gemini ด้วยการสร้างข้อความสำหรับผู้ใช้จากพรอมต์นั้นด้วยคำสั่ง prompt.toUserMessage()

เรียกใช้ตัวอย่าง

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

คุณควรจะเห็นเอาต์พุตที่สร้างขึ้นซึ่งมีลักษณะคล้ายกับเอาต์พุตนี้

**Strawberry Shortcake**

Ingredients:

* 1 pint strawberries, hulled and sliced
* 1/2 cup sugar
* 1/4 cup cornstarch
* 1/4 cup water
* 1 tablespoon lemon juice
* 1/2 cup heavy cream, whipped
* 1/4 cup confectioners' sugar
* 1/4 teaspoon vanilla extract
* 6 graham cracker squares, crushed

Instructions:

1. In a medium saucepan, combine the strawberries, sugar, cornstarch, 
water, and lemon juice. Bring to a boil over medium heat, stirring 
constantly. Reduce heat and simmer for 5 minutes, or until the sauce has 
thickened.
2. Remove from heat and let cool slightly.
3. In a large bowl, combine the whipped cream, confectioners' sugar, and 
vanilla extract. Beat until soft peaks form.
4. To assemble the shortcakes, place a graham cracker square on each of 
6 dessert plates. Top with a scoop of whipped cream, then a spoonful of 
strawberry sauce. Repeat layers, ending with a graham cracker square.
5. Serve immediately.

**Tips:**

* For a more elegant presentation, you can use fresh strawberries 
instead of sliced strawberries.
* If you don't have time to make your own whipped cream, you can use 
store-bought whipped cream.

คุณสามารถเปลี่ยนแปลงค่า dish และ ingredients ในแผนที่ และปรับอุณหภูมิ, topK และ tokP และเรียกใช้โค้ดอีกครั้ง วิธีนี้จะช่วยให้คุณเห็นผลของการเปลี่ยนแปลงพารามิเตอร์เหล่านี้ใน LLM

เทมเพลตพรอมต์เป็นวิธีที่ดีในการมีวิธีการที่ใช้ซ้ำและปรับแต่งได้สำหรับการเรียกใช้ LLM คุณสามารถส่งผ่านข้อมูลและปรับแต่งข้อความแจ้งสำหรับค่าต่างๆ ที่ผู้ใช้ให้ไว้

9. การจัดประเภทข้อความด้วยการแสดงข้อความแจ้งแบบ 2-3 ช็อต

LLM สามารถจำแนกประเภทข้อความตามหมวดหมู่ต่างๆ ได้ค่อนข้างดี คุณช่วย LLM ในงานดังกล่าวได้โดยระบุตัวอย่างข้อความและหมวดหมู่ที่เกี่ยวข้อง วิธีนี้มักเรียกว่าข้อความแจ้งเพียง 2-3 ช็อต

ลองดู TextClassification.java ในไดเรกทอรี app/src/main/java/gemini/workshop เพื่อทำการจัดประเภทข้อความประเภทใดประเภทหนึ่งโดยเฉพาะ ซึ่งได้แก่ การวิเคราะห์ความเห็น

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;

package gemini.workshop;

import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;

import java.util.Map;

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

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

            INPUT: This is fantastic news!
            OUTPUT: POSITIVE

            INPUT: Pi is roughly equal to 3.14
            OUTPUT: NEUTRAL

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

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

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

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

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

ในเมธอด main() คุณจะสร้างโมเดลแชทของ Gemini ได้ตามปกติ แต่ด้วยจำนวนโทเค็นเอาต์พุตสูงสุดเพียงเล็กน้อย เนื่องจากคุณต้องการเพียงคำตอบสั้นๆ เท่านั้น ข้อความคือ POSITIVE, NEGATIVE หรือ NEUTRAL

จากนั้นจึงสร้างเทมเพลตพรอมต์ที่ใช้ซ้ำได้ด้วยเทคนิคข้อความแจ้ง 2-3 ช็อต โดยสอนโมเดลเกี่ยวกับตัวอย่างบางส่วนของอินพุตและเอาต์พุต ซึ่งจะช่วยให้โมเดลดำเนินการตามเอาต์พุตจริงได้ด้วย Gemini จะไม่ตอบกลับด้วยประโยคเต็มๆ แต่จะได้รับคำสั่งให้ตอบกลับด้วยคำเพียงคำเดียว

คุณใช้ตัวแปรกับเมธอด apply() เพื่อแทนที่ตัวยึดตำแหน่ง {{text}} ด้วยพารามิเตอร์จริง ("I love strawberries") และเปลี่ยนเทมเพลตนั้นเป็นข้อความสำหรับผู้ใช้ด้วย toUserMessage()

เรียกใช้ตัวอย่าง

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

คุณจะเห็นเพียงคำเดียวดังนี้

POSITIVE

ดูเหมือนว่ากำลังหลงรักสตรอว์เบอร์รีนะ

10. การดึงข้อมูล Augmented Generation

LLM จะได้รับการฝึกในข้อความจำนวนมาก อย่างไรก็ตาม ความรู้ของพวกเขาครอบคลุมเฉพาะข้อมูลที่เห็นระหว่างการฝึกอบรมเท่านั้น หากมีข้อมูลใหม่เผยแพร่หลังจากวันสุดท้ายของการฝึกโมเดล รายละเอียดเหล่านั้นจะไม่พร้อมใช้งานสำหรับโมเดล ดังนั้น โมเดลจะไม่สามารถตอบคำถามเกี่ยวกับข้อมูลที่ไม่เคยเห็นมาก่อน

นั่นคือเหตุผลว่าทำไมวิธีการต่างๆ อย่างเช่น Retrieval Augmented Generation (RAG) ช่วยให้ข้อมูลเพิ่มเติมที่ LLM อาจจำเป็นต้องทราบเพื่อดำเนินการตามคำขอของผู้ใช้ โดยตอบกลับด้วยข้อมูลที่อาจเป็นปัจจุบันมากกว่าหรือมีข้อมูลส่วนตัวที่เข้าถึงไม่ได้ขณะฝึก

กลับมาที่การสนทนาต่อ ครั้งนี้คุณจะสามารถถามคำถามเกี่ยวกับเอกสารของคุณ คุณจะสร้างแชทบ็อตที่สามารถเรียกข้อมูลที่เกี่ยวข้องจากฐานข้อมูลที่มีเอกสารของคุณที่แบ่งเป็นชิ้นส่วนย่อย ("ส่วน") และโมเดลจะใช้ข้อมูลดังกล่าวเพื่อประกอบคำตอบ แทนที่จะใช้ความรู้ที่มีอยู่ในการฝึกเพียงอย่างเดียว

ใน RAG มี 2 ระยะดังนี้

  1. ระยะการส่งผ่านข้อมูล - ระบบจะคำนวณและจัดเก็บเอกสารในหน่วยความจำ โดยแยกส่วนเล็กๆ และการฝังเวกเตอร์ (การแสดงเวกเตอร์ที่มีมิติสูงของกลุ่มข้อมูล) จะได้รับการคำนวณและจัดเก็บไว้ในฐานข้อมูลเวกเตอร์ที่ใช้ค้นหาเชิงอรรถศาสตร์ได้ โดยปกติขั้นตอนการส่งผ่านข้อมูลนี้จะทำครั้งเดียวเมื่อต้องเพิ่มเอกสารใหม่ในคลังเอกสาร

cd07d33d20ffa1c8.png

  1. ระยะการค้นหา - ตอนนี้ผู้ใช้สามารถถามคำถามเกี่ยวกับเอกสารได้ ระบบจะแปลงคำถามเป็นเวกเตอร์ด้วย และเปรียบเทียบกับเวกเตอร์อื่นๆ ทั้งหมดในฐานข้อมูล เวกเตอร์ที่คล้ายกันมากที่สุดมักจะเกี่ยวข้องกันทางอรรถศาสตร์และจะส่งคืนโดยฐานข้อมูลเวกเตอร์ จากนั้น LLM จะได้รับบริบทของการสนทนา กลุ่มข้อความที่สอดคล้องกับเวกเตอร์ที่ฐานข้อมูลแสดงผล และระบบจะขอให้อธิบายคำตอบของโมเดลโดยดูที่ส่วนเหล่านั้น

a1d2e2deb83c6d27.png

เตรียมเอกสาร

ในการสาธิตใหม่นี้ คุณจะได้ถามคำถามเกี่ยวกับเอกสารงานวิจัย "Attention is all you need" โดยได้อธิบายถึงสถาปัตยกรรมโครงข่ายระบบประสาทเทียมแบบ Transformer ซึ่ง Google เป็นผู้บุกเบิก ซึ่งเป็นวิธีนำโมเดลภาษาขนาดใหญ่ที่ทันสมัยทั้งหมดมาใช้ในปัจจุบัน

ดาวน์โหลดบทความไปยัง attention-is-all-you-need.pdf ในที่เก็บแล้ว

ใช้แชทบ็อต

มาดูวิธีสร้างแนวทางแบบ 2 ระยะกัน นั่นคือ เริ่มจากการนำเข้าเอกสาร ตามด้วยเวลาสืบค้นเมื่อผู้ใช้ถามคำถามเกี่ยวกับเอกสาร

ในตัวอย่างนี้ ทั้ง 2 ระยะมีการใช้งานในคลาสเดียวกัน โดยปกติ คุณจะต้องมีแอปพลิเคชันหนึ่งสำหรับการส่งผ่านข้อมูลและอีกแอปพลิเคชันหนึ่งที่เสนออินเทอร์เฟซแชทบ็อตสำหรับผู้ใช้

นอกจากนี้ ในตัวอย่างนี้เราจะใช้ฐานข้อมูลเวกเตอร์ในหน่วยความจำ ในสถานการณ์จริง เฟสการนำเข้าและขั้นตอนการค้นหาจะแยกออกจากกันในแอปพลิเคชันที่แตกต่างกัน 2 รายการ และเวกเตอร์จะยังคงอยู่ในฐานข้อมูลแบบสแตนด์อโลน

การนำเข้าเอกสาร

ขั้นตอนแรกในการนำเข้าเอกสารคือการค้นหาไฟล์ PDF ที่เราได้ดาวน์โหลดไว้แล้ว และเตรียม PdfParser ให้อ่าน

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

คุณจะสร้างอินสแตนซ์ของโมเดลการฝังแทนการสร้างโมเดลภาษาแชทตามปกติ ซึ่งเป็นโมเดลที่มีบทบาทในการสร้างการแสดงเวกเตอร์ของส่วนข้อความ (คำ ประโยค หรือแม้แต่ย่อหน้า) โดยจะแสดงผลเวกเตอร์ของจำนวนทศนิยม แทนที่จะแสดงผลข้อความตอบกลับ

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

ถัดไป คุณจะต้องมีชั้นเรียนต่างๆ เพื่อทำงานร่วมกันเพื่อทำสิ่งต่อไปนี้

  • โหลดและแบ่งเอกสาร PDF เป็นกลุ่ม
  • สร้างการฝังเวกเตอร์สำหรับกลุ่มเหล่านี้ทั้งหมด
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

EmbeddingStoreIngestor storeIngestor = EmbeddingStoreIngestor.builder()
    .documentSplitter(DocumentSplitters.recursive(500, 100))
    .embeddingModel(embeddingModel)
    .embeddingStore(embeddingStore)
    .build();
storeIngestor.ingest(document);

อินสแตนซ์ของ InMemoryEmbeddingStore ซึ่งเป็นฐานข้อมูลเวกเตอร์ในหน่วยความจำ สร้างขึ้นเพื่อจัดเก็บการฝังเวกเตอร์

เอกสารจะแบ่งออกเป็นส่วนๆ จากชั้นเรียน DocumentSplitters ไฟล์ PDF จะแยกข้อความในไฟล์ PDF เป็นตัวอย่างอักขระ 500 ตัว โดยมีอักขระทับซ้อนกัน 100 ตัว (โดยแบ่งส่วนต่อไปนี้เพื่อหลีกเลี่ยงการตัดคำหรือประโยคทีละส่วน)

เครื่องมือนำเข้าพื้นที่เก็บข้อมูลจะลิงก์ตัวแยกเอกสาร โมเดลการฝังเพื่อคำนวณเวกเตอร์ และฐานข้อมูลเวกเตอร์ในหน่วยความจำ จากนั้นเมธอด ingest() จะดำเนินการนำเข้า

ในตอนนี้ระยะแรกได้จบลงแล้ว เอกสารจะถูกเปลี่ยนรูปแบบให้เป็นกลุ่มข้อความซึ่งมีการฝังเวกเตอร์ที่เกี่ยวข้อง และจัดเก็บไว้ในฐานข้อมูลเวกเตอร์

การถามคำถาม

ได้เวลาเตรียมพร้อมถามคำถามแล้ว สร้างโมเดลแชทเพื่อเริ่มการสนทนา

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

นอกจากนี้ คุณต้องมีคลาสรีทรีฟเวอร์เพื่อลิงก์ฐานข้อมูลเวกเตอร์ (ในตัวแปร embeddingStore) กับโมเดลการฝัง หน้าที่ของมันคือการค้นหาฐานข้อมูลเวกเตอร์ด้วยการคำนวณเวกเตอร์ที่ฝังสำหรับการค้นหาของผู้ใช้ เพื่อค้นหาเวกเตอร์ที่คล้ายกันในฐานข้อมูล

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

นอกเหนือจากวิธีการหลัก ให้สร้างอินเทอร์เฟซที่แสดงถึงผู้ช่วยผู้เชี่ยวชาญ LLM ซึ่งเป็นอินเทอร์เฟซที่คลาส AiServices จะนำไปใช้ในการโต้ตอบกับโมเดล

interface LlmExpert {
    String ask(String question);
}

ในจุดนี้ คุณจะกำหนดค่าบริการ AI ใหม่ได้โดยทำดังนี้

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

บริการนี้จะเชื่อมโยงเข้าด้วยกัน

  • โมเดลภาษาการแชทที่คุณกำหนดค่าไว้ก่อนหน้านี้
  • ความทรงจำของแชทสำหรับติดตามการสนทนา
  • รีทรีฟเวอร์จะเปรียบเทียบการค้นหาที่ฝังเวกเตอร์กับเวกเตอร์ในฐานข้อมูล
  • เทมเพลตพรอมต์บอกอย่างชัดเจนว่าโมเดลแชทควรตอบกลับโดยอ้างอิงคำตอบจากข้อมูลที่ระบุ (เช่น ข้อความที่เกี่ยวข้องซึ่งตัดตอนมาจากเอกสารซึ่งมีการฝังเวกเตอร์ที่คล้ายกับเวกเตอร์ของคำถามของผู้ใช้)
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in large language models,\s
            you excel at explaining simply and clearly questions about LLMs.

            Here is the question: {{userMessage}}

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

ในที่สุดคุณก็พร้อมจะถามคำถามแล้ว

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

ซอร์สโค้ดทั้งหมดอยู่ใน RAG.java ในไดเรกทอรี app/src/main/java/gemini/workshop:

เรียกใช้ตัวอย่าง

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

ในผลลัพธ์ คุณควรจะเห็นคำตอบสำหรับคำถามของคุณ:

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

 Transformer architecture 


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

 The different components of a transformer neural network are:

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


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

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

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

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

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


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

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

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

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

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

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

11. กำลังเรียกฟังก์ชัน

นอกจากนี้ยังมีสถานการณ์ที่คุณต้องการให้ LLM มีสิทธิ์เข้าถึงระบบภายนอก เช่น API ของเว็บระยะไกลที่ดึงข้อมูลหรือมีการดำเนินการ หรือบริการที่ทำการคำนวณบางอย่าง เช่น

API เว็บระยะไกล:

  • ติดตามและอัปเดตคำสั่งซื้อของลูกค้า
  • ค้นหาหรือสร้างคำขอแจ้งปัญหาในตัวติดตามปัญหา
  • ดึงข้อมูลแบบเรียลไทม์ เช่น ราคาหุ้นหรือค่าจากเซ็นเซอร์ IoT
  • ส่งอีเมล

เครื่องมือการคำนวณ:

  • เครื่องคิดเลขสำหรับปัญหาทางคณิตศาสตร์ขั้นสูง
  • การตีความโค้ดสำหรับการเรียกใช้โค้ดเมื่อ LLM ต้องการตรรกะการให้เหตุผล
  • แปลงคำขอที่เป็นภาษาธรรมชาติเป็นการค้นหา SQL เพื่อให้ LLM ค้นหาฐานข้อมูลได้

การเรียกใช้ฟังก์ชันคือความสามารถของโมเดลในการขอให้เรียกใช้ฟังก์ชันอย่างน้อย 1 ครั้งในนามของโมเดลนั้น โมเดลจึงสามารถตอบรับข้อความแจ้งของผู้ใช้พร้อมข้อมูลที่ใหม่กว่าได้อย่างถูกต้อง

LLM จะตอบกลับด้วยคำขอการเรียกใช้ฟังก์ชันได้เมื่อมีข้อความแจ้งบางอย่างจากผู้ใช้และความรู้เกี่ยวกับฟังก์ชันที่มีอยู่ซึ่งอาจเกี่ยวข้องกับบริบทนั้น แอปพลิเคชันที่ผสานรวม LLM จะเรียกใช้ฟังก์ชันนี้แล้วตอบกลับไปยัง LLM พร้อมคำตอบได้ แล้ว LLM จะตีความข้อมูลกลับไปเป็นคำตอบแบบข้อความ

การเรียกใช้ฟังก์ชัน 4 ขั้นตอน

มาดูตัวอย่างการเรียกฟังก์ชันกัน นั่นคือการรับข้อมูลเกี่ยวกับพยากรณ์อากาศ

ถ้าคุณถาม Gemini หรือ LLM อื่นๆ เกี่ยวกับสภาพอากาศในปารีส พวกเขาจะตอบว่าไม่มีข้อมูลพยากรณ์อากาศ หากต้องการให้ LLM เข้าถึงข้อมูลสภาพอากาศแบบเรียลไทม์ คุณต้องกำหนดฟังก์ชันที่ LLM จะใช้ได้

ดูแผนภาพต่อไปนี้

31e0c2aba5e6f21c.png

1️️ อันดับแรก ผู้ใช้ถามเกี่ยวกับสภาพอากาศในปารีส แอปแชทบ็อตทราบว่ามีฟังก์ชันอย่างน้อย 1 รายการที่พร้อมช่วยให้ LLM ตอบคำถามได้ ทั้งแชทบ็อตจะส่งพรอมต์เริ่มต้นและรายการฟังก์ชันที่เรียกใช้ได้ ในที่นี้คือฟังก์ชันที่ชื่อว่า getWeather() ซึ่งรับพารามิเตอร์สตริงสําหรับสถานที่ตั้ง

8863be53a73c4a70.png

เนื่องจาก LLM ไม่ทราบเกี่ยวกับการพยากรณ์อากาศ จึงส่งคำขอการดำเนินการฟังก์ชันกลับมาแทนการตอบกลับผ่านข้อความ แชทบ็อตต้องเรียกใช้ฟังก์ชัน getWeather() ที่มี "Paris" เป็นพารามิเตอร์ตำแหน่ง

d1367cc69c07b14d.png

2️️ แชทบ็อตเรียกใช้ฟังก์ชันในนามของ LLM และดึงข้อมูลการตอบสนองของฟังก์ชัน จากตรงนี้ เราคิดว่าคำตอบคือ {"forecast": "sunny"}

73a5f2ed19f47d8.png

3️️ แอปแชทบ็อตจะส่งการตอบสนอง JSON กลับไปยัง LLM

20832cb1ee6fbfeb.png

4️️ LLM จะดูการตอบสนอง JSON ตีความข้อมูลดังกล่าว และสุดท้ายจะตอบกลับด้วยข้อความว่าสภาพอากาศมีแดดจัดในปารีส

แต่ละขั้นตอนเป็นโค้ด

ขั้นแรก ให้กำหนดค่าโมเดล Gemini ตามปกติ ดังนี้

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

คุณระบุข้อมูลจำเพาะของเครื่องมือที่อธิบายถึงฟังก์ชันที่เรียกใช้ได้

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

มีการกำหนดชื่อของฟังก์ชัน ตลอดจนชื่อและประเภทของพารามิเตอร์ แต่สังเกตว่าทั้งฟังก์ชันและพารามิเตอร์จะได้รับคำอธิบาย คำอธิบายมีความสำคัญมากและช่วยให้ LLM เข้าใจอย่างแท้จริงว่าฟังก์ชันหนึ่งๆ ทำอะไรได้บ้าง และตัดสินได้ว่าจำเป็นต้องเรียกใช้ฟังก์ชันนี้ในบริบทของการสนทนาหรือไม่

มาเริ่มขั้นตอนที่ 1 กันด้วยการส่งคำถามเริ่มต้นเกี่ยวกับสภาพอากาศในปารีส

List<ChatMessage> allMessages = new ArrayList<>();

// 1) Ask the question about the weather
UserMessage weatherQuestion = UserMessage.from("What is the weather in Paris?");
allMessages.add(weatherQuestion);

ในขั้นตอนที่ 2 เราจะส่งต่อเครื่องมือที่ต้องการให้โมเดลใช้ และโมเดลตอบกลับด้วยคำขอการดำเนินการมากเกินไป

// 2) The model replies with a function call request
Response<AiMessage> messageResponse = model.generate(allMessages, weatherToolSpec);
ToolExecutionRequest toolExecutionRequest = messageResponse.content().toolExecutionRequests().getFirst();
System.out.println("Tool execution request: " + toolExecutionRequest);
allMessages.add(messageResponse.content());

ขั้นตอนที่ 3 เมื่อถึงขั้นนี้ เราก็ได้ทราบฟังก์ชันที่ LLM ต้องการให้เราเรียกใช้แล้ว ในโค้ด เราไม่ได้เรียกใช้ API ภายนอกจริงๆ แต่แสดงการพยากรณ์อากาศสมมติโดยตรง

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

และในขั้นตอนที่ 4 LLM จะเรียนรู้เกี่ยวกับผลของการเรียกใช้ฟังก์ชัน แล้วสังเคราะห์ข้อความตอบกลับได้ดังนี้

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

ผลลัพธ์ที่ได้คือ

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

คุณสามารถดูเอาต์พุตเหนือคำขอการดำเนินการกับเครื่องมือ รวมถึงคำตอบได้

ซอร์สโค้ดทั้งหมดอยู่ใน FunctionCalling.java ในไดเรกทอรี app/src/main/java/gemini/workshop:

เรียกใช้ตัวอย่าง

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

คุณควรเห็นผลลัพธ์ที่คล้ายกับข้อความต่อไปนี้

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

12. LangChain4j จัดการการเรียกใช้ฟังก์ชัน

ในขั้นตอนก่อนหน้านี้ คุณได้เห็นวิธีที่คำถาม/คำตอบแบบข้อความปกติและการโต้ตอบระหว่างคำขอ/การตอบกลับฟังก์ชันเป็นข้อความ และในระหว่างนั้นคุณได้ให้การตอบกลับฟังก์ชันที่ขอโดยตรง โดยไม่ต้องเรียกใช้ฟังก์ชันจริง

อย่างไรก็ตาม LangChain4j ยังมี Abstraction ระดับสูงกว่าที่สามารถจัดการกับการเรียกใช้ฟังก์ชันอย่างโปร่งใสสำหรับคุณ ในขณะที่จัดการการสนทนาตามปกติ

การเรียกใช้ฟังก์ชันเดียว

เรามาลองดู FunctionCallingAssistant.java ทีละส่วนกัน

ขั้นแรก ให้คุณสร้างระเบียนที่แสดงโครงสร้างข้อมูลการตอบสนองของฟังก์ชัน ดังนี้

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

คำตอบจะมีข้อมูลเกี่ยวกับตำแหน่ง พยากรณ์อากาศ และอุณหภูมิ

จากนั้นให้สร้างคลาสที่มีฟังก์ชันจริงที่คุณต้องการให้ใช้ได้ในโมเดล

static class WeatherForecastService {
    @Tool("Get the weather forecast for a location")
    WeatherForecast getForecast(@P("Location to get the forecast for") String location) {
        if (location.equals("Paris")) {
            return new WeatherForecast("Paris", "Sunny", 20);
        } else if (location.equals("London")) {
            return new WeatherForecast("London", "Rainy", 15);
        } else {
            return new WeatherForecast("Unknown", "Unknown", 0);
        }
    }
}

โปรดทราบว่าคลาสนี้มีฟังก์ชันเดียว แต่มีคำอธิบายประกอบด้วยคำอธิบายประกอบ @Tool ที่สอดคล้องกับคำอธิบายของฟังก์ชันที่โมเดลสามารถขอเรียกได้

พารามิเตอร์ของฟังก์ชัน (พารามิเตอร์เดียวในที่นี้) จะมีการใส่คำอธิบายประกอบด้วย แต่คำอธิบายประกอบ @P สั้นๆ นี้ ซึ่งให้คำอธิบายเกี่ยวกับพารามิเตอร์ด้วย คุณสามารถเพิ่มฟังก์ชันได้มากเท่าที่ต้องการเพื่อทำให้ฟังก์ชันเหล่านั้นพร้อมใช้งานในโมเดลสำหรับสถานการณ์ที่ซับซ้อนยิ่งขึ้น

ในชั้นเรียนนี้ คุณจะแสดงผลคำตอบสำเร็จรูปบางส่วน แต่หากต้องการเรียกใช้บริการพยากรณ์อากาศภายนอกจริง การดำเนินการดังกล่าวจะอยู่ในส่วนของวิธีการที่คุณใช้เรียกใช้บริการนั้น

ดังที่เราได้เห็นเมื่อคุณสร้าง ToolSpecification ในวิธีการก่อนหน้านี้ คุณควรบันทึกว่าฟังก์ชันทำอะไรและอธิบายว่าพารามิเตอร์นั้นสอดคล้องกับอะไร ซึ่งจะช่วยให้โมเดลเข้าใจว่าสามารถใช้ฟังก์ชันนี้อย่างไรและเมื่อใด

ถัดไป LangChain4j จะให้คุณสร้างอินเทอร์เฟซที่สอดคล้องกับสัญญาที่คุณต้องการใช้เพื่อโต้ตอบกับโมเดล อินเทอร์เฟซนี้เป็นอินเทอร์เฟซที่เรียบง่ายซึ่งใช้สตริงที่แสดงถึงข้อความผู้ใช้ และแสดงผลสตริงที่สอดคล้องกับคำตอบของโมเดล

interface WeatherAssistant {
    String chat(String userMessage);
}

นอกจากนี้ยังสามารถใช้ลายเซ็นที่ซับซ้อนมากขึ้นซึ่งเกี่ยวข้องกับ UserMessage ของ LangChain4j (สำหรับข้อความของผู้ใช้) หรือ AiMessage (สำหรับการตอบกลับโมเดล) หรือแม้แต่ TokenStream ได้หากคุณต้องการจัดการสถานการณ์ขั้นสูงกว่า เนื่องจากออบเจ็กต์ที่ซับซ้อนขึ้นเหล่านั้นยังมีข้อมูลเพิ่มเติม เช่น จำนวนโทเค็นที่ใช้ ฯลฯ แต่เพื่อความเรียบง่าย เราจะรับสตริงในอินพุตและสตริงในเอาต์พุต

มาปิดท้ายด้วยเมธอด main() ที่เชื่อมโยงส่วนต่างๆ เข้าด้วยกัน

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

    WeatherForecastService weatherForecastService = new WeatherForecastService();

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

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

คุณกำหนดค่าโมเดลการแชทของ Gemini ได้ตามปกติ จากนั้นจึงสร้างอินสแตนซ์บริการพยากรณ์อากาศที่มี "ฟังก์ชัน" ที่โมเดลจะขอให้เราเรียกใช้

ตอนนี้คุณสามารถใช้คลาส AiServices อีกครั้งเพื่อเชื่อมโยงโมเดลแชท หน่วยความจำแชท และเครื่องมือ (เช่น บริการพยากรณ์อากาศที่มีฟังก์ชันในตัว) AiServices จะแสดงผลออบเจ็กต์ที่ใช้อินเทอร์เฟซ WeatherAssistant ที่คุณกำหนด สิ่งเดียวที่เหลือก็คือการเรียกใช้เมธอด chat() ของผู้ช่วยนั้น เมื่อเรียกใช้ คุณจะเห็นเฉพาะข้อความตอบกลับเท่านั้น แต่นักพัฒนาซอฟต์แวร์จะไม่เห็นคำขอการเรียกฟังก์ชันและการตอบกลับการเรียกฟังก์ชัน และคำขอเหล่านั้นจะได้รับการจัดการโดยอัตโนมัติและโปร่งใส หาก Gemini คิดว่าควรเรียกฟังก์ชันใด Gemini จะตอบกลับด้วยคำขอการเรียกใช้ฟังก์ชัน และ LangChain4j จะดูแลการเรียกใช้ฟังก์ชันในพื้นที่ในนามของคุณ

เรียกใช้ตัวอย่าง

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

คุณควรเห็นผลลัพธ์ที่คล้ายกับข้อความต่อไปนี้

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

นี่เป็นตัวอย่างของฟังก์ชันเดียว

การเรียกใช้ฟังก์ชันหลายครั้ง

นอกจากนี้ คุณยังสามารถใช้หลายฟังก์ชันและให้ LangChain4j จัดการการเรียกใช้ฟังก์ชันหลายรายการแทนคุณ ลองดู MultiFunctionCallingAssistant.java เพื่อดูตัวอย่างฟังก์ชันหลายรายการ

เครื่องมือนี้มีฟังก์ชันในการแปลงสกุลเงิน ดังนี้

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

    double result = amount;

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

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

    return result;
}

ฟังก์ชันอื่นที่ใช้เรียกมูลค่าของหุ้นมีดังนี้

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

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

    return result;
}

ฟังก์ชันอื่นที่จะนำเปอร์เซ็นต์ไปใช้กับจำนวนที่กำหนด

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

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

    return result;
}

จากนั้นคุณจะรวมฟังก์ชันเหล่านี้ทั้งหมดเข้ากับคลาส MultiTools แล้วถามคำถาม เช่น "ราคาหุ้น AAPL 10% ที่แปลงจาก USD เป็น EUR เท่ากับเท่าไร"

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

    MultiTools multiTools = new MultiTools();

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

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

โดยให้เรียกใช้ดังนี้

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

และคุณจะเห็นฟังก์ชันหลายรายการที่ชื่อว่า

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

ต่อตัวแทน

การเรียกฟังก์ชันเป็นกลไกส่วนขยายที่ยอดเยี่ยมสำหรับโมเดลภาษาขนาดใหญ่อย่าง Gemini เครื่องมือนี้ช่วยให้เราสร้างระบบที่ซับซ้อนขึ้นซึ่งมักเรียกว่า "ตัวแทน" หรือ "ผู้ช่วย AI" ได้ ตัวแทนเหล่านี้จะโต้ตอบกับโลกภายนอกได้ผ่าน API ภายนอก และกับบริการที่อาจส่งผลข้างเคียงต่อสภาพแวดล้อมภายนอก (เช่น ส่งอีเมล สร้างคำขอแจ้งปัญหา ฯลฯ)

คุณควรสร้างตัวแทนที่ทรงพลังอย่างมีความรับผิดชอบเมื่อสร้างตัวแทนที่ทรงพลังเช่นนี้ คุณควรพิจารณาการทำงานแบบมนุษย์ในลูปก่อนที่จะดำเนินการอัตโนมัติ คุณต้องคำนึงถึงความปลอดภัยเมื่อออกแบบตัวแทนที่ขับเคลื่อนด้วย LLM ซึ่งโต้ตอบกับโลกภายนอก

13. การเรียกใช้ Gemma ด้วย Ollama และ TestContainers

ตอนนี้เราก็ใช้ Gemini อยู่ แต่ก็มี Gemma ที่เป็นโมเดลน้องสาวของมันด้วย

Gemma คือตระกูลโมเดลแบบเปิดที่ทันสมัยและน้ำหนักเบา สร้างขึ้นจากการวิจัยและเทคโนโลยีเดียวกันกับที่ใช้ในการสร้างโมเดล Gemini Gemma วางจำหน่ายใน 2 รูปแบบคือ Gemma1 และ Gemma2 มีขนาดแตกต่างกันไป Gemma1 มี 2 ขนาด ได้แก่ 2B และ 7B Gemma2 มี 2 ขนาด ได้แก่ 9B และ 27B เครื่องมือชั่งน้ำหนักมีให้ใช้ได้อย่างอิสระ และขนาดเล็กช่วยให้คุณเรียกใช้ด้วยตัวเองได้ แม้จะใช้งานบนแล็ปท็อปหรือใน Cloud Shell ก็ตาม

คุณจะใช้ Gemma ได้อย่างไร

คุณเรียกใช้ Gemma ได้หลายวิธี ไม่ว่าจะเป็นในระบบคลาวด์ผ่าน Vertex AI เพียงคลิกปุ่มเดียว หรือ GKE ด้วย GPU บางส่วน แต่คุณก็เรียกใช้ภายในเครื่องได้เช่นกัน

ตัวเลือกที่ดีอย่างหนึ่งในการเรียกใช้ Gemma ภายในเครื่องคือการใช้ Ollama ซึ่งเป็นเครื่องมือที่ให้คุณเรียกใช้โมเดลขนาดเล็ก เช่น Llama 2, Mistral และอื่นๆ ในเครื่องของคุณได้ ซึ่งคล้ายกับ Docker แต่สำหรับ LLM

ติดตั้ง Ollama โดยทำตามวิธีการสำหรับระบบปฏิบัติการ

หากใช้สภาพแวดล้อม Linux คุณจะต้องเปิดใช้ Ollama ก่อนหลังจากติดตั้ง

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

เมื่อติดตั้งในเครื่องแล้ว คุณสามารถเรียกใช้คำสั่งเพื่อดึงโมเดลได้ดังนี้

ollama pull gemma:2b

รอให้โมเดลถูกดึง ซึ่งอาจใช้เวลาสักครู่

เรียกใช้โมเดล:

ollama run gemma:2b

ตอนนี้คุณสามารถโต้ตอบกับโมเดลได้แล้ว โดยทำดังนี้

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

หากต้องการออกจากข้อความแจ้ง ให้กด Ctrl+D

การเรียกใช้ Gemma ใน Ollama บน TestContainers

แทนที่จะต้องติดตั้งและเรียกใช้ Ollama ภายในเครื่อง คุณสามารถใช้ Ollama ภายในคอนเทนเนอร์ที่จัดการโดย TestContainers

TestContainers ไม่เพียงมีประโยชน์สำหรับการทดสอบ แต่คุณยังใช้คอนเทนเนอร์เพื่อเรียกใช้คอนเทนเนอร์ได้ด้วย นอกจากนี้ยังมี OllamaContainer ให้คุณใช้ประโยชน์ได้ด้วย

โดยภาพรวมทั้งหมดมีดังนี้

2382c05a48708dfd.png

การใช้งาน

เรามาลองดู GemmaWithOllamaContainer.java ทีละส่วนกัน

ก่อนอื่น คุณต้องสร้างคอนเทนเนอร์ Ollama ที่ได้มา ซึ่งจะดึงลงในโมเดล Gemma มีอิมเมจนี้อยู่แล้วจากการเรียกใช้ก่อนหน้าหรือระบบจะสร้างอิมเมจดังกล่าวขึ้น หากมีรูปภาพอยู่แล้ว คุณก็แค่บอก TestContainers ว่าคุณต้องการแทนที่รูปภาพ Ollama เริ่มต้นด้วยตัวแปรที่ขับเคลื่อนโดย Gemma ดังนี้

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

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

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

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

ถัดไป คุณจะต้องสร้างและเริ่มต้นคอนเทนเนอร์ทดสอบ Ollama แล้วสร้างโมเดลการแชท Ollama โดยชี้ไปยังที่อยู่และพอร์ตของคอนเทนเนอร์ที่มีโมเดลที่คุณต้องการใช้ สุดท้าย คุณเรียกใช้ model.generate(yourPrompt) ตามปกติ:

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

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

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

    System.out.println(response);
}

โดยให้เรียกใช้ดังนี้

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

การเรียกใช้ครั้งแรกจะใช้เวลาสักครู่ในการสร้างและเรียกใช้คอนเทนเนอร์ แต่เมื่อดำเนินการเสร็จแล้ว คุณควรเห็น Gemma ตอบสนองดังนี้

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

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

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

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

คุณมี Gemma กำลังทำงานใน Cloud Shell!

14. ขอแสดงความยินดี

ยินดีด้วย คุณสร้างแอปพลิเคชันแชท Generative AI รายการแรกใน Java โดยใช้ LangChain4j และ Gemini API สำเร็จแล้ว ระหว่างทางคุณได้ค้นพบว่าโมเดลภาษาขนาดใหญ่แบบหลายโมดัลนั้นมีประสิทธิภาพและสามารถจัดการงานต่างๆ ได้มากมาย เช่น การถาม/ตอบคำถาม แม้แต่ในเอกสารประกอบของคุณเอง การดึงข้อมูล การโต้ตอบกับ API ภายนอก และอื่นๆ อีกมากมาย

สิ่งที่ต้องทำต่อไป

ถึงเวลาที่คุณจะปรับปรุงแอปพลิเคชันด้วยการผสานรวม LLM ที่มีประสิทธิภาพแล้ว

อ่านเพิ่มเติม

เอกสารอ้างอิง