Gemini на Java с Vertex AI и LangChain4j

1. Введение

В этой лаборатории кода основное внимание уделяется модели большого языка Gemini (LLM), размещенной на Vertex AI в облаке Google. 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 for Google Cloud на базе Gemini — это средство совместной работы на базе искусственного интеллекта, доступное в Google Cloud и IDE, которое поможет вам делать больше и быстрее. Gemini Code Assist обеспечивает завершение кода, генерацию кода, объяснения кода и позволяет вам общаться с ним в чате, чтобы задавать технические вопросы.

Что такое Близнецы?

Gemini — это семейство генеративных моделей искусственного интеллекта, разработанное Google DeepMind и предназначенное для мультимодальных случаев использования. Мультимодальность означает, что он может обрабатывать и генерировать различные виды контента, такие как текст, код, изображения и аудио.

b9913d011999e7c7.png

Близнецы бывают разных вариаций и размеров:

  • Gemini Ultra : Самая большая и мощная версия для сложных задач.
  • Gemini Flash : самый быстрый и экономичный, оптимизированный для задач большого объема.
  • Gemini Pro : среднего размера, оптимизирован для масштабирования для решения различных задач.
  • Gemini Nano : самый эффективный, предназначен для задач на устройстве.

Ключевая особенность:

  • Мультимодальность : способность Gemini понимать и обрабатывать несколько форматов информации является значительным шагом по сравнению с традиционными языковыми моделями, состоящими только из текста.
  • Производительность : Gemini Ultra превосходит современные модели по многим тестам и является первой моделью, которая превзошла экспертов-людей в сложном тесте MMLU (массовое многозадачное понимание языка).
  • Гибкость : различные размеры Gemini позволяют адаптировать его для различных случаев использования: от крупномасштабных исследований до развертывания на мобильных устройствах.

Как вы можете взаимодействовать с Gemini в Vertex AI из Java?

У вас есть два варианта:

  1. Официальный Java API Vertex AI для библиотеки Gemini .
  2. Фреймворк LangChain4j .

В этой лаборатории кода вы будете использовать платформу LangChain4j .

Что такое платформа LangChain4j?

Платформа LangChain4j — это библиотека с открытым исходным кодом для интеграции LLM в ваши приложения Java путем координации различных компонентов, таких как сам LLM, а также других инструментов, таких как векторные базы данных (для семантического поиска), загрузчики документов и разделители (для анализа документов и обучения). из них), парсеры вывода и многое другое.

Проект был вдохновлен проектом LangChain Python, но его целью было обслуживание разработчиков Java.

bb908ea1e6c96ac2.png

Что вы узнаете

  • Как настроить проект Java для использования Gemini и LangChain4j
  • Как отправить первое приглашение в Gemini программно
  • Как транслировать ответы от Gemini
  • Как создать разговор между пользователем и Близнецами
  • Как использовать Gemini в мультимодальном контексте, отправляя как текст, так и изображения
  • Как извлечь полезную структурированную информацию из неструктурированного контента
  • Как манипулировать шаблонами подсказок
  • Как выполнить классификацию текста, например анализ настроений
  • Как общаться со своими собственными документами (Поисковая расширенная генерация)
  • Как расширить возможности ваших чат-ботов вызовом функций
  • Как использовать Gemma локально с Ollama и TestContainers

Что вам понадобится

  • Знание языка программирования Java
  • Проект Google Cloud
  • Браузер, например Chrome или Firefox.

2. Настройка и требования

Самостоятельная настройка среды

  1. Войдите в Google Cloud Console и создайте новый проект или повторно используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • Имя проекта — это отображаемое имя для участников этого проекта. Это строка символов, не используемая API Google. Вы всегда можете обновить его.
  • Идентификатор проекта уникален для всех проектов Google Cloud и является неизменяемым (невозможно изменить после его установки). Cloud Console автоматически генерирует уникальную строку; обычно тебя не волнует, что это такое. В большинстве лабораторий кода вам потребуется указать идентификатор проекта (обычно идентифицируемый как PROJECT_ID ). Если вам не нравится сгенерированный идентификатор, вы можете создать другой случайный идентификатор. Альтернативно, вы можете попробовать свой собственный и посмотреть, доступен ли он. Его нельзя изменить после этого шага и он сохраняется на протяжении всего проекта.
  • К вашему сведению, есть третье значение — номер проекта , которое используют некоторые API. Подробнее обо всех трех этих значениях читайте в документации .
  1. Затем вам необходимо включить выставление счетов в Cloud Console, чтобы использовать облачные ресурсы/API. Прохождение этой кодовой лаборатории не будет стоить много, если вообще что-то стоить. Чтобы отключить ресурсы и избежать выставления счетов за пределами этого руководства, вы можете удалить созданные вами ресурсы или удалить проект. Новые пользователи Google Cloud имеют право на участие в программе бесплатной пробной версии стоимостью 300 долларов США .

