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

前言

通过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 [email protected]
 * @date 2024-10-18 10:23
 **/

@Component
public class InterviewService {
    
    

    @Value("classpath:Java基础面试题.md")
    private Resource resource;

    @Autowired
    private 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);

	// 将问题提取出来存入Metadata
	list.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 [email protected]
 * @date 2024-10-18 10:23
 **/
public class MarkdownSplitter extends TextSplitter {
    
    

    @Override
    protected 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 [email protected]
 * @date 2024-10-18 10:23
 **/
public class QjcOllamaEmbeddingClient extends OllamaEmbeddingClient {
    
    
    public QjcOllamaEmbeddingClient (OllamaApi ollamaApi) {
    
    
        super(ollamaApi);
    }

    @Override
    public List<Double> embed(Document document) 

		// 单独对问题进行向量化
        String question = (String) document.getMetadata().get("question");

        return this.embed(question);
    }
}

@Bean
    public 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 [email protected]
 * @date 2024-10-18 10:23
 **/
@RestController
public class ChatController {
    
    

    @Autowired
    private StreamingChatClient chatClient;

    @Autowired
    private 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;
        });
    }
}

猜你喜欢

转载自blog.csdn.net/qq_42731358/article/details/143039552
AI
今日推荐