SpringAI 全栈开发 + RAG 检索增强实战

SpringAI 全栈开发 + RAG 检索增强实战

前言

随着生成式AI技术的规模化落地,企业级AI应用开发已从技术验证走向生产级部署。Java作为企业级开发的主流语言,长期以来缺乏原生适配Spring生态的AI开发框架,导致开发者需要对接多套异构SDK、处理复杂的适配逻辑、难以快速落地核心AI能力。SpringAI的出现彻底改变了这一现状,它以Spring生态原生的设计理念,提供了统一的大模型接入抽象、全链路的RAG能力支持、无缝整合Spring Boot的自动配置特性,让Java开发者可以用极低的成本完成企业级AI应用的开发与落地。

一、核心技术栈底层原理与选型

1.1 SpringAI核心架构与设计理念

SpringAI是Spring官方推出的开源AI应用开发框架,完全遵循Spring生态的设计哲学,提供了可移植的API抽象,支持主流大模型服务、向量数据库、文档处理、RAG、Function Calling等AI应用开发的全场景能力。 其核心架构分为四层:

  • 接入层:统一封装主流大模型、Embedding模型、向量数据库的SDK,屏蔽底层异构差异
  • 抽象层:定义ChatModel、EmbeddingModel、VectorStore、DocumentReader等核心接口,实现业务代码与底层服务的解耦
  • 能力层:提供文档拆分、文本向量化、向量检索、Prompt工程、Function Calling、记忆管理等RAG全链路能力
  • 整合层:原生适配Spring Boot、Spring Security、Spring Cloud等生态组件,支持自动配置、依赖注入、事务管理等企业级特性 核心优势在于:切换大模型/向量数据库仅需修改配置,无需改动业务代码;完全兼容Spring Boot 3.x,无缝融入现有Java企业级项目;提供了生产级的异常处理、限流重试、监控观测能力。

1.2 RAG检索增强生成底层逻辑

RAG(Retrieval Augmented Generation,检索增强生成)是解决大模型幻觉、知识滞后、私有数据安全接入三大核心痛点的最优方案,其核心逻辑是在大模型生成回答前,先从私有知识库中检索与用户问题相关的上下文信息,将其与用户问题拼接成完整Prompt后再输入大模型,让大模型基于精准的私有数据生成回答,从根源上降低幻觉概率。 RAG全流程分为两大核心阶段,对应的流程图如下:

这里必须明确区分RAG与大模型微调(Fine-tuning)的核心差异,避免开发者选型错误:

特性RAG检索增强生成大模型微调
核心能力实时接入私有数据,解决知识滞后与幻觉优化大模型在特定领域的生成风格与能力
数据更新实时更新,新增文档无需重新训练数据更新需重新微调,成本高周期长
数据安全私有数据无需传入大模型训练环境,合规性高需将训练数据传入大模型,存在数据泄露风险
开发成本极低,小时级可落地极高,需要大量标注数据与算力资源
适用场景企业知识库、智能客服、文档问答、私有数据查询特定领域的生成风格优化、垂类任务能力增强

1.3 技术栈选型与版本规范

本文所有实战内容均采用当前最新的稳定GA版本,确保兼容性与安全性,核心选型如下:

  • 开发环境:JDK 17(LTS版本)
  • 项目框架:Spring Boot 3.3.5(最新稳定GA版)
  • AI开发框架:Spring AI 1.2.0(最新稳定GA版)
  • 项目管理:Maven 3.9.x
  • 持久层框架:MyBatis-Plus 3.5.7
  • 数据库:MySQL 8.0(LTS版本)
  • 向量数据库:Milvus 2.4.x(企业级开源向量数据库,SpringAI原生支持)
  • 文档解析:Apache Tika 2.9.2
  • JSON处理:FastJSON2 2.0.53
  • 工具类:Spring Framework 内置工具类、Guava 33.2.1-jre
  • 接口文档:SpringDoc OpenAPI 3(Swagger3)2.6.0
  • 日志框架:SLF4J + Logback(Spring Boot 内置)

二、项目初始化与环境搭建

2.1 Maven核心依赖配置

创建Spring Boot项目,pom.xml文件完整配置如下:

