Java 版 Gemini 与 Vertex AI 和 LangChain4j

1. 简介

此 Codelab 重点介绍 Google Cloud 上的 Vertex AI 托管的 Gemini 大语言模型 (LLM)。Vertex AI 是一个平台,其中包含 Google Cloud 上的所有机器学习产品、服务和模型。

您将使用 Java 和 LangChain4j 框架与 Gemini API 进行交互。您将通过具体示例,利用 LLM 来解答问题、生成想法、提取实体和结构化内容、检索增强生成和函数调用。

什么是生成式 AI?

生成式 AI 是指使用人工智能技术创作新内容,例如文本、图片、音乐、音频和视频。

生成式 AI 由大语言模型 (LLM) 提供支持,这种模型可以同时处理多项任务并执行摘要、问答、分类等开箱即用的任务。只需极少的训练,基础模型即可针对目标应用场景进行调整,而样本数据非常少。

生成式 AI 的工作原理是什么?

生成式 AI 使用机器学习 (ML) 模型来学习包含人类创建内容的数据集中的模式和关系。然后,它会使用学到的模式生成新内容。

训练生成式 AI 模型的最常用方法是使用监督式学习。模型会获得一组人工创建的内容及相应的标签。然后,它会学习生成与人类创建的内容类似的内容。

常见的生成式 AI 应用有哪些?

生成式 AI 可用于:

  • 通过增强的聊天和搜索体验,改善客户互动。
  • 通过对话界面和摘要功能探索大量非结构化数据。
  • 协助处理重复性任务,例如回复提案请求、将营销内容本地化为不同的语言,以及检查客户合同是否符合相关规定等。

Google Cloud 提供哪些生成式 AI 产品?

借助 Vertex AI,您只需很少甚至完全没有机器学习专业知识,即可与基础模型进行交互、自定义基础模型并将其嵌入您的应用。您可以在 Model Garden 上访问基础模型,通过 Vertex AI Studio 上的简单界面对模型进行调参,或者在数据科学笔记本中使用模型。

Vertex AI Search and Conversation 为开发者提供了构建生成式 AI 赋能的搜索引擎和聊天机器人的最快方法。

Google Cloud 专用 Gemini 由 Gemini 提供支持,是 AI 赋能的协作工具,可在 Google Cloud 和各种 IDE 中使用,帮助您更快地完成更多任务。Gemini Code Assist 提供代码补全、代码生成和代码说明功能,让您可以与其聊天咨询技术问题。

Gemini 是什么?

Gemini 是 Google DeepMind 开发的一系列生成式 AI 模型,专为多模态用例而设计。多模态意味着它可以处理和生成不同类型的内容,例如文本、代码、图片和音频。

b9913d011999e7c7.png

Gemini 有多种变体和大小:

  • Gemini Ultra:构建最大、功能最强大的复杂任务版本。
  • Gemini Flash:速度最快,性价比高,针对大量任务进行了优化。
  • Gemini Pro:中型应用,经过优化,适合处理各种任务。
  • Gemini Nano:效率最高,专为设备端任务而设计。

主要特性:

  • 多模态:Gemini 能够理解和处理多种信息格式,比传统的纯文字语言模型迈出了重要的一步。
  • 性能:Gemini Ultra 在许多基准测试中的性能优于当前最先进的模型,并且是第一个在极具挑战性的 MMLU(大规模多任务语言理解)基准测试中超越人类专家的模型。
  • 灵活性:Gemini 的不同规模使其能够适应从大规模研究到在移动设备上部署的各种应用场景。

如何通过 Java 与 Vertex AI 上的 Gemini 交互?

您有两种选择:

  1. 官方适用于 Gemini 的 Vertex AI Java API 库。
  2. LangChain4j 框架。

在此 Codelab 中,您将使用 LangChain4j 框架。

什么是 LangChain4j 框架?

LangChain4j 框架是一个开源库,通过编排各种组件(例如 LLM 本身),以及矢量数据库(用于语义搜索)、文档加载器和拆分器(用于分析文档并从中学习)、输出解析器等,将 LLM 集成到 Java 应用中。

