Java 21、Spring AI 和 OpenAI 的综合指南

[

Mina

](https://.com/@minadev?source=post_page—–92e25066df86——————————–)

掌握大型语言模型的通信:使用 Spring AI 克服挑战

与大型语言模型 (LLMs) 通信会带来一些挑战,包括解析响应、集成我们自己的数据、增强我们正在发送的上下文 (RAG) 等等。

Spring AI 在 Spring Framework 中提供 API,并允许您创建复杂性较低的基于 Java 的 AI 应用程序。它通过提供工具和框架来促进与 LLMs的无缝通信,从而解决了其中的一些问题。

Spring AI 支持多种 AI 模型,包括 OpenAI、Azure OpenAI、Amazon Bedrock(Anthropic、Llama、Cohere、Titan、Jurassic2)、HuggingFace 和 Google VertexAI(PaLM2、Gemini)等。

在这个项目中,我将使用 OpenAI GPT(聊天生成预训练转换器)模型。首先,您需要注册一个帐户并生成 API 密钥。请记住,这不是免费的;它是有成本的,成本取决于模型的功能。
您可以在 OpenAI 网站上查看每种型号的定价详细信息。定价是基于代币的,但代币到底是什么?

 令 牌

LLMs(大型语言模型)使用标记处理文本,标记是单词片段,不受单词边界的严格约束,并且可以包含子单词和空格。每个标记代表大约 4 个字符或单词的 3/4。
例如,“学习很有趣”这句话由 4 个标记和 16 个字符组成。代币是 的LLMs货币,对其使用有限制,并且定价因型号而异。

您可以使用 OpenAPI tokenizer 来查看其计数方式:

https://platform.openai.com/tokenizer

需要注意的是,确切的标记化过程因模型而异。GPT-3.5 和 GPT-4 等较新的模型使用与以前的模型不同的标记器,并且将为相同的输入文本生成不同的标记。

现在,我将创建一个 Spring Boot 应用程序,以展示如何使用 Spring AI 与 LLMs进行交互。这包括分析输出、创建提示以及提供全面的分步说明。

 项目设置

首先使用 Spring Initializr 创建一个新的 Spring Boot 项目,并选择以下依赖项。将项目导入到首选的 IDE 中。

  • 用于构建 Restful API 的 Spring Web
  • Spring AI 对 ChatGPT 的支持

要与 OpenAI 通信,我们必须首先在 application.properties 文件中定义配置属性。将您之前生成的 API 密钥粘贴到此文件中。

<span id="ced5" data-selectable-paragraph="">spring.application.name=chat-model-demo  
spring.ai.openai.api-key=${OPENAI_API_KEY} // define it as an environment variable  
spring.ai.ollama.chat.model=gpt-3.5-turboprop // the name of the chat model that you are using

接下来,我们需要创建一个 REST 控制器类来公开 API。当调用此 API 时,它将使用我们的问题(提示符)调用 OpenAI。

为了与 OpenAI 模型进行交互,我们需要注入 ChatClient 接口,该接口包含两种方法:

<span id="5ec0" data-selectable-paragraph="">@FunctionalInterface  
public interface ChatClient extends ModelClient&lt;Prompt, ChatResponse&gt; {  
  
 default String call(String message) {  
  Prompt prompt = new Prompt(new UserMessage(message));  
  Generation generation = call(prompt).getResult();  
  return (generation != null) ? generation.getOutput().getContent() : "";  
 }  
 @Override  
 ChatResponse call(Prompt prompt);

第一种方法接受消息并返回 String,而第二种方法接受 Prompt 并返回 ChatResponse。

现在,让我们公开一个 API 来讲述一个故事:

<span id="b0f5" data-selectable-paragraph="">package dev.mina.chatmodeldemo;  
  
import org.springframework.ai.chat.ChatClient;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
public class StoryController {  
  
  private final ChatClient chatClient;  
  
  public StoryController(ChatClient chatClient) {  
    this.chatClient = chatClient;  
  }  
  
  @GetMapping("/story")  
  public String generateStory(@RequestParam(value = "message", defaultValue = "Tell me a short funny story") String message) {  
    return chatClient.call(message);  
  }  
}

现在,如果我们运行应用程序并调用 api ,使用 HTTPie,一个功能强大且易于使用的命令行 HTTP 客户端,就像这个http :8080/story 一样,它将返回一个字符串响应,其中包含一个有趣的故事。

让我们来探讨一下 ChatClient 接口的第二种方法,创建并传递提示,但什么是提示:提示是我们发送的输入或问题LLM。

如果我们看一下 Prompt 类,它有不同的构造函数,它会创建一个新的 UserMessage,它是 AbstractMessage 的实现。

以下是 AbstractMessage 的实现:

  • AssistantMessage.java
  • ChatMessage.java
  • FunctionMessage.java
  • SystemMessage.java
  • UserMessage.java

在创建提示时,我们还需要了解不同的角色:

 系统角色:

通过在对话开始之前设置规则来指导 AI 的行为和响应方式。这就像向AI发出指令一样。

 用户角色:

表示用户的输入,构成 AI 响应的基础。

 助理角色:

AI 对用户输入的响应,对于通过跟踪 AI 之前的回应(其“助手角色”消息)来维持对话流程至关重要。

 功能角色:

在对话期间执行特定的任务或操作,而不仅仅是执行计算或获取数据等谈话。

在此处找到有关提示的完整详细信息:https://docs.spring.io/spring-ai/reference/api/prompt.html

现在,让我们创建一个 API,演示如何使用提示。

公开 API 以按类型查找阅读次数最多的故事:

<span id="175d" data-selectable-paragraph="">@GetMapping("/stories/must-read")  
  public String getMustReadStories(@RequestParam(value = "genre", defaultValue = "bedtime") String genre) {  
    String message = """  
        Give me the top 10 must-read {genre} stories.  
        """;  
    PromptTemplate promptTemplate = new PromptTemplate(genre);  
    Prompt prompt = promptTemplate.create(Map.of("genre", genre));  
    return chatClient.call(prompt).getResult().getOutput().getContent();  
  }

运行应用程序并进行 API 调用:http :8080/most-read

它将返回包含 10 个阅读次数最多的冒险故事的响应字符串。

<span id="b928" data-selectable-paragraph="">1. "Goodnight Moon" by Margaret Wise Brown  
2. "Where the Wild Things Are" by Maurice Sendak  
3. "The Tale of Peter Rabbit" by Beatrix Potter  
4. "Winnie-the-Pooh" by A.A. Milne  
5. "The Very Hungry Caterpillar" by Eric Carle  
6. "Guess How Much I Love You" by Sam McBratney  
7. "Corduroy" by Don Freeman  
8. "Love You Forever" by Robert Munsch  
9. "Bedtime for Frances" by Russell Hoban  
10. "Harold and the Purple Crayon" by Crockett Johnson

需要注意的一点是,我们需要避免对消息进行硬编码。相反,我们可以在 resources 文件夹下创建提示模板。只需在 resources 下创建一个名为 “prompts” 的文件夹,并将所有字符串模板放在那里。例如:/resources/prompts/story.st。在里面,放字符串模板。给我十大必读的{genre}故事。

<span id="cdf5" data-selectable-paragraph="">@RestController  
public class StoryController {  
  
  private final ChatClient chatClient;  
  @Value("classpath:/prompts/story.st")  
  private Resource storyPromptResource;  
  
  public StoryController(ChatClient chatClient) {  
    this.chatClient = chatClient;  
  }  
  
  @GetMapping("/story")  
  public String generateStory(@RequestParam(value = "message", defaultValue = "Tell me a short funny story") String message) {  
    return chatClient.call(message);  
  }  
  
  @GetMapping("/stories/most-read")  
  public String getMostReadStories(@RequestParam(value = "genre", defaultValue = "bedtime") String genre) {  
    PromptTemplate promptTemplate = new PromptTemplate(storyPromptResource);  
    Prompt prompt = promptTemplate.create(Map.of("genre", genre));  
    return chatClient.call(prompt).getResult().getOutput().getContent();  
  }  
}

 输出解析

如您所见,响应LLM始终是 String,但是如果我们想要其他东西,例如字符串列表怎么办?我们如何处理从LLMs我们系统收到的响应并将它们解析为对象。

Spring AI 提供了 OutputParser,它目前带有三种实现:

<span id="79b8" data-selectable-paragraph="">Certainly! You can present this information in a table format like this:  
  
| OutputParser      | Description                                                                                                                                                     |  
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|  
| BeanOutputParser  | Converts the LLM output to a specific Java bean instance. |  
| ListOutputParser  | Converts the LLM output into a List instance.  
| MapOutputParser   | Converts the LLM output into a java.util.Map&lt;String, Object&gt; instance.

ListOutputParser

它指定提示应以列表格式请求LLM输出,其中值用逗号分隔。后来,Spring AI 使用 Jackson 将这些逗号分隔的值解析为一个 List。

现在让我们探索一下 ListOutputParser,公开一个 API 以返回科学类型中 10 个最受欢迎的故事的列表。

<span id="d038" data-selectable-paragraph="">@GetMapping("/stories/popular")  
public List&lt;String&gt; getPopularStories(@RequestParam(value = "genre", defaultValue = "Science Fiction") String genre) {  
  var message = """  
        Give me a list of 10 most popular stories for the genre: {genre}. If you don't know the answer, just say "I don't know" {format}  
        """;  
  ListOutputParser outputParser = new ListOutputParser(new DefaultConversionService());  
  
  PromptTemplate promptTemplate = new PromptTemplate(message, Map.of("genre", genre, "format", outputParser.getFormat()));  
  Prompt prompt = promptTemplate.create();  
  
  ChatResponse response = chatClient.call(prompt);  
  return outputParser.parse(response.getResult().getOutput().getContent());  
}

然后,向 http :8080/stories/popular 发出请求。响应将如下所示:

<span id="dc87" data-selectable-paragraph="">[  
  "Dune",  
  "The Hitchhiker's Guide to the Galaxy",  
  "Neuromancer",  
  "Foundation",  
  "1984",  
  "Brave New World",  
  "Snow Crash",  
  "Ender's Game",  
  "The War of the Worlds",  
  "The Martian"MapOutputParser"  
]

MapOutputParser

它使用预配置的 MappingJackson2MessageConverter 将LLM输出转换为 java.util.Map 实例。

公开一个 API 以返回作者的故事列表,包括其类型。

<span id="575e" data-selectable-paragraph="">@GetMapping("/stories/author")  
public Map&lt;String, Object&gt; getAuthorsStories(@RequestParam(value = "author", defaultValue = "Jill McDonald") String author) {  
  var message = """  
        Give me a list of stories for the author {author}, including their genres. If you don't know the answer, just say "I don't know" {format}  
        """;  
  MapOutputParser outputParser = new MapOutputParser();  
  
  PromptTemplate promptTemplate = new PromptTemplate(message, Map.of("author", author, "format", outputParser.getFormat()));  
  Prompt prompt = promptTemplate.create();  
  
  ChatResponse response = chatClient.call(prompt);  
  return outputParser.parse(response.getResult().getOutput().getContent());  
}

调用 http :8080/stories/author 将返回作者到她的故事的地图。

BeanOutputParser

BeanOutputParser 有助于解析LLM输出并将其转换为所需的类型。

公开一个 API,以返回作者的随机故事。

<span id="562f" data-selectable-paragraph="">public record Story(String author, String title, String genre) {  
  
}  
  
@GetMapping("/stories/random-by-author")  
public Story getRandomStoryByAuthor(@RequestParam(value = "author", defaultValue = "J.K Rowling") String author) {  
  var message = """  
        Give me a story for the author {author}. If you don't know the answer, just say "I don't know" {format}  
        """;  
  var outputParser = new BeanOutputParser&lt;&gt;(Story.class);  
  
  PromptTemplate promptTemplate = new PromptTemplate(message, Map.of("author", author, "format", outputParser.getFormat()));  
  Prompt prompt = promptTemplate.create();  
  
  ChatResponse response = chatClient.call(prompt);  
  return outputParser.parse(response.getResult().getOutput().getContent());  
}

调用 http :8080/stories/random-by-author 将返回一个包含作者、标题和流派的故事对象。