一、文件功能

让AI能够根据上传的文档来回答问题,下面我们来实现这个功能

1.添加文件读取pom

每种文档类型有各自的解析处理方式,本文使用的这个可以读取大多数类型的文档。

         <!--Tika文档读取器-->
         <dependency>
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-tika-document-reader</artifactId>
         </dependency>

2.编写文档解析的工具类

package com.Luoyi.util;

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @author Luoyi
 * @description: 文件解析
 */
public class parseFileUtil {

    /**
     * 从上传的文件中提取文本内容
     *
     * @param file 上传的文件
     * @return 提取的文本内容
     */
    public static String extractContent(MultipartFile file) {
        try {
            // 将 MultipartFile 转换为 Resource
            Resource resource = file.getResource();
            // 使用 TikaDocumentReader 读取文档
            TikaDocumentReader reader = new TikaDocumentReader(resource);
            List<Document> documents = reader.get();
            // 提取所有文档的内容并合并
            return documents.stream()
                    .map(doc -> doc.getText())
                    .collect(Collectors.joining("\n\n"));
        } catch (Exception e) {
            // 如果解析失败,返回错误信息
            return "[文档解析失败: " + e.getMessage() + "]";
        }
    }

}

3.在MultiModelsController控制类中新增

    /**
     * @Description: 带文档上传的对话流式输出
     * @param file 上传的文档
     * @param message 消息
     * @param model 指定模型
     * @param conversationId 会话ID
     * @return reactor.core.publisher.Flux<java.lang.String>
     */
    @PostMapping(value = "/chat/upload", produces = "text/html;charset=utf-8")
    public Flux<String> chatWithDocument(@RequestParam("file") MultipartFile file,
                                          @RequestParam String message,
                                          @RequestParam String model,
                                          @RequestParam String conversationId) {
        ChatClient chatClient = chatClientMap.get(model);
        // 提取文档内容
        String documentContent = parseFileUtil.extractContent(file);
        // 将文档内容附加到用户消息中
        String enrichedMessage = message + "\n\n【文档内容】\n" + documentContent;

        Flux<String> content = chatClient.prompt()
                .user(enrichedMessage)
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId))
                .stream()
                .content();

        return content;
    }

4.前端增加文件上传

html增加文件上传功能,这一步就不展示了,大家可以使用AI进行前后端接口对接。

5.运行

UI界面进行了简单的修改,同时上述功能实现成功读取文件并分析正确

二、RAG功能

如果我不想每次都上传文件而是,让AI默认从我指定的文件中读取信息,这该如何实现呢?下面我们使用RAG来进行实现,通过下面的学习可以简单构建自己的知识库让AI根据知识库来进行回答。

RAG是什么?RAG是检索增强生成。哈哈哈哈大家自己查查吧,我这里放个图

1.添加pom配置

		<!--向量存储-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-advisors-vector-store</artifactId>
        </dependency>

向量解析需要使用专用的向量解析模型,我这里使用的是ollama的模型,打开ollama筛选Embedding,就都是向量模型了

拉取模型后在配置文件中进行添加

spring:
  ai:
    #ollama模型配置
    ollama:
      chat:
        model: qwen3:1.7b
      base-url: http://localhost:11434
      embedding:
        model: qwen3-embedding:4b #RAG模型

2.编写测试类

/**
 * @author Luoyi
 * @description: 向量模型测试
 */
@SpringBootTest
public class EmbaddingTest {
    @Test
    public void testEmbadding(@Autowired OllamaEmbeddingModel ollamaEmbeddingModel) {

        float[] embedded = ollamaEmbeddingModel.embed("我叫德德");
        System.out.println(embedded.length);
        System.out.println(Arrays.toString(embedded));

    }
}

运行查看效果,可以看到“我是德德”这句被向量模型向量化为数组

3.测试升级

提前准备一个文档或者pdf,我这里准备了2025年国家公务员考试.docx,通过拦截器设置读取文档并将文档向量化

/**
 * @author Luoyi
 * @description: 向量模型测试
 */