该项目的灵感来自LangChain Python 项目,但其目标是服务于 Java 开发者。

bb908ea1e6c96ac2.png

学习内容

  • 如何设置 Java 项目以使用 Gemini 和 LangChain4j
  • 如何以编程方式向 Gemini 发送第一个问题
  • 如何流式传输 Gemini 的回答
  • 如何在用户与 Gemini 之间进行对话
  • 如何在多模态环境中通过发送文字和图片来使用 Gemini
  • 如何从非结构化内容中提取有用的结构化信息
  • 如何操作提示模板
  • 如何进行文本分类,例如情感分析
  • 如何与您自己的文档聊天(检索增强生成)
  • 如何通过函数调用扩展聊天机器人
  • 如何在本地将 Gemma 与 Ollama 和 TestContainers 搭配使用

所需条件

  • 了解 Java 编程语言
  • Google Cloud 项目
  • 浏览器,例如 Chrome 或 Firefox

2. 设置和要求

自定进度的环境设置

  1. 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个

fbef9caa1602edd0.png

a99b7ace416376c4.png

5e3ff691252acf41.png

  • 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
  • 项目 ID 在所有 Google Cloud 项目中是唯一的,并且是不可变的(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用 PROJECT_ID 标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除项目。Google Cloud 新用户符合参与 300 美元免费试用计划的条件。

启动 Cloud Shell

虽然 Google Cloud 可以通过笔记本电脑远程操作,但在此 Codelab 中,您将使用 Cloud Shell,这是一个在云端运行的命令行环境。

激活 Cloud Shell

  1. 在 Cloud Console 中,点击激活 Cloud Shell853e55310c205094

3c1dabeca90e44e5

如果这是您第一次启动 Cloud Shell,系统会显示一个中间屏幕,说明它是什么。如果您看到中间屏幕,请点击继续

9c92662c6a846a5c

预配和连接到 Cloud Shell 只需花几分钟时间。

9f0e51b578fecce5

这个虚拟机装有所需的所有开发工具。它提供了一个持久的 5 GB 主目录,并在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。您在此 Codelab 中的大部分(即使不是全部)工作都可以通过浏览器完成。

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设为您的项目 ID。

  1. 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list

命令输出

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目:
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果不是上述结果,您可以使用以下命令进行设置:

gcloud config set project <PROJECT_ID>

命令输出

Updated property [core/project].

3. 准备开发环境

在此 Codelab 中,您将使用 Cloud Shell 终端和 Cloud Shell 编辑器来开发 Java 程序。

启用 Vertex AI API

在 Google Cloud 控制台中,确保您的项目名称显示在 Google Cloud 控制台的顶部。如果不是,请点击选择项目以打开项目选择器,然后选择所需的项目。

您可以在 Google Cloud 控制台的 Vertex AI 部分或 Cloud Shell 终端启用 Vertex AI API。

如需通过 Google Cloud 控制台启用,请先前往 Google Cloud 控制台菜单的 Vertex AI 部分:

451976f1c8652341

点击 Vertex AI 信息中心内的启用所有推荐的 API

这将启用多个 API,但对于此 Codelab,最重要的一个是 aiplatform.googleapis.com

或者,您也可以从 Cloud Shell 终端使用以下命令启用此 API:

gcloud services enable aiplatform.googleapis.com

克隆 GitHub 代码库

在 Cloud Shell 终端中,克隆此 Codelab 的代码库:

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

要检查项目是否已准备好运行,您可以尝试运行“Hello World”计划。

确保您位于顶层文件夹:

cd gemini-workshop-for-java-developers/ 

创建 Gradle 封装容器:

gradle wrapper

使用 gradlew 运行:

./gradlew run

您应该会看到以下输出内容:

..
> Task :app:run
Hello World!

打开并设置 Cloud Editor

在 Cloud Shell 中使用 Cloud Code Editor 打开代码:

42908e11b28f4383

在 Cloud Code 编辑器中,选择 File ->,打开 Codelab 源代码文件夹Open Folder 并指向 Codelab 源文件夹(例如/home/username/gemini-workshop-for-java-developers/)。

安装 Java 版 Gradle

如需让 Cloud Code Editor 与 Gradle 正常配合使用,请安装 Gradle for Java 扩展程序。

首先,转到“Java 项目”部分,然后按加号:

84d15639ac61c197

选择 Gradle for Java

34d6c4136a3cc9ff.png

选择 Install Pre-Release 版本:

3b044fb450cccb7.png

安装后,您应该会看到 DisableUninstall 按钮:

46410fe86d777f9c

最后,清理工作区,以应用新设置:

31e27e9bb61d975d

这会要求您重新加载并删除研讨会。继续操作并选择 Reload and delete

d6303bc49e391dc.png

如果您打开其中一个文件(例如 App.java),现在应该会看到编辑器在语法突出显示的情况下正常运行:

fed1b1b5de0dff58.png

现在,您可以针对 Gemini 运行一些示例了!

设置环境变量

选择 Terminal ->,在 Cloud Code 编辑器中打开一个新终端New Terminal。设置运行代码示例所需的两个环境变量:

  • PROJECT_ID - 您的 Google Cloud 项目 ID
  • 位置 - 部署 Gemini 模型的区域

按如下方式导出变量:

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

4. 首次调用 Gemini 模型

现在,项目已正确设置,接下来可以调用 Gemini API 了。

查看 app/src/main/java/gemini/workshop 目录中的 QA.java

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?"));
    }
}

