生成式人工智能(GenAI)是当今科技界的一个热门话题。它是人工智能的一个子集,专注于创造新的内容,例如文本、图像或音乐。GenAI组件的一种流行类型是大型语言模型(LLM),它可以根据提示生成类似人类的文本。检索增强生成(RAG)是一种基于外部知识来源来提高生成式人工智能模型准确性和可靠性的技术。虽然大多数GenAI应用程序和相关内容都以Python及其生态系统为中心,但如果采用Java编写GenAI应用程序应该怎么办?

本文将介绍如何使用Spring AI框架采用Java编写GenAI应用程序,并利用RAG改进答案。

什么是Spring AI?

Spring AI是一个用于在Java中构建GenAI应用程序的框架。它提供了一组工具和实用程序,用于处理GenAI模型和架构,例如LLM和RAG。Spring AI构建在Spring框架之上,Spring框架是一个用于构建企业应用程序的流行Java框架,允许那些已经熟悉或参与Spring生态系统的用户将GenAI策略合并到他们已经存在的应用程序和工作流中。

在Java中也有其他GenAI的选择,例如Langchain4j,但是在这篇文章中将关注Spring AI。

创建项目

在开始使用Spring AI时,需要创建一个新项目或向现有项目添加适当的依赖项。可以使用Spring Initializr创建一个新项目,这是一个用于生成Spring Boot项目的基于Web的工具。

当创建一个新项目时,需要添加以下依赖项:

  • Spring Web
  • OpenAI或其他LLM模型(例如Mistral、Ollama等)
  • Neo4j Vector Database (其他矢量数据库选项也可用)
  • Spring Data Neo4j

如果人工将这些依赖项添加到现有项目中,可以在相关的GitHub存储库中看到依赖项的详细信息。

Spring Web依赖项允许开发人员为GenAI应用程序创建REST API。需要OpenAI依赖来访问OpenAI模型,这是一个流行的LLM。Neo4j Vector Database依赖项允许存储和查询用于相似性搜索的向量。最后,添加Spring Data Neo4j依赖项为在Spring应用程序中使用Neo4j数据库提供了支持,允许在Neo4j中运行Cypher查询并将实体映射到Java对象。

继续生成项目,然后在喜欢的IDE中打开它。查看pom.xml文件,应该会看到里程碑存储库已包含在内。由于Spring AI还不是一个通用版本,需要包含里程碑存储库来获得依赖项的预发布版本。

一个样板文件

构建Neo4j数据库可以使用Neo4j Aura免费层,因为它管理了实例,当然还有Docker镜像和其他方法。

根据选择的LLM模型,还需要API密钥。对于OpenAI,可以通过在OpenAI注册获得API密钥。

一旦有了Neo4j数据库和API密钥,就可以在应用程序中设置application.properties文件。以下是一个可能的示例:

Properties files 
 spring.ai.openai.api-key=<YOUR API KEY HERE>
 spring.neo4j.uri=<NEO4J URI HERE>
 spring.neo4j.authentication.username=<NEO4J USERNAME HERE>
 spring.neo4j.authentication.password=<NEO4J PASSWORD HERE>
 spring.data.neo4j.database=<NEO4J DATABASE NAME HERE>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

注意:将API密钥和密码等敏感信息保存在环境变量或应用程序外部的其他位置是一个很好的想法。要创建环境变量,可以在终端中使用export命令或在IDE中设置它们。

可以为OpenAI客户端和Neo4j矢量存储设置Spring Beans,这将允许在应用程序中需要的任何地方访问必要的组件。

可以在SpringAiApplication类中添加以下代码:

Java 
 @Bean
 public EmbeddingClient embeddingClient() {
 return new OpenAiEmbeddingClient(new OpenAiApi(System.getenv("SPRING_AI_OPENAI_API_KEY")));
 }

 @Bean
 public Neo4jVectorStore vectorStore(Driver driver, EmbeddingClient embeddingClient) {
 return new Neo4jVectorStore(driver, embeddingClient,
 Neo4jVectorStore.Neo4jVectorStoreConfig.builder()
 .withLabel("Review")
 .withIndexName("review-embedding-index")
 .build());
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

EmbeddingClient bean为OpenAI API创建一个客户端,并传入API密钥环境变量。最后,Neo4jVectorStore bean将Neo4j配置为嵌入(向量)的存储。通过为存储嵌入的节点指定标签来定制配置,因为Spring的默认设置是查找Document实体。还为嵌入指定索引名(默认是spring-ai-document-index)。

数据集

对于这个例子,将使用来自Goodreads的书籍和评论数据集。可以从这里获取数据集的精选版本。该数据集包含有关书籍的信息以及相关评论。

已经使用OpenAI的API生成了嵌入,所以用户如果想生成自己的嵌入,需要在脚本中注释掉最后的Cypher语句,而不是运行generate-embeddings.py脚本(或自定义版本)来生成和加载Neo4j的审查嵌入。

应用程序模型

接下来,需要在应用程序中创建一个域模型,以映射到数据库模型。本例将创建一个表示图书节点的Book实体。还将创建一个Review实体,它表示对某本书的评论。Review实体将有一个与之关联的嵌入(向量),将使用它进行相似性搜索。

这些实体是标准的Spring Data Neo4j代码,所以不会在这里展示代码。但是,每个类的完整代码都可以在GitHub存储库中获得(Book类、Review类)。

此外,还需要定义一个存储库接口,以便与数据库进行交互。虽然需要定义一个自定义查询,但稍后将返回并添加它。

Java 
 public interface BookRepository extends Neo4jRepository<Book, String> {
 }
  • 1.
  • 2.
  • 3.

接下来,这个应用程序的核心是控制器类。这个类将包含获取用户提供的搜索短语并调用Neo4jVectorStore来计算和返回最相似短语的逻辑。然后,可以将这些类似的评论传递到Neo4j查询中,以检索连接的实体,在LLM的提示中提供额外的场景。它将使用提供的所有信息,为原始搜索短语提供一些类似的书籍推荐。

控制器

首先,控制器类包含两个常见的注解。还将注入之前定义的Neo4jVectorStore和BookRepository bean,以及用于嵌入客户端的OpenAiChatClient。

下一步是为提示符定义一个字符串。这是将传递给LLM以生成响应的文本,将使用用户提供的搜索短语和在数据库中找到的类似评论,在几分钟内填充提示参数。接下来,定义控制器类的构造函数,它将注入必要的bean。

Java 
 @RestController
 @RequestMapping("/")
 public class BookController {
 private final OpenAiChatClient client;
 private final Neo4jVectorStore vectorStore;
 private final BookRepository repo;

 String prompt = """
 You are a book expert with high-quality book information in the CONTEXT section.
 Answer with every book title provided in the CONTEXT.
 Do not add extra information from any outside sources.
 If you are unsure about a book, list the book and add that you are unsure.

 CONTEXT{context}

 PHRASE{searchPhrase}
 """;

 public BookController(OpenAiChatClient client, Neo4jVectorStore vectorStore, BookRepository repo) {
 this.client = client;
 this.vectorStore = vectorStore;
 this.repo = repo;
 }

 //Retrieval Augmented Generation with Neo4j - vector search + retrieval query for related context
 @GetMapping("/rag")
 public String generateResponseWithContext(@RequestParam String searchPhrase) {
 List<Document> results = vectorStore.similaritySearch(SearchRequest.query(searchPhrase).withTopK(5).withSimilarityThreshold(0.8));
 //more code shortly!
 }
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

最后,定义了一个方法,当用户向/rag端点发出GET请求时将调用该方法。该方法首先将搜索短语作为查询参数,并将其传递给向量存储的similaritySearch()方法,以查找相似的评论。还通过限制前5个结果(. withtopk(5))并只提取最相似的结果(withSimilarityThreshold(0.8))向查询添加了两个自定义过滤器。

Spring AI的similaritySearch()方法的实际实现如下所示。

Java 
 @Override
 public List<Document> similaritySearch(SearchRequest request) {
 Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
 Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
 "The similarity score is bounded between 0 and 1; least to most similar respectively.");

 var embedding = Values.value(toFloatArray(this.embeddingClient.embed(request.getQuery())));

 try (var session = this.driver.session(this.config.sessionConfig)) {
 StringBuilder condition = new StringBuilder("score >= $threshold");
 
 if (request.hasFilterExpression()) {
 condition.append(" AND ")
 .append(this.filterExpressionConverter.convertExpression(request.getFilterExpression()));
 }

 String query = """
 CALL db.index.vector.queryNodes($indexName, $numberOfNearestNeighbours, $embeddingValue)
 YIELD node, score
 WHERE %s
 RETURN node, score""".formatted(condition);
 
 return session
 .run(query,
 Map.of("indexName", this.config.indexName, "numberOfNearestNeighbours", request.getTopK(),
 "embeddingValue", embedding, "threshold", request.getSimilarityThreshold()))
 .list(Neo4jVectorStore::recordToDocument);
 }
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

然后,将类似的Review节点映射回Document实体,因为Spring AI需要通用的文档类型。Neo4jVectorStore类包含将Document转换为自定义记录的方法,以及将记录反向转换为Document的方法。以下将显示这些方法的实际实现。

Java 
 private Map<String, Object> documentToRecord(Document document) {
 var embedding = this.embeddingClient.embed(document);
 document.setEmbedding(embedding);

 var row = new HashMap<String, Object>();
 row.put("id", document.getId());

 var properties = new HashMap<String, Object>();
 properties.put("text", document.getContent());

 document.getMetadata().forEach((k, v) -> properties.put("metadata." + k, Values.value(v)));
 row.put("properties", properties);
 row.put(this.config.embeddingProperty, Values.value(toFloatArray(embedding)));

 return row;
 }

 private static Document recordToDocument(org.neo4j.driver.Record neoRecord) {
 var node = neoRecord.get("node").asNode();
 var score = neoRecord.get("score").asFloat();
 var metaData = new HashMap<String, Object>();

 metaData.put("distance", 1 - score);

 node.keys().forEach(key -> {
 if (key.startsWith("metadata.")) {
 metaData.put(key.substring(key.indexOf(".") + 1), node.get(key).asObject());
 }
 });

 return new Document(node.get("id").asString(), node.get("text").asString(), Map.copyOf(metaData));
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

回到图书推荐的控制器方法,现在对用户搜索的短语有类似的评论。但是评论(以及附带的文字)并不能真正帮助推荐书籍。所以现在需要在Neo4j中运行一个查询来检索这些评论的相关书籍。这是应用程序的检索增强生成(RAG)部分。

可以在BookRepository接口中编写查询,以查找与这些评论相关的图书。

Java 
 public interface BookRepository extends Neo4jRepository<Book, String> {
 @Query("MATCH (b:Book)<-[rel:WRITTEN_FOR]-(r:Review) " +
 "WHERE r.id IN $reviewIds " +
 "AND r.text <> 'RTC' " +
 "RETURN b, collect(rel), collect(r);")
 List<Book> findBooks(List<String> reviewIds);

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

在查询中,从相似性搜索($ reviews wids)中传入评论的ID,并为这些评论提取Review -> Book模式。还过滤掉任何带有文本“RTC”的评论(这是没有文本的评论的占位符)。然后返回Book节点、关系和Review节点。

现在需要在控制器中调用该方法,并将结果传递给提示模板。将把它传递给LLM,以根据用户的搜索短语生成一个带有图书推荐列表的响应。

Java 
 //Retrieval Augmented Generation with Neo4j - vector search + retrieval query for related context
 @GetMapping("/rag")
 public String generateResponseWithContext(@RequestParam String searchPhrase) {
 List<Document> results = vectorStore.similaritySearch(SearchRequest.query(searchPhrase).withTopK(5).withSimilarityThreshold(0.8));
 List<Book> bookList = repo.findBooks(results.stream().map(Document::getId).collect(Collectors.toList()));

 var template = new PromptTemplate(prompt, Map.of("context", bookList.stream().map(b -> b.toString()).collect(Collectors.joining("\n")), "searchPhrase", searchPhrase));
 System.out.println("----- PROMPT -----");
 System.out.println(template.render());

 return client.call(template.create().getContents());
 }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

从相似性搜索之后开始,调用新的findBooks()方法,并传入相似性搜索中的评论ID列表。检索查询返回一个名为bookList的图书列表。接下来,使用提示字符串、图中的场景数据和用户的搜索短语创建提示模板,并将场景和searchPhrase提示参数分别映射到图数据(每个项目位于新行上的列表)和用户的搜索短语。还添加了System.out.println()来将提示打印到控制台,以便可以看到传递给LLM的内容。

最后,调用模板的create()方法从LLM生成响应。返回的JSON对象有一个内容键,其中包含响应字符串,基于用户搜索短语的图书推荐列表。

以下测试一下!

运行应用程序

要运行Goodreads AI应用程序,可以在终端中使用./mvnw spring-boot:run命令。一旦应用程序开始运行,就可以使用搜索短语作为查询参数向/rag端点发出GET请求。下面包括一些示例。

Shell 
 http ":8080/rag?searchPhrase=happy%20ending"
 http ":8080/rag?searchPhrase=encouragement"
 http ":8080/rag?searchPhrase=high%tech"
  • 1.
  • 2.
  • 3.
  • 4.

示例调用和输出+完整提示符

电话和退货书推荐:

Shell 
 jenniferreif@elf-lord springai-goodreads % http ":8080/rag?searchPhrase=encouragement"

 The Cross and the Switchblade
 The Art of Recklessness: Poetry as Assertive Force and Contradiction
 I am unsure about 90 Minutes in Heaven: A True Story of Death and Life
 The Greatest Gift: The Original Story That Inspired the Christmas Classic It's a Wonderful Life
 I am unsure about Aligned: Volume 1 (Aligned, #1)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

应用程序日志输出:

Shell 
 ----- PROMPT -----
 You are a book expert with high-quality book information in the CONTEXT section.
 Answer with every book title provided in the CONTEXT.
 Do not add extra information from any outside sources.
 If you are unsure about a book, list the book and add that you are unsure.

 CONTEXT:
 Book[book_id=772852, title=The Cross and the Switchblade, isbn=0515090255, isbn13=9780515090253, reviewList=[Review[id=f70c68721a0654462bcc6cd68e3259bd, text=encouraging, rating=4]]]
 Book[book_id=89375, title=90 Minutes in Heaven: A True Story of Death and Life, isbn=0800759494, isbn13=9780800759490, reviewList=[Review[id=85ef80e09c64ebd013aeebdb7292eda9, text=inspiring & hope filled, rating=5]]]
 Book[book_id=1488663, title=The Greatest Gift: The Original Story That Inspired the Christmas Classic It's a Wonderful Life, isbn=0670862045, isbn13=9780670862047, reviewList=[Review[id=b74851666f2ec1841ca5876d977da872, text=Inspiring, rating=4]]]
 Book[book_id=7517330, title=The Art of Recklessness: Poetry as Assertive Force and Contradiction, isbn=1555975623, isbn13=9781555975623, reviewList=[Review[id=2df3600d488e182a3ef06bff7fc82eb8, text=Great insight, great encouragement, and great company., rating=4]]]
 Book[book_id=27802572, title=Aligned: Volume 1 (Aligned, #1), isbn=1519114796, isbn13=9781519114792, reviewList=[Review[id=60b9aa083733e751ddd471fa1a77535b, text=healing, rating=3]]]

 PHRASE:
 encouragement
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

可以看到,LLM生成了一个响应,其中包含基于数据库中找到的图书推荐列表(提示的CONTEXT部分)。用户搜索短语的相似度搜索+图形检索查询的结果在提示符中,LLM的回答使用该数据作为响应。

结语

本文学习了如何在Java中使用Spring AI构建GenAI应用程序。使用OpenAI模型根据用户的搜索短语生成图书推荐。以及使用Neo4j Vector Database来存储和查询用于相似性搜索的向量。还将域模型映射到数据库模型,编写与数据库交互的存储库接口,并创建控制器类来处理用户请求和生成响应。

希望这篇文章能帮助人们使用Spring AI。

Loading

作者 yinhua

发表回复