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

1. บทนำ

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

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

Generative AI คืออะไร

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

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

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

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

วิธีที่พบบ่อยที่สุดในการฝึกโมเดล Generative AI คือการใช้การเรียนรู้แบบควบคุมดูแล โมเดลจะได้รับชุดเนื้อหาที่มนุษย์สร้างขึ้นและป้ายกำกับที่เกี่ยวข้อง จากนั้น 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 ทำงานด้วยระบบ AI ของ Gemini เพื่อช่วยให้คุณทำงานได้มากขึ้นและเร็วขึ้น ซึ่งพร้อมใช้งานใน Google Cloud และ IDE Gemini Code Assist ให้บริการเติมโค้ด การสร้างโค้ด คำอธิบายโค้ด และให้คุณแชทเพื่อถามคำถามทางเทคนิค

Gemini คืออะไร

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

b9913d011999e7c7.png

Gemini มีผลิตภัณฑ์ย่อยและขนาดต่างๆ ดังนี้

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

ฟีเจอร์หลัก

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

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

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

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

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

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

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

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

เริ่ม Cloud Shell

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

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

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

3c1dabeca90e44e5.png

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

9c92662c6a846a5c.png

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

9f0e51b578fecce5.png

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

เมื่อเชื่อมต่อกับ 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. การเตรียมความพร้อมสภาพแวดล้อมในการพัฒนา

ในโค้ดแล็บนี้ คุณจะใช้เทอร์มินัล 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

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

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 Editor จาก Cloud Shell โดยทำดังนี้

42908e11b28f4383.png

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

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

เปิดเทอร์มินัลใหม่ในเครื่องมือแก้ไขโค้ดของ Cloud โดยเลือก 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-002")
            .build();

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

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

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

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

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

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

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

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

คุณควรเห็นเอาต์พุตที่คล้ายกับตัวอย่างนี้

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

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

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

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

การตอบกลับแบบสตรีม

คุณสังเกตเห็นไหมว่าคำตอบแสดงขึ้นพร้อมกันหลังจากผ่านไป 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 static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;