Запустить Cloud Shell

Хотя Google Cloud можно управлять удаленно с вашего ноутбука, в этой лаборатории вы будете использовать Cloud Shell , среду командной строки, работающую в облаке.

Активировать Cloud Shell

  1. В Cloud Console нажмите «Активировать Cloud Shell». 853e55310c205094.png .

3c1dabeca90e44e5.png

Если вы запускаете Cloud Shell впервые, вы увидите промежуточный экран с описанием того, что это такое. Если вам был представлен промежуточный экран, нажмите «Продолжить» .

9c92662c6a846a5c.png

Подготовка и подключение к Cloud Shell займет всего несколько минут.

9f0e51b578fecce5.png

Эта виртуальная машина загружена всеми необходимыми инструментами разработки. Он предлагает постоянный домашний каталог объемом 5 ГБ и работает в 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.

Включить API Vertex AI

В консоли Google Cloud убедитесь, что название вашего проекта отображается в верхней части консоли Google Cloud . Если это не так, нажмите «Выбрать проект» , чтобы открыть «Выбор проекта» , и выберите нужный проект.

Вы можете включить API Vertex AI либо из раздела Vertex AI консоли Google Cloud, либо из терминала Cloud Shell.

Чтобы включить консоль Google Cloud, сначала перейдите в раздел Vertex AI меню консоли Google Cloud:

451976f1c8652341.png

Нажмите «Включить все рекомендуемые 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 из Cloud Shell:

42908e11b28f4383.png

В редакторе облачного кода откройте папку с исходным кодом, выбрав File -> Open Folder и укажите папку с исходным кодом (например, /home/username/gemini-workshop-for-java-developers/ ).

Установите Gradle для Java

Чтобы редактор облачного кода правильно работал с Gradle, установите расширение Gradle для Java.

Сначала перейдите в раздел «Проекты Java» и нажмите знак «плюс»:

84d15639ac61c197.png

Выберите Gradle for Java :

34d6c4136a3cc9ff.png

Выберите Install Pre-Release версию»:

3b044fb450cccb7.png

После установки вы должны увидеть кнопки Disable и Uninstall :

46410fe86d777f9c.png

Наконец, очистите рабочую область, чтобы применить новые настройки:

31e27e9bb61d975d.png

Вам будет предложено перезагрузить и удалить мастерскую. Идите вперед и выберите Reload and delete :

d6303bc49e391dc.png

Если вы откроете один из файлов, например App.java, вы увидите, что редактор работает корректно с подсветкой синтаксиса:

Fed1b1b5de0dff58.png

Теперь вы готовы протестировать несколько образцов против Gemini!

Настройка переменных среды

Откройте новый терминал в Cloud Code Editor, выбрав Terminal -> New Terminal . Настройте две переменные среды, необходимые для запуска примеров кода:

  • PROJECT_ID — идентификатор вашего проекта Google Cloud.
  • РАСПОЛОЖЕНИЕ — регион, в котором развернута модель Gemini.

Экспортируйте переменные следующим образом:

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

4. Первый вызов модели Близнецов

Теперь, когда проект правильно настроен, пришло время вызвать Gemini API.

Взгляните на QA.java в каталоге app/src/main/java/gemini/workshop :

package gemini.workshop;

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

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

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

В этом первом примере вам необходимо импортировать класс VertexAiGeminiChatModel , который реализует интерфейс ChatModel .

В main методе вы настраиваете модель языка чата с помощью построителя VertexAiGeminiChatModel и указываете:

  • Проект
  • Расположение
  • Название модели ( gemini-1.5-flash-001 ).

Теперь, когда языковая модель готова, вы можете вызвать generate() и передать приглашение, вопрос или инструкции для отправки в LLM. Здесь вы задаете простой вопрос о том, что делает небо голубым.

Не стесняйтесь изменять эту подсказку, чтобы попробовать разные вопросы или задачи.

Запустите образец в корневой папке исходного кода:

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

Вы должны увидеть вывод, похожий на этот:

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

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

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

Поздравляем, вы сделали первый звонок в Близнецы!