在第一个示例中,您需要导入实现 ChatModel 接口的 VertexAiGeminiChatModel 类。

main 方法中,您可以使用 VertexAiGeminiChatModel 构建器配置聊天语言模型,并指定以下内容:

  • 项目
  • 位置
  • 模型名称 (gemini-1.5-flash-001)。

现在,语言模型已准备就绪,您可以调用 generate() 方法并将提示、问题或指令传递给 LLM。在这里,您要问一个简单的问题,了解天空是蓝色的因素。

您可以随时更改此提示,以尝试不同的问题或任务。

在源代码根文件夹中运行该示例:

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

您应该会看到类似于以下内容的输出:

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

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

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

恭喜,你向 Gemini 发起了首次通话!

流式响应

您是否注意到,系统是在几秒钟后一次性给出回应?此外,由于流式响应变体,您还能逐步获取响应。对于流式响应,模型会在可用时逐条返回响应。

在此 Codelab 中,我们将继续使用非流式响应,但让我们看一下流式响应,了解如何实现。

app/src/main/java/gemini/workshop 目录的 StreamQA.java 中,您可以看到流式传输响应的实际效果:

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();
            }
        });
    }
}

这一次,我们导入实现 StreamingChatLanguageModel 接口的流式传输类变体 VertexAiGeminiStreamingChatModel。此外,您还需要一个StreamingResponseHandler

这一次,generate() 方法的签名略有不同。返回值类型是 void,而不是返回字符串。除提示外,您还必须传递流式响应处理程序。在这里,您将通过创建具有 onNext(String text)onError(Throwable error) 两个方法的匿名内部类来实现该接口。前者会在每次有新的响应时调用,而后者仅在发生错误时调用。

运行以下命令:

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

您将得到与上一节课类似的答案,但这一次,您会注意到答案会逐渐显示在 shell 中,而不是等待显示完整答案。

额外配置

对于配置,我们仅定义了项目、位置和模型名称,但您可以为模型指定其他参数:

  • temperature(Float temp) - 定义您希望响应的广告素材(0 表示广告素材不足,通常更符合实际情况,而 1 表示更多广告素材输出)
  • topP(Float topP) - 用于选择总概率相加等于该浮点数(介于 0 和 1 之间)的可能字词
  • topK(Integer topK) - 从最多可能的字词中随机选择一个字词来完成文本补全(从 1 到 40)
  • maxOutputTokens(Integer max) - 用于指定模型提供的答案的最大长度(通常,4 个词元表示大约 3 个字词)
  • maxRetries(Integer retries) - 如果您超出每次请求配额,或者平台遇到一些技术问题,您可以让模型重试调用 3 次