<?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.3.5</version>         <relativePath/>     </parent>     <groupId>com.jam.demo</groupId>     <artifactId>spring-ai-rag-demo</artifactId>     <version>0.0.1-SNAPSHOT</version>     <name>spring-ai-rag-demo</name>     <description>SpringAI RAG企业级实战项目</description>     <properties>         <java.version>17</java.version>         <spring-ai.version>1.2.0</spring-ai.version>         <mybatis-plus.version>3.5.7</mybatis-plus.version>         <milvus.version>2.4.5</milvus.version>         <tika.version>2.9.2</tika.version>         <fastjson2.version>2.0.53</fastjson2.version>         <guava.version>33.2.1-jre</guava.version>         <springdoc.version>2.6.0</springdoc.version>     </properties>     <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>     <dependencies>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.ai</groupId>             <artifactId>spring-ai-starter-doubao</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.ai</groupId>             <artifactId>spring-ai-starter-milvus</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-validation</artifactId>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-jdbc</artifactId>         </dependency>         <dependency>             <groupId>com.baomidou</groupId>             <artifactId>mybatis-plus-boot-starter</artifactId>             <version>${mybatis-plus.version}</version>         </dependency>         <dependency>             <groupId>com.mysql</groupId>             <artifactId>mysql-connector-j</artifactId>             <scope>runtime</scope>         </dependency>         <dependency>             <groupId>org.apache.tika</groupId>             <artifactId>tika-core</artifactId>             <version>${tika.version}</version>         </dependency>         <dependency>             <groupId>org.apache.tika</groupId>             <artifactId>tika-parsers-standard-package</artifactId>             <version>${tika.version}</version>             <type>pom</type>         </dependency>         <dependency>             <groupId>com.alibaba.fastjson2</groupId>             <artifactId>fastjson2</artifactId>             <version>${fastjson2.version}</version>         </dependency>         <dependency>             <groupId>com.google.guava</groupId>             <artifactId>guava</artifactId>             <version>${guava.version}</version>         </dependency>         <dependency>             <groupId>org.springdoc</groupId>             <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>             <version>${springdoc.version}</version>         </dependency>         <dependency>             <groupId>org.projectlombok</groupId>             <artifactId>lombok</artifactId>             <version>1.18.34</version>             <scope>provided</scope>         </dependency>         <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-test</artifactId>             <scope>test</scope>         </dependency>     </dependencies>     <build>         <plugins>             <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> 

本文采用字节跳动豆包大模型作为示例,SpringAI官方原生支持,国内访问稳定,无需代理;如需切换OpenAI、通义千问、文心一言等大模型,仅需替换对应的starter依赖与配置,业务代码无需任何修改。

2.2 应用配置文件

application.yml完整配置如下,所有配置项均有注释,可直接修改对应参数即可运行:

server:   port: 8080   servlet:     context-path: /ai spring:   application:     name: spring-ai-rag-demo   datasource:     driver-class-name: com.mysql.cj.jdbc.Driver     url: jdbc:mysql://127.0.0.1:3306/ai_rag_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true     username: root     password: root   ai:     doubao:       api-key: 你的豆包API密钥       base-url: https://ark.cn-beijing.volces.com/api/v3       chat:         options:           model: doubao-pro-32k           temperature: 0.3           top-p: 0.9           max-tokens: 4096       embedding:         options:           model: doubao-embedding-text-240515   milvus:     client:       host: 127.0.0.1       port: 19530       username: root       password: milvus       database-name: default mybatis-plus:   mapper-locations: classpath*:/mapper/**/*.xml   type-aliases-package: com.jam.demo.entity   configuration:     map-underscore-to-camel-case: true     cache-enabled: false     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl springdoc:   api-docs:     enabled: true     path: /v3/api-docs   swagger-ui:     enabled: true     path: /swagger-ui.html     tags-sorter: alpha     operations-sorter: alpha 

2.3 数据库初始化

MySQL 8.0 初始化SQL脚本,用于存储文档元数据,确保SQL可直接执行:

CREATE DATABASE IF NOT EXISTS ai_rag_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE ai_rag_db; DROP TABLE IF EXISTS tb_document_chunk; CREATE TABLE tb_document_chunk (     id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',     document_id VARCHAR(64) NOT NULL COMMENT '文档唯一ID',     document_name VARCHAR(255) NOT NULL COMMENT '文档名称',     chunk_id INT NOT NULL COMMENT '分块序号',     chunk_content TEXT NOT NULL COMMENT '分块文本内容',     vector_id VARCHAR(64) NOT NULL COMMENT '对应向量数据库的向量ID',     create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',     update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',     PRIMARY KEY (id),     UNIQUE KEY uk_document_chunk (document_id, chunk_id),     KEY idx_document_id (document_id),     KEY idx_create_time (create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文档分块元数据表'; 

2.4 项目启动类与基础配置

项目启动类,包名com.jam.demo:

package com.jam.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.jam.demo.mapper") public class SpringAiRagDemoApplication {     public static void main(String[] args) {         SpringApplication.run(SpringAiRagDemoApplication.class, args);     } } 

Swagger3配置类,用于全局接口文档配置:

package com.jam.demo.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SpringDocConfig {     @Bean     public OpenAPI openAPI() {         return new OpenAPI()                 .info(new Info()                         .title("SpringAI RAG实战项目接口文档")                         .description("企业级AI应用开发与RAG检索增强系统接口文档")                         .version("v1.0.0")                         .contact(new Contact().name("ken").email("[email protected]"))                         .license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0"))                 );     } } 

Milvus向量库配置类,用于初始化VectorStore,SpringAI的核心向量存储接口:

package com.jam.demo.config; import io.milvus.param.IndexType; import io.milvus.param.MetricType; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.MilvusVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MilvusConfig {     private static final int VECTOR_DIMENSION = 1024;     private static final String COLLECTION_NAME = "rag_document_vector";     @Bean     public VectorStore vectorStore(EmbeddingModel embeddingModel) {         return MilvusVectorStore.builder(embeddingModel)                 .withCollectionName(COLLECTION_NAME)                 .withVectorDimension(VECTOR_DIMENSION)                 .withIndexType(IndexType.HNSW)                 .withMetricType(MetricType.COSINE)                 .withAutoCreateCollection(true)                 .withAutoCreateIndex(true)                 .build();     } } 

这里必须说明:Embedding模型的输出维度必须与向量数据库配置的维度完全一致,否则会出现向量写入失败的问题,豆包Embedding模型输出维度固定为1024,OpenAI text-embedding-3-small输出维度为1536,切换模型时必须同步修改维度配置。

三、SpringAI对接大模型API全实战

3.1 SpringAI大模型核心抽象

SpringAI对所有大模型的对话能力做了统一的抽象,核心接口与类如下:

  • ChatModel:大模型对话能力的顶层接口,定义了call方法,接收Prompt对象,返回ChatResponse对象,屏蔽不同大模型的SDK差异
  • ChatClient:流式API的封装类,提供了流式对话、函数调用、记忆管理等高级能力,是业务开发的首选
  • Prompt:用户输入的完整提示词对象,包含一个或多个Message对象
  • Message:对话消息对象,分为UserMessage、SystemMessage、AssistantMessage、FunctionMessage四种类型,对应大模型对话的不同角色
  • ChatResponse:大模型返回的完整响应对象,包含生成的文本、Token消耗、结束原因等信息

这种抽象的核心价值在于:业务代码完全与底层大模型解耦,如需从豆包切换到通义千问,仅需修改pom依赖与application.yml配置,业务代码一行都不需要改,这是SpringAI相比其他AI框架的核心优势。

3.2 基础对话能力实现

首先定义通用的请求与响应DTO,用于接口参数接收与返回,符合Swagger规范:

package com.jam.demo.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Data @Schema(description = "对话请求DTO") public class ChatRequestDTO {     @NotBlank(message = "用户提问不能为空")     @Schema(description = "用户提问内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java中HashMap和ConcurrentHashMap的区别")     private String query;     @Schema(description = "会话ID,用于多轮对话", example = "123456789")     private String sessionId; } 
package com.jam.demo.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Schema(description = "对话响应DTO") public class ChatResponseDTO {     @Schema(description = "大模型生成的回答内容")     private String answer;     @Schema(description = "输入Token消耗")     private Long promptTokens;     @Schema(description = "输出Token消耗")     private Long completionTokens;     @Schema(description = "总Token消耗")     private Long totalTokens; } 

然后编写对话Service层,核心业务逻辑,符合代码规范:

package com.jam.demo.service; import com.jam.demo.dto.ChatRequestDTO; import com.jam.demo.dto.ChatResponseDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; /**  * 大模型对话服务  * @author ken  * @date 2026-02-27  */ @Service @Slf4j public class ChatService {     private final ChatClient chatClient;     public ChatService(ChatClient.Builder chatClientBuilder) {         this.chatClient = chatClientBuilder.build();     }     /**      * 基础单轮对话      * @param requestDTO 对话请求参数      * @return 大模型响应结果      */     public ChatResponseDTO singleChat(ChatRequestDTO requestDTO) {         String query = requestDTO.getQuery();         log.info("收到用户对话请求,query:{}", query);         ChatResponse chatResponse = chatClient.prompt()                 .user(query)                 .call()                 .chatResponse();         String answer = chatResponse.getResult().getOutput().getText();         Long promptTokens = chatResponse.getMetadata().getUsage().getPromptTokens();         Long completionTokens = chatResponse.getMetadata().getUsage().getCompletionTokens();         Long totalTokens = chatResponse.getMetadata().getUsage().getTotalTokens();         log.info("大模型响应完成,总Token消耗:{}", totalTokens);         return new ChatResponseDTO(answer, promptTokens, completionTokens, totalTokens);     }     /**      * 带系统提示词的对话      * @param requestDTO 对话请求参数      * @param systemPrompt 系统提示词      * @return 大模型响应结果      */     public ChatResponseDTO chatWithSystemPrompt(ChatRequestDTO requestDTO, String systemPrompt) {         String query = requestDTO.getQuery();         log.info("收到带系统提示词的对话请求,query:{}", query);         Prompt prompt;         if (StringUtils.hasText(systemPrompt)) {             prompt = new Prompt(                     org.springframework.ai.chat.messages.SystemMessage.from(systemPrompt),                     org.springframework.ai.chat.messages.UserMessage.from(query)             );         } else {             prompt = new Prompt(org.springframework.ai.chat.messages.UserMessage.from(query));         }         ChatResponse chatResponse = chatClient.prompt(prompt).call().chatResponse();         String answer = chatResponse.getResult().getOutput().getText();         Long promptTokens = chatResponse.getMetadata().getUsage().getPromptTokens();         Long completionTokens = chatResponse.getMetadata().getUsage().getCompletionTokens();         Long totalTokens = chatResponse.getMetadata().getUsage().getTotalTokens();         return new ChatResponseDTO(answer, promptTokens, completionTokens, totalTokens);     } } 

然后编写Controller层,带Swagger注解,接口规范:

package com.jam.demo.controller; import com.jam.demo.dto.ChatRequestDTO; import com.jam.demo.dto.ChatResponseDTO; import com.jam.demo.service.ChatService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/chat") @Tag(name = "大模型对话接口", description = "SpringAI对接大模型的基础对话能力接口") public class ChatController {     private final ChatService chatService;     public ChatController(ChatService chatService) {         this.chatService = chatService;     }     @PostMapping("/single")     @Operation(summary = "单轮对话接口", description = "基础的大模型单轮对话能力,无上下文记忆")     public ResponseEntity<ChatResponseDTO> singleChat(@Valid @RequestBody ChatRequestDTO requestDTO) {         return ResponseEntity.ok(chatService.singleChat(requestDTO));     }     @PostMapping("/system")     @Operation(summary = "带系统提示词的对话接口", description = "可自定义系统提示词,指定大模型的角色与行为规范")     public ResponseEntity<ChatResponseDTO> chatWithSystemPrompt(             @Valid @RequestBody ChatRequestDTO requestDTO,             @Parameter(description = "系统提示词", example = "你是一名资深Java技术专家,只回答Java相关的技术问题,回答要专业、简洁、有代码示例")             @RequestParam(required = false) String systemPrompt     ) {         return ResponseEntity.ok(chatService.chatWithSystemPrompt(requestDTO, systemPrompt));     } } 

以上代码可直接运行,启动项目后,访问http://localhost:8080/ai/swagger-ui.html 即可打开接口文档,测试对话接口。

3.3 流式对话能力实现

流式对话是AI应用的核心体验,可实现打字机效果,降低用户等待时长,SpringAI原生支持流式响应,实现代码如下,新增到ChatService中:

/**  * 流式对话,返回Flux响应流  * @param requestDTO 对话请求参数  * @return 大模型流式响应内容  */ public reactor.core.publisher.Flux<String> streamChat(ChatRequestDTO requestDTO) {     String query = requestDTO.getQuery();     log.info("收到流式对话请求,query:{}", query);     return chatClient.prompt()             .user(query)             .stream()             .content(); } 

Controller层新增流式接口,注意流式接口必须返回Flux,produces设置为text/event-stream,符合SSE规范:

@GetMapping(value = "/stream", produces = "text/event-stream") @Operation(summary = "流式对话接口", description = "SSE流式响应,实现打字机效果,支持前端实时渲染") public reactor.core.publisher.Flux<String> streamChat(         @Parameter(description = "用户提问内容", required = true)         @RequestParam String query ) {     ChatRequestDTO requestDTO = new ChatRequestDTO();     requestDTO.setQuery(query);     return chatService.streamChat(requestDTO); } 

这里必须说明:流式接口采用SSE(Server-Sent Events)协议,前端可通过EventSource直接对接,实现打字机效果,无需额外的WebSocket配置,SpringAI原生支持,无需处理大模型的流式响应解析,大幅降低开发成本。

四、RAG技术栈全链路落地实战

RAG的核心是让大模型基于私有知识库生成精准回答,全链路分为文档处理、文本向量化、向量存储、智能检索、Prompt拼接、大模型生成六大环节,本节将完整实现每个环节的生产级代码。

4.1 文档处理与文本分块

文档处理是RAG的第一步,核心是从不同格式的文档中提取文本,然后拆分成合适大小的文本块,确保检索的精准性。 首先实现文档解析工具类,支持txt、md、pdf、docx等主流格式,基于Apache Tika实现:

package com.jam.demo.util; import lombok.extern.slf4j.Slf4j; import org.apache.tika.Tika; import org.apache.tika.exception.TikaException; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import java.io.IOException; import java.io.InputStream; /**  * 文档解析工具类  * @author ken  * @date 2026-02-27  */ @Component @Slf4j public class DocumentParseUtil {     private final Tika tika = new Tika();     /**      * 从输入流中提取文档文本内容      * @param inputStream 文档输入流      * @param fileName 文档名称,用于识别格式      * @return 提取的纯文本内容      * @throws IOException IO异常      * @throws TikaException 文档解析异常      */     public String extractText(InputStream inputStream, String fileName) throws IOException, TikaException {         if (ObjectUtils.isEmpty(inputStream)) {             throw new IllegalArgumentException("文档输入流不能为空");         }         if (!org.springframework.util.StringUtils.hasText(fileName)) {             throw new IllegalArgumentException("文档名称不能为空");         }         log.info("开始解析文档,fileName:{}", fileName);         String content = tika.parseToString(inputStream);         log.info("文档解析完成,文本长度:{}", content.length());         return content;     } } 

然后实现文本分块工具类,基于SpringAI的TokenTextSplitter,按Token数量拆分,确保每个文本块的Token数量符合Embedding模型与大模型的上下文限制,这是企业级最佳实践,比按字符拆分更精准:

package com.jam.demo.util; import org.springframework.ai.document.Document; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.List; import java.util.Map; /**  * 文本分块工具类  * @author ken  * @date 2026-02-27  */ @Component public class TextSplitUtil {     private static final int DEFAULT_CHUNK_SIZE = 800;     private static final int DEFAULT_CHUNK_OVERLAP = 150;     private final TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(             DEFAULT_CHUNK_SIZE,             DEFAULT_CHUNK_OVERLAP,             5,             10000     );     /**      * 文本分块处理      * @param content 原始文本内容      * @param metadata 分块元数据,会附加到每个Document对象中      * @return 拆分后的文档对象列表      */     public List<Document> splitText(String content, Map<String, Object> metadata) {         if (!StringUtils.hasText(content)) {             throw new IllegalArgumentException("文本内容不能为空");         }         Document document = new Document(content, metadata);         return tokenTextSplitter.split(List.of(document));     } } 

这里必须讲透分块的核心逻辑:

  • chunk size过大:单个分块包含太多无关信息,检索精准度下降,容易引入噪声,导致大模型幻觉
  • chunk size过小:单个分块上下文信息不足,大模型无法理解完整语义,回答不完整
  • chunk overlap:相邻分块保留重叠内容,避免关键信息被拆分到两个分块中,导致检索遗漏
  • 企业级最佳实践:通用场景chunk size 500-1000,overlap 100-200;长文档场景chunk size 1000-2000,overlap 200-300;问答场景chunk size 300-500,overlap 50-100

4.2 文档元数据实体与Mapper层

基于MyBatisPlus实现文档分块元数据的持久化,实体类如下:

package com.jam.demo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("tb_document_chunk") @Schema(description = "文档分块元数据实体") public class DocumentChunkEntity {     @TableId(type = IdType.AUTO)     @Schema(description = "主键ID")     private Long id;     @Schema(description = "文档唯一ID")     private String documentId;     @Schema(description = "文档名称")     private String documentName;     @Schema(description = "分块序号")     private Integer chunkId;     @Schema(description = "分块文本内容")     private String chunkContent;     @Schema(description = "对应向量数据库的向量ID")     private String vectorId;     @Schema(description = "创建时间")     private LocalDateTime createTime;     @Schema(description = "更新时间")     private LocalDateTime updateTime; } 

Mapper接口,继承MyBatisPlus的BaseMapper:

package com.jam.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.jam.demo.entity.DocumentChunkEntity; import org.apache.ibatis.annotations.Mapper; @Mapper public interface DocumentChunkMapper extends BaseMapper<DocumentChunkEntity> { } 

4.3 文档上传与索引构建服务

实现文档上传、解析、分块、向量化、向量存储、元数据持久化的全流程服务,这里使用编程式事务,确保向量存储与数据库持久化的一致性:

package com.jam.demo.service; import com.jam.demo.entity.DocumentChunkEntity; import com.jam.demo.mapper.DocumentChunkMapper; import com.jam.demo.util.DocumentParseUtil; import com.jam.demo.util.TextSplitUtil; import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.apache.tika.exception.TikaException; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionTemplate; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.UUID; /**  * 文档索引服务,负责文档上传与RAG索引构建  * @author ken  * @date 2026-02-27  */ @Service @Slf4j public class DocumentIndexService {     private final DocumentParseUtil documentParseUtil;     private final TextSplitUtil textSplitUtil;     private final VectorStore vectorStore;     private final DocumentChunkMapper documentChunkMapper;     private final TransactionTemplate transactionTemplate;     public DocumentIndexService(DocumentParseUtil documentParseUtil, TextSplitUtil textSplitUtil,                                 VectorStore vectorStore, DocumentChunkMapper documentChunkMapper,                                 TransactionTemplate transactionTemplate) {         this.documentParseUtil = documentParseUtil;         this.textSplitUtil = textSplitUtil;         this.vectorStore = vectorStore;         this.documentChunkMapper = documentChunkMapper;         this.transactionTemplate = transactionTemplate;     }     /**      * 文档上传与索引构建全流程      * @param file 上传的文档文件      * @return 文档唯一ID      * @throws IOException IO异常      * @throws TikaException 文档解析异常      */     public String uploadDocumentAndBuildIndex(MultipartFile file) throws IOException, TikaException {         if (file.isEmpty()) {             throw new IllegalArgumentException("上传的文件不能为空");         }         String fileName = file.getOriginalFilename();         if (!StringUtils.hasText(fileName)) {             throw new IllegalArgumentException("文件名称不能为空");         }         String documentId = UUID.randomUUID().toString().replace("-", "");         log.info("开始处理文档,documentId:{}, fileName:{}", documentId, fileName);         String content = documentParseUtil.extractText(file.getInputStream(), fileName);         if (!StringUtils.hasText(content)) {             throw new RuntimeException("文档内容为空,无法构建索引");         }         Map<String, Object> metadata = Map.of("documentId", documentId, "fileName", fileName);         List<Document> documentList = textSplitUtil.splitText(content, metadata);         if (CollectionUtils.isEmpty(documentList)) {             throw new RuntimeException("文本分块结果为空,无法构建索引");         }         log.info("文档分块完成,分块数量:{}", documentList.size());         vectorStore.add(documentList);         Boolean executeResult = transactionTemplate.execute(status -> {             try {                 List<DocumentChunkEntity> entityList = Lists.newArrayListWithCapacity(documentList.size());                 for (int i = 0; i < documentList.size(); i++) {                     Document doc = documentList.get(i);                     DocumentChunkEntity entity = new DocumentChunkEntity();                     entity.setDocumentId(documentId);                     entity.setDocumentName(fileName);                     entity.setChunkId(i + 1);                     entity.setChunkContent(doc.getText());                     entity.setVectorId(doc.getId());                     entityList.add(entity);                 }                 documentChunkMapper.insertBatch(entityList);                 return Boolean.TRUE;             } catch (Exception e) {                 status.setRollbackOnly();                 log.error("文档元数据持久化失败,documentId:{}", documentId, e);                 return Boolean.FALSE;             }         });         if (!Boolean.TRUE.equals(executeResult)) {             List<String> vectorIdList = documentList.stream().map(Document::getId).toList();             vectorStore.delete(vectorIdList);             throw new RuntimeException("文档索引构建失败,事务回滚");         }         log.info("文档索引构建完成,documentId:{}", documentId);         return documentId;     } } 

Controller层接口,支持文档上传:

package com.jam.demo.controller; import com.jam.demo.service.DocumentIndexService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.apache.tika.exception.TikaException; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @RestController @RequestMapping("/document") @Tag(name = "文档管理接口", description = "RAG文档上传、索引构建、管理接口") public class DocumentController {     private final DocumentIndexService documentIndexService;     public DocumentController(DocumentIndexService documentIndexService) {         this.documentIndexService = documentIndexService;     }     @PostMapping("/upload")     @Operation(summary = "文档上传与索引构建", description = "支持txt、md、pdf、docx等格式,自动完成解析、分块、向量化、索引构建全流程")     public ResponseEntity<String> uploadDocument(@RequestParam("file") MultipartFile file) throws IOException, TikaException {         String documentId = documentIndexService.uploadDocumentAndBuildIndex(file);         return ResponseEntity.ok("文档索引构建成功,documentId:" + documentId);     } } 

这里必须说明:编程式事务的使用场景,因为向量数据库的操作不在Spring的事务管理范围内,所以采用编程式事务,先写入向量数据库,再写入MySQL,如果MySQL写入失败,回滚MySQL的同时删除向量数据库中的数据,保证数据的最终一致性,这是企业级开发的最佳实践。

4.4 智能检索与RAG问答实现

智能检索是RAG的核心环节,核心是将用户的问题向量化,然后从向量数据库中检索出语义最相似的文本块,作为上下文提供给大模型,实现基于私有知识库的精准问答。 首先实现RAG检索服务,核心检索逻辑:

package com.jam.demo.service; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.List; /**  * RAG智能检索服务  * @author ken  * @date 2026-02-27  */ @Service @Slf4j public class RagRetrievalService {     private final VectorStore vectorStore;     private static final int DEFAULT_TOP_K = 4;     private static final double DEFAULT_SIMILARITY_THRESHOLD = 0.75;     public RagRetrievalService(VectorStore vectorStore) {         this.vectorStore = vectorStore;     }     /**      * 基础向量相似度检索      * @param query 用户提问内容      * @return 召回的相关文档列表      */     public List<Document> retrieval(String query) {         if (!StringUtils.hasText(query)) {             throw new IllegalArgumentException("检索query不能为空");         }         log.info("开始执行RAG检索,query:{}", query);         SearchRequest searchRequest = SearchRequest.builder()                 .query(query)                 .topK(DEFAULT_TOP_K)                 .similarityThreshold(DEFAULT_SIMILARITY_THRESHOLD)                 .build();         List<Document> documentList = vectorStore.similaritySearch(searchRequest);         log.info("RAG检索完成,召回文档数量:{}", documentList.size());         return documentList;     }     /**      * 自定义参数的检索      * @param query 用户提问内容      * @param topK 召回数量      * @param similarityThreshold 相似度阈值      * @return 召回的相关文档列表      */     public List<Document> retrievalWithParams(String query, int topK, double similarityThreshold) {         if (!StringUtils.hasText(query)) {             throw new IllegalArgumentException("检索query不能为空");         }         if (topK < 1 || topK > 20) {             throw new IllegalArgumentException("topK必须在1-20之间");         }         if (similarityThreshold < 0 || similarityThreshold > 1) {             throw new IllegalArgumentException("相似度阈值必须在0-1之间");         }         SearchRequest searchRequest = SearchRequest.builder()                 .query(query)                 .topK(topK)                 .similarityThreshold(similarityThreshold)                 .build();         return vectorStore.similaritySearch(searchRequest);     } } 

然后实现RAG问答服务,将检索到的上下文与用户问题拼接成Prompt,输入大模型生成回答,核心是Prompt工程,这是决定RAG效果的关键:

package com.jam.demo.service; import com.jam.demo.dto.ChatRequestDTO; import com.jam.demo.dto.ChatResponseDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.document.Document; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.stream.Collectors; /**  * RAG智能问答服务  * @author ken  * @date 2026-02-27  */ @Service @Slf4j public class RagChatService {     private final ChatClient chatClient;     private final RagRetrievalService ragRetrievalService;     private static final String RAG_SYSTEM_PROMPT = """             你是一名专业的智能问答助手,必须严格基于以下提供的参考上下文回答用户的问题,禁止编造信息。             1. 如果参考上下文中包含用户问题的答案,必须基于上下文内容进行回答,回答要准确、完整、逻辑清晰             2. 如果参考上下文中没有用户问题的相关内容,必须直接回答"参考资料中没有找到相关内容,无法为您解答该问题",禁止编造任何信息             3. 禁止使用参考上下文以外的任何知识回答问题,即使你知道该问题的答案             4. 回答时不要提及"参考上下文"、"参考资料"等相关词汇,直接给出答案即可             以下是参考上下文:             {context}             """;     public RagChatService(ChatClient.Builder chatClientBuilder, RagRetrievalService ragRetrievalService) {         this.chatClient = chatClientBuilder.build();         this.ragRetrievalService = ragRetrievalService;     }     /**      * RAG智能问答核心方法      * @param requestDTO 问答请求参数      * @return 大模型生成的回答结果      */     public ChatResponseDTO ragChat(ChatRequestDTO requestDTO) {         String query = requestDTO.getQuery();         log.info("收到RAG问答请求,query:{}", query);         List<Document> documentList = ragRetrievalService.retrieval(query);         String context;         if (CollectionUtils.isEmpty(documentList)) {             context = "无相关参考内容";         } else {             context = documentList.stream()                     .map(Document::getText)                     .collect(Collectors.joining("\n\n"));         }         String systemPrompt = RAG_SYSTEM_PROMPT.replace("{context}", context);         ChatResponse chatResponse = chatClient.prompt()                 .system(systemPrompt)                 .user(query)                 .call()                 .chatResponse();         String answer = chatResponse.getResult().getOutput().getText();         Long promptTokens = chatResponse.getMetadata().getUsage().getPromptTokens();         Long completionTokens = chatResponse.getMetadata().getUsage().getCompletionTokens();         Long totalTokens = chatResponse.getMetadata().getUsage().getTotalTokens();         log.info("RAG问答完成,总Token消耗:{}", totalTokens);         return new ChatResponseDTO(answer, promptTokens, completionTokens, totalTokens);     } } 

Controller层新增RAG问答接口:

@PostMapping("/rag") @Operation(summary = "RAG智能问答接口", description = "基于私有知识库的智能问答,自动检索相关内容,避免大模型幻觉") public ResponseEntity<ChatResponseDTO> ragChat(@Valid @RequestBody ChatRequestDTO requestDTO) {     return ResponseEntity.ok(ragChatService.ragChat(requestDTO)); } 

这里必须讲透RAG系统提示词的核心逻辑:

  • 必须严格约束大模型仅基于提供的上下文回答,这是避免幻觉的核心
  • 必须明确告知大模型,没有相关内容时应该如何回答,避免编造信息
  • 必须禁止大模型使用自身的预训练知识回答,确保回答完全来自私有知识库
  • 上下文内容必须放在系统提示词的末尾,避免被用户的输入覆盖
  • 企业级最佳实践:系统提示词要简洁、明确、无歧义,避免复杂的逻辑,减少大模型的理解成本

五、生产级最佳实践与踩坑指南

5.1 RAG效果优化核心方案

RAG的效果取决于检索的精准度与Prompt的质量,以下是经过生产验证的优化方案:

  1. 混合检索优化:基础的向量检索只能捕捉语义相似度,无法捕捉关键词匹配,企业级场景必须采用「向量检索+关键词检索」的混合检索方案,将两种检索结果进行融合重排序,大幅提升召回率。关键词检索可采用BM25算法,基于MySQL的全文索引实现,然后将两种检索结果的得分进行加权融合,再取TopK返回。
  2. 分块策略优化:针对不同类型的文档采用不同的分块策略,例如:结构化文档(表格、代码)采用语义分块,避免拆分破坏结构;长文档采用层级分块,先按章节拆分,再按段落拆分,保留层级结构;问答对文档采用问答对分块,每个问答对作为一个分块。
  3. 重排序(Reranker)优化:检索阶段先召回Top20的结果,然后用Reranker模型对召回结果进行精细的语义重排序,再取Top4输入大模型,大幅提升检索精准度,解决向量检索的粗粒度问题。SpringAI支持主流的Reranker模型,可直接集成。
  4. 多轮对话优化:多轮对话场景下,先将历史对话与当前问题一起输入大模型,生成一个独立的检索Query,再用这个Query进行检索,避免历史对话导致检索偏移,这是多轮RAG的核心优化点,行业内称为Query改写。

5.2 常见踩坑与解决方案

踩坑场景问题原因解决方案
向量写入失败,提示维度不匹配Embedding模型的输出维度与向量数据库配置的维度不一致切换Embedding模型时,必须同步修改向量数据库的维度配置,确保完全一致
大模型频繁出现幻觉,回答与上下文不符系统提示词约束不足、检索噪声太多、分块不合理优化系统提示词,严格约束大模型行为;提高相似度阈值,减少噪声;优化分块策略,确保分块语义完整
流式接口响应中断,出现乱码前端EventSource配置错误、后端响应格式不符合SSE规范确保接口produces设置为text/event-stream,每条响应以data: 开头,以\n\n结尾;前端EventSource配置正确的URL
文档解析乱码,中文显示异常文档编码格式不兼容、Tika解析器配置错误升级Tika到最新稳定版,指定文档编码格式为UTF-8;避免上传加密的文档
高并发场景下大模型接口限流大模型API有QPS限制,并发请求超过阈值集成Resilience4j实现限流与重试机制;采用请求池化,控制并发请求数量;异步处理非实时请求
向量数据库检索性能下降向量数据量过大,索引配置不合理针对海量数据采用HNSW索引,优化索引参数;对数据进行分区,按时间或业务维度分区检索;定期优化向量数据库的索引

5.3 安全与合规最佳实践

  1. 数据安全:私有文档数据必须存储在企业内部,禁止上传到大模型服务商的训练环境;Embedding处理必须在企业内部完成,仅将向量化后的向量数据存入向量数据库,避免原始数据泄露。
  2. 权限控制:对接Spring Security实现文档的权限隔离,不同用户只能检索自己有权限的文档,检索时增加权限过滤条件,避免越权访问。
  3. 内容安全:集成内容安全审核接口,对用户的输入与大模型的输出进行审核,过滤敏感内容,避免违规内容生成。
  4. 可追溯性:所有的对话请求、检索结果、大模型响应都必须持久化到数据库,实现全链路可追溯,满足合规审计要求。

六、总结

SpringAI为Java开发者提供了一套完整的企业级AI应用开发解决方案,它原生适配Spring生态,屏蔽了底层大模型与向量数据库的异构差异,让开发者可以专注于业务逻辑的实现,无需处理复杂的SDK对接与适配工作。RAG技术则解决了大模型接入私有数据的核心痛点,让企业可以基于自身的私有知识库,快速落地智能问答、智能客服、文档审核、知识管理等AI应用,无需投入大量的算力与数据进行大模型微调。 本文从大模型对接、文档处理、向量存储、智能检索到RAG问答,完整覆盖了企业级AI应用开发的全流程,开发者可基于此项目快速扩展,落地符合自身业务需求的AI应用。 随着生成式AI技术的不断发展,SpringAI与RAG技术将成为Java企业级AI开发的标准方案,掌握这套技术栈,将大幅提升开发者的核心竞争力,在AI时代的企业级开发中占据先机。

Read more

Cursor IDE 中 Java 项目无法跳转到方法定义问题解决方案

问题描述 在 Cursor IDE 中打开 Maven Java 项目时,点击方法(如 Cmd+Click 或 Ctrl+Click)无法跳转到方法定义,Go to Definition 功能失效。 问题原因 1. Java 语言服务器未正确启动或索引未完成 2. Maven 项目未正确导入或依赖未下载 3. Java 扩展未安装或配置不正确 4. 工作区配置问题 5. Java 环境路径未正确配置 解决方案 方案一:清理并重新加载 Java 语言服务器(推荐) 1. 打开命令面板: * macOS: Cmd + Shift + P * Windows/Linux: Ctrl

By Ne0inhk
Java 大视界 -- Java 大数据在智能教育学习社区互动模式创新与用户活跃度提升中的应用(426)

Java 大视界 -- Java 大数据在智能教育学习社区互动模式创新与用户活跃度提升中的应用(426)

Java 大视界 -- Java 大数据在智能教育学习社区互动模式创新与用户活跃度提升中的应用(426) * 引言: * 正文: * 一、智能教育社区的互动痛点与 Java 大数据的破局思路 * 1.1 三大核心痛点:从数据看互动效率低下的根源 * 1.2 Java 大数据的破局逻辑:用 “数据驱动” 替代 “经验判断” * 1.2.1 互动行为数据化 * 1.2.2 匹配逻辑算法化 * 1.2.3 互动过程实时化 * 二、Java 大数据技术栈的架构设计:支撑千万级用户互动的底层逻辑 * 2.1 整体架构:五层联动的互动引擎 * 2.1.1 数据采集层:全链路捕捉互动信号 * 2.

By Ne0inhk
2023第十四届蓝桥杯大赛软件赛国赛C/C++ 大学 B 组(真题&题解)(C++/Java题解)

2023第十四届蓝桥杯大赛软件赛国赛C/C++ 大学 B 组(真题&题解)(C++/Java题解)

本来想刷省赛题呢,结果一不小心刷成国赛了 真是个小迷糊〒▽〒 但,又如何( •̀ ω •́ )✧ 记录刷题的过程、感悟、题解。 希望能帮到,那些与我一同前行的,来自远方的朋友😉 注:感谢@Witton的提示,题目部分已完成修改( •̀ ω •́ )y 大纲: 一、子2023-(题解)-递推or动态规划 二、双子数-(题解)-筛法、类型(unsigned long long)😥 三、班级活动-(题解)-不出所料、贪心+计数 四、合并数列-(题解)-妥妥的前缀和😥,当然双指针也能做 五、数三角-(题解)-这个真的就是算术题了,还要用到各种优化(叉乘、用半径分组) 六、

By Ne0inhk
Java 虚拟机:JVM篇(八股)

Java 虚拟机:JVM篇(八股)

📌JVM篇 1.1 说一下JVM的内存结构?哪些是线程共享的,哪些是线程私有的? ✅ 正确回答思路: 这个问题我从JVM运行时数据区的5个部分来回答,先说整体结构,再说线程共享和私有的区别。 一、JVM运行时数据区的5个部分: JVM运行时数据区 ├── 线程共享区域 │ ├── 堆(Heap) ← 存储对象实例 │ └── 方法区(Method Area) ← 存储类信息、常量、静态变量 │ └── 运行时常量池 │ └── 线程私有区域 ├── 程序计数器(PC Register) ← 记录当前线程执行的字节码行号 ├── 虚拟机栈(VM Stack) ← 存储局部变量、操作数栈、方法出口 └── 本地方法栈(Native Stack) ← 为Native方法服务 详细说每一部分: 1. 堆(Heap)—— 线程共享 * 作用:存放对象实例和数组,几乎所有的对象实例都在这里分配内存 * 结构:分为新生代(Young

By Ne0inhk