@SpringBootTest
public class EmbaddingTest {
    @Test
    public void testEmbadding(@Autowired OllamaEmbeddingModel ollamaEmbeddingModel) {

        float[] embedded = ollamaEmbeddingModel.embed("我叫德德");
        System.out.println(embedded.length);
        System.out.println(Arrays.toString(embedded));

    }
    /**
     * @Description: 初始化向量数据库
     * @param vectorStore
     * @return void
     */
    @BeforeEach
    public void init(@Autowired VectorStore vectorStore) throws IOException {
        // 1. 读取resources中的docx文档
        Resource resource = new ClassPathResource("2025年国家公务员考试.docx");

        // 2. 使用TikaDocumentReader读取docx文件
        TikaDocumentReader documentReader = new TikaDocumentReader(resource);
        List<Document> documents = documentReader.get();

        // 3. 将文档进行向量化,并且存入向量数据库(无需再手动向量化)
        vectorStore.add(documents);
    }
    /**
     * @Description: RAG 测试
     * @param vectorStore
     * @param chatModel
     * @return void
     */
    @Test
    void chatRagTest(
            @Autowired
            VectorStore vectorStore,
            @Autowired DeepSeekChatModel chatModel
    ) {

        ChatClient chatClient = ChatClient.builder(chatModel)
                .build();

        String message="考前做什么准备需要?";
        String content = chatClient.prompt().user(message)
                .advisors(
                        new SimpleLoggerAdvisor(),
                        QuestionAnswerAdvisor.builder(vectorStore)
                                .searchRequest(
                                        SearchRequest
                                                .builder().query(message)
                                                .topK(5)
                                                .similarityThreshold(0.3)
                                                .build())
                                .build()
                ).call().content();

        System.out.println(content);

    }

}

运行结果和文档中内容一样

4.RAG配置类

/**
 * @author Luoyi
 * @description: RAG(检索增强生成)配置
 */
@Configuration
public class RagConfig {

    /**
     * 创建 RAG Advisor - 问答增强顾问
     * @param vectorStore 向量数据库
     * @return RAG Advisor 实例
     */
    @Bean
    public Advisor ragAdvisor(VectorStore vectorStore) {
        return QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(
                        SearchRequest.builder()
                                .similarityThreshold(0.6)    // 相似度阈值 60%
                                .topK(5)                     // 检索前5个最相关文档
                                .build())
                .build();
    }
}

5.在AIConfig配置中修改

添加向量存储实例

/**
 * @author Luoyi
 * @description: 模型配置
 */
@Configuration
public class AIConfig {

    @Autowired
    private DeepSeekChatModel deepSeekChatModel;
    @Autowired
    private OllamaApi ollamaApi;
    @Autowired
    private OllamaChatProperties options;
    @Autowired
    private ChatMemory chatMemory;
    @Autowired
    private UserTool userTool;


    /**
     * @Description: deepseek模型(支持RAG检索增强)
     * @param
     * @return org.springframework.ai.chat.client.ChatClient
     */
    @Bean
    @Primary
    public ChatClient deepseek(ToolCallbackProvider toolCallbackProvider){
        return ChatClient.builder(deepSeekChatModel)
                .defaultSystem("你是一个智能助手。如果用户问题超出知识库范围,某些操作无法进行时请明确告知用户。")
                .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())//MCP自动注册为工具
                .defaultAdvisors(new SimpleLoggerAdvisor()) // 记录日志
                .defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build()) // 维护对话历史
                .defaultTools(userTool) // 集成用户工具
                .build();
    }
    /**
     * @Description: 本地ollama模型(支持RAG检索增强)
     * @param
     * @return org.springframework.ai.chat.client.ChatClient
     */
    @Bean
    public ChatClient ollama(ToolCallbackProvider toolCallbackProvider) {
        OllamaChatModel ollamaChatModel = OllamaChatModel.builder()
                .ollamaApi(ollamaApi)
                .defaultOptions(OllamaOptions.builder().model(options.getModel()).build())
                .build();

        return ChatClient.builder(ollamaChatModel)
                .defaultSystem("你是一个智能助手。如果用户问题超出知识库范围,某些操作无法进行时请明确告知用户。")
                .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())//MCP自动注册为工具
                .defaultAdvisors(new SimpleLoggerAdvisor())//记录日志
                .defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())
                .build();
    }
    /**
     * 创建向量存储实例
     * @param embeddingModel 嵌入模型,用于将文本转换为向量表示
     * @return VectorStore 向量存储实例,用于存储和检索向量数据
     */
    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        // 使用SimpleVectorStore构建器创建向量存储
        SimpleVectorStore.SimpleVectorStoreBuilder builder = SimpleVectorStore.builder(embeddingModel);
        // 构建并返回向量存储实例
        return builder.build();
    }
}

6.编写向量数据库服务接口

(1)接口编写 IVectorStoreService
/**
 * @author Luoyi
 * @description: 向量数据库服务接口
 */
public interface IVectorStoreService {

    /**
     * 添加文本到向量数据库
     * @param text 文本内容
     */
    void addText(String text);

    /**
     * 批量添加文本到向量数据库
     * @param texts 文本列表
     */
    void addTexts(List<String> texts);

    /**
     * 添加文档到向量数据库
     * @param resource 文档资源(支持PDF、Word、Excel等)
     */
    void addDocument(Resource resource);

}
(2)接口实现VectorStoreServiceImpl
/**
 * @author Luoyi
 * @description: 向量数据库服务实现
 */