Потоковое ответ

Вы заметили, что ответ был дан сразу, через несколько секунд? Также возможно получать ответ постепенно благодаря варианту потокового ответа. При потоковом ответе модель возвращает ответ по частям по мере его поступления.

В этой кодовой лаборатории мы будем придерживаться непотокового ответа, но давайте посмотрим на потоковый ответ, чтобы увидеть, как это можно сделать.

В StreamQA.java в каталоге app/src/main/java/gemini/workshop вы можете увидеть потоковый ответ в действии:

package gemini.workshop;

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

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

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

На этот раз мы импортируем варианты класса потоковой передачи VertexAiGeminiStreamingChatModel , который реализует интерфейс StreamingChatLanguageModel . Вам также понадобится StreamingResponseHandler .

На этот раз сигнатура generate() немного другая. Вместо возврата строки тип возвращаемого значения — void. В дополнение к приглашению вам необходимо передать обработчик потокового ответа. Здесь вы реализуете интерфейс, создавая анонимный внутренний класс с двумя методами onNext(String text) и onError(Throwable error) . Первый вызывается каждый раз, когда доступна новая часть ответа, а второй вызывается только в случае возникновения ошибки.

Бегать:

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

Вы получите ответ, аналогичный предыдущему классу, но на этот раз вы заметите, что ответ постепенно появляется в вашей оболочке, а не ждет отображения полного ответа.

Дополнительная конфигурация

Для конфигурации мы определили только проект, местоположение и имя модели, но есть и другие параметры, которые вы можете указать для модели:

  • temperature(Float temp) — чтобы определить, насколько креативным должен быть ответ (0 означает низкую креативность и часто более фактическую, а 1 — для более творческих результатов)
  • topP(Float topP) — для выбора возможных слов, общая вероятность которых в сумме равна этому числу с плавающей запятой (от 0 до 1)
  • topK(Integer topK) — случайным образом выбрать слово из максимального количества вероятных слов для завершения текста (от 1 до 40)
  • maxOutputTokens(Integer max) — чтобы указать максимальную длину ответа, заданного моделью (обычно 4 токена представляют примерно 3 слова)
  • maxRetries(Integer retries) — если вы превышаете квоту запроса на время или на платформе возникла какая-то техническая проблема, вы можете попросить модель повторить вызов 3 раза.

Пока что вы задали Близнецам один вопрос, но можете вести и многоходовой разговор. Это то, что вы изучите в следующем разделе.

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-1.5-flash-001")
            .build();

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

        interface ConversationService {
            String chat(String message);
        }

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

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

Пара новых интересных импортов в этом классе:

  • MessageWindowChatMemory — класс, который поможет обрабатывать многоходовой аспект разговора и сохранять в локальной памяти предыдущие вопросы и ответы.
  • AiServices — класс, который свяжет воедино модель чата и память чата.

В основном методе вы собираетесь настроить модель, память чата и службу искусственного интеллекта. Модель настраивается как обычно с использованием информации о проекте, местоположении и названии модели.

Для памяти чата мы используем построитель MessageWindowChatMemory для создания памяти, в которой хранятся последние 20 сообщений, которыми обменивались. Это скользящее окно разговора, контекст которого хранится локально в нашем клиенте класса Java.

Затем вы создаете AI service , которая связывает модель чата с памятью чата.

Обратите внимание, как служба AI использует определенный нами пользовательский интерфейс ConversationService , который реализует LangChain4j, который принимает запрос String и возвращает ответ String .

Теперь пришло время поговорить с Близнецами. Сначала отправляется простое приветствие, затем первый вопрос об Эйфелевой башне, чтобы узнать, в какой стране ее можно найти. Обратите внимание, что последнее предложение связано с ответом на первый вопрос, поскольку вам интересно, сколько жителей в стране, где расположена Эйфелева башня, без явного упоминания страны, указанной в предыдущем ответе. Это показывает, что прошлые вопросы и ответы отправляются с каждым приглашением.

Запустите образец:

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

Вы можете задавать одноходовые вопросы или вести многоходовые беседы с Близнецами, но до сих пор вводимые данные представляли собой только текст. А как насчет изображений? Давайте рассмотрим изображения на следующем этапе.

6. Мультимодальность с Близнецами

Gemini — мультимодальная модель. Он принимает не только текст в качестве входных данных, но также принимает изображения или даже видео в качестве входных данных. В этом разделе вы увидите вариант использования для смешивания текста и изображений.

