Новый перевод от команды Spring АйО расскажет вам, как новые библиотеки и фреймворки расширяют экосистему Java, делая возможной интеграцию ИИ-решений в приложения, написанные на Java.

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


Generative AI, в особенности большие языковые модели  (Large Language Models — LLMs) привлекли значительное внимание к своей способности создавать новый контент и расширять пользовательский опыт через обработку естественного языка, став новым приоритетом для предпринимателей, желающих обогатить свои приложения возможностями ИИ. По мере того, как искусственный интеллект (ИИ) становится движущей силой для технических инноваций, экосистема Java быстро эволюционирует под требования ИИ решений. 

Появилось несколько фреймворков и библиотек для работы с ИИ, некоторые из них open source, стимулируя разработчиков интегрировать функционал, основанный на ИИ. Более того, Enterprise решения, такие как Generative AI от Oracle, расширяют набор возможностей ИИ, доступный для Java приложений и облачных окружений.

В этой статье мы посмотрим на известные библиотеки и фреймворки для встраивания функционала generative AI в приложения Java, такие как LangChain4j, Spring AI и Jlama, а также на Enterprise решения, в частности, Generative AI от Oracle. Вооружившись этим знанием, мы разработаем чатбот, способный поддерживать диалог, используя сервис Generative AI от Oracle и его Java SDK, а затем повысим точность его ответов, добавив больше возможностей от LangChain4j.

LangChain4J

LangChain4j — это Java библиотека, созданная для упрощения интеграции больших языковых моделей (Large Language Models, LLMs) в приложения на Java. Проект начал развиваться в 2023 на фоне растущей эйфории по поводу ChatGPT. Создатели LangChain4j обратили внимание на отсутствие — на тот момент — альтернатив на Java растущему количеству LLM библиотек и фреймворков для Python и JavaScript. Этот пробел в экосистеме Java натолкнул их на мысль о разработке LangChain4j в качестве решения этой проблемы.

Комментарий от редакции Spring АйО

На момент перевода статьи, Spring AI, как модуль для работы с AI в экосистеме Spring, не имеет GA (Generally Available) релиза.

LangChain4j быстро заслужила внимание и признание в сообществе Java, примерно за полтора года, особенно среди разработчиков, работающих над приложениями на основе LLM. Библиотека активно развивается, новые возможности и интеграции добавляются регулярно. 

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

Комментарий от редакции Spring АйО

Под "хранлищами эмбеддингов" идёт речь речь о векторных базах данных, таких как ChromaDB, например.

Ее унифицированный API дает возможность интегрироваться с большим количеством LLM провайдеров и их моделями, включая серийные модели GPT-3.5 и GPT-4 от OpenAI, Gemini от Google, Claude от Anthropic, модели Cohere, локальные модели и различные модели open-source, находящиеся на хостинге Hugging Face. Она также поддерживает модели эмбеддингов и хранилища векторных представлений, число которых постоянно растет. 

Многофункциональный набор инструментов от LangChain4j включает в себя Prompt Templates, что облегчает создание динамических промптов через задание структуры промптов, которые позволяют заполнять некоторые значения в рантайме. Библиотека предлагает инструменты Chat Memory Management, которые позволяют хранить и извлекать историю взаимодействия с пользователем, что очень важно при создании агентов-собеседников.  Tools внутри API позволяют вызывать функции и выполнять динамически сгенерированный код языковыми моделями. LangChain4j также предоставляет инструменты для генерации с использованием RAG (Retrieval-Augmented Generation), включая загрузчики документов, сплиттеры и механизмы извлечения. Более того, она предлагает AI Services (high-level API), который позволяет уйти от чрезмерной сложности работы с LLMs через абстракции, давая разработчикам возможность взаимодействовать с языковыми моделями через простые интерфейсы Java.

LangChain4j поддерживает интеграцию с моделями текстов и графики, позволяя разрабатывать более гибкие приложения на ИИ. Она также хорошо интегрируется с популярными Java фреймворками, такими как Spring Boot и Quarkus, благодаря чему становится проще встраивать этот функционал в существующие проекты Java.

Spring AI