到目前为止,你向 Gemini 提出了一个问题,但也可以进行多轮对话。这就是您将在下一部分中探索的内容。

5. 与 Gemini 对话

在上一步中,您问了一个问题。现在是时候让用户与 LLM 进行一次真正的对话了。每个问题和答案都可以在前面的问题和答案的基础上形成真正的讨论。

查看 app/src/main/java/gemini/workshop 文件夹中的 Conversation.java

package gemini.workshop;

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

import java.util.List;

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

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

        interface ConversationService {
            String chat(String message);
        }

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

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

此类中新增了几个有趣的导入:

  • MessageWindowChatMemory - 一个类,有助于处理对话的多轮对话,并将之前的问题和回答保存在本地内存中
  • AiServices - 用于将聊天模型和聊天内存联系在一起的类

在主方法中,您将设置模型、聊天记忆和 AI 服务。模型照常配置了项目、位置和模型名称信息。

对于聊天记忆,我们使用 MessageWindowChatMemory 的构建器创建用于保存最近 20 条消息的记忆。它是对话之上的滑动窗口,其上下文保存在本地 Java 类客户端中。

然后,您将创建 AI service,用于将聊天模型与聊天内存绑定。

请注意 AI 服务如何使用我们定义的自定义 ConversationService 接口,该接口由 LangChain4j 实现,该接口接受 String 查询并返回 String 响应。

现在,我们来看一下 Gemini 的对话。首先,用户会发送简单的问候,接着发送第一个关于埃菲尔铁塔的问题,以了解埃菲尔铁塔位于哪个国家/地区。请注意,最后一句与第一个问题的答案相关,因为您想知道埃菲尔铁塔所在的国家/地区有多少居民,却没有明确提及上一个答案中提到的国家/地区。它显示,过去的问题和答案随每个提示一起发送。

运行该示例:

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

您应该会看到三个类似于下面所示的答案:

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

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

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

你可以使用 Gemini 提出单轮问题或进行多轮对话,但到目前为止只能输入文字。那图片呢?让我们在下一步中探索图片。

6. Gemini 的多模态

Gemini 是一个多模态模型。它不仅接受文本作为输入,还能接受图片甚至视频作为输入。在本部分中,您将了解混合使用文字和图片的用例。

你觉得 Gemini 能识别这只猫吗?

af00516493ec9ade.png

维基百科上拍摄的雪中猫的图片https://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg

查看 app/src/main/java/gemini/workshop 目录中的 Multimodal.java

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 可以同时包含 TextContentImageContent 对象。这就是多模态在发挥作用:将文字和图片混合在一起。模型发回一个包含 AiMessageResponse

然后,您通过 content() 从响应中检索 AiMessage,然后借助 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。

查看 app/src/main/java/gemini/workshop 中的 ExtractData.java

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(仅为 1)以确保答案非常确定。这也有助于模型更好地按照说明操作。具体而言,我们不希望 Gemini 在 JSON 响应中添加额外的 Markdown 标记。
  • 得益于 LangChain4j 的 AiServices 类,系统创建了一个 PersonExtractor 对象。
  • 然后,只需调用 Person person = extractor.extractPerson(...) 即可从非结构化文本中提取人物的详细信息,并获取包含姓名和年龄的 Person 实例。

运行该示例:

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

您应该会看到以下输出内容:

Anna
23

对,我是 Anna,他们今年 23 岁!

使用此 AiServices 方法,您可以使用强类型对象。您不会直接与 LLM 互动。而是使用具体的类(例如用于表示所提取个人信息的 Person 记录),并得到一个包含 extractPerson() 方法的 PersonExtractor 对象,该方法会返回 Person 实例。LLM 的概念被抽象化了,作为 Java 开发者,您只需操控普通类和对象。

8. 使用提示模板设计提示结构