Как вы думаете, Близнецы узнают этого кота?

af00516493ec9ade.png

Фотография кота на снегу взята из Википедии https://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

Взгляните на Multimodal.java в каталоге app/src/main/java/gemini/workshop :

package gemini.workshop;

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

public class Multimodal {

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


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

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

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

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

Обратите внимание, что при импорте мы различаем разные типы сообщений и содержимого. UserMessage может содержать как объект TextContent , так и объект ImageContent . Это мультимодальность: смешивание текста и изображений. Модель отправляет обратно Response , содержащий AiMessage .

Затем вы извлекаете AiMessage из ответа через content() , а затем текст сообщения благодаря text() .

Запустите образец:

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

Название изображения, безусловно, дало вам представление о том, что оно содержит, но вывод Gemini аналогичен следующему:

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

Смешение изображений и текстовых подсказок открывает интересные варианты использования. Вы можете создавать приложения, которые могут:

  • Распознавание текста на картинках.
  • Проверьте, безопасно ли изображение для отображения.
  • Создайте подписи к изображениям.
  • Поиск в базе данных изображений с текстовым описанием.

Помимо извлечения информации из изображений, вы также можете извлекать информацию из неструктурированного текста. Это то, что вы узнаете в следующем разделе.

7. Извлекайте структурированную информацию из неструктурированного текста

Во многих ситуациях важная информация предоставляется в отчетных документах, электронных письмах или других длинных текстах в неструктурированном виде. В идеале вы хотели бы иметь возможность извлекать ключевые детали, содержащиеся в неструктурированном тексте, в форме структурированных объектов. Давайте посмотрим, как вы можете это сделать.

Допустим, вы хотите извлечь имя и возраст человека, учитывая биографию или описание этого человека. Вы можете поручить LLM извлекать JSON из неструктурированного текста с помощью умело настроенной подсказки (это обычно называется «подсказкой» ).

Взгляните на ExtractData.java в app/src/main/java/gemini/workshop :

package gemini.workshop;

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

public class ExtractData {

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

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

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

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

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

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

Давайте посмотрим на различные шаги в этом файле:

  • Запись Person предназначена для представления сведений, описывающих человека (имя и возраст).
  • Интерфейс PersonExtractor определен с помощью метода, который по неструктурированной текстовой строке возвращает экземпляр Person .
  • extractPerson() помечен аннотацией @UserMessage , которая связывает с ним приглашение. Это приглашение, которое модель будет использовать для извлечения информации и возврата деталей в форме документа JSON, который будет проанализирован для вас и неупорядочен в экземпляре Person .

Теперь давайте посмотрим на содержимое метода main() :

