О практической работе
1. Введение
Эта кодовая лаборатория фокусируется на Gemini Large Language Model (LLM), размещенной на Vertex AI в Google Cloud. Vertex AI — это платформа, которая охватывает все продукты, сервисы и модели машинного обучения в Google Cloud.
Вы будете использовать Java для взаимодействия с API Gemini с использованием фреймворка LangChain4j . Вы рассмотрите конкретные примеры, чтобы воспользоваться преимуществами LLM для ответов на вопросы, генерации идей, извлечения сущностей и структурированного контента, расширенной генерации поиска и вызова функций.
Что такое генеративный ИИ?
Генеративный ИИ подразумевает использование искусственного интеллекта для создания нового контента, такого как текст, изображения, музыка, аудио и видео.
Генеративный ИИ работает на основе больших языковых моделей (LLM), которые могут выполнять многозадачность и нестандартные задачи, такие как реферирование, вопросы и ответы, классификация и т. д. При минимальном обучении базовые модели можно адаптировать для целевых вариантов использования с очень небольшим количеством данных примеров.
Как работает генеративный ИИ?
Генеративный ИИ работает, используя модель машинного обучения (ML) для изучения закономерностей и взаимосвязей в наборе данных контента, созданного человеком. Затем он использует изученные закономерности для генерации нового контента.
Наиболее распространенный способ обучения генеративной модели ИИ — это использование контролируемого обучения. Модель получает набор контента, созданного человеком, и соответствующие метки. Затем она учится генерировать контент, похожий на контент, созданный человеком.
Каковы распространенные области применения генеративного ИИ?
Генеративный ИИ может использоваться для:
- Улучшите взаимодействие с клиентами с помощью усовершенствованных чатов и поиска.
- Исследуйте огромные объемы неструктурированных данных с помощью диалоговых интерфейсов и обобщений.
- Помощь в выполнении повторяющихся задач, таких как ответы на запросы предложений, локализация маркетингового контента на разных языках, проверка клиентских договоров на соответствие требованиям и многое другое.
Какие возможности генеративного ИИ предлагает Google Cloud?
С Vertex AI вы можете взаимодействовать с моделями фундамента, настраивать их и встраивать в свои приложения, имея лишь небольшой опыт в области машинного обучения или не имея его вовсе. Вы можете получить доступ к моделям фундамента в Model Garden , настроить модели с помощью простого пользовательского интерфейса в Vertex AI Studio или использовать модели в блокноте по науке о данных.
Vertex AI Search and Conversation предлагает разработчикам самый быстрый способ создания поисковых систем и чат-ботов на базе генеративного искусственного интеллекта.
Gemini для Google Cloud , работающий на базе Gemini, — это ИИ-совместимый помощник, доступный в Google Cloud и IDE, который поможет вам сделать больше и быстрее. Gemini Code Assist обеспечивает автодополнение кода, генерацию кода, пояснения кода и позволяет вам общаться с ним, чтобы задавать технические вопросы.
Что такое Близнецы?
Gemini — это семейство генеративных моделей ИИ, разработанных Google DeepMind, которые предназначены для мультимодальных вариантов использования. Мультимодальный означает, что он может обрабатывать и генерировать различные виды контента, такие как текст, код, изображения и аудио.
Gemini бывают разных вариаций и размеров:
- Gemini 2.0 Flash : наши новейшие функции следующего поколения и улучшенные возможности.
- Gemini 2.0 Flash-Lite : модель Gemini 2.0 Flash, оптимизированная для обеспечения экономической эффективности и низкой задержки.
- Gemini 2.5 Pro : наша самая продвинутая модель рассуждений на сегодняшний день.
- Gemini 2.5 Flash : мыслящая модель, которая предлагает всесторонние возможности. Она разработана для обеспечения баланса между ценой и производительностью.
Основные характеристики:
- Мультимодальность : способность Gemini понимать и обрабатывать различные форматы информации является значительным шагом вперед по сравнению с традиционными текстовыми языковыми моделями.
- Производительность : Gemini 2.5 Pro превосходит современные технологии по многим показателям и стал первой моделью, которая превзошла экспертов в сложном тесте MMLU (Massive Multitask Language Understanding).
- Гибкость : различные размеры Gemini позволяют адаптировать его для различных вариантов использования: от крупномасштабных исследований до развертывания на мобильных устройствах.
Как можно взаимодействовать с Gemini on Vertex AI из Java?
У вас есть два варианта:
- Официальная библиотека Vertex AI Java API для Gemini .
- Фреймворк LangChain4j .
В этой лабораторной работе вы будете использовать фреймворк LangChain4j .
Что такое фреймворк LangChain4j?
Фреймворк LangChain4j — это библиотека с открытым исходным кодом для интеграции LLM в ваши приложения Java путем организации различных компонентов, таких как сам LLM, а также других инструментов, таких как векторные базы данных (для семантического поиска), загрузчики и разделители документов (для анализа документов и обучения на их основе), парсеры вывода и многое другое.
Проект был вдохновлен проектом LangChain Python, но с целью обслуживания разработчиков Java.
Чему вы научитесь
- Как настроить проект Java для использования Gemini и LangChain4j
- Как программно отправить свой первый запрос в Gemini
- Как транслировать ответы от Gemini
- Как создать беседу между пользователем и Gemini
- Как использовать Gemini в мультимодальном контексте, отправляя как текст, так и изображения
- Как извлечь полезную структурированную информацию из неструктурированного контента
- Как манипулировать шаблонами подсказок
- Как выполнить классификацию текста, например анализ настроений
- Как общаться с собственными документами (Retrieval Augmented Generation)
- Как расширить возможности чат-ботов с помощью вызова функций
- Как использовать Gemma локально с Ollama и TestContainers
Что вам понадобится
- Знание языка программирования Java
- Проект Google Cloud
- Браузер, например Chrome или Firefox
2. Настройка и требования
Самостоятельная настройка среды
- Войдите в Google Cloud Console и создайте новый проект или повторно используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .
- Имя проекта — это отображаемое имя для участников этого проекта. Это строка символов, не используемая API Google. Вы всегда можете ее обновить.
- Идентификатор проекта уникален для всех проектов Google Cloud и неизменяем (его нельзя изменить после установки). Cloud Console автоматически генерирует уникальную строку; обычно вам все равно, что это такое. В большинстве кодовых лабораторий вам нужно будет ссылаться на свой идентификатор проекта (обычно идентифицируемый как
PROJECT_ID
). Если вам не нравится сгенерированный идентификатор, вы можете сгенерировать другой случайный идентификатор. В качестве альтернативы вы можете попробовать свой собственный и посмотреть, доступен ли он. Его нельзя изменить после этого шага, и он остается на протяжении всего проекта. - Для информации, есть третье значение, Project Number , которое используют некоторые API. Узнайте больше обо всех трех этих значениях в документации .
- Далее вам нужно будет включить биллинг в Cloud Console для использования ресурсов/API Cloud. Прохождение этой кодовой лаборатории не будет стоить много, если вообще будет стоить. Чтобы отключить ресурсы и избежать выставления счетов за пределами этого руководства, вы можете удалить созданные вами ресурсы или удалить проект. Новые пользователи Google Cloud имеют право на бесплатную пробную программу стоимостью 300 долларов США .
Запустить Cloud Shell
Хотя Google Cloud можно управлять удаленно с вашего ноутбука, в этой лабораторной работе вы будете использовать Cloud Shell — среду командной строки, работающую в облаке.
Активировать Cloud Shell
- В консоли Cloud нажмите «Активировать Cloud Shell» .
.
Если вы впервые запускаете Cloud Shell, вам будет представлен промежуточный экран с описанием того, что это такое. Если вам был представлен промежуточный экран, нажмите Продолжить .
Подготовка и подключение к Cloud Shell займет всего несколько минут.
Эта виртуальная машина загружена всеми необходимыми инструментами разработки. Она предлагает постоянный домашний каталог размером 5 ГБ и работает в Google Cloud, значительно повышая производительность сети и аутентификацию. Значительную часть, если не всю, работы в этой кодовой лаборатории можно выполнить с помощью браузера.
После подключения к Cloud Shell вы должны увидеть, что вы прошли аутентификацию и что проекту присвоен ваш идентификатор проекта.
- Выполните следующую команду в 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`
- Выполните следующую команду в 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.
Включить API Vertex AI
В консоли Google Cloud убедитесь, что имя вашего проекта отображается в верхней части консоли Google Cloud . Если это не так, нажмите Выбрать проект , чтобы открыть селектор проектов , и выберите нужный проект.
Вы можете включить API Vertex AI либо из раздела Vertex AI консоли Google Cloud, либо из терминала Cloud Shell.
Чтобы включить функцию из консоли Google Cloud, сначала перейдите в раздел Vertex AI в меню консоли Google Cloud:
Нажмите «Включить все рекомендуемые API» на панели инструментов Vertex AI.
Это позволит использовать несколько API, но наиболее важным для кодовой лаборатории является aiplatform.googleapis.com
.
Кроме того, вы также можете включить этот API из терминала Cloud Shell с помощью следующей команды:
gcloud services enable aiplatform.googleapis.com
Клонируйте репозиторий Github
В терминале Cloud Shell клонируйте репозиторий для этой кодовой лаборатории:
git clone https://github.com/glaforge/gemini-workshop-for-java-developers.git
Чтобы проверить готовность проекта к запуску, вы можете попробовать запустить программу «Hello World».
Убедитесь, что вы находитесь в папке верхнего уровня:
cd gemini-workshop-for-java-developers/
Создайте оболочку Gradle:
gradle wrapper
Запустить с помощью gradlew
:
./gradlew run
Вы должны увидеть следующий вывод:
.. > Task :app:run Hello World!
Откройте и настройте Cloud Editor
Откройте код с помощью редактора кода Cloud Code Editor из Cloud Shell:
В редакторе кода Cloud откройте исходную папку codelab, выбрав File
-> Open Folder
, и укажите исходную папку codelab (например, /home/username/gemini-workshop-for-java-developers/
).
Настройка переменных среды
Откройте новый терминал в Cloud Code Editor, выбрав Terminal
-> New Terminal
. Настройте две переменные среды, необходимые для запуска примеров кода:
- PROJECT_ID — Ваш идентификатор проекта Google Cloud
- РАСПОЛОЖЕНИЕ — Регион, где развернута модель Gemini.
Экспортируйте переменные следующим образом:
export PROJECT_ID=$(gcloud config get-value project) export LOCATION=us-central1
4. Первый звонок модели Gemini
Теперь, когда проект правильно настроен, пришло время вызвать API Gemini.
Взгляните на QA.java
в каталоге app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.model.chat.ChatLanguageModel;
public class QA {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
System.out.println(model.generate("Why is the sky blue?"));
}
}
В этом первом примере вам необходимо импортировать класс VertexAiGeminiChatModel
, который реализует интерфейс ChatModel
.
В main
методе вы настраиваете модель языка чата, используя конструктор для VertexAiGeminiChatModel
, и указываете:
- Проект
- Расположение
- Название модели (
gemini-2.0-flash
).
Теперь, когда языковая модель готова, вы можете вызвать метод generate()
и передать вашу подсказку, ваш вопрос или инструкции для отправки в LLM. Здесь вы задаете простой вопрос о том, что делает небо голубым.
Вы можете свободно изменять эту подсказку, чтобы попробовать другие вопросы или задания.
Запустите пример в корневой папке исходного кода:
./gradlew run -q -DjavaMainClass=gemini.workshop.QA
Вы должны увидеть вывод, подобный этому:
The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlight enters the atmosphere, it is made up of a mixture of different wavelengths of light, each with a different color. The different wavelengths of light interact with the molecules and particles in the atmosphere in different ways. The shorter wavelengths of light, such as those corresponding to blue and violet light, are more likely to be scattered in all directions by these particles than the longer wavelengths of light, such as those corresponding to red and orange light. This is because the shorter wavelengths of light have a smaller wavelength and are able to bend around the particles more easily. As a result of Rayleigh scattering, the blue light from the sun is scattered in all directions, and it is this scattered blue light that we see when we look up at the sky. The blue light from the sun is not actually scattered in a single direction, so the color of the sky can vary depending on the position of the sun in the sky and the amount of dust and water droplets in the atmosphere.
Поздравляем, вы совершили свой первый звонок Близнецам!
Потоковый ответ
Вы заметили, что ответ был дан сразу, через несколько секунд? Также возможно получать ответ постепенно, благодаря потоковому варианту ответа. Потоковый ответ, модель возвращает ответ по частям, по мере его поступления.
В этой лабораторной работе мы остановимся на непотоковом ответе, но давайте рассмотрим потоковый ответ, чтобы увидеть, как это можно реализовать.
В StreamQA.java
в каталоге app/src/main/java/gemini/workshop
вы можете увидеть потоковый ответ в действии:
package gemini.workshop;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiStreamingChatModel;
import static dev.langchain4j.model.LambdaStreamingResponseHandler.onNext;
public class StreamQA {
public static void main(String[] args) {
StreamingChatLanguageModel model = VertexAiGeminiStreamingChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(4000)
.build();
model.generate("Why is the sky blue?", onNext(System.out::println));
}
}
На этот раз мы импортируем варианты потокового класса VertexAiGeminiStreamingChatModel
, который реализует интерфейс StreamingChatLanguageModel
. Вам также понадобится статический импорт LambdaStreamingResponseHandler.onNext
, который является удобным методом, предоставляющим StreamingResponseHandler
для создания потокового обработчика с лямбда-выражениями Java.
На этот раз сигнатура метода generate()
немного отличается. Вместо возврата строки возвращаемый тип — void. В дополнение к приглашению вам нужно передать потоковый обработчик ответа. Здесь, благодаря статическому импорту, о котором мы упоминали выше, мы можем определить лямбда-выражение, которое вы передаете методу onNext()
. Лямбда-выражение вызывается каждый раз, когда становится доступна новая часть ответа, тогда как последний вызывается только в случае возникновения ошибки.
Бегать:
./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 раза.
До сих пор вы задавали один вопрос Близнецам, но вы также можете вести многоходовой разговор. Это то, что вы изучите в следующем разделе.
5. Чат с Близнецами
На предыдущем шаге вы задали один вопрос. Теперь пришло время для настоящего разговора между пользователем и LLM. Каждый вопрос и ответ могут основываться на предыдущих, чтобы сформировать настоящее обсуждение.
Взгляните на Conversation.java
в папке app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.AiServices;
import java.util.List;
public class Conversation {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20)
.build();
interface ConversationService {
String chat(String message);
}
ConversationService conversation =
AiServices.builder(ConversationService.class)
.chatLanguageModel(model)
.chatMemory(chatMemory)
.build();
List.of(
"Hello!",
"What is the country where the Eiffel tower is situated?",
"How many inhabitants are there in that country?"
).forEach( message -> {
System.out.println("\nUser: " + message);
System.out.println("Gemini: " + conversation.chat(message));
});
}
}
Пара новых интересных импортов в этом классе:
-
MessageWindowChatMemory
— класс, который поможет справиться с многоходовым аспектом разговора и сохранить в локальной памяти предыдущие вопросы и ответы. -
AiServices
— класс абстракции более высокого уровня, который свяжет модель чата и память чата.
В основном методе вы собираетесь настроить модель, память чата и службу ИИ. Модель настраивается как обычно с информацией о проекте, местоположении и названии модели.
Для памяти чата мы используем конструктор MessageWindowChatMemory
для создания памяти, которая хранит последние 20 сообщений, которыми мы обменялись. Это скользящее окно над разговором, контекст которого хранится локально в нашем клиенте класса Java.
Затем вы создаете AI service
, которая связывает модель чата с памятью чата.
Обратите внимание, как служба ИИ использует определенный нами пользовательский интерфейс ConversationService
, который реализует LangChain4j и который принимает String
запрос и возвращает String
ответ.
Теперь пришло время поговорить с Gemini. Сначала отправляется простое приветствие, затем первый вопрос об Эйфелевой башне, чтобы узнать, в какой стране она находится. Обратите внимание, что последнее предложение связано с ответом на первый вопрос, поскольку вы интересуетесь, сколько жителей проживает в стране, где находится Эйфелева башня, без явного упоминания страны, которая была указана в предыдущем ответе. Это показывает, что прошлые вопросы и ответы отправляются с каждой подсказкой.
Запустите образец:
./gradlew run -q -DjavaMainClass=gemini.workshop.Conversation
Вы должны увидеть три ответа, похожих на эти:
User: Hello! Gemini: Hi there! How can I assist you today? User: What is the country where the Eiffel tower is situated? Gemini: France User: How many inhabitants are there in that country? Gemini: As of 2023, the population of France is estimated to be around 67.8 million.
Вы можете задавать однопоточные вопросы или вести многопоточные беседы с Gemini, но до сих пор ввод был только текстовым. А как насчет изображений? Давайте рассмотрим изображения на следующем этапе.
6. Мультимодальность с Gemini
Gemini — это многомодальная модель. Она принимает не только текст в качестве входных данных, но и изображения, или даже видео в качестве входных данных. В этом разделе вы увидите пример использования для смешивания текста и изображений.
Как вы думаете, Близнецы узнают этого кота?
Фотография кота на снегу взята из Википедии
Взгляните на Multimodal.java
в каталоге app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.TextContent;
public class Multimodal {
static final String CAT_IMAGE_URL =
"https://upload.wikimedia.org/wikipedia/" +
"commons/b/b6/Felis_catus-cat_on_snow.jpg";
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.build();
UserMessage userMessage = UserMessage.from(
ImageContent.from(CAT_IMAGE_URL),
TextContent.from("Describe the picture")
);
Response<AiMessage> response = model.generate(userMessage);
System.out.println(response.content().text());
}
}
Обратите внимание, что в импортах мы различаем разные типы сообщений и содержимого. UserMessage
может содержать как объект TextContent
, так и объект ImageContent
. Это мультимодальность в действии: смешивание текста и изображений. Мы не просто отправляем простую строку-подсказку, мы отправляем более структурированный объект, представляющий пользовательское сообщение, состоящее из части содержимого изображения и части текстового содержимого. Модель отправляет обратно Response
, который содержит AiMessage
.
Затем вы извлекаете AiMessage
из ответа с помощью content()
, а затем текст сообщения — с помощью text()
.
Запустите образец:
./gradlew run -q -DjavaMainClass=gemini.workshop.Multimodal
Название картинки, безусловно, дало намёк на то, что на ней изображено, но вывод Gemini похож на следующий:
A cat with brown fur is walking in the snow. The cat has a white patch of fur on its chest and white paws. The cat is looking at the camera.
Смешивание изображений и текстовых подсказок открывает интересные варианты использования. Вы можете создавать приложения, которые могут:
- Распознавать текст на картинках.
- Проверьте, безопасно ли отображать изображение.
- Создавайте подписи к изображениям.
- Поиск по базе данных изображений с простыми текстовыми описаниями.
Помимо извлечения информации из изображений, вы также можете извлекать информацию из неструктурированного текста. Это то, что вы узнаете в следующем разделе.
7. Извлечение структурированной информации из неструктурированного текста
Существует множество ситуаций, когда важная информация предоставляется в отчетных документах, электронных письмах или других длинных текстах в неструктурированном виде. В идеале вы хотели бы иметь возможность извлекать ключевые детали, содержащиеся в неструктурированном тексте, в форме структурированных объектов. Давайте посмотрим, как это можно сделать.
Допустим, вы хотите извлечь имя и возраст человека, имея биографию, резюме или описание этого человека. Вы можете поручить LLM извлечь JSON из неструктурированного текста с помощью хитроумно настроенной подсказки (это обычно называется «инженерия подсказок» ).
Но в приведенном ниже примере вместо создания подсказки, описывающей вывод JSON, мы воспользуемся мощной функцией Gemini, называемой структурированным выводом или иногда ограниченной генерацией, заставляя модель выводить только допустимый контент JSON, следующий указанной схеме JSON .
Взгляните на ExtractData.java
в app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import static dev.langchain4j.model.vertexai.SchemaHelper.fromClass;
public class ExtractData {
record Person(String name, int age) { }
interface PersonExtractor {
@SystemMessage("""
Your role is to extract the name and age
of the person described in the biography.
""")
Person extractPerson(String biography);
}
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.responseMimeType("application/json")
.responseSchema(fromClass(Person.class))
.build();
PersonExtractor extractor = AiServices.create(PersonExtractor.class, model);
String bio = """
Anna is a 23 year old artist based in Brooklyn, New York. She was born and
raised in the suburbs of Chicago, where she developed a love for art at a
young age. She attended the School of the Art Institute of Chicago, where
she studied painting and drawing. After graduating, she moved to New York
City to pursue her art career. Anna's work is inspired by her personal
experiences and observations of the world around her. She often uses bright
colors and bold lines to create vibrant and energetic paintings. Her work
has been exhibited in galleries and museums in New York City and Chicago.
""";
Person person = extractor.extractPerson(bio);
System.out.println(person.name()); // Anna
System.out.println(person.age()); // 23
}
}
Давайте рассмотрим различные шаги в этом файле:
- Запись
Person
предназначена для представления сведений, описывающих человека (имя и возраст). - Интерфейс
PersonExtractor
определен с помощью метода, который, получив неструктурированную текстовую строку, возвращает экземплярPerson
. -
extractPerson()
аннотируется аннотацией@SystemMessage
, которая связывает с ним приглашение инструкции. Это приглашение, которое модель будет использовать для руководства извлечением информации и возврата деталей в форме документа JSON, который будет проанализирован для вас и демаршаллирован в экземплярPerson
.
Теперь давайте посмотрим на содержимое метода main()
:
- Модель чата настроена и инстанцирована. Мы используем 2 новых метода класса конструктора моделей:
responseMimeType()
иresponseSchema()
. Первый из них сообщает Gemini о необходимости генерировать допустимый JSON на выходе. Второй метод определяет схему объекта JSON, который должен быть возвращен. Кроме того, последний делегирует полномочия удобному методу, который может преобразовать класс или запись Java в надлежащую схему JSON. - Объект
PersonExtractor
создается благодаря классуAiServices
LangChain4j. - Затем вы можете просто вызвать
Person person = extractor.extractPerson(...)
чтобы извлечь сведения о человеке из неструктурированного текста и получить обратно экземплярPerson
с именем и возрастом.
Запустите образец:
./gradlew run -q -DjavaMainClass=gemini.workshop.ExtractData
Вы должны увидеть следующий вывод:
Anna 23
Да, это Анна и им 23!
При таком подходе AiServices
вы работаете со строго типизированными объектами. Вы не взаимодействуете напрямую с LLM. Вместо этого вы работаете с конкретными классами, такими как запись Person
, для представления извлеченной личной информации, и у вас есть объект PersonExtractor
с методом extractPerson()
, который возвращает экземпляр Person
. Понятие LLM абстрагируется, и как разработчик Java вы просто манипулируете обычными классами и объектами, когда используете этот интерфейс PersonExtractor
.
8. Структурируйте подсказки с помощью шаблонов подсказок
Когда вы взаимодействуете с LLM, используя общий набор инструкций или вопросов, есть часть этого приглашения, которая никогда не меняется, в то время как другие части содержат данные. Например, если вы хотите создать рецепты, вы можете использовать приглашение типа «Вы талантливый повар, пожалуйста, создайте рецепт со следующими ингредиентами: ...», а затем вы добавите ингредиенты в конец этого текста. Вот для чего нужны шаблоны приглашений — аналогично интерполированным строкам в языках программирования. Шаблон приглашения содержит заполнители, которые вы можете заменить правильными данными для конкретного вызова LLM.
Более конкретно, давайте изучим TemplatePrompt.java
в каталоге app/src/main/java/gemini/workshop
:
package gemini.workshop;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.output.Response;
import java.util.HashMap;
import java.util.Map;
public class TemplatePrompt {
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(500)
.temperature(1.0f)
.topK(40)
.topP(0.95f)
.maxRetries(3)
.build();
PromptTemplate promptTemplate = PromptTemplate.from("""
You're a friendly chef with a lot of cooking experience.
Create a recipe for a {{dish}} with the following ingredients: \
{{ingredients}}, and give it a name.
"""
);
Map<String, Object> variables = new HashMap<>();
variables.put("dish", "dessert");
variables.put("ingredients", "strawberries, chocolate, and whipped cream");
Prompt prompt = promptTemplate.apply(variables);
Response<AiMessage> response = model.generate(prompt.toUserMessage());
System.out.println(response.content().text());
}
}
Как обычно, вы настраиваете модель VertexAiGeminiChatModel
с высоким уровнем креативности с высокой температурой, а также высокими значениями topP и topK. Затем вы создаете PromptTemplate
с его статическим методом from()
, передавая строку нашего приглашения, и используете переменные-заполнители с двойными фигурными скобками: и
.
Окончательное приглашение создается путем вызова метода 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. Классификация текста с подсказками в несколько кадров
LLM довольно хорошо классифицируют текст по разным категориям. Вы можете помочь LLM в этой задаче, предоставив несколько примеров текстов и связанных с ними категорий. Этот подход часто называют подсказкой с небольшим количеством выстрелов .
Давайте откроем TextClassification.java
в каталоге app/src/main/java/gemini/workshop
, чтобы выполнить определенный тип классификации текста: анализ настроений.
package gemini.workshop;
import com.google.cloud.vertexai.api.Schema;
import com.google.cloud.vertexai.api.Type;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import java.util.List;
public class TextClassification {
enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(10)
.maxRetries(3)
.responseSchema(Schema.newBuilder()
.setType(Type.STRING)
.addAllEnum(List.of("POSITIVE", "NEUTRAL", "NEGATIVE"))
.build())
.build();
interface SentimentAnalysis {
@SystemMessage("""
Analyze the sentiment of the text below.
Respond only with one word to describe the sentiment.
""")
Sentiment analyze(String text);
}
MessageWindowChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
memory.add(UserMessage.from("This is fantastic news!"));
memory.add(AiMessage.from(Sentiment.POSITIVE.name()));
memory.add(UserMessage.from("Pi is roughly equal to 3.14"));
memory.add(AiMessage.from(Sentiment.NEUTRAL.name()));
memory.add(UserMessage.from("I really disliked the pizza. Who would use pineapples as a pizza topping?"));
memory.add(AiMessage.from(Sentiment.NEGATIVE.name()));
SentimentAnalysis sentimentAnalysis =
AiServices.builder(SentimentAnalysis.class)
.chatLanguageModel(model)
.chatMemory(memory)
.build();
System.out.println(sentimentAnalysis.analyze("I love strawberries!"));
}
}
Перечисление Sentiment
перечисляет различные значения настроения: отрицательное, нейтральное или положительное.
В методе main()
вы создаете модель чата Gemini как обычно, но с небольшим максимальным номером выходного токена, так как вам нужен только короткий ответ: текст POSITIVE
, NEGATIVE
или NEUTRAL
. И чтобы ограничить модель, чтобы она возвращала только эти значения, исключительно, вы можете воспользоваться структурированной поддержкой вывода, которую вы обнаружили в разделе извлечения данных. Вот почему используется метод responseSchema()
. На этот раз вы не используете удобный метод из SchemaHelper
для вывода определения схемы, а вместо этого используете конструктор Schema
, чтобы понять, как выглядит определение схемы.
После настройки модели вы создаете интерфейс SentimentAnalysis
, который LangChain4j's AiServices
реализует для вас с помощью LLM. Этот интерфейс содержит один метод: analyze()
. Он принимает текст для анализа на входе и возвращает значение перечисления Sentiment
. Таким образом, вы манипулируете только строго типизированным объектом, представляющим класс распознаваемой тональности.
Затем, чтобы предоставить «несколько примеров», которые подтолкнут модель к выполнению работы по классификации, вы создаете чат-память для передачи пар пользовательских сообщений и ответов ИИ, которые представляют текст и связанное с ним настроение.
Давайте свяжем все вместе с помощью метода AiServices.builder()
, передав наш интерфейс SentimentAnalysis
, модель для использования и память чата с примерами из нескольких снимков. Наконец, вызовем метод analyze()
с текстом для анализа.
Запустите образец:
./gradlew run -q -DjavaMainClass=gemini.workshop.TextClassification
Вы должны увидеть одно слово:
POSITIVE
Похоже, любовь к клубнике — это позитивное чувство!
10. Дополненная генерация поиска
LLM обучаются на большом количестве текста. Однако их знания охватывают только ту информацию, которую они видели во время обучения. Если после даты окончания обучения модели будет выпущена новая информация, эти данные не будут доступны модели. Таким образом, модель не сможет отвечать на вопросы по информации, которую она не видела.
Вот почему такие подходы, как Retrieval Augmented Generation (RAG) , которые будут рассмотрены в этом разделе, помогают предоставить дополнительную информацию, которая может понадобиться LLM для выполнения запросов своих пользователей, предоставления более актуальной информации или конфиденциальной информации, которая недоступна во время обучения.
Давайте вернемся к разговорам. На этот раз вы сможете задавать вопросы о своих документах. Вы создадите чат-бота, который сможет извлекать соответствующую информацию из базы данных, содержащей ваши документы, разделенные на более мелкие части («куски»), и эта информация будет использоваться моделью для обоснования ее ответов, вместо того чтобы полагаться исключительно на знания, содержащиеся в ее обучении.
В тряпке есть две фазы:
- Фаза проглатывания - документы загружаются в память, разделяются на более мелкие куски, а векторные встроения (высокомерное векторное представление векторных векторных Эта фаза приема обычно выполняется один раз, когда новые документы должны быть добавлены в корпус документа.
- Этап запроса - теперь пользователи могут задавать вопросы о документах. Вопрос также будет преобразован в вектор и сравнивается со всеми другими векторами в базе данных. Наиболее похожие векторы обычно семантически связаны и возвращаются векторной базой данных. Затем LLM дается контекст разговора, куски текста, которые соответствуют векторам, возвращенным базой данных, и просят заземлить его ответ, посмотрев на эти кусочки.
Подготовьте документы
Для этого нового примера вы зададите вопросы о вымышленной модели автомобиля из также вымышленного автомобиля: автомобиль Cymbal Starlight! Идея состоит в том, что документ о вымышленном автомобиле не должен быть частью знания модели. Поэтому, если Gemini может правильно ответить на вопросы об этом автомобиле, то это означает, что подход RAG работает: он способен искать в вашем документе.
Реализуйте чат -бот
Давайте рассмотрим, как построить 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();
Далее вам понадобится несколько занятий, чтобы сотрудничать вместе с:
- Загрузите и разделите документ 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 символов (со следующей частью, чтобы избежать разрезания слов или предложений, в кусочках).
Магазин Ingestor связывает сплиттер документа, модель встраивания для расчета векторов и векторную базу данных в памяти. Затем метод ingest()
позаботится о проглатывании.
Теперь первое этап закончился, документ был преобразован в текстовые куски с связанными с ними векторными встроениями и хранится в векторной базе данных.
Задавать вопросы
Пора подготовиться к вопросам! Создайте модель чата, чтобы начать разговор:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(1000)
.build();
Вам также нужен класс ретривера, чтобы связать векторную базу данных (в переменной embeddingStore
) с моделью встраивания. Его задача - запросить векторную базу данных путем вычисления векторного встраивания для запроса пользователя, чтобы найти аналогичные векторы в базе данных:
EmbeddingStoreContentRetriever retriever =
new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
Создайте интерфейс, который представляет помощника эксперта автомобиля, это интерфейс, который класс AiServices
будет реализован для вас, чтобы взаимодействовать с моделью:
interface CarExpert {
Result<String> ask(String question);
}
Интерфейс CarExpert
возвращает ответ строки, завернутый в класс Result
Langchain4J. Зачем использовать эту обертку? Потому что не только это даст вам ответ, но и позволит вам проверить куски из базы данных, которые были возвращены Content Retriever. Таким образом, вы можете отобразить источники документа, которые используются для заземления пользователя.
На этом этапе вы можете настроить новую службу искусственного интеллекта:
CarExpert expert = AiServices.builder(CarExpert.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(retriever)
.build();
Эта услуга связывается вместе:
- Модель языка чата , которую вы настроили ранее.
- Память чата , чтобы отслеживать разговор.
- Ретривер сравнивает векторный запрос в встраиваемость с векторами в базе данных.
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("""
You are an expert in car automotive, and you answer concisely.
Here is the question: {{userMessage}}
Answer using the following information:
{{contents}}
"""))
.build())
.contentRetriever(retriever)
.build())
Вы наконец -то готовы задать свои вопросы!
List.of(
"What is the cargo capacity of Cymbal Starlight?",
"What's the emergency roadside assistance phone number?",
"Are there some special kits available on that car?"
).forEach(query -> {
Result<String> response = expert.ask(query);
System.out.printf("%n=== %s === %n%n %s %n%n", query, response.content());
System.out.println("SOURCE: " + response.sources().getFirst().textSegment().text());
});
Полный исходный код находится в RAG.java
в app/src/main/java/gemini/workshop
Directory.
Запустите образец:
./gradlew -q run -DjavaMainClass=gemini.workshop.RAG
В результате вы должны увидеть ответы на свои вопросы:
=== What is the cargo capacity of Cymbal Starlight? === The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. SOURCE: Cargo The Cymbal Starlight 2024 has a cargo capacity of 13.5 cubic feet. The cargo area is located in the trunk of the vehicle. To access the cargo area, open the trunk lid using the trunk release lever located in the driver's footwell. When loading cargo into the trunk, be sure to distribute the weight evenly. Do not overload the trunk, as this could affect the vehicle's handling and stability. Luggage === What's the emergency roadside assistance phone number? === The emergency roadside assistance phone number is 1-800-555-1212. SOURCE: Chapter 18: Emergencies Roadside Assistance If you experience a roadside emergency, such as a flat tire or a dead battery, you can call roadside assistance for help. Roadside assistance is available 24 hours a day, 7 days a week. To call roadside assistance, dial the following number: 1-800-555-1212 When you call roadside assistance, be prepared to provide the following information: Your name and contact information Your vehicle's make, model, and year Your vehicle's location === Are there some special kits available on that car? === Yes, the Cymbal Starlight comes with a tire repair kit. SOURCE: Lane keeping assist: This feature helps to keep you in your lane by gently steering the vehicle back into the lane if you start to drift. Adaptive cruise control: This feature automatically adjusts your speed to maintain a safe following distance from the vehicle in front of you. Forward collision warning: This feature warns you if you are approaching another vehicle too quickly. Automatic emergency braking: This feature can automatically apply the brakes to avoid a collision.
11. Вызов функции
Существуют ситуации, когда вы хотите, чтобы LLM имел доступ к внешним системам, таким как удаленный веб -API, который получает информацию или имеет действие, или услуги, которые выполняют какие -то вычисления. Например:
Удаленные веб -API:
- Отслеживать и обновлять заказы клиентов.
- Найдите или создайте билет в трекере выпуска.
- Получите данные в реальном времени, такие как котировки с запасами или измерения датчика IoT.
- Отправить электронное письмо.
Инструменты вычисления:
- Калькулятор для более продвинутых математических задач.
- Интерпретация кода для запуска кода, когда LLMS нуждается в логике рассуждений.
- Преобразовать запросы естественного языка в запросы SQL, чтобы LLM мог запросить базу данных.
Вызовов функций (иногда называемые инструментами или использование инструментов) - это возможность для модели запросить один или несколько вызовов функций, выполняемых от его имени, поэтому она может правильно ответить на подсказку пользователя с более свежими данными.
Учитывая конкретную подсказку от пользователя и знание существующих функций, которые могут иметь отношение к этому контексту, LLM может ответить с помощью запроса вызова функции. Приложение, интегрирующее LLM, может затем вызвать функцию от ее имени, а затем ответить на LLM ответом, а затем LLM интерпретирует обратно, отвечая с текстовым ответом.
Четыре этапа вызова функции
Давайте посмотрим на пример функционального вызова: получение информации о прогнозе погоды.
Если вы спросите Gemini или любой другой LLM о погоде в Париже, они отвечали бы, сказав, что у него нет информации о текущем прогнозе погоды. Если вы хотите, чтобы LLM имел доступ в режиме реального времени к данным погоды, вам необходимо определить некоторые функции, которые он может запросить для использования.
Взгляните на следующую диаграмму:
1⃣ Во -первых, пользователь спрашивает о погоде в Париже. Приложение Chatbot (с использованием langchain4j) знает, что есть одна или несколько функций, которые находятся в его распоряжении, чтобы помочь LLM выполнить запрос. Чатбот отправляет первоначальную подсказку, а также список функций, которые можно вызвать. Здесь функция называется getWeather()
, которая принимает параметр строки для местоположения.
Поскольку LLM не знает о прогнозах погоды, вместо того, чтобы отвечать через текст, он отправляет обратно запрос на выполнение функции. Чатбот должен вызвать функцию getWeather()
с "Paris"
в качестве параметра местоположения.
2⃣ Чатбот вызывает, что функционирует от имени LLM, извлекает ответ функции. Здесь мы представляем, что ответ {"forecast": "sunny"}
.
3⃣ Приложение Chatbot отправляет ответ JSON обратно в LLM.
4⃣ LLM смотрит на ответ JSON, интерпретирует эту информацию и в конечном итоге отвечает на текст, что погода солнечная в Париже.
Каждый шаг как код
Во -первых, вы настроите модель Gemini как обычно:
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(100)
.build();
Вы определяете спецификацию инструмента, которая описывает функцию, которую можно назвать:
ToolSpecification weatherToolSpec = ToolSpecification.builder()
.name("getWeather")
.description("Get the weather forecast for a given location or city")
.parameters(JsonObjectSchema.builder()
.addStringProperty(
"location",
"the location or city to get the weather forecast for")
.build())
.build();
Имя функции определено, а также имя и тип параметра, но обратите внимание, что как функции, так и параметры приведены описания. Описания очень важны и помогают LLM действительно понять, что может выполнять функция, и, таким образом, судить, должна ли эта функция быть вызвана в контексте разговора.
Давайте начнем шаг № 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());
Полный исходный код находится в 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-2.0-flash")
.build();
WeatherForecastService weatherForecastService = new WeatherForecastService();
WeatherAssistant assistant = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(weatherForecastService)
.build();
System.out.println(assistant.chat("What is the weather in Paris?"));
System.out.println(assistant.chat("Is it warmer in London or in Paris?"));
}
Как обычно, вы настраиваете модель чата Gemini. Затем вы создаете экземпляр своей службы прогноза погоды, которая содержит «функцию», которую модель попросит нас позвонить.
Теперь вы снова используете класс AiServices
, чтобы связать модель чата, память чата и инструмент (т.е. служба прогноза погоды с ее функцией). AiServices
возвращает объект, который реализует ваш определенный интерфейс WeatherAssistant
. Единственное, что осталось, - это вызвать метод chat()
этого помощника. При его вызове вы увидите только текстовые ответы, но запросы вызова функции и ответы вызова функции не будут видны у разработчика, и эти запросы будут обрабатываться автоматически и прозрачно. Если Gemini считает, что должна быть вызвана функция, она ответит с помощью запроса вызова функции, и Langchain4J позаботится о вызове локальной функции от вашего имени.
Запустите образец:
./gradlew run -q -DjavaMainClass=gemini.workshop.FunctionCallingAssistant
Вы должны увидеть выход, похожий на следующее:
OK. The weather in Paris is sunny with a temperature of 20 degrees.
It is warmer in Paris (20 degrees) than in London (15 degrees).
Это был пример одной функции.
Несколько вызовов функций
Вы также можете иметь несколько функций и позволить Langchain4J обрабатывать несколько вызовов функций от вашего имени. Взгляните на MultiFunctionCallingAssistant.java
для множественного примера функции.
Он имеет функцию для преобразования валют:
@Tool("Convert amounts between two currencies")
double convertCurrency(
@P("Currency to convert from") String fromCurrency,
@P("Currency to convert to") String toCurrency,
@P("Amount to convert") double amount) {
double result = amount;
if (fromCurrency.equals("USD") && toCurrency.equals("EUR")) {
result = amount * 0.93;
} else if (fromCurrency.equals("USD") && toCurrency.equals("GBP")) {
result = amount * 0.79;
}
System.out.println(
"convertCurrency(fromCurrency = " + fromCurrency +
", toCurrency = " + toCurrency +
", amount = " + amount + ") == " + result);
return result;
}
Другая функция для получения стоимости акции:
@Tool("Get the current value of a stock in US dollars")
double getStockPrice(@P("Stock symbol") String symbol) {
double result = 170.0 + 10 * new Random().nextDouble();
System.out.println("getStockPrice(symbol = " + symbol + ") == " + result);
return result;
}
Другая функция, чтобы применить процент к данной сумме:
@Tool("Apply a percentage to a given amount")
double applyPercentage(@P("Initial amount") double amount, @P("Percentage between 0-100 to apply") double percentage) {
double result = amount * (percentage / 100);
System.out.println("applyPercentage(amount = " + amount + ", percentage = " + percentage + ") == " + result);
return result;
}
Затем вы можете объединить все эти функции и класс с несколькими кругами и задать такие вопросы, как «Что такое 10% цены акций AAPL, преобразованную из USD в EUR?» »
public static void main(String[] args) {
ChatLanguageModel model = VertexAiGeminiChatModel.builder()
.project(System.getenv("PROJECT_ID"))
.location(System.getenv("LOCATION"))
.modelName("gemini-2.0-flash")
.maxOutputTokens(100)
.build();
MultiTools multiTools = new MultiTools();
MultiToolsAssistant assistant = AiServices.builder(MultiToolsAssistant.class)
.chatLanguageModel(model)
.chatMemory(withMaxMessages(10))
.tools(multiTools)
.build();
System.out.println(assistant.chat(
"What is 10% of the AAPL stock price converted from USD to EUR?"));
}
Запустите его следующим образом:
./gradlew run -q -DjavaMainClass=gemini.workshop.MultiFunctionCallingAssistant
И вы должны увидеть несколько функций, называемых:
getStockPrice(symbol = AAPL) == 172.8022224055534 convertCurrency(fromCurrency = USD, toCurrency = EUR, amount = 172.8022224055534) == 160.70606683716468 applyPercentage(amount = 160.70606683716468, percentage = 10.0) == 16.07060668371647 10% of the AAPL stock price converted from USD to EUR is 16.07060668371647 EUR.
К агентам
Функциональный вызов - это отличный механизм расширения для крупных языковых моделей, таких как Близнецы. Это позволяет нам создавать более сложные системы, которые часто называют «агентами» или «помощниками искусственного интеллекта». Эти агенты могут взаимодействовать с внешним миром с помощью внешних API и услуг, которые могут иметь побочные эффекты на внешней среде (например, отправка электронных писем, создание билетов и т. Д.)
При создании таких влиятельных агентов вы должны сделать это ответственно. Вы должны рассмотреть человеку в петле, прежде чем совершать автоматические действия. Важно помнить о безопасности при проектировании агентов с LLM, которые взаимодействуют с внешним миром.
13. Запуск Gemma с Ollama и TestContainers
До сих пор мы использовали Близнецы, но есть и Джемма , ее младшая сестра модель.
Gemma -это семейство легких, современных открытых моделей, созданных из тех же исследований и технологий, используемых для создания моделей Gemini. Последняя модель Gemma-GEMMA3, доступная в четырех размерах: 1B ( только текстовый ), 4B, 12B и 27B. Их веса свободно доступны, а их небольшие размеры означает, что вы можете запустить его самостоятельно, даже на своем ноутбуке или в облачной оболочке.
Как вы управляете Джеммой?
Есть много способов запустить Gemma: в облаке, через AI Vertex AI с нажатием кнопки или GKE с некоторыми графическими процессорами, но вы также можете запустить ее локально.
Один хороший вариант для запуска Gemma Locally - это Ollama , инструмент, который позволяет вам запускать небольшие модели, такие как Llama, Mistral и многие другие на вашей местной машине. Это похоже на Docker, но для LLMS.
Установите Ollama после инструкции для вашей операционной системы.
Если вы используете среду Linux, вам нужно сначала включить Ollama после ее установки.
ollama serve > /dev/null 2>&1 &
После установки локально вы можете запустить команды, чтобы вытащить модель:
ollama pull gemma3:1b
Подождите, пока модель будет вытянута. Это может занять некоторое время.
Запустите модель:
ollama run gemma3:1b
Теперь вы можете взаимодействовать с моделью:
>>> Hello! Hello! It's nice to hear from you. What can I do for you today?
Чтобы выйти из приглашения нажатия Ctrl+D
Запуск Gemma в Ollama на TestContainers
Вместо того, чтобы устанавливать и запускать Ollama на локальном уровне, вы можете использовать Ollama в контейнере, обрабатываемые TestContainers .
TestContainers не только полезны для тестирования, но и вы можете использовать его для выполнения контейнеров. Есть даже конкретный OllamaContainer
, которым вы можете воспользоваться!
Вот целая картина:
Выполнение
Давайте посмотрим на GemmaWithOllamaContainer.java
, кусок по кусочкам.
Во -первых, вам нужно создать производный контейнер Ollama, который втягивает модель Gemma. Это изображение либо уже существует из предыдущего запуска, либо будет создано. Если изображение уже существует, вы просто сообщите TestContainers, что вы хотите заменить изображение Ollama по умолчанию вашим вариантом, способствующим GEMMA:
private static final String TC_OLLAMA_GEMMA3 = "tc-ollama-gemma3-1b";
public static final String GEMMA_3 = "gemma3:1b";
// Creating an Ollama container with Gemma 3 if it doesn't exist.
private static OllamaContainer createGemmaOllamaContainer() throws IOException, InterruptedException {
// Check if the custom Gemma Ollama image exists already
List<Image> listImagesCmd = DockerClientFactory.lazyClient()
.listImagesCmd()
.withImageNameFilter(TC_OLLAMA_GEMMA3)
.exec();
if (listImagesCmd.isEmpty()) {
System.out.println("Creating a new Ollama container with Gemma 3 image...");
OllamaContainer ollama = new OllamaContainer("ollama/ollama:0.7.1");
System.out.println("Starting Ollama...");
ollama.start();
System.out.println("Pulling model...");
ollama.execInContainer("ollama", "pull", GEMMA_3);
System.out.println("Committing to image...");
ollama.commitToImage(TC_OLLAMA_GEMMA3);
return ollama;
}
System.out.println("Ollama image substitution...");
// Substitute the default Ollama image with our Gemma variant
return new OllamaContainer(
DockerImageName.parse(TC_OLLAMA_GEMMA3)
.asCompatibleSubstituteFor("ollama/ollama"));
}
Затем вы создаете и запускаете тестовый контейнер Ollama, а затем создаете модель чата Ollama, указывая на адрес и порт контейнера с помощью модели, которую вы хотите использовать. Наконец, вы только что вызываете model.generate(yourPrompt)
как обычно:
public static void main(String[] args) throws IOException, InterruptedException {
OllamaContainer ollama = createGemmaOllamaContainer();
ollama.start();
ChatLanguageModel model = OllamaChatModel.builder()
.baseUrl(String.format("http://%s:%d", ollama.getHost(), ollama.getFirstMappedPort()))
.modelName(GEMMA_3)
.build();
String response = model.generate("Why is the sky blue?");
System.out.println(response);
}
Запустите его следующим образом:
./gradlew run -q -DjavaMainClass=gemini.workshop.GemmaWithOllamaContainer
Первый пробег займет некоторое время, чтобы создать и запустить контейнер, но как только сделано, вы должны увидеть, как Джемма отвечает:
INFO: Container ollama/ollama:0.7.1 started in PT7.228339916S
The sky appears blue due to Rayleigh scattering. Rayleigh scattering is a phenomenon that occurs when sunlight interacts with molecules in the Earth's atmosphere.
* **Scattering particles:** The main scattering particles in the atmosphere are molecules of nitrogen (N2) and oxygen (O2).
* **Wavelength of light:** Blue light has a shorter wavelength than other colors of light, such as red and yellow.
* **Scattering process:** When blue light interacts with these molecules, it is scattered in all directions.
* **Human eyes:** Our eyes are more sensitive to blue light than other colors, so we perceive the sky as blue.
This scattering process results in a blue appearance for the sky, even though the sun is actually emitting light of all colors.
In addition to Rayleigh scattering, other atmospheric factors can also influence the color of the sky, such as dust particles, aerosols, and clouds.
У вас есть Джемма, работающая в облачной оболочке!
14. Поздравления
Поздравляем, вы успешно создали свое первое генеративное приложение для AI в Java, используя Langchain4J и API Gemini! Вы обнаружили, что мультимодальные крупные языковые модели довольно мощные и способны выполнять различные задачи, такие как вопрос/ответ, даже по вашей собственной документации, извлечение данных, взаимодействие с внешними API и многое другое.
Что дальше?
У вас есть очередь, чтобы улучшить ваши приложения с мощными интеграциями LLM!
Дальнейшее чтение
- Генеративные варианты общего использования ИИ
- Учебные ресурсы по генеративному ИИ
- Взаимодействовать с Близнецами через генеративную студию ИИ
- Ответственный ИИ