欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > Spring AI Java程序员的AI之Spring AI(四)

Spring AI Java程序员的AI之Spring AI(四)

2024/10/22 21:31:02 来源:https://blog.csdn.net/qq_42731358/article/details/143039552  浏览:    关键词:Spring AI Java程序员的AI之Spring AI(四)

Spring AI之Java经典面试题智能小助手

  • 前言
  • 一、准备面试题
  • 二、搭建工程
  • 三、文件读取与解析
  • 四、Markdown文件解析
  • 五、问题搜索
  • 六、自定义EmbeddingClient
  • 七、定义请求Controller

前言

通过Ollama在本地部署了Llama3大模型,这篇来基于Llama3和Spring AI,以及ChatGPT Web来实现一个Java经典面试题智能小助手。
私有化部署大模型最佳解决方案 Ollama

一、准备面试题

建议优先使用marddown或txt等文本格式,因为经过尝试,如果导出为PDF格式,会出现格式错乱。

ThreadLocal和InheritableThreadLocal的区别
ThreadLocal和InheritableThreadLocal都可以用通过线程来共享数据,区别在于当前线程在InheritableThreadLocal中设置的值可以被子线程继承,并且是复制(也就是子线程和父线程一开始InheritableThreadLocal中的值时一致的,但是后续的修改互不影响),而当前线程在ThreadLocal中设置的值不会被子线程所继承。

如何理解Java中的装箱与拆箱
装箱,就是int类型包装为Integer类型,拆箱,就是反过来,因为Java中支持8种基本数据类型,每种基本类型都有对应的包装类型,装箱会调用valueOf()方法,传入基本类型,返回包装类型,这个方法中通常会有一个缓存,比如用来缓存数字1对应的Integer对象,拆箱会调用intValue()方法,返回基本类型,不要过多的进行装箱和拆箱,毕竟是在调方法,是消耗性能的。

Java中为什么要有基础类型
Java是面向对象的,一切都是对象,但是像字符、数字这些常用类型,每次用的时候也去new对象,就会比较费性能和内存了,所以Java设计了8种基础类型,在使用基础类型时,对应的内存空间是直接分配在栈上的,而不是分配在堆上,这样性能也更好。

说说进程和线程的区别
一个操作系统上会运行很多个程序,这些程序都有自己的代码,以及都要用内存来存代码,和代码运行过程中产生的数据,进程就是用来隔离各个程序的内存空间的,使得程序之间互不干扰,还是这多个程序,为了让它们能同时运行,CPU就需要先执行这个程序的几条指令,然后切换到另外一个程序去执行,然后再切回来,就像同时在运行多条指令流水线,而这个流水线就是线程,是CPU调度的最小单位

为什么Java不支持多继承?
首先,思考这么一种场景,假如现在A类继承了B类和C类,并且B类和C类中,都存在test()方法,那么当A类对象调用test()方法时,该调用B类的test()呢?还是C类的test()呢?是没有答案的,所以Java中不允许多继承。

String、StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高

二、搭建工程

引入SpringBoot:

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.1</version>
</parent>

引入Spring AI

<dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>0.8.1-SNAPSHOT</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

引入spring web

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

引入Ollama

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

引入Markdown解析器

<dependency><groupId>com.vladsch.flexmark</groupId><artifactId>flexmark</artifactId><version>0.42.14</version>
</dependency>

引入Redis向量数据库相关

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-redis</artifactId>
</dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>5.1.0</version>
</dependency>

指定仓库

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

三、文件读取与解析

新建InterviewService,提供向量存储、向量搜索功能:

@Bean
public RedisVectorStore vectorStore(EmbeddingClient embeddingClient) {RedisVectorStore.RedisVectorStoreConfig config = RedisVectorStore.RedisVectorStoreConfig.builder().withURI("redis://localhost:6379").withIndexName("interview-assistant-index").withMetadataFields(RedisVectorStore.MetadataField.text("filename")).build();return new RedisVectorStore(config, embeddingClient);
}
package com.qjc.demo.service;import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;import java.util.List;/**** @projectName spring-ollama-demo* @packageName com.qjc.demo.service* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-18 10:23**/@Component
public class InterviewService {@Value("classpath:Java基础面试题.md")private Resource resource;@Autowiredprivate VectorStore vectorStore;public List<Document> loadText() {// 读取文件内容TextReader textReader = new TextReader(resource);List<Document> documents = textReader.get();// 解析文件内容MarkdownSplitter textSplitter = new MarkdownSplitter();List<Document> list = textSplitter.apply(documents);// 将问题提取出来存入Metadatalist.forEach(document -> {String title = document.getContent().split("==title==")[0];String replace = title.replace("##", "");document.getMetadata().put("question", replace.trim());});// 向量化以及向量存储vectorStore.add(list);return list;}public List<Document> search(String message){// ...}
}

四、Markdown文件解析

思路是:通过解析文件中的二级标题和标题下的内容,得到一个Document,标题和内容直接用"title"分割。

package com.com.qjc.demo.utils;import com.vladsch.flexmark.ast.Heading;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.collection.iteration.ReversiblePeekingIterator;
import org.springframework.ai.transformer.splitter.TextSplitter;import java.util.ArrayList;
import java.util.List;
/**** @projectName spring-ollama-demo* @packageName com.qjc.demo.utils* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-18 10:23**/
public class MarkdownSplitter extends TextSplitter {@Overrideprotected List<String> splitText(String text) {Parser parser = Parser.builder().build();Document markdownDocument = parser.parse(text);List<String> result = new ArrayList<>();ReversiblePeekingIterator<Node> iterator = markdownDocument.getChildren().iterator();StringBuilder builder = new StringBuilder();while (iterator.hasNext()) {Node node = iterator.next();// 如果是二级标题if (node instanceof Heading && ((Heading) node).getLevel() == 2) {if (!builder.isEmpty()) {result.add(builder.toString());}builder.delete(0, builder.length());builder.append(node.getChars());builder.append("==title==");} else {builder.append(node.getChars());}}if (!builder.isEmpty()) {result.add(builder.toString());}return result;}
}

五、问题搜索

public List<Document> search(String question){// 先查元数据SearchRequest metaSearchRequest = SearchRequest.query(question).withTopK(3).withSimilarityThreshold(0.9).withFilterExpression(String.format("question in ['%s']", question));List<Document> metaDocuments = vectorStore.similaritySearch(metaSearchRequest);if (!CollectionUtils.isEmpty(metaDocuments)) {return metaDocuments;}// 元数据没查到在相似搜索SearchRequest searchRequest = SearchRequest.query(question).withTopK(3).withSimilarityThreshold(0.9);return vectorStore.similaritySearch(searchRequest);}

六、自定义EmbeddingClient

默认情况下是对问题和答案同时进行向量化,如果只想对问题进行向量化,则需要自定义EmbeddingClient:

package com.qjc.demo.config;import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.OllamaEmbeddingClient;
import org.springframework.ai.ollama.api.OllamaApi;import java.util.List;/**** @projectName spring-ollama-demo* @packageName com.qjc.demo.config* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-18 10:23**/
public class QjcOllamaEmbeddingClient extends OllamaEmbeddingClient {public QjcOllamaEmbeddingClient (OllamaApi ollamaApi) {super(ollamaApi);}@Overridepublic List<Double> embed(Document document) // 单独对问题进行向量化String question = (String) document.getMetadata().get("question");return this.embed(question);}
}
@Beanpublic QjcOllamaEmbeddingClient ollamaEmbeddingClient(OllamaApi ollamaApi, OllamaEmbeddingProperties properties) {QjcOllamaEmbeddingClient qjcOllamaEmbeddingClient = new QjcOllamaEmbeddingClient (ollamaApi);qjcOllamaEmbeddingClient.withModel("nomic-embed-text:v1.5");return qjcOllamaEmbeddingClient;}

七、定义请求Controller

package com.qjc.demo.controller;import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.StreamingChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/**** @projectName spring-ollama-demo* @packageName com.qjc.demo.controller* @author qjc* @description TODO* @Email qjc1024@aliyun.com* @date 2024-10-18 10:23**/
@RestController
public class ChatController {@Autowiredprivate StreamingChatClient chatClient;@Autowiredprivate InterviewService interviewService;@GetMapping("/document")public List<Document> document() {return interviewService.loadText();}@GetMapping("/documentSearch")public List<Document> documentSearch(@RequestParam String message) {return interviewService.search(message);}@PostMapping(value = "/v1/chat/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<OpenAiApi.ChatCompletionChunk> interview(@RequestBody OpenAiApi.ChatCompletionRequest request) {String question = request.messages().get(1).content();// 向量搜索List<Document> documentList = interviewService.search(question);// 提示词模板PromptTemplate promptTemplate = new PromptTemplate("{userMessage}\n\n 用中文,并根据以下信息回答问题:\n {contents}");// 组装提示词Prompt prompt = promptTemplate.create(Map.of("userMessage", question, "contents", documentList));// 调用大模型Flux<ChatResponse> stream = chatClient.stream(prompt);return stream.map(chatResponse -> {String content = chatResponse.getResult().getOutput().getContent();// 需要优化OpenAiApi.ChatCompletionChunk chatCompletionChunk = new OpenAiApi.ChatCompletionChunk("1",List.of(new OpenAiApi.ChatCompletionChunk.ChunkChoice(OpenAiApi.ChatCompletionFinishReason.STOP,1,new OpenAiApi.ChatCompletionMessage(content,OpenAiApi.ChatCompletionMessage.Role.ASSISTANT), new OpenAiApi.LogProbs(null))),null, null, null, null);return chatCompletionChunk;});}
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com