  • Создается экземпляр модели чата. Обратите внимание, что мы используем очень низкую temperature , равную нулю, и topK , равную только единице, чтобы гарантировать очень детерминированный ответ. Это также помогает модели лучше следовать инструкциям. В частности, мы не хотим, чтобы Gemini обертывал ответ JSON дополнительной разметкой Markdown.
  • Объект 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 вы просто манипулируете обычными классами и объектами.

8. Структурируйте подсказки с помощью шаблонов подсказок.

Когда вы взаимодействуете с LLM, используя общий набор инструкций или вопросов, часть этого приглашения никогда не меняется, в то время как другие части содержат данные. Например, если вы хотите создавать рецепты, вы можете использовать подсказку типа «Вы талантливый шеф-повар, создайте рецепт со следующими ингредиентами: ...», а затем добавить ингредиенты в конец этот текст. Для этого и нужны шаблоны подсказок — они аналогичны интерполированным строкам в языках программирования. Шаблон приглашения содержит заполнители, которые вы можете заменить нужными данными для конкретного вызова LLM.

Более конкретно, давайте изучим TemplatePrompt.java в каталоге app/src/main/java/gemini/workshop :

package gemini.workshop;

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

import java.util.HashMap;
import java.util.Map;

public class TemplatePrompt {
    public static void main(String[] args) {
        ChatLanguageModel model = VertexAiGeminiChatModel.builder()
            .project(System.getenv("PROJECT_ID"))
            .location(System.getenv("LOCATION"))
            .modelName("gemini-1.5-flash-001")
            .maxOutputTokens(500)
            .temperature(0.8f)
            .topK(40)
            .topP(0.95f)
            .maxRetries(3)
            .build();

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

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

        Prompt prompt = promptTemplate.apply(variables);

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

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

Как обычно, вы настраиваете модель VertexAiGeminiChatModel с высоким уровнем креативности, высокой температурой, а также высокими значениями topP и topK. Затем вы создаете PromptTemplate с его статическим методом from() , передавая строку нашего приглашения и используете переменные-заполнители в двойных фигурных скобках: и .

Вы создаете последнее приглашение, вызывая 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 dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.input.Prompt;

package gemini.workshop;

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

import java.util.Map;

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

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

            INPUT: This is fantastic news!
            OUTPUT: POSITIVE

            INPUT: Pi is roughly equal to 3.14
            OUTPUT: NEUTRAL

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

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

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

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

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

В методе main() вы создаете модель чата Gemini, как обычно, но с небольшим максимальным числом выходных токенов, так как вам нужен только короткий ответ: текст POSITIVE , NEGATIVE или NEUTRAL .

Затем вы создаете многоразовый шаблон подсказки с помощью техники подсказок в несколько раз, сообщая модели несколько примеров входных и выходных данных. Это также помогает модели следовать фактическим результатам. Близнецы не будут отвечать полным предложением, вместо этого им предлагается ответить всего одним словом.

Вы применяете переменные с помощью метода apply() , чтобы заменить заполнитель реальным параметром ( "I love strawberries" ), и превращаете этот шаблон в сообщение пользователя с помощью toUserMessage() .

Запустите образец:

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

Вы должны увидеть одно слово:

POSITIVE

Похоже, любовь к клубнике – это позитивное чувство!

10. Поисковая дополненная генерация

LLM обучаются на большом количестве текста. Однако их знания охватывают только ту информацию, которую он видел во время обучения. Если после окончания обучения модели появится новая информация, эта информация не будет доступна модели. Таким образом, модель не сможет отвечать на вопросы по информации, которую она не видела.

Вот почему такие подходы, как поисковая дополненная генерация (RAG), помогают предоставить дополнительную информацию, которую LLM может потребоваться знать для выполнения запросов своих пользователей, для ответа на информацию, которая может быть более актуальной, или на личную информацию, которая недоступна во время обучения. .

Вернемся к разговорам. На этот раз вы сможете задавать вопросы о ваших документах. Вы создадите чат-бота, который сможет извлекать соответствующую информацию из базы данных, содержащей ваши документы, разделенные на более мелкие части («куски»), и эта информация будет использоваться моделью для обоснования ее ответов, вместо того, чтобы полагаться исключительно на знания, содержащиеся в его обучение.

В RAG есть два этапа:

  1. Фаза приема . Документы загружаются в память, разбиваются на более мелкие фрагменты, а векторные внедрения (многомерное векторное представление фрагментов) рассчитываются и сохраняются в векторной базе данных, способной выполнять семантический поиск. Эта фаза приема обычно выполняется один раз, когда в корпус документов необходимо добавить новые документы.

cd07d33d20ffa1c8.png

  1. Фаза запроса . Теперь пользователи могут задавать вопросы о документах. Вопрос также будет преобразован в вектор и сравнен со всеми остальными векторами в базе данных. Наиболее похожие векторы обычно семантически связаны и возвращаются базой данных векторов. Затем LLM дается контекст разговора, куски текста, которые соответствуют векторам, возвращенным базой данных, и просят заземлить его ответ, посмотрев на эти кусочки.

A1D2E2DEB83C6D27.PNG

Подготовьте свои документы

Для этой новой демонстрации вы будете задавать вопросы о том, как «Внимание - все, что вам нужно». Он описывает архитектуру нейронной сети трансформатора, впервые подготовленную Google, как в настоящее время реализуются все современные крупные языковые модели.

Бумага уже загружена с вниманием-все-you-need.pdf в репозитории.

Реализуйте чат -бот

Давайте рассмотрим, как построить 2-фазный подход: сначала с приглашением документа, а затем временем запроса, когда пользователи задают вопросы о документе.

В этом примере обе фазы реализованы в одном классе. Обычно у вас будет одно приложение, которое позаботится о приеме, и другое приложение, которое предлагает интерфейс чатбота для ваших пользователей.

Кроме того, в этом примере мы будем использовать векторную базу данных в памяти. В реальном сценарии производства проглатывание и фазы запроса будут разделены в двух различных приложениях, а векторы сохраняются в автономной базе данных.

Проглатывание документов

Самым первым шагом фазы проглатывания документа является поиск файла PDF, который мы уже загрузили, и подготовить PdfParser , чтобы прочитать его:

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

Вместо того, чтобы создавать обычную модель языка чата, вы создаете экземпляр модели встраивания . Это конкретная модель, роль которого состоит в том, чтобы создать векторные представления текстовых произведений (слова, предложения или даже абзацы). Он возвращает векторы чисел с плавающей запятой, а не возвращает текстовые ответы.

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

Далее вам понадобится несколько занятий, чтобы сотрудничать вместе с:

  • Загрузите и разделите документ PDF на куски.
  • Создайте векторные встроения для всех этих кусков.
InMemoryEmbeddingStore<TextSegment> embeddingStore = 
    new InMemoryEmbeddingStore<>();

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

Экземпляр InMemoryEmbeddingStore , векторной базы данных в памяти, создан для хранения векторных вторжений.

Документ разделен в куски благодаря классу DocumentSplitters . Он собирается разделить текст файла PDF на фрагменты 500 символов, с перекрытием 100 символов (со следующей частью, чтобы избежать разрезания слов или предложений, в кусочках).

Магазин Ingestor связывает сплиттер документа, модель встраивания для расчета векторов и векторную базу данных в памяти. Затем метод ingest() позаботится о проглатывании.

Теперь первое этап закончился, документ был преобразован в текстовые куски с связанными с ними векторными встроениями и хранится в векторной базе данных.

Задавать вопросы

Пора подготовиться к вопросам! Создайте модель чата, чтобы начать разговор:

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

Вам также нужен класс ретривера, чтобы связать векторную базу данных (в переменной embeddingStore ) с моделью встраивания. Его задача - запросить векторную базу данных путем вычисления векторного встраивания для запроса пользователя, чтобы найти аналогичные векторы в базе данных:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

Помимо основного метода, создайте интерфейс, который представляет помощника эксперта LLM, это интерфейс, который класс AiServices будет реализовать для вас, чтобы взаимодействовать с моделью:

interface LlmExpert {
    String ask(String question);
}

На этом этапе вы можете настроить новую службу искусственного интеллекта:

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

Эта услуга связывается вместе:

  • Модель языка чата , которую вы настроили ранее.
  • Память чата , чтобы отслеживать разговор.
  • Ретривер сравнивает векторный запрос в встраиваемость с векторами в базе данных.
  • В быстром шаблоне явно говорится, что модель чата должна ответить, основывая свой ответ на предоставленной информации (то есть соответствующие выдержки документации, векторная встраиваемость которого аналогична вектору вопроса пользователя).
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in large language models,\s
            you excel at explaining simply and clearly questions about LLMs.

            Here is the question: {{userMessage}}

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

Вы наконец -то готовы задать свои вопросы!

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

Полный исходный код находится в RAG.java в app/src/main/java/gemini/workshop Directory:

Запустите образец:

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

В результате вы должны увидеть ответы на свои вопросы:

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

 Transformer architecture 


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

 The different components of a transformer neural network are:

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


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

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

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

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

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


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

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

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

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

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

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

11. Функциональный вызов

Существуют также ситуации, когда вы хотите, чтобы LLM имел доступ к внешним системам, таким как удаленный веб -API, который получает информацию или имеет действие, или услуги, которые выполняют какие -то вычисления. Например:

Удаленные веб -API:

  • Отслеживать и обновлять заказы клиентов.
  • Найдите или создайте билет в трекере выпуска.
  • Получите данные в реальном времени, такие как котировки с запасами или измерения датчика IoT.
  • Отправить письмо.

Инструменты вычисления:

  • Калькулятор для более продвинутых математических задач.
  • Интерпретация кода для запуска кода, когда LLMS нуждается в логике рассуждений.
  • Преобразовать запросы естественного языка в запросы SQL, чтобы LLM мог запросить базу данных.

Функциональный вызов - это возможность для модели запросить один или несколько вызовов функций от его имени, поэтому она может правильно ответить на подсказку пользователя с более свежими данными.

Учитывая конкретную подсказку от пользователя и знание существующих функций, которые могут иметь отношение к этому контексту, LLM может ответить с помощью запроса вызова функции. Приложение, интегрирующее LLM, может затем вызвать функцию, а затем ответить на LLM с ответом, и LLM затем интерпретирует обратно, отвечая с текстовым ответом.

Четыре этапа вызова функции

Давайте посмотрим на пример функционального вызова: получение информации о прогнозе погоды.

Если вы спросите Gemini или о любом другом LLM о погоде в Париже, они отвечали бы, сказав, что у него нет информации о прогнозе погоды. Если вы хотите, чтобы LLM имел в реальном времени ACCCESS к данным погоды, вам необходимо определить некоторые функции, которые он может использовать.

Взгляните на следующую диаграмму:

31E0C2ABA5E6F21C.PNG

1⃣ Во -первых, пользователь спрашивает о погоде в Париже. Приложение Chatbot знает, что есть одна или несколько функций, которые находятся в его распоряжении, чтобы помочь LLM выполнить запрос. Чатбот отправляет первоначальную подсказку, а также список функций, которые можно вызвать. Здесь функция называется getWeather() , которая принимает параметр строки для местоположения.

8863be53a73c4a70.png

Поскольку LLM не знает о прогнозах погоды, вместо того, чтобы отвечать через текст, он отправляет обратно запрос на выполнение функции. Чатбот должен вызвать функцию getWeather() с "Paris" в качестве параметра местоположения.

D1367CC69C07B14D.PNG

2⃣ Чатбот вызывает, что функционирует от имени LLM, извлекает ответ функции. Здесь мы представляем, что ответ {"forecast": "sunny"} .

73a5f2ed19f47d8.png

3⃣ Приложение Chatbot отправляет ответ JSON обратно в LLM.

20832CB1EE6FBFEB.PNG

4⃣ LLM смотрит на ответ JSON, интерпретирует эту информацию и в конечном итоге отвечает на текст, что погода солнечная в Париже.

Каждый шаг как код

Во -первых, вы настроите модель Gemini как обычно:

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

Вы указываете спецификацию инструмента, которая описывает функцию, которую можно назвать:

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

Имя функции определено, а также имя и тип параметра, но обратите внимание, что как функции, так и параметры приведены описания. Описания очень важны и помогают LLM действительно понять, что может выполнять функция, и, таким образом, судить, должна ли эта функция быть вызвана в контексте разговора.

Давайте начнем шаг № 1, отправив первоначальный вопрос о погоде в Париже:

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

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

На шаге № 2 мы передаем инструмент, который мы хотели бы использовать модель, и модель отвечает с помощью запроса на выполнение:

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

Шаг 3. На этом этапе мы знаем, какую функцию LLM хотела бы, чтобы мы позвонили. В коде мы не делаем реального призывания к внешнему API, мы просто возвращаем гипотетический прогноз погоды напрямую:

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

А на шаге № 4 LLM узнает о результате выполнения функции, а затем может синтезировать текстовый ответ:

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

Вывод:

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

Вы можете увидеть в выводе выше запроса выполнения инструмента, а также ответ.

Полный исходный код находится в FunctionCalling.java app/src/main/java/gemini/workshop

Запустите образец:

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

Вы должны увидеть выход, похожий на следующее:

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

12. Langchain4J обрабатывает функции вызова функции

На предыдущем этапе вы увидели, как обычные текстовые вопросы/ответа и запрос функции/ответ/ответ взаимодействуют, и между ними вы напрямую предоставили запрошенную функцию ответа, не вызывая реальную функцию.

Тем не менее, Langchain4J также предлагает абстракцию более высокого уровня, которая может обрабатывать функцию прозрачно для вас, одновременно обрабатывая разговор, как обычно.

Вызов одной функции

Давайте посмотрим на 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 (для пользовательского сообщения) или AiMessage (для ответа на модель) или даже TokenStream , если вы хотите справиться с более продвинутыми ситуациями, так как эти более сложные объекты также содержат дополнительные Информация, такая как количество потребляемых токенов и т. Д., Но ради простоты мы просто возьмем строку в вводе и строку в выводе.

Давайте закончим с помощью метода main() , который связывает все части вместе:

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

    WeatherForecastService weatherForecastService = new WeatherForecastService();

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

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

Как обычно, вы настраиваете модель чата Gemini. Затем вы создаете экземпляр своей службы прогноза погоды, которая содержит «функцию», которую модель попросит нас позвонить.

Теперь вы снова используете класс AiServices , чтобы связать модель чата, память чата и инструмент (т.е. служба прогноза погоды с ее функцией). AiServices возвращает объект, который реализует ваш определенный интерфейс WeatherAssistant . Единственное, что осталось, - это вызвать метод chat() этого помощника. При его вызове вы увидите только текстовые ответы, но запросы вызова функции и ответы вызова функции не будут видны у разработчика, и эти запросы будут обрабатываться автоматически и прозрачно. Если Gemini считает, что должна быть вызвана функция, она ответит с помощью запроса вызова функции, и 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;
}

Затем вы можете объединить все эти функции и класс с несколькими кругами и задать такие вопросы, как «Что такое 10% цены акций AAPL, конвертированная из доллара США в EUR?» »

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

