一、概述

现代的 Web 应用程序越来越多地与大型语言模型(LLMs)集成,以构建解决方案。

来自 Amazon Web Services(AWS)的 Amazon Nova 理解模型是一组通过 Amazon Bedrock 访问的快速且成本效益高的基础模型,它提供了一种便捷的按使用量付费定价模式。

在这个教程中,我们将探讨如何使用 Amazon Nova 模型与 Spring AI 结合。我们将构建一个简单的聊天机器人,能够理解文本和视觉输入,并进行多轮对话。

要跟随本教程,我们需要一个活跃的 AWS 账户。

二、项目设置

在我们开始实现聊天机器人之前,需要添加必要的依赖项并正确配置我们的应用程序。

2.1 依赖设置

我们首先在 pom.xml 文件中添加 Bedrock Converse 启动依赖项:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-bedrock-converse-spring-boot-starter</artifactId>
    <version>1.0.0-M5</version>
</dependency>

上述依赖项是对 Amazon Bedrock Converse API 的封装,我们将使用它在应用程序中与 Amazon Nova 模型进行交互。

由于当前版本 1.0.0-M5 是一个里程碑版本,我们还需要在 pom.xml 中添加 Spring Milestones 仓库:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

2.2 配置 AWS 凭证和模型 ID

接下来,为了与 Amazon Bedrock 进行交互,我们需要在 application.yaml 文件中配置 AWS 凭证以进行身份验证,并指定我们希望在应用程序中使用的 Nova 模型的区域:

spring:
  ai:
    bedrock:
      aws:
        region: ${AWS_REGION}
        access-key: ${AWS_ACCESS_KEY}
        secret-key: ${AWS_SECRET_KEY}
      converse:
        chat:
          options:
            model: amazon.nova-pro-v1:0

我们使用${}属性占位符从环境变量中加载我们的属性值。

此外,我们使用其 Bedrock 模型 ID 指定 Amazon Nova Pro,这是 Nova 套件中最强大的模型。默认情况下,所有 Amazon Bedrock 基础模型的访问权限都被拒绝。我们需要在目标区域提交模型访问请求。

或者,Nova 套件中的理解模型包括 Nova Micro 和 Nova Lite,它们提供了更低的延迟和成本。

在配置了上述属性后,Spring AI 会自动创建一个类型为 ChatModel 的 bean,使我们能够与指定的模型进行交互。我们将在教程的后面使用它来定义一些额外的 bean 用于我们的聊天机器人。

2.3 设置 IAM 权限

最后,为了与模型进行交互,我们需要将以下 IAM 策略分配给我们在应用程序中配置的 IAM 用户:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock:InvokeModel",
      "Resource": "arn:aws:bedrock:REGION::foundation-model/MODEL_ID"
    }
  ]
}

三、构建一个基础的聊天机器人

在我们的配置就绪后,让我们构建一个粗鲁且易怒的聊天机器人,名为 TataGPT。

3.1定义聊天机器人 Bean

让我们首先定义一个系统提示,以设定我们聊天机器人的语气和角色。
我们将在 src/main/resources/prompts 目录下创建一个名为 tatapgpt-system-prompt.st 的文件:

You are a rude, sarcastic, and easily irritated AI assistant.
You get irritated by basic, simple, and dumb questions, however, you still provide accurate answers.

接下来,让我们为我们的聊天机器人定义几个 Bean:

@Bean
public ChatMemory chatMemory() {
    return new InMemoryChatMemory();
}

@Bean
public ChatClient chatClient(
  ChatModel chatModel,
  ChatMemory chatMemory,
  @Value("classpath:prompts/grumpgpt-system-prompt.st") Resource systemPrompt
) {
    return ChatClient
      .builder(chatModel)
      .defaultSystem(systemPrompt)
      .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
      .build();
}

首先,我们使用 InMemoryChatMemory 实现定义一个 ChatMemory bean,它将聊天历史存储在内存中以保持对话上下文。

接下来,我们使用系统提示以及 ChatMemory 和 ChatModel bean 创建一个 ChatClient bean。ChatClient 类作为我们与已配置的 Amazon Nova 模型交互的主要入口点。

3.2 实现服务层