Spring AI — это еще один популярный фреймворк, интегрирующий ИИ в Java. Это расширение экосистемы Spring, специально созданной для того, чтобы помочь Enterprise приложениям интегрироваться с моделями ИИ. Он поддерживает все основные провайдеры ИИ моделей и предлагает поддержку на уровне API для всех этих провайдеров. Он также поддерживает векторные базы данных и абстракцию моделей, что упрощает переключение между различными моделями ИИ. Его API позволяет моделям запрашивать вызов инструментов и функций со стороны клиента, позволяя модели выполнять задачи динамически. Spring AI хорошо интегрируется с существующими приложениями на Spring, благодаря чему он стал отличным решением для Enterprise окружений, которые уже опираются на экосистему Spring.

Jlama

Jlama предлагает нативный движок для работы с LLM прямо из Java приложения. Если вы хотите запускать большие языковые модели локально и эффективно внутри окружения Java, Jlama выглядит, как идеальное решение. Jlama полностью построена на Java, используя библиотеки и API на Java для всех LLM операций. Она облегчает локальное выполнение модели внутри виртуальной машины Java (JVM).

Комментарий от редакции Spring АйО

Да, на самом деле так и происходит. Модель бежит прямо в рамках виртуальной машины. Но, конечно, надо понимать, что языковые модели, как правило, довольно большие сами по себе и могут занимать довольно большое количество ресурсов при своей работе. Для решения подобной проблемы используют Квантизацию (иногда говорят "Квантование") языковых моделей, но это другая история.

Таким образом убирается необходимость использования удаленных API или облачных сервисов для работы с AI, что делает Jlama подходящим решением для use case-ов, которые требуют защиты данных, низкой latency или возможности работать оффлайн. Она использует preview Vector API для более быстрого вывода, что требует Java 20 или более поздних версий.

Комментарий от редакции Spring АйО

Речь про Preview API для работы с векторными инструкциями CPU, подробнее можно почитать в рамках JEP 448

Кроме того, она поддерживает квантизированные модели, значительно снижая использование локальной памяти и время вывода.

Сервис Oracle Generative AI

Oracle Generative AI — это управляемый сервис Oracle Cloud Infrastructure (OCI), предоставляющий набор кастомизируемых больших языковых моделей (LLMs), покрывающих широкий набор use case-ов, включая чаты, генерацию текста, обзор содержимого и создание текстовых эмбеддингов. Более того, он также предлагает  SDK для Java для интеграции моделей generative AI в приложения на Java. Чтобы получить доступ и установить SDK для Java, перейдите по этой ссылке.

Отметим, что для доступа к любому OCI сервису необходим аккаунт в Oracle Cloud Infrastructure. Перейдите по этой ссылке, чтобы узнать больше о бесплатном пакете и создать собственный аккаунт.

Более того, сервис OCI Generative AI предлагает ряд больших языковых моделей для универсального чата.

Рисунок 1: OCI модели чатов
Рисунок 1: OCI модели чатов

Теперь, когда мы знаем, что существует популярная опция для работы с Java и generative AI, давайте изучим ее получше, написав простую программу, которая создает чат-бот командной строки, поддерживаемый LLM моделью от сервиса OCI Generative AI.

Создание чат-бота в Java с использованием OCI Generative AI

Мы разработаем чат-бот, используя OCI SDK для Java, и позволим пользователям вступить с ним в разговор.

Рисунок 2: Чат-бот, использующий OCI SDK для Java
Рисунок 2: Чат-бот, использующий OCI SDK для Java

Работать над этим примером мы начнем с проекта на Java с Maven. Вы можете воспользоваться своей любимой IDE, чтобы сгенерировать базовый pom.xml и структуру каталогов по умолчанию для Maven-проекта с заданным каталогом для исходников. Файл pom.xml — это единый конфигурационный файл, содержащий большую часть информации, необходимой для создания проекта. 