    MultiTools multiTools = new MultiTools();

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

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

Запустите его следующим образом:

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

И вы должны увидеть несколько функций, называемых:

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

К агентам

Функциональный вызов - это отличный механизм расширения для крупных языковых моделей, таких как Близнецы. Это позволяет нам создавать более сложные системы, которые часто называют «агентами» или «помощниками искусственного интеллекта». Эти агенты могут взаимодействовать с внешним миром с помощью внешних API и услуг, которые могут иметь побочные эффекты на внешней среде (например, отправка электронных писем, создание билетов и т. Д.)

При создании таких влиятельных агентов вы должны сделать это ответственно. Вы должны рассмотреть человеку в петле, прежде чем совершать автоматические действия. Важно помнить о безопасности при проектировании агентов с LLM, которые взаимодействуют с внешним миром.

13. Запуск Джеммы с Ollama и TestContainers

До сих пор мы использовали Близнецы, но есть и Джемма , ее младшая сестра модель.

Gemma -это семейство легких, современных открытых моделей, созданных из тех же исследований и технологий, используемых для создания моделей Gemini. Gemma доступна в двух вариациях Gemma1 и Gemma2 с различными размерами. GEMMA1 доступен в двух размерах: 2B и 7B. GEMMA2 доступен в двух размерах: 9B и 27B. Их веса свободно доступны, а их небольшие размеры означает, что вы можете запустить его самостоятельно, даже на своем ноутбуке или в облачной оболочке.

Как вы управляете Джеммой?

Есть много способов запустить Gemma: в облаке, через AI Vertex AI с нажатием кнопки или GKE с некоторыми графическими процессорами, но вы также можете запустить ее локально.

Один хороший вариант для запуска Gemma Locally - это Ollama , инструмент, который позволяет запускать небольшие модели, такие как Llama 2, Mistral и многие другие на вашей местной машине. Это похоже на Docker, но для LLMS.

Установите Ollama после инструкции для вашей операционной системы.

Если вы используете среду Linux, вам нужно сначала включить Ollama после ее установки.

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

После установки локально вы можете запустить команды, чтобы вытащить модель:

ollama pull gemma:2b

Подождите, пока модель будет вытянута. Это может занять некоторое время.

Запустите модель:

ollama run gemma:2b

Теперь вы можете взаимодействовать с моделью:

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

Чтобы выйти из приглашения нажатия Ctrl+D

Запуск Gemma в Ollama на TestContainers

Вместо того, чтобы устанавливать и запускать Ollama на локальном уровне, вы можете использовать Ollama в контейнере, обрабатываемые TestContainers .

TestContainers не только полезны для тестирования, но и вы можете использовать его для выполнения контейнеров. Есть даже конкретный OllamaContainer , которым вы можете воспользоваться!

Вот целая картина:

2382C05A48708DFD.PNG

Выполнение

Давайте посмотрим на GemmaWithOllamaContainer.java , кусок по кусочкам.

Во -первых, вам нужно создать производный контейнер Ollama, который втягивает модель Gemma. Это изображение либо уже существует из предыдущего запуска, либо будет создано. Если изображение уже существует, вы просто сообщите TestContainers, что вы хотите заменить изображение Ollama по умолчанию вашим вариантом, способствующим GEMMA:

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

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

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

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

Затем вы создаете и запускаете тестовый контейнер Ollama, а затем создаете модель чата Ollama, указывая на адрес и порт контейнера с помощью модели, которую вы хотите использовать. Наконец, вы только что вызываете model.generate(yourPrompt) как обычно:

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

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

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

    System.out.println(response);
}

Запустите его следующим образом:

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

Первый пробег займет некоторое время, чтобы создать и запустить контейнер, но как только сделано, вы должны увидеть, как Джемма отвечает:

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.

У вас есть Джемма, работающая в облачной оболочке!

14. Поздравляю

Поздравляем, вы успешно создали свое первое генеративное приложение для AI в Java, используя Langchain4J и API Gemini! Вы обнаружили, что мультимодальные крупные языковые модели довольно мощные и способны выполнять различные задачи, такие как вопрос/ответ, даже по вашей собственной документации, извлечение данных, взаимодействие с внешними API и многое другое.

Что дальше?

У вас есть очередь, чтобы улучшить ваши приложения с мощными интеграциями LLM!

дальнейшее чтение

Справочные документы