当您使用一组常见的指令或问题与 LLM 互动时,提示的部分内容永远不会发生变化,而其他部分则会包含数据。例如,如果您要创建食谱,可以使用“您是一名才华横溢的厨师,请使用以下食材创建一个食谱:...”,然后将所需食材附加到该文本的末尾。这就是提示模板的用途,类似于编程语言中的插值字符串。提示模板包含占位符,您可以将这些占位符替换为特定 LLM 调用的正确数据。

更具体地说,我们来研究一下 app/src/main/java/gemini/workshop 目录中的 TemplatePrompt.java

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 值。然后,通过传递提示的字符串来使用其 from() 静态方法创建 PromptTemplate,并使用双大括号占位符变量:{{dish}}{{ingredients}}

您可以通过调用 apply() 来创建最终提示,该方法接受键值对的映射,表示占位符的名称和要用来替换占位符的字符串值。

最后,调用 Gemini 模型的 generate() 方法,使用 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.

您可以随意更改地图中 dishingredients 的值,调整温度、topKtokP,然后重新运行代码。这样,你就可以观察更改这些参数对 LLM 的影响。

提示模板是为 LLM 调用提供可重复使用且可参数化的指令的好方法。您可以传递数据,并针对用户提供的不同值自定义提示。

9. 使用少样本提示进行文本分类

LLM 非常擅长将文本分为不同类别。你可以提供一些文本示例及其相关类别,帮助 LLM 完成这项任务。这种方法通常称为少样本提示。

请查看 app/src/main/java/gemini/workshop 目录中的 TextClassification.java,以执行特定类型的文本分类:情感分析。

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 对话模型,但输出词元数量越小,输出词元数量越小,因为您只需要简短的回答:文本是 POSITIVENEGATIVENEUTRAL

然后,您可以使用少样本提示技术创建一个可重复使用的提示模板,指示模型获取一些输入和输出样本。这也有助于模型跟踪实际输出。Gemini 不会给出一个完整的句子,而是只会用一个字词来回答。

您可以使用 apply() 方法应用变量,将 {{text}} 占位符替换为实际参数 ("I love strawberries"),并使用 toUserMessage() 将模板转换为用户消息。

运行该示例:

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

您应该会看到单个字词:

POSITIVE

看来喜欢草莓是一种积极的情绪!

10. 检索增强生成

LLM 使用大量文本进行训练。但是,他们的知识仅涵盖在训练期间看到的信息。如果在模型训练截止日期之后发布了新信息,模型将无法获得这些详细信息。因此,模型无法回答关于其未发现的信息的问题。

正因如此,诸如检索增强生成 (RAG) 之类的方法有助于提供 LLM 可能需要知道的额外信息,以便满足用户的要求,以便提供更新的信息或训练时无法获取的私密信息。

我们再回到对话。届时,您可以询问与您的文档相关的问题。您将构建一个聊天机器人,它能够从数据库中检索相关信息,该数据库包含将文档拆分为多个小部分(“数据块”)的文档,而模型将根据这些信息来为其答案提供依据,而不是仅依赖于其训练中包含的知识。

在 RAG 中,有两个阶段:

  1. 提取阶段 - 将文档加载到内存中,将其拆分为较小的区块,然后计算向量嵌入(区块的高多维向量表示),并将其存储在能够执行语义搜索的向量数据库中。此提取阶段通常在需要将新文档添加到文档语料库时执行一次。

cd07d33d20ffa1c8.png

  1. 查询阶段 - 用户现在可以询问有关文档的问题。该问题也将转换为一个向量,并与数据库中的所有其他向量进行比较。最相似的向量通常在语义上相关,并由向量数据库返回。然后,向 LLM 提供对话上下文,即与数据库返回的向量对应的文本块,并要求 LLM 查看这些文本块,以给出回答。

a1d2e2deb83c6d27.png

准备文件

在这个新的演示中,您需要提出有关“只需注意”研究论文。其中介绍了由 Google 开创的 Transformer 神经网络架构,如今所有现代大语言模型都是通过该架构实现的。

论文已下载到资源库的 attention-is-all-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 个字符(以下文本块是为了避免将单词或句子连成句段剪切下来)。

存储提取器关联文档拆分器、用于计算向量的嵌入模型和内存中向量数据库。然后,ingest() 方法将负责执行提取操作。