Затем мы кастомизируем pom файл, добавив в него следующие зависимости, чтобы получить доступ к OCI SDK для Java.

	<dependency>
    		<groupId>com.oracle.oci.sdk</groupId>
    		<artifactId>oci-java-sdk-common</artifactId>
    		<version>3.55.1</version>
	</dependency>
	<dependency>
    		<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-common-httpclient-jersey</artifactId>
    		<version>3.55.1</version>
	</dependency>
	<dependency>
    		<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-generativeaiinference</artifactId>
    		<version>3.55.1</version>
	</dependency>

Библиотека oci-java-sdk-common является центральным модулем OCI Java SDK. Она предоставляет важные утилиты, разделенные конфигурации и фундаментальные компоненты для приложений на Java, чтобы они могли эффективно взаимодействовать с OCI сервисами. Модуль oci-java-sdk-common-httpclient-jersey предоставляет функциональность, которая фокусируется на взаимодействии с HTTP клиентами, используя фреймворк Jersey. И, наконец, модуль oci-java-sdk-generativeaiinference поддерживает взаимодействие с сервисом OCI Generative AI.

Чтобы использовать OCI SDK, мы прежде всего должны аутентифицироваться. Самый простой способ это сделать — это воспользоваться интерфейсом командной строки Oracle Cloud Infrastructure (OCI), oci, который можно инсталлировать, воспользовавшись этими инструкциями. Далее, следуем инструкциям, чтобы настроить конфигурационный файл. После прохождения всех этих шагов мы получим файл config, чье местоположение зависит от вашей операционной системы, например, в окружениях семейства Unix он будет находиться по адресу ~/.oci/config.

Как только мы получили файл config, мы можем предоставить эту локацию нашему приложению чат-бота и настроить для него доступ к OCI, включая эндпоинт для generative AI, детали конфигурации и compartment ID. В дальнейшем вы можете использовать информацию о местоположении файла config при настройке класса OCIGenAIEnv, пример которого приведен ниже, чтобы хранить настройки конфигурации, специфичные для окружения, для сервиса Oracle Cloud Infrastructure (OCI) Generative AI.

public class OCIGenAIEnv {
    // The OCI Generative AI service is available in several regions.
    // https://docs.oracle.com/en-us/iaas/Content/generative-ai/overview.htm#regions
    // Here, we use the Generative AI service endpoint in Chicago region.
    private static final String ENDPOINT = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com";

    // location of the OCI configuration file
    private static final String CONFIG_LOCATION = "~/.oci/config";

    // profile name within the OCI configuration file to use
    private static final String CONFIG_PROFILE = "DEFAULT";

    // unique identifier of the compartment that has the policies granting
    // permissions for using the Generative AI Service. See how to set policies:
    // https://docs.oracle.com/en-us/iaas/Content/generative-ai/iam-policies.htm
    private static final String COMPARTMENT_ID = "ocid1.compartment.oc1..xxx";
}

Чтобы Compartment ID хранился безопасно, установите COMPARTMENT_ID как переменную окружения и получайте к ней доступ через System.getenv("COMPARTMENT_ID"). Хардкодить ее не надо.

