1 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	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>

		<!-- pgvector -->
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-starter-vector-store-pgvector</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:
  datasource:
    username: postgres
    password: 123456
    url: jdbc:postgresql://localhost:5432/postgres
  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:
      pgvector:
        dimensions: 512
        table-name: vector_store
        schema-name: public
        distance-type: COSINE_DISTANCE
        index-type: HNSW
        initialize-schema: false

3 ONNX Embedding 下载

4 PGVector

-- 启用vector
create extension vector;
create extension if not exists vector;
select version();

-- 查看vector
select * from pg_extension where extname = 'vector';

-- 创建vector表
drop table if exists vector_store;
create table if not exists vector_store (
id uuid primary key,
content text,
metadata jsonb,
embedding vector(512)
);
create index on vector_store
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);

select * from vector_store;

5 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.VectorStore;
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 VectorStore store;

    @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");
        }
        store.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);
        store.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.VectorStore;
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 VectorStore store;

    @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 = store.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编程工具,助力开发者即刻编程。

更多推荐