@Service
public class VectorStoreServiceImpl implements IVectorStoreService {

    @Autowired
    private VectorStore vectorStore;

    @Override
    public void addText(String text) {
        // 创建文档并添加到向量数据库
        Document document = new Document(text);
        vectorStore.add(List.of(document));
    }

    @Override
    public void addTexts(List<String> texts) {
        // 批量转换为文档并添加
        List<Document> documents = texts.stream()
                .map(Document::new)
                .collect(Collectors.toList());
        vectorStore.add(documents);
    }

    @Override
    public void addDocument(Resource resource) {
        // 使用 Tika 读取文档(支持PDF、Word、Excel等)
        TikaDocumentReader reader = new TikaDocumentReader(resource);
        List<Document> documents = reader.get();
        vectorStore.add(documents);
    }

}

7.添加RAG功能

增加变量enableKb 用于判断是否开启知识库功能 进行功能拼接.

增加知识库上传功能可以上传文本和文件

@RestController
public class MultiModelsController {

    //注册ChatClient
    @Autowired
    private Map<String, ChatClient> chatClientMap;
    @Autowired
    private IChatService chatService;
    @Autowired
    private Advisor ragAdvisor;
    @Autowired
    private VectorStore vectorStore;

    /**
     * @param message 消息
     * @param model   指定模型
     * @return reactor.core.publisher.Flux<java.lang.String>
     * @Description: 模型对话流式输出
     */
    @RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> generation(@RequestParam String message,
                                   @RequestParam String model,
                                   @RequestParam String conversationId,
                                   @RequestParam(name = "enableKb", required = false, defaultValue = "false") boolean enableKb) {
        ChatClient chatClient = chatClientMap.get(model);
        var prompt = chatClient.prompt()//创建聊天请求对象
                .user(message)//设置用户输入信息
                //接受一个 AdvisorSpec 类型的参数 (Consumer 方式)
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId));
        if (enableKb) {
            prompt = prompt.advisors(ragAdvisor);
        }
        Flux<String> content = prompt.stream()//开启流式响应并获取内容
                .content();

        return content;
    }

    /**
     * @param file           上传的文档
     * @param message        消息
     * @param model          指定模型
     * @param conversationId 会话ID
     * @return reactor.core.publisher.Flux<java.lang.String>
     * @Description: 带文档上传的对话流式输出
     */
    @PostMapping(value = "/chat/upload", produces = "text/html;charset=utf-8")
    public Flux<String> chatWithDocument(@RequestParam("file") MultipartFile file,
                                         @RequestParam String message,
                                         @RequestParam String model,
                                         @RequestParam String conversationId,
                                         @RequestParam(name = "enableKb", required = false, defaultValue = "false") boolean enableKb) {
        ChatClient chatClient = chatClientMap.get(model);
        // 提取文档内容
        String documentContent = extractContent(file);
        // 将文档内容附加到用户消息中
        String enrichedMessage = message + "\n\n【文档内容】\n" + documentContent;

        var prompt = chatClient.prompt()
                .user(enrichedMessage)
                .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, conversationId));
        if (enableKb) {
            prompt = prompt.advisors(ragAdvisor);
        }
        Flux<String> content = prompt.stream()
                .content();

        return content;
    }

    /**
     * @param text 要添加到知识库的文本内容
     * @return java.lang.String
     * @Description: 将文本内容添加到向量知识库
     */
    @PostMapping("/kb/text")
    public String addKbText(@RequestParam String text) {
        vectorStore.add(java.util.List.of(new Document(text)));
        return "ok";
    }

    /**
     * @param file 要添加到知识库的文件
     * @return java.lang.String
     * @Description: 将文件内容解析并添加到向量知识库
     */
    @PostMapping("/kb/file")
    public String addKbFile(@RequestParam("file") MultipartFile file) {
        try {
            Resource resource = file.getResource();
            TikaDocumentReader reader = new TikaDocumentReader(resource);
            vectorStore.add(reader.get());
            return "ok";
        } catch (Exception e) {
            return "error: " + e.getMessage();
        }
    }
}

8.前端文件修改

修改前端文件适配接口,这一步也是交给AI,也可以访问我的仓库Gitee,*https://gitee.com/Luoyi_good/aitest*

9.运行

先配置知识库,大家也可以把文件存放到数据库中,我这里目前没有做知识库内容的回显。

启用知识库

关闭知识库后AI无法进行检索

打开知识库后就可以进行检索

到这RAG知识库功能就实现了,大家可以自己继续进行深入开发,如设立多个知识库每个知识库不同...

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