现在,第一阶段结束,文档已通过关联的向量嵌入转换为文本块,并存储在向量数据库中。

提问

准备提问吧!创建聊天模型以开始对话:

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

您还需要一个检索器类,将矢量数据库(在 embeddingStore 变量中)与嵌入模型相关联。它的任务是通过计算用户查询的矢量嵌入来查询矢量数据库,以便在数据库中查找类似的矢量:

EmbeddingStoreContentRetriever retriever =
    new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);

在主方法之外,创建一个代表 LLM 专家助理的接口,该接口是 AiServices 类为方便您与模型进行交互而实现的接口:

interface LlmExpert {
    String ask(String question);
}

此时,您可以配置新的 AI 服务:

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

此服务绑定在一起:

  • 您之前配置的聊天语言模型。
  • 用于跟踪对话的聊天记忆
  • 检索器会将矢量嵌入查询与数据库中的矢量进行比较。
  • 提示模板明确指出聊天模型应根据提供的信息(即向量嵌入与用户问题向量相似的文档摘录的相关摘录)作出回答。
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
    .contentInjector(DefaultContentInjector.builder()
        .promptTemplate(PromptTemplate.from("""
            You are an expert in large language models,\s
            you excel at explaining simply and clearly questions about LLMs.

            Here is the question: {{userMessage}}

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

您终于准备好提问了!

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

完整的源代码位于 app/src/main/java/gemini/workshop 目录下的 RAG.java 中:

运行该示例:

./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 能够访问外部系统,例如检索信息或执行操作的远程 Web API,或执行某种计算的服务。例如:

远程 Web API:

  • 跟踪和更新客户订单。
  • 在问题跟踪器中查找或创建工单。
  • 提取实时数据,例如股票报价或 IoT 传感器测量结果。
  • 发送电子邮件。

计算工具:

  • 用于更高级数学问题的计算器。
  • 当 LLM 需要推理逻辑时,用于运行代码的代码解释。
  • 将自然语言请求转换为 SQL 查询,以便 LLM 可以查询数据库。

函数调用是指模型可以请求以其的名义进行一个或多个函数调用,以便使用更多新数据正确回答用户的提示。

给定用户给出的特定提示,并了解与该上下文相关的现有函数,LLM 可以用函数调用请求进行回复。然后,集成了 LLM 的应用可以调用该函数,接着用回复来回应 LLM,然后 LLM 通过文本回答来做出解释。

函数调用的四个步骤

我们来看一个函数调用示例:获取天气预报的相关信息。

如果你向 Gemini 或任何其他 LLM 询问巴黎的天气情况,对方会回答说它不掌握天气预报方面的信息。如果您希望 LLM 能够实时访问天气数据,则需要定义它可以使用的一些函数。

请看下图:

31e0c2aba5e6f21c

1️▶ 首先,用户询问巴黎的天气情况。聊天机器人应用知道有一项或多项功能可供 LLM 执行查询。聊天机器人既会发送初始提示,也会发送可调用的函数列表。在这里,我们有一个名为 getWeather() 的函数,该函数接受表示营业地点的字符串参数。

8863be53a73c4a70

由于 LLM 不了解天气预报,因此它会发回函数执行请求,而不是通过文本进行回复。聊天机器人必须使用 "Paris" 作为位置参数来调用 getWeather() 函数。

d1367cc69c07b14d.png

2️ plus 聊天机器人代表 LLM 调用该函数,检索函数响应。在这里,我们假设响应为 {"forecast": "sunny"}

73a5f2ed19f47d8

3️ \n 聊天机器人应用将 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 步中,我们传递我们要让模型使用的工具,然后模型用“Today”请求回复:

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

您可以在工具执行请求上方的输出结果以及答案中查看。

完整的源代码位于 app/src/main/java/gemini/workshop 目录下的 FunctionCalling.java 中:

运行该示例:

./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);
}

如果您想处理更高级的情况,还可以使用涉及 LangChain4j 的 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 对话模型。然后实例化包含“function”的天气预报服务模型会要求我们调用的上下文。

现在,您需要再次使用 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;
}

然后,您可以将所有这些函数与 MultiTools 类结合使用,并提出一些问题,例如“AAPL 股票价格的 10% 是多少从美元换算为欧元?”

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

    MultiTools multiTools = new MultiTools();

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

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

运行以下命令:

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

您应该会看到调用的多个函数:

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

面向代理

函数调用是 Gemini 等大语言模型的绝佳扩展机制。它使我们能够构建更复杂的系统,通常称为“代理”简称“AI 助理”这些代理可以通过外部 API 与外部世界交互,以及与可能会对外部环境产生负面影响的服务(例如发送电子邮件、创建工单等)

在打造如此强大的特工时,你应该以负责任的方式着手。在执行自动操作之前,您应该考虑人机协同。在设计与外部世界交互且由 LLM 提供支持的代理时,请务必注意安全。

13. 使用 Ollama 和 TestContainers 运行 Gemma

到目前为止,我们一直在使用 Gemini,但还有它的姊妹模型 Gemma

Gemma 是一系列先进的轻量级开放模型,采用与 Gemini 模型相同的研究和技术构建而成。Gemma 有两种变体 Gemma1 和 Gemma2,每种都有不同的尺寸。Gemma1 有两种大小:2B 和 7B。Gemma2 有两种尺寸:9B 和 27B。它们的重量免费,它们的体积很小,因此您可以自行运行它,甚至在笔记本电脑或 Cloud Shell 中也可以。

如何运行 Gemma?

运行 Gemma 的方法有很多种:在云端运行、通过点击按钮通过 Vertex AI 运行,或者在配有一些 GPU 的 GKE 上运行,不过您也可以在本地运行 Gemma。

在本地运行 Gemma 的一个不错的选择是使用 Ollama,这款工具可以让您在本地机器上运行小模型,如 Llama 2、Mistral 以及许多其他模型。它与 Docker 类似,但适用于 LLM。

按照您所用操作系统的说明安装 Ollama。

如果您使用的是 Linux 环境,则需要先启用 Ollama,然后再安装它。

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

在本地安装后,您可以运行命令来拉取模型:

ollama pull gemma:2b

等待模型拉取完成。此过程可能需要一段时间。

运行模型:

ollama run gemma:2b

现在,您可以与模型进行交互了:

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

如需退出提示,请按 Ctrl+D

在 TestContainers 的 Ollama 中运行 Gemma

您可以在由 TestContainers 处理的容器内使用 Ollama,而不必在本地安装和运行 Ollama。

TestContainers 不仅适用于测试,也可用于执行容器。甚至可以利用特定的 OllamaContainer

完整信息如下:

2382c05a48708dfd.png

实现

我们来逐一了解 GemmaWithOllamaContainer.java

首先,您需要创建一个派生的 Ollama 容器,用于拉取 Gemma 模型。此映像在上一次运行中已存在,或者系统即将创建此映像。如果映像已存在,您只需告知 TestContainers 您想要将默认的 Ollama 映像替换为由 Gemma 提供支持的变体:

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

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

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

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

接下来,您将创建并启动一个 Ollama 测试容器,然后创建一个 Ollama 聊天模型,方法是将鼠标指向该容器(其中包含您想要使用的模型)的地址和端口。最后,只需照常调用 model.generate(yourPrompt) 即可:

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

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

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

    System.out.println(response);
}

运行以下命令:

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

第一次运行需要一段时间来创建并运行容器,但完成后,您应该会看到 Gemma 的响应:

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

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

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

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

您已经在 Cloud Shell 中运行 Gemma!

14. 恭喜

恭喜!您已成功使用 LangChain4j 和 Gemini API 构建了自己的首个 Java 生成式 AI 聊天应用!在此过程中,您发现多模态大语言模型非常强大,能够处理各种任务(例如问答),甚至可处理您自己的文档、数据提取、与外部 API 交互等。

后续操作

现在该您亲自通过强大的 LLM 集成来增强自己的应用了!

深入阅读

参考文档