Следующим шагом мы обеспечиваем безопасные коммуникации с сервисами OCI, инстанциируя AuthenticationDetailsProvider с константами OCIGenAIEnv. Затем мы используем провайдер для создания GenerativeAiInferenceClient, который упрощает взаимодействие с языковой моделью OCI.

    // read configuration details from the config file and create an AuthenticationDetailsProvider
    ConfigFileReader.ConfigFile configFile;
    AuthenticationDetailsProvider provider;
    try {
        configFile = ConfigFileReader.parse(OCIGenAIEnv.getConfigLocation(), OCIGenAIEnv.getConfigProfile());
        provider = new ConfigFileAuthenticationDetailsProvider(configFile);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    // Set up Generative AI inference client with credentials and endpoint
    ClientConfiguration clientConfiguration =
            ClientConfiguration.builder()
                    .readTimeoutMillis(240000)
                    .build();
    GenerativeAiInferenceClient generativeAiInferenceClient =
            GenerativeAiInferenceClient.builder()
                    .configuration(clientConfiguration)
                    .endpoint(ENDPOINT)
                    .build(provider);

Чтобы сгенерировать ответ на запрос пользователя, чат-бот должен знать, как вызвать модели Generative AI. Для достижения этой цели SDK предлагает представление режима выдачи ответа через класс ServingMode, который можно дополнительно конфигурировать через чат-модель OCI. Для нашего примера я использовал модель meta.llama-3.1-405b-instruct и задал ее вызов через OnDemandServingMode, представляющий режим, при котором модели Generative AI вызываются по требованию, не требуя предварительного деплоймента или постоянного хостинга для модели.

ServingMode chatServingmode = OnDemandServingMode.builder()
                .modelId("meta.llama-3.1-405b-instruct")
                .build();

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

// Send the given prompt to the LLM to generate a response.
public ChatResponse generateResponse(String prompt) {
    // create ChatContent and UserMessage using the given prompt string
    ChatContent content = TextContent.builder()
            .text(prompt)
            .build();
    List<ChatContent> contents = List.of(content);

    Message message = UserMessage.builder()
            .content(contents)
            .build();
    // put the message into a List
    List<Message> messages = List.of(message);

    // create a GenericChatRequest including the current message, and the
    // parameters for the LLM model
    GenericChatRequest genericChatRequest = GenericChatRequest.builder()
            .messages(messages)
            .maxTokens(1000)
            .numGenerations(1)
            .frequencyPenalty(0.0)
            .topP(1.0)
            .topK(1)
            .temperature(0.75)
            .isStream(false)
            .build();

    // create ChatDetails and ChatRequest providing it with the compartment ID
    // and the parameters for the LLM model
   ChatDetails details = ChatDetails.builder()
            .chatRequest(genericChatRequest)
            .compartmentId(OCIGenAIEnv.getCompartmentID())
            .servingMode(chatServingMode)
            .build();
   ChatRequest request = ChatRequest.builder()
            .chatDetails(details)
            .build();

   // send chat request to the AI inference client
   return generativeAiInferenceClient.chat(request);
}

Текст ответа включает в объект ChatResponse, возвращаемый методом generativeAiInferenceClient.chat(). Чтобы извлечь текст из ChatResponse, вы можете попробовать воспользоваться приведенным ниже фрагментом кода.

public String extractResponseText(ChatResponse chatResponse) {
    // get BaseChatResponse from ChatResponse
    BaseChatResponse bcr = chatResponse
            .getChatResult()
            .getChatResponse();
    // extract text from the GenericChatResponse response type
    // GenericChatResponse represents response from llama models
    if (bcr instanceof GenericChatResponse resp) {
        List<ChatChoice> choices = resp.getChoices();
        List<ChatContent> contents = choices.get(choices.size() - 1)
                .getMessage()
                .getContent();
        ChatContent content = contents.get(contents.size() - 1);
        if (content instanceof TextContent textContent) {
            return textContent.getText();
        }
    }
    throw new RuntimeException("Unexpected ChatResponse");
}

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

String prompt = "How do I troubleshoot memory leaks?";
ChatResponse response = generateResponse(prompt);
System.out.println(extractResponseText(response));

Для валидации нашей работы нам необходимо, чтобы ассистент был поднят и работал:

  1. Воспользуйтесь возможностями вашей IDE для управления циклом жизни проекта на Maven или быстро запустите команду mvn verify в окне терминала. Это действие создаст файл troubleshoot-assist-1.0.0.jar внутри каталога target.

  2. Запустите приложение с помощью команды mvn exec:java -Dexec.mainClass=JavaTroubleshootingAssistant.

Наш чат-бот пришлет следующий ответ:

Много кода
The age-old problem of memory leaks! Troubleshooting memory leaks can be a challenging and time-consuming process, but with the right tools and techniques, you can identify and fix them. Here's a step-by-step guide to help you troubleshoot memory leaks:

**Preparation**

1. **Understand the symptoms**: Identify the symptoms of the memory leak, such as:
	* Increasing memory usage over time
	* Performance degradation
	* Crashes or errors due to out-of-memory conditions
2. **Gather information**: Collect relevant data about the system, application, and environment, including:
	* Operating system and version
	* Application version and architecture (32-bit or 64-bit)
	* Memory configuration (RAM, virtual memory, and paging file settings)
3. **Choose the right tools**: Select the tools you'll use to troubleshoot the memory leak, such as:
	* Memory profiling tools (e.g., Visual Studio, Valgrind, or AddressSanitizer)
	* System monitoring tools (e.g., Task Manager, Performance Monitor, or top)
	* Debugging tools (e.g., gdb, WinDbg, or Visual Studio Debugger)

**Step 1: Monitor System Resources**

1. **Track memory usage**: Use system monitoring tools to track memory usage over time, including:
	* Total memory usage
	* Memory usage by process or application
	* Memory allocation and deallocation patterns
2. **Identify memory-intensive processes**: Determine which processes or applications are consuming the most memory.

**Step 2: Analyze Memory Allocations**

1. **Use memory profiling tools**: Run memory profiling tools to analyze memory allocations and identify potential leaks.
2. **Collect memory dumps**: Collect memory dumps or snapshots to analyze memory allocation patterns.
3. **Analyze memory allocation patterns**: Look for patterns of memory allocation and deallocation, such as:
	* Memory allocated but not released
	* Memory allocated repeatedly without being released
	* Large memory allocations

**Step 3: Inspect Code and Data Structures**

1. **Review code**: Inspect the code for potential memory leaks, focusing on areas with high memory allocation and deallocation activity.
2. **Check data structures**: Verify that data structures, such as arrays, lists, or trees, are properly managed and released.
3. **Look for circular references**: Identify potential circular references that could prevent memory from being released.

**Step 4: Debug and Test**

1. **Use debugging tools**: Use debugging tools to step through the code, inspect variables, and identify potential issues.
2. **Test hypotheses**: Test hypotheses about the cause of the memory leak by modifying the code or data structures.
3. **Verify fixes**: Verify that fixes or changes resolve the memory leak.

**Additional Tips**

1. **Test under various conditions**: Test the application under different conditions, such as varying loads, inputs, or environments.
2. **Use automated testing**: Use automated testing tools to simulate user interactions and identify potential memory leaks.
3. **Monitor memory usage regularly**: Regularly monitor memory usage to detect potential memory leaks early.

By following these steps and using the right tools, you'll be well-equipped to troubleshoot and fix memory leaks in your application. Happy debugging!

Поскольку мы не дали модели инструкций, на каком контексте фокусироваться, ответ нельзя считать ориентированным на Java или на отладку проблем с памятью в JVM.

Что же делать, если мы хотим, чтобы ответы от LLM относились именно к отладке Java и JVM? Мы посмотрим на это в следующем разделе.

Расширение функций чат-бота с помощью LangChain4j

Мы часто слышим упоминания о том, что большие языковые модели могут галлюцинировать и генерировать неточные или абсурдные ответы, либо такие ответы, которые не имеют отношения к специализированной области, в которой заинтересованы мы. Однако, существует несколько приемов, которые могут повышать точность этих моделей и привязывать их вывод к реальным фактам. RAG (Retrieval Augmented Generation) — это один из самых популярных и эффективных таких приемов.

В этом разделе рассказывается, как повысить привязку ранее разработанного Ассистента по отладке Java (Java Troubleshooting Assistant) к контексту, заставив его отвечать на наши вопросы по отладке программ на Java с использованием техники RAG с помощью фреймворка LangChain4j. Ассистент будет генерировать свои ответы на базе документа Java Platform, Standard Edition, Troubleshooting Guide.

Рисунок 3: Ассистент по отладке Java
Рисунок 3: Ассистент по отладке Java

Начнем с включения следующих зависимостей для LangChain4j в файл pom.xml проекта.

	<dependency>
    		<groupId>dev.langchain4j</groupId>
    		<artifactId>langchain4j</artifactId>
    		<version>0.36.2</version>
	</dependency>
	<dependency>
    		<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
    		<version>0.36.2</version>
	</dependency>
	<dependency>
    		<groupId>dev.langchain4j</groupId>
    		<artifactId>langchain4j-chroma</artifactId>
    		<version>0.36.2</version>
	</dependency>

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

1. Создаем эмбеддинги с использованием модели эмбеддингов 

Первым шагом станет создание базы знаний по информации, доступной в гайде по отладке (например,Java Troubleshooting Guide). Для этого необходимо загрузить документ как файл pdf, извлечь из него текст и разбить его на чанки приемлемого размера:

  • FileSystemDocumentLoader из модуля LangChain4j может помочь загрузить pdf файлы,

  • ApachePdfBoxDocumentParser может извлечь из них текст.

  • DocumentSplitter может разбить загруженные документы на чанки TextSegment, которые мы передадим модели эмбеддингов для генерации эмбеддингов, как  показано ниже.

DocumentSplitter documentSplitter = DocumentSplitters.recursive(
                800,    // Maximum chunk size in tokens
                40,     // Overlap between chunks
                null    // Default separator
);
public List<TextSegment> chunkPDFFiles(String filePath) {
    List<Document> documents = null;
    try {
        // Load all *.pdf documents from the given directory
        PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.pdf");
        documents = FileSystemDocumentLoader.loadDocuments(
                filePath,
                pathMatcher,
                new ApachePdfBoxDocumentParser());
    } catch (Exception e) {
        e.printStackTrace();
    }
    // Split documents into TextSegments and add them to a List
    return documents.stream().flatMap(d -> documentSplitter.split(d).stream()).toList();
}

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

Рисунок 4: Эмбеддинги и хранилище векторных представлений
Рисунок 4: Эмбеддинги и хранилище векторных представлений

Для создания этих эмбеддингов мы будем использовать модели эмбеддингов cohere.embed-english-v3.0, предоставляемую сервисом OCI Generative AI.

Рисунок 5: Модели эмбеддингов OCI
Рисунок 5: Модели эмбеддингов OCI

Затем мы создадим новый ServingMode для использования модели эмбеддингов.

// Create a ServingMode specifying the embedding model to be used
ServingMode embeddingServingMode = OnDemandServingMode.builder()
            .modelId("cohere.embed-english-v3.0")
            .build();

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

public Embedding embedContent(String content) {
    List<String> inputs = Collections.singletonList(content);
    // Build embed text details and request from the input string
    // use the embedding model as the serving mode
    EmbedTextDetails embedTextDetails = EmbedTextDetails.builder()
            .servingMode(embeddingServingMode)
            .compartmentId(OCIGenAIEnv.getCompartmentID())
            .inputs(inputs)
            .truncate(EmbedTextDetails.Truncate.None)
            .build();
    EmbedTextRequest embedTextRequest = EmbedTextRequest.builder().embedTextDetails(embedTextDetails).build();

    // send embed text request to the AI inference client
    EmbedTextResponse embedTextResponse = generativeAiInferenceClient.embedText(embedTextRequest);

    // extract embeddings from the embed text response
    List<Float> embeddings = embedTextResponse.getEmbedTextResult().getEmbeddings().get(0);
    // put the embeddings in a float[]
    int len = embeddings.size();
    float[] embeddingsVector = new float[len];
    for (int i = 0; i < len; i++) {
        embeddingsVector[i] = embeddings.get(i);
    }
    // return Embedding of LangChain4j that wraps a float[]
    return new Embedding(embeddingsVector);
}

public List<Embedding> createEmbeddings(List<TextSegment> segments) {
    return segments.stream().map(s -> embModel.embedContent(s.text())).toList();
}

2. Хранилище эмбеддингов

Как только мы создали эмбеддинги для нашего документа, они должны сохраняться в базу данных, которую часто называют хранилищем эмбеддингов или хранилищем векторных представлений. Для нашего примера мы будет использовать хранилище эмбеддингов ChromaDB, однако следует отметить, что LangChain4j поддерживает широкий диапазон хранилищ эмбеддингов.

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

docker pull chromadb/chroma
docker run -p 8000:8000 chromadb/chroma

Теперь, когда у нас есть база данных для векторных представлений, которая работает и слушает по адресу http://localhost:8000, мы можем сохранять в ней эмбеддинги нашего документа. Давайте создадим экземпляр EmbeddingStore, имеющий доступ к этому хранилищу.

ChromaEmbeddingStore embeddingStore = ChromaEmbeddingStore.builder()
                .baseUrl("http://localhost:8000")
                .collectionName("Java-collection")
                .build();

Далее вызовем embeddingStore.addAll(), чтобы добавить сгенерированные эмбеддинги к хранилищу.

public void storeEmbeddings(List<Embedding> embeddings, List<TextSegment> segments) {
    embeddingStore.addAll(embeddings, segments);
}

Ну и наконец соберем все части вместе: разделим pdf файл на чанки, создадим эмбеддинги и сохраним их в хранилище векторных представлений.

public void createVectorStore(String filePath) {
    List<TextSegment> segments = chunkPDFFiles(filePath);
    List<Embedding> embeddings = createEmbeddings(segments);
    storeEmbeddings(embeddings, segments);
}

3. Извлечение и дополнение промптов

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

Рисунок 6: Имплементация RAG с помощью LangChain4j
Рисунок 6: Имплементация RAG с помощью LangChain4j

Давайте используем тот же вопрос, который мы задавали в предыдущем разделе:

String question = "How do I troubleshoot memory leaks?";

Чтобы извлечь контекст, относящийся к этому вопросу, из базы знаний, необходимо провести семантический поиск в хранилище векторных представлений. Для этого мы сначала конвертируем строку в эмбеддинги, используя метод embedContent(), который написали ранее.

Embedding queryEmbedding = embedContent(question);

Затем мы осуществляем поиск похожих фрагментов текста, чтобы извлечь их из базы знаний и связать вместе. С помощью PromptTemplate мы комбинируем оригинальный вопрос и извлеченный контекст, чтобы сконструировать новый промпт.

    // Find relevant embeddings in embedding store by semantic similarity
    int maxResults = 10;
    double minScore = 0.7;
    List<EmbeddingMatch<TextSegment>> relevantEmbeddings
            = embeddingStore.findRelevant(queryEmbedding, maxResults, minScore);
    String context = relevantEmbeddings.stream()
            .map(match -> match.embedded().text())
            .collect(joining("\n\n"));

    PromptTemplate template = PromptTemplate.from("""
                You are a Java Troubleshooting Assistant. Answer the question in the context of Java or HotSpot JVM.
                Always ask if the user would like to know more about the topic. Do not add signature at the end of the answer.
                Use only the following pieces of context to answer the question at the end.

                Context: 

                Question: 

                Helpful Answer:
                """);
    // add the question and the retrieved context to the prompt template
    Map<String, Object> variables = Map.of(
            "question", question,
            "context", context
    );
    Prompt prompt = template.apply(variables);

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

    String query = prompt.text();
    response = chatModel.generateResponse(query);
    System.out.println(extractText(response));

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

To troubleshoot memory leaks in Java applications, you can follow these steps:

1. **Enable Options and Flags for JVM Troubleshooting**: Update the Java Runtime and enable options and flags for JVM troubleshooting to collect useful data.
2. **Gather Relevant Data**: Collect diagnostic data such as heap histograms, garbage collection logs, and Java Flight Recordings to analyze memory usage.
3. **Analyze Heap Histograms**: Examine heap histograms to quickly narrow down memory leaks.
4. **Use Heap Dump Analysis Tools**: Utilize tools like Eclipse Memory Analyzer Tool (MAT) or YourKit to analyze heap dumps.
5. **Analyze Java Flight Recordings**: Use JDK Mission Control (JMC) to analyze Java Flight Recordings and identify leaking objects.
6. **Monitor Memory Usage**: Use JConsole and JDK Mission Control to monitor memory usage and live sets.
7. **Detect Native Memory Leaks**: Use Native Memory Tracking (NMT) to detect native memory leaks.

Additionally, you can use tools like JConsole, JDK Mission Control, and native tools like pmap or PerfMon to monitor memory usage and detect memory leaks.

Would you like to know more about troubleshooting memory leaks or any specific topic mentioned above?

Ух ты! Этот ответ действительно помогает отлаживать утечки памяти в Java и является довольно точным!

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

4. Добавление памяти чата

Мы хотим, чтобы модель могла получать доступ к ранее заданным вопросам и ответам в рамках беседы, чтобы мы могли задавать уточняющие вопросы. Внедрение такой функциональности становится возможным, если сохранять предшествующую цепочку вопросов и ответов в памяти чата. LangChain4j предлагает несколько реализаций ChatMemory, которые можно использовать для запоминания бесед. OCI Generative AI также предоставляет механизм сохранения истории сообщений беседы, и мы воспользуемся им как примером. Как показано ниже, мы сохраним ответы от чата как список объектов класса ChatChoice и воспользуемся ими в дальнейшем при создании нового запроса к чату.

List<ChatChoice> chatMemory;

public void saveChatResponse(ChatResponse chatResponse) {
    BaseChatResponse bcr = chatResponse.getChatResult().getChatResponse();
    if (bcr instanceof GenericChatResponse resp) {
        chatMemory = resp.getChoices();
    }
}

Чат-бот может посылать эти сообщения, сохраненные в памяти чата, в LLM вместе с нашим дополненным промптом из метода generateResponse().

public ChatResponse generateResponse(String prompt) {
    ...
    Message message = UserMessage.builder()
            .content(contents)
            .build();

    // messages below holds previous messages from the conversation
    List<Message> messages = chatMemory == null ? new ArrayList<>() :
                        chatMemory.stream()
                        .map(ChatChoice::getMessage)
                        .collect(Collectors.toList());
    // add the current query message to list of history messages.
    messages.add(message);

    GenericChatRequest genericChatRequest = GenericChatRequest.builder()
            .messages(messages)  // holds current message + history
            ...
            ...
    ...
    // send chat request to the AI inference client and receive response
    ChatResponse response = generativeAiInferenceClient.chat(request);
    // save the response to the chat memory
    saveChatResponse(response);

Поскольку мы модифицировали проект, давайте почистим и пересоберем его командой mvn clean verify. Эта команда генерирует новый файл troubleshoot-assist-1.0.0.jar в каталоге target. Теперь запустим приложения, используя команду mvn exec:java -Dexec.mainClass=JavaTroubleshootingAssistant.

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

question = "How do I enable NMT?"

Приведенный ниже вывод показывает, что ответ дает правильную инструкцию по включению Native Memory Tracking (NMT) для отладки проблем с нативной памятью.

To enable Native Memory Tracking (NMT) in the HotSpot JVM, you can use the following command-line option:

`-XX:NativeMemoryTracking=[off|summary|detail]`

You can set the level of detail to either `summary` or `detail`, depending on the level of information you need. The `summary` level provides a high-level overview of native memory usage, while the `detail` level provides a more detailed breakdown of native memory usage.

For example, to enable NMT with the `summary` level, you can use the following command:

`java -XX:NativeMemoryTracking=summary -jar your_java_app.jar`

You can also enable NMT at runtime using the `jcmd` command:

`jcmd <pid> VM.native_memory summary`

Replace `<pid>` with the process ID of the Java process you want to enable NMT for.

Would you like to know more about using NMT to troubleshoot native memory leaks or how to interpret the NMT output?

И таким образом у нас появляется свой простой ассистент по отладке Java!

Полный код примера, который мы разобрали в статье, можно найти здесь: Java-Gen-AI.

Заключение

В этой статье мы рассмотрели несколько фреймворков и библиотек, которые вносят огромный вклад в расширение экосистемы Java с целью поддержки интеграции ИИ. Фреймворки LangChain4j, Spring AI и Jlama упрощают интеграцию больших языковых моделей в существующие и новые приложения на Java, позволяя разработчикам собирать сложные сценарии работы с ИИ. Более того, облачные сервисы generative AI, такие как Oracle Generative AI, могут масштабироваться и дальше, достигая уровня Enterprise для решений, использующих ИИ. Эти разработки ставят язык и платформу Java на позиции ключевых игроков в основанном на ИИ решениях будущем Enterprise технологий.

Регистрируйтесь на главную конференцию про Spring на русском языке от сообщества Spring АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.

Комментарии (1)


  1. fil_lost
    20.02.2025 05:30

    в пет проекте, прикрутил к тг боту через spring ai
    это весело
    советую всем попробовать
    писать заклинания над функциями вместо функций)