public class StreamQA {
    public static void main(String[] args) {
        StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(4000)
            .build();

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

ในครั้งนี้ เราจะนําเข้าตัวแปรชั้นเรียนสตรีมมิง VertexAiGeminiStreamingChatModel ที่ใช้อินเทอร์เฟซ StreamingChatLanguageModel นอกจากนี้ คุณจะต้องนําเข้า LambdaStreamingResponseHandler.onNext แบบคงที่ ซึ่งเป็นเมธอดที่สะดวกซึ่งให้ StreamingResponseHandler เพื่อสร้างตัวแฮนเดิลสตรีมมิงด้วยนิพจน์ Lambda ของ Java

ในครั้งนี้ ลายเซ็นของเมธอด generate() จะแตกต่างออกไปเล็กน้อย ประเภทผลลัพธ์จะเป็น void แทนที่จะแสดงผลสตริง นอกจากพรอมต์แล้ว คุณยังต้องส่งตัวแฮนเดิลการตอบกลับแบบสตรีมด้วย ที่นี่ เราสามารถกำหนดนิพจน์ Lambda ที่ส่งไปยังเมธอด onNext() ได้ด้วยการนำเข้าแบบคงที่ที่เราพูดถึงข้างต้น ระบบจะเรียกใช้นิพจน์ Lambda ทุกครั้งที่มีคำตอบใหม่ ส่วนนิพจน์หลังจะเรียกใช้ก็ต่อเมื่อมีข้อผิดพลาดเท่านั้น

เรียกใช้

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

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

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

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

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

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

5. แชทกับ Gemini

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

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

package gemini.workshop;

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

import java.util.List;

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

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

        interface ConversationService {
            String chat(String message);
        }

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

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

การนําเข้าใหม่ที่น่าสนใจ 2 รายการในชั้นเรียนนี้ ได้แก่

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

คุณสามารถถามคำถามแบบ 1 ประโยคหรือสนทนาแบบหลายประโยคกับ 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-002")
            .build();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

package gemini.workshop;

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

import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;

public class ExtractData {

    record Person(String name, int age) { }

    interface PersonExtractor {
        @SystemMessage("""
            Your role is to extract the name and age 
            of the person described in the biography.
            """)
        Person extractPerson(String biography);
    }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .responseMimeType("application/json")
            .responseSchema(fromClass(Person.class))
            .build();

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

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

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

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

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

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

  • กำหนดค่าและสร้างอินสแตนซ์โมเดลการแชท เราใช้เมธอดใหม่ 2 รายการของคลาสเครื่องมือสร้างโมเดล ได้แก่ responseMimeType() และ responseSchema() รายการแรกบอกให้ Gemini สร้าง JSON ที่ถูกต้องในเอาต์พุต เมธอดที่ 2 จะกำหนดสคีมาของออบเจ็กต์ JSON ที่ควรแสดงผล นอกจากนี้ คลาสหลังยังมอบสิทธิ์ให้เมธอดที่สะดวกซึ่งสามารถแปลงคลาสหรือเรคคอร์ด Java เป็นสคีมา 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 คุณจะจัดการคลาสและออบเจ็กต์ปกติเท่านั้นเมื่อใช้อินเทอร์เฟซ PersonExtractor นี้

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-002")
            .maxOutputTokens(500)
            .temperature(1.0f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

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

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

        Prompt prompt = promptTemplate.apply(variables);

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

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

ตามปกติ คุณจะกําหนดค่ารูปแบบ VertexAiGeminiChatModel ที่มีความคิดสร้างสรรค์ระดับสูงซึ่งมีอุณหภูมิสูง รวมถึงค่า topP และ topK ที่สูง จากนั้นสร้าง PromptTemplate ด้วยเมธอดแบบคงที่ from() โดยส่งสตริงของพรอมต์ และใช้ตัวแปรตัวยึดตําแหน่งวงเล็บปีกกาคู่ {{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. การจัดประเภทข้อความด้วยพรอมต์แบบ Few-Shot

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

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

package gemini.workshop;

import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;

import java.util.List;

public class TextClassification {

    enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }

    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-002")
            .maxOutputTokens(10)
            .maxRetries(3)
            .responseSchema(Schema.newBuilder()
                .setType(Type.STRING)
                .addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
                .build())
            .build();


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

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

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

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

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

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

Sentiment enum จะแสดงค่าต่างๆ สำหรับความรู้สึก ได้แก่ เชิงลบ เป็นกลาง หรือเชิงบวก

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

เมื่อกําหนดค่าโมเดลแล้ว คุณก็สร้างอินเทอร์เฟซ SentimentAnalysis ที่ AiServices ของ LangChain4j จะใช้ LLM ให้คุณ อินเทอร์เฟซนี้มีเมธอดเดียวคือ analyze() โดยจะใช้ข้อความในอินพุตเพื่อวิเคราะห์และแสดงผลค่า Sentiment enum คุณจึงจัดการได้เฉพาะออบเจ็กต์ที่มีการจัดประเภทอย่างเข้มงวดซึ่งแสดงถึงคลาสของความรู้สึกที่ระบบจดจำได้

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

มารวมทุกอย่างเข้าด้วยกันด้วยเมธอด AiServices.builder() โดยส่งอินเทอร์เฟซ SentimentAnalysis, โมเดลที่จะใช้ และหน่วยความจำแชทที่มีตัวอย่างแบบไม่กี่ช็อต สุดท้าย ให้เรียกใช้เมธอด analyze() พร้อมข้อความที่จะวิเคราะห์

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

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

คุณควรเห็นคําเดียวดังต่อไปนี้

POSITIVE

ดูเหมือนว่าความรักสตรอเบอร์รี่จะเป็นความรู้สึกเชิงบวก

10. การสร้างที่เพิ่มการเรียกข้อมูล

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

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

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

การประเมิน RAG มี 2 ระยะ ได้แก่

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

cd07d33d20ffa1c8.png

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

a1d2e2deb83c6d27.png

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

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

ติดตั้งใช้งานแชทบ็อต

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

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

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

การส่งผ่านข้อมูลเอกสาร

ขั้นตอนแรกสุดของระยะการส่งผ่านข้อมูลเอกสารคือการค้นหาไฟล์ 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();

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

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

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

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

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

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

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

การถามคําถาม

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

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

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

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

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

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

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

เมื่อถึงขั้นตอนนี้ คุณจะกําหนดค่าบริการ AI ใหม่ได้ ดังนี้

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

บริการนี้เชื่อมโยงสิ่งต่อไปนี้เข้าด้วยกัน

  • รูปแบบภาษาแชทที่คุณกําหนดค่าไว้ก่อนหน้านี้
  • ความทรงจำในแชทเพื่อติดตามการสนทนา
  • retriever จะเปรียบเทียบการค้นหาการฝังเวกเตอร์กับเวกเตอร์ในฐานข้อมูล
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in car automotive, and you answer concisely.

            Here is the question: {{userMessage}}

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

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

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

ซอร์สโค้ดแบบเต็มอยู่ใน RAG.java ในไดเรกทอรี app/src/main/java/gemini/workshop

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

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

คุณควรเห็นคำตอบของคำถามต่อไปนี้ในเอาต์พุต

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

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

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

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

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

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

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

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

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

11. การเรียกใช้ฟังก์ชัน

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

Web API ระยะไกล

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

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

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

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

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

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

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

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

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

31e0c2aba5e6f21c.png

1️⃣ ก่อนอื่น ผู้ใช้ถามเกี่ยวกับสภาพอากาศในปารีส แอปแชทบ็อต (โดยใช้ LangChain4j) รู้ว่ามีฟังก์ชันอย่างน้อย 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-002")
    .maxOutputTokens(100)
    .build();

คุณกำหนดข้อกำหนดของเครื่องมือที่อธิบายฟังก์ชันที่เรียกใช้ได้ดังนี้

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

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

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

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

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

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

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

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

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

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

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

เอาต์พุตคือ

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

คุณจะเห็นคำขอการเรียกใช้เครื่องมือและคำตอบในเอาต์พุตเหนือคำขอ

ซอร์สโค้ดแบบเต็มอยู่ใน FunctionCalling.java ในไดเรกทอรี app/src/main/java/gemini/workshop

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

interface WeatherAssistant {
    String chat(String userMessage);
}

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

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

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

    WeatherForecastService weatherForecastService = new WeatherForecastService();

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

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

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

ตอนนี้คุณใช้คลาส AiServices อีกครั้งเพื่อเชื่อมโยงโมเดลแชท หน่วยความจำแชท และเครื่องมือ (เช่น บริการพยากรณ์อากาศที่มีฟังก์ชันการทำงาน) AiServices จะแสดงผลออบเจ็กต์ที่ใช้อินเทอร์เฟซ WeatherAssistant ที่คุณกำหนด ขั้นตอนสุดท้ายคือการเรียกใช้เมธอด chat() ของ Assistant นั้น เมื่อเรียกใช้ คุณจะไม่เห็นคำตอบที่เป็นข้อความเท่านั้น แต่นักพัฒนาแอปจะไม่เห็นคําขอเรียกใช้ฟังก์ชันและการตอบกลับการเรียกใช้ฟังก์ชัน และระบบจะจัดการคําขอเหล่านั้นโดยอัตโนมัติและโปร่งใส หาก 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-002")
        .maxOutputTokens(100)
        .build();

    MultiTools multiTools = new MultiTools();

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

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

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

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

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

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

สำหรับตัวแทน

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

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

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

ที่ผ่านมาเราใช้ Gemini แต่ก็มี Gemma ซึ่งเป็นรุ่นน้องด้วย

Gemma คือกลุ่มโมเดลแบบเปิดที่ทันสมัยและน้ำหนักเบาซึ่งสร้างขึ้นจากงานวิจัยและเทคโนโลยีเดียวกับที่ใช้สร้างโมเดล Gemini Gemma มี 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 ภายในคอนเทนเนอร์ที่จัดการโดย TestContainers แทนที่จะต้องติดตั้งและเรียกใช้ Ollama ในเครื่อง

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 อันทรงพลัง

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

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