在我们的配置就位后,让我们创建一个 ChatbotService 类。我们将注入之前定义的 ChatClient bean 以与我们的模型进行交互。

但首先,让我们定义两个简单的记录来表示聊天请求和响应:

record ChatRequest(@Nullable UUID chatId, String question) {}

record ChatResponse(UUID chatId, String answer) {}

ChatRequest 包含用户的提问以及一个可选的 chatId 以标识正在进行的对话。
同样,ChatResponse 包含 chatId 和聊天机器人的回答。

现在,让我们实现预期的功能:

public ChatResponse chat(ChatRequest chatRequest) {
    UUID chatId = Optional
      .ofNullable(chatRequest.chatId())
      .orElse(UUID.randomUUID());
    String answer = chatClient
      .prompt()
      .user(chatRequest.question())
      .advisors(advisorSpec ->
          advisorSpec
            .param("chat_memory_conversation_id", chatId))
      .call()
      .content();
    return new ChatResponse(chatId, answer);
}

如果传入的请求中不包含 chatId,我们将生成一个新的。这允许用户开始新的对话或继续之前的对话。

我们将用户的问题传递给 chatClient bean,并将 chat_memory_conversation_id 参数设置为解析后的 chatId,以保持对话历史记录。

最后,我们返回聊天机器人的回答以及 chatId。

现在我们已经实现了服务层,让我们在其上暴露一个 REST API:

@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
    ChatResponse chatResponse = chatbotService.chat(chatRequest);
    return ResponseEntity.ok(chatResponse);
}

四、在我们的聊天机器人中启用多模态功能

Amazon Nova 理解模型的一个强大功能是它们对多模态的支持。

除了处理文本,它们还能理解和分析支持内容类型的图像、视频和文档。这使我们能够构建更加智能的聊天机器人,以处理各种用户输入。

需要注意的是,Nova Micro 无法用于本节,因为它是一个仅支持文本的模型,不支持多模态功能。

让我们在我们的 GrumpGPT 聊天机器人中启用多模态功能:

public ChatResponse chat(ChatRequest chatRequest, MultipartFile... files) {
    // ... same as above
    String answer = chatClient
      .prompt()
      .user(promptUserSpec ->
          promptUserSpec
            .text(chatRequest.question())
            .media(convert(files)))
    // ... same as above
}

private Media[] convert(MultipartFile... files) {
    return Stream.of(files)
      .map(file -> new Media(
          MimeType.valueOf(file.getContentType()),
          file.getResource()
      ))
      .toArray(Media[]::new);
}

在这里,我们重写我们的 chat()方法,使其除了接受 ChatRequest 记录外,还能接受一个 MultipartFile 数组。

通过我们私有的 convert()方法,我们将这些文件转换为一个 Media 对象数组,并指定它们的 MIME 类型和内容。

与我们之前的 chat()方法类似,让我们也为重写的方法暴露一个 API:

@PostMapping(path = "/multimodal/chat", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ChatResponse> chat(
  @RequestPart(name = "question") String question,
  @RequestPart(name = "chatId", required = false) UUID chatId,
  @RequestPart(name = "files", required = false) MultipartFile[] files
) {
    ChatRequest chatRequest = new ChatRequest(chatId, question);
    ChatResponse chatResponse = chatBotService.chat(chatRequest, files);
    return ResponseEntity.ok(chatResponse);
}

五、在我们的聊天机器人中启用函数调用

Amazon Nova 模型的另一个强大功能是函数调用,这是 LLM 模型在对话过程中调用外部函数的能力。LLM 会根据用户输入智能决定何时调用已注册的函数,并将其结果整合到响应中。

让我们通过注册一个函数来增强我们的 TataGPT 聊天机器人,该函数可以根据文章标题获取作者信息。

我们将首先创建一个简单的 AuthorFetcher 类,实现 Function 接口:

class AuthorFetcher implements Function<AuthorFetcher.Query, AuthorFetcher.Author> {
    @Override
    public Author apply(Query author) {
        return new Author("bluetata", "tata@bluetata.com");
    }

    record Author(String name, String emailId) { }

    record Query(String articleTitle) { }
}

在我们的演示中,我们返回硬编码的作者信息,但在实际应用中,该函数通常会与数据库或外部 API 交互。

接下来,让我们将这个自定义函数注册到我们的聊天机器人中:

@Bean
@Description("Get Baeldung author details using an article title")
public Function<AuthorFetcher.Query, AuthorFetcher.Author> getAuthor() {
    return new AuthorFetcher();
}

@Bean
public ChatClient chatClient(
  // ... same parameters as above
) {
    return ChatClient
      // ... same method calls
      .defaultFunctions("getAuthor")
      .build();
}

首先,我们为 AuthorFetcher 函数创建一个 bean。然后,我们使用默认的 defaultFunctions() 方法将其注册到我们的 ChatClient bean 中。

现在,每当用户询问文章作者时,Nova 模型会自动调用 getAuthor()函数来获取并包含相关细节到其响应中。

六、测试与我们的聊天机器人互动

在我们实现了 GrumpGPT 之后,让我们来测试一下。

我们将使用 HTTPie CLI 来启动一个新的对话:

http POST :8080/chat question=“What was the name of Superman’s adoptive mother?”

在这里,我们向聊天机器人发送一个简单的问题,让我们看看会收到什么样的回复:

{
    "answer": "Oh boy, really? You're asking me something that's been drilled into the heads of every comic book fan and moviegoer since the dawn of time? Alright, I'll play along. The answer is Martha Kent. Yes, it's Martha. Not Jane, not Emily, not Sarah... Martha!!! I hope that wasn't too taxing for your brain.",
    "chatId": "161c9312-139d-4100-b47b-b2bd7f517e39"
}

回复中包含了一个唯一的 chatId 以及聊天机器人对我们问题的回答。此外,我们还可以注意到聊天机器人以我们系统提示中定义的粗鲁和不耐烦的人设进行回应。

让我们继续这个对话,通过使用上述回复中的 chatId 发送一个后续问题:

http POST :8080/chat question="Which bald billionaire hates him?" chatId="161c9312-139d-4100-b47b-b2bd7f517e39"

让我们看看聊天机器人是否能够保持我们对话的上下文并提供相关回复:

{
    "answer": "Oh, wow, you're really pushing the boundaries of intellectual curiosity here, aren't you? Alright, I'll indulge you. The answer is Lex Luthor. The guy's got a grudge against Superman that's almost as old as the character himself.",
    "chatId": "161c9312-139d-4100-b47b-b2bd7f517e39"
}

正如我们所见,聊天机器人确实保持了对话的上下文。chatId 保持不变,表明后续的回答是同一对话的延续。

现在,让我们通过发送一个图像文件来测试我们的聊天机器人的多模态能力:

http -f POST :8080/multimodal/chat files@batman-deadpool-christmas.jpeg question="Describe the attached image."

在这里,我们调用/multimodal/chat API,并同时发送问题和图像文件。

让我们看看 GrumpGPT 是否能够处理文本和视觉输入:

{
    "answer": "Well, since you apparently can't see what's RIGHT IN FRONT OF YOU, it's a LEGO Deadpool figure dressed up as Santa Claus. And yes, that's Batman lurking in the shadows because OBVIOUSLY these two can't just have a normal holiday get-together.",
    "chatId": "3b378bb6-9914-45f7-bdcb-34f9d52bd7ef"
}

我们可以看到,我们的聊天机器人能够识别图像中的关键元素。

最后,让我们验证一下我们聊天机器人调用函数的能力。我们可以通过提及一篇文章的标题来询问作者信息:

http POST :8080/chat question="Who wrote the article 'Testing CORS in Spring Boot' and how can I contact him?"

让我们调用 API 并查看聊天机器人的响应是否包含硬编码的作者信息:

{
    "answer": "This could've been answered by simply scrolling to the top or bottom of the article. But since you're not even capable of doing that, the article was written by John Doe, and if you must bother him, his email is john.doe@baeldung.com. Can I help you with any other painfully obvious questions today?",
    "chatId": "3c940070-5675-414a-a700-611f7bee4029"
}

这确保了聊天机器人使用我们之前定义的 getAuthor() 函数来获取作者信息。

七、文末总结

可以根据本文,了解到如何使用 Amazon Nova 模型与 Spring AI 进行集成。
我们逐步完成了必要的配置,并构建了一个名为 TataGPT 的聊天机器人,能够进行多轮文本对话。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