1 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.xu</groupId>
    <artifactId>spring-openai-onnx</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-openai-onnx</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>25</java.version>
        <spring-ai.version>1.1.2</spring-ai.version>
    </properties>

    <dependencies>

        <!-- SpringBoot 前端请求 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- SpringAi openai -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-openai</artifactId>
        </dependency>

        <!-- Rag 文档解析 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-tika-document-reader</artifactId>
        </dependency>

        <!-- transformers 模型 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-transformers</artifactId>
        </dependency>

        <!-- es vector -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId>
        </dependency>

        <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.42</version>
        </dependency>

        <!-- devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2 配置

spring:
  elasticsearch:
    uris: http://localhost:9200
    username: elastic
    password: elastic
  application:
    name: spring-openai-onnx
  ai:
    openai:
      api-key: 你的DeepSeek的api-key
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
    embedding:
      transformer:
        onnx:
          model-Uri: classpath:onnx/bge-small-zh-v1.5.onnx
          model-output-name: last_hidden_state
        tokenizer:
          uri: classpath:onnx/bge-small-zh-v1.5-tokenizer.json
    vectorstore:
      elasticsearch:
        initialize-schema: true
        index-name: zhaoxin-train-rag
        dimensions: 512

3 ONNX Embedding 下载

4 Java

1 RAG 存储

package com.xu;

import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;

import org.junit.jupiter.api.Test;

import com.xu.conf.ZhaoXinTextSplitter;

import java.util.List;

@SpringBootTest
class RagTest {

    @Autowired
    private ElasticsearchVectorStore elasticsearchVectorStore;

    @Test
    void text() {
        Resource resource = new FileSystemResource("D:\\SourceCode\\简历\\a.txt");
        TextReader reader = new TextReader(resource);
        List<Document> origin = reader.get();
        // 中文拆分
        TextSplitter splitter = new ZhaoXinTextSplitter();
        List<Document> documents = splitter.split(origin);
        for (Document doc : documents) {
            doc.getMetadata().put("tenant", "11");
        }
        elasticsearchVectorStore.add(documents);
    }

    @Test
    void file() {
        Resource pdf = new FileSystemResource("D:\\SourceCode\\简历\\个人简历.pdf");
        Resource doc = new FileSystemResource("D:\\SourceCode\\简历\\个人简历.docx");
        Resource excel = new FileSystemResource("D:\\SourceCode\\简历\\个人简历.xlsx");
        TikaDocumentReader reader = new TikaDocumentReader(pdf);
        List<Document> origin = reader.get();
        // 中文拆分
        TextSplitter splitter = new ZhaoXinTextSplitter();
        List<Document> documents = splitter.split(origin);
        elasticsearchVectorStore.add(documents);
    }

}

2 RAG 搜索

package com.xu;

import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import org.junit.jupiter.api.Test;

import cn.hutool.json.JSONUtil;

import java.util.List;

@SpringBootTest
class RagTest {

    @Autowired
    private ElasticsearchVectorStore elasticsearchVectorStore;

    @Autowired
    private EmbeddingModel embeddingModel;

    @Test
    void length() {
        EmbeddingResponse response = embeddingModel.embedForResponse(List.of("测试文本"));
        System.out.println("向量维度: " + response.getResult().getOutput().length);
    }

    @Test
    void search() {
        SearchRequest request = SearchRequest.builder()
                .query("艾拉·万斯") // 查询
                .topK(2)  // 返回的相似文档数量
                //.similarityThreshold(0.7) // 相似度阈值
                .filterExpression("tenant == '11'")  // 过滤条件
                .build();
        List<Document> docs = elasticsearchVectorStore.similaritySearch(request);
        System.out.println(JSONUtil.toJsonPrettyStr(docs));
    }
    
}

3 Agent + RAG

package com.xu;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import org.junit.jupiter.api.Test;

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

import lombok.extern.slf4j.Slf4j;

@Slf4j
@SpringBootTest
public class ChatTest {

    @Autowired
    private VectorStore redisVectorStore;

    @Autowired
    private ChatClient chatClient;

    @Test
    void common() {
        ChatResponse chatResponse = chatClient.prompt("艾拉·万斯是谁, 知道回答知道, 不知道回答不知道").call().chatResponse();
        String answer = chatResponse.getResult().getOutput().getText();
        System.out.println(answer);
        System.out.println("豆包生成回答:\n" + answer);
    }

    @Test
    void spring() {

        String userQuery = "艾拉·万斯是谁, 知道回答知道, 不知道回答不知道";
        SearchRequest request = SearchRequest.builder()
                .query("艾拉·万斯")
                .topK(2)
                .similarityThreshold(0.7)
                .build();
        List<Document> docs = redisVectorStore.similaritySearch(request);

        String context = docs.stream()
                .map(Document::getText)
                .collect(Collectors.joining("\n\n"));

        String promptTemplate = """
                请你基于以下提供的上下文信息,回答用户的问题。
                要求:
                1. 严格按照上下文内容回答,不要添加你自己的额外知识;
                2. 如果上下文没有相关信息,直接回答"无法从现有资料中获取该信息";
                3. 回答语言简洁、准确,符合中文表达习惯;
                4. 保留上下文的关键信息,不遗漏重要内容。
                
                上下文信息:
                {context}
                
                用户问题:
                {userQuery}
                """;

        PromptTemplate promptTemp = new PromptTemplate(promptTemplate);
        Map<String, Object> promptParams = new HashMap<>();
        promptParams.put("context", context);
        promptParams.put("userQuery", userQuery);
        Prompt finalPrompt = promptTemp.create(promptParams);

        ChatResponse chatResponse = chatClient.prompt(finalPrompt).call().chatResponse();

        String answer = chatResponse.getResult().getOutput().getText();
        System.out.println("豆包生成回答:\n" + answer);
    }

}

4 ChatClient

package com.xu.conf;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

/**
 * @author hyacinth
 */
@Component
@Configuration
public class ChatClientConf {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder.build();
    }

}

5 RAG文字拆分

package com.xu.conf;

import org.springframework.ai.transformer.splitter.TextSplitter;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 优化后的中文专属文本拆分器
 * 适配中文语义边界,避免拆分后语义断裂
 *
 * @author hyacinth
 */
public class ZhaoXinTextSplitter extends TextSplitter {

    // 中文核心分隔符正则(按「强语义边界→弱语义边界」排序,优先拆分大的语义单元)
    private static final Pattern CHINESE_SEPARATOR_PATTERN = Pattern.compile(
            "(。|?|!|;|:|\\n\\n|\\n|,)",
            Pattern.UNICODE_CASE | Pattern.MULTILINE
    );

    // 空白字符过滤正则(去除全角/半角空白、制表符等)
    private static final Pattern BLANK_PATTERN = Pattern.compile("\\s+");

    @Override
    protected List<String> splitText(String text) {
        // 步骤1:预处理文本,去除多余空白,避免无效拆分
        String processedText = preprocessText(text);

        // 步骤2:按中文语义边界拆分文本,得到初步拆分结果
        List<String> initialSplits = splitByChineseSemantic(processedText);

        // 步骤3:过滤无效分块,合并过短分块,得到最终有效分块
        return filterAndOptimizeChunks(initialSplits);
    }

    /**
     * 文本预处理:去除多余空白、制表符,统一换行格式
     */
    private String preprocessText(String text) {
        if (text == null || text.isBlank()) {
            return "";
        }
        // 1. 替换全角空白为半角
        String result = text.replace(" ", " ");
        // 2. 去除连续多个空白(空格、制表符等),保留单个空格
        result = BLANK_PATTERN.matcher(result).replaceAll(" ");
        // 3. 去除文本首尾空白
        return result.trim();
    }

    /**
     * 核心:按中文语义边界拆分文本,保留分隔符(保证语义完整)
     */
    private List<String> splitByChineseSemantic(String text) {
        List<String> chunks = new ArrayList<>();
        if (text.isBlank()) {
            return chunks;
        }

        // 利用正则拆分,同时保留分隔符(中文标点是语义的重要组成,不可丢弃)
        String[] splitParts = CHINESE_SEPARATOR_PATTERN.split(text);
        Matcher matcher = CHINESE_SEPARATOR_PATTERN.matcher(text);
        List<String> separators = new ArrayList<>();

        // 提取所有匹配到的中文分隔符(用于后续拼接,保留语义)
        while (matcher.find()) {
            separators.add(matcher.group());
        }

        // 拼接拆分后的内容和对应的分隔符,形成完整语义块
        for (int i = 0; i < splitParts.length; i++) {
            String part = splitParts[i].trim();
            if (part.isBlank()) {
                continue;
            }
            // 拼接当前内容和对应的分隔符(避免分隔符丢失)
            StringBuilder chunk = new StringBuilder(part);
            if (i < separators.size()) {
                chunk.append(separators.get(i));
            }
            chunks.add(chunk.toString().trim());
        }

        return chunks;
    }

    /**
     * 过滤无效分块,优化分块质量(去除空值、合并过短分块)
     */
    private List<String> filterAndOptimizeChunks(List<String> initialSplits) {
        // 步骤1:过滤空字符串、仅含标点的无效分块
        List<String> validChunks = initialSplits.stream()
                .filter(chunk -> chunk != null && !chunk.isBlank() && !isOnlyPunctuation(chunk))
                .collect(Collectors.toList());

        // 步骤2:合并过短分块(避免出现单个词语、几个汉字的无效小分块)
        return mergeShortChunks(validChunks, 100);
    }

    /**
     * 判断是否仅含标点符号(过滤无效分块)
     */
    private boolean isOnlyPunctuation(String chunk) {
        return chunk.matches("[。|?|!|;|:|,|、|\\s]+");
    }

    /**
     * 合并过短分块,保证分块语义完整性
     *
     * @param validChunks    有效分块列表
     * @param minChunkLength 最小分块字符数
     * @return 优化后的分块列表
     */
    private List<String> mergeShortChunks(List<String> validChunks, int minChunkLength) {
        List<String> mergedChunks = new ArrayList<>();
        StringBuilder currentChunk = new StringBuilder();

        for (String chunk : validChunks) {
            // 若当前拼接的分块长度不足,继续拼接下一个分块
            if (currentChunk.length() + chunk.length() < minChunkLength) {
                currentChunk.append(chunk);
            } else {
                // 长度达标,存入结果列表,重置当前拼接器
                if (!currentChunk.isEmpty()) {
                    mergedChunks.add(currentChunk.toString().trim());
                    currentChunk.setLength(0);
                }
                mergedChunks.add(chunk.trim());
            }
        }

        // 处理最后一个未完成拼接的分块
        if (!currentChunk.isEmpty()) {
            mergedChunks.add(currentChunk.toString().trim());
        }

        return mergedChunks;
    }

    // 可选:重写拆分入口方法,适配Spring AI的Document拆分
    @Override
    public List<org.springframework.ai.document.Document> split(List<org.springframework.ai.document.Document> documents) {
        return super.split(documents);
    }

}

在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