一、RAG 技术全景与架构设计
1.1 为什么需要 RAG?
┌─────────────────────────────────────────────────────────────────┐
│ 大模型的局限性 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 知识截止:训练数据有截止时间,无法获取最新信息 │
│ ❌ 幻觉问题:可能生成看似合理但实际错误的内容 │
│ ❌ 私有数据:无法访问企业内部文档和数据 │
│ ❌ 领域专业:缺乏特定领域的专业知识 │
│ ❌ 不可追溯:无法提供答案的来源和依据 │
│ │
│ RAG 解决方案: │
│ ✅ 检索外部知识库 → 增强上下文 → 生成准确答案 │
│ │
└─────────────────────────────────────────────────────────────────┘
RAG 的核心价值:
| 问题 |
传统 LLM |
RAG 增强 |
| 知识时效 |
训练数据截止 |
实时检索最新数据 |
| 准确性 |
可能产生幻觉 |
基于检索内容生成 |
| 私有数据 |
无法访问 |
可检索内部文档 |
| 可追溯性 |
无来源 |
可追溯文档来源 |
| 成本 |
需要微调 |
无需训练,成本低 |
1.2 RAG 工作原理
┌─────────────────────────────────────────────────────────────────┐
│ RAG 完整工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【离线流程】文档处理与向量化 │
│ │
│ 原始文档 文本分块 向量化 存储 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ PDF │ │ Chunk 1│ │ Vector │ │ 向量 │ │
│ │ Word │ ───▶ │ Chunk 2│ ───▶ │ Embed │ ───▶ │ 数据库 │ │
│ │ Markdown│ │ Chunk 3│ │ [768] │ │ │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ 【在线流程】检索增强生成 │
│ │
│ 用户问题 语义检索 增强提示 生成答案 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 用户 │ │ 向量 │ │ LLM │ │ 带 │ │
│ │ 提问 │ ───▶ │ 检索 │ ───▶ │ Prompt │ ───▶ │ 来源 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │ │ │ │ │
│ │ "如何请假?" │ Top-K 相似 │ 问题 + 上下文 │ "根据...│ │
│ │ │ 文档片段 │ │ 第 5 条 │ │
│ │ │ │ │ ..." │ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 RAG 系统架构
┌─────────────────────────────────────────────────────────────────┐
│ Spring AI RAG 系统架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ API Gateway │ │
│ │ (Spring Boot + REST API) │ │
│ └────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 文档管理 │ │ 检索服务 │ │ 对话服务 │ │
│ │ Service │ │ Service │ │ Service │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Vector │ │ Embedding │ │ LLM │ │
│ │ Database │ │ Service │ │ Client │ │
│ │ (pgvector) │ │ (本地/云) │ │ (OpenAI 等) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 核心组件: │
│ • DocumentReader - 文档加载器(PDF/Word/Markdown) │
│ • TextSplitter - 文本分块器 │
│ • EmbeddingModel - 向量化模型 │
│ • VectorStore - 向量存储(pgvector/Milvus/Chroma) │
│ • RetrievalTemplate - 检索模板 │
│ • ChatClient - 大模型客户端 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.4 技术选型
┌─────────────────────────────────────────────────────────────────┐
│ 技术栈选型 │
├──────────────────┬──────────────────────────────────────────────┤
│ 组件类型 │ 推荐方案 │
├──────────────────┼──────────────────────────────────────────────┤
│ 开发框架 │ Spring Boot 3.x + Spring AI 1.x │
├──────────────────┼──────────────────────────────────────────────┤
│ 向量数据库 │ PostgreSQL + pgvector(推荐) │
│ │ Milvus / Chroma / Weaviate(可选) │
├──────────────────┼──────────────────────────────────────────────┤
│ Embedding 模型 │ OpenAI text-embedding-3-small(云端) │
│ │ BGE-M3 / m3e-base(本地部署) │
├──────────────────┼──────────────────────────────────────────────┤
│ 大语言模型 │ OpenAI GPT-4 / GPT-3.5(云端) │
│ │ Azure OpenAI / 通义千问(企业) │
│ │ Ollama + Llama3(本地部署) │
├──────────────────┼──────────────────────────────────────────────┤
│ 文档处理 │ Apache Tika / Spring AI DocumentReader │
├──────────────────┼──────────────────────────────────────────────┤
│ 文本分块 │ RecursiveCharacterTextSplitter │
├──────────────────┼──────────────────────────────────────────────┤
│ 缓存 │ Redis(缓存检索结果和对话历史) │
├──────────────────┼──────────────────────────────────────────────┤
│ 消息队列 │ RabbitMQ / Kafka(异步文档处理) │
└──────────────────┴──────────────────────────────────────────────┘
二、Spring AI 快速入门
2.1 什么是 Spring AI?
Spring AI 是 Spring 官方推出的 AI 应用开发框架,为 Java 开发者提供:
- 🎯 统一的 API 抽象:屏蔽不同 AI 服务商的差异
- 🎯 开箱即用的集成:支持主流大模型和向量数据库
- 🎯 Spring 生态融合:与 Spring Boot、Spring Data 无缝集成
- 🎯 企业级特性:事务管理、监控、安全等
2.2 项目初始化
Maven 依赖配置
<!-- 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
http://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.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-rag-demo</artifactId>
<version>1.0.0</version>
<name>Spring AI RAG Demo</name>
<description>Spring AI RAG 知识库系统</description>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI pgvector -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- PostgreSQL 驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring AI 文档处理 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
应用配置
# application.yml
spring:
application:
name: spring-ai-rag-demo
# 数据源配置
datasource:
url: jdbc:postgresql://localhost:5432/rag_db
username: postgres
password: postgres123
driver-class-name: org.postgresql.Driver
# JPA 配置
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
# Redis 配置
data:
redis:
host: localhost
port: 6379
database: 0
# Spring AI 配置
spring:
ai:
# OpenAI 配置
openai:
api-key: ${OPENAI_API_KEY:sk-your-api-key}
chat:
options:
model: gpt-3.5-turbo
temperature: 0.7
max-tokens: 1000
embedding:
options:
model: text-embedding-3-small
dimensions: 1536
# pgvector 配置
pgvector:
store:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
# 业务配置
rag:
# 检索配置
retrieval:
# 检索文档数量
top-k: 4
# 相似度阈值
similarity-threshold: 0.7
# 是否启用元数据过滤
enable-metadata-filter: true
# 文本分块配置
chunking:
# 分块大小
chunk-size: 500
# 重叠大小
chunk-overlap: 50
# 文档处理配置
document:
# 上传目录
upload-dir: ./uploads
# 支持的扩展名
allowed-extensions: pdf,doc,docx,txt,md
# 日志配置
logging:
level:
com.example.rag: DEBUG
org.springframework.ai: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
2.3 核心配置类
// config/RagConfig.java
package com.example.rag.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* RAG 核心配置类
*/
@Slf4j
@Configuration
public class RagConfig {
/**
* 聊天客户端(带 RAG 增强)
*/
@Bean
@Primary
public ChatClient ragChatClient(ChatModel chatModel, VectorStore vectorStore) {
return ChatClient.builder(chatModel)
.defaultSystem("""
你是一个智能助手,基于知识库内容回答用户问题。
请遵循以下原则:
1. 优先使用检索到的上下文信息回答问题
2. 如果上下文不足以回答问题,请诚实告知
3. 引用上下文时请注明来源
4. 回答应简洁明了,避免冗长
""")
.build();
}
/**
* 普通聊天客户端(无 RAG)
*/
@Bean
public ChatClient defaultChatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
/**
* 向量存储配置
*/
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel,
PgVectorStore pgVectorStore) {
// 使用 pgvector 作为向量存储
return pgVectorStore;
}
}
三、向量数据库搭建
3.1 PostgreSQL + pgvector 安装
Docker 快速部署
# 拉取带 pgvector 的 PostgreSQL 镜像
docker pull pgvector/pgvector:pg16
# 启动容器
docker run -d \
--name postgres-pgvector \
-e POSTGRES_PASSWORD=postgres123 \
-e POSTGRES_DB=rag_db \
-p 5432:5432 \
-v pgvector_data:/var/lib/postgresql/data \
pgvector/pgvector:pg16
# 验证安装
docker exec -it postgres-pgvector psql -U postgres -d rag_db -c "CREATE EXTENSION IF NOT EXISTS vector;"
docker exec -it postgres-pgvector psql -U postgres -d rag_db -c "\dx"
手动安装(Linux)
# 安装 PostgreSQL 16
sudo apt update
sudo apt install postgresql-16 postgresql-contrib-16
# 安装 pgvector
cd /tmp
git clone --branch v0.6.0 https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install
# 启用扩展
sudo -u postgres psql
CREATE EXTENSION vector;
\dx
3.2 数据库初始化
-- 创建数据库
CREATE DATABASE rag_db;
\c rag_db
-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建向量表(Spring AI 会自动创建,这里展示手动创建)
CREATE TABLE IF NOT EXISTS vector_store (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT,
embedding vector(1536),
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建向量索引(HNSW)
CREATE INDEX IF NOT EXISTS vector_index
ON vector_store
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 创建文档元数据表
CREATE TABLE IF NOT EXISTS document_metadata (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
filename VARCHAR(255) NOT NULL,
file_type VARCHAR(50),
file_size BIGINT,
chunk_count INTEGER,
status VARCHAR(20) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP,
error_message TEXT
);
-- 创建索引
CREATE INDEX idx_document_status ON document_metadata(status);
CREATE INDEX idx_document_created ON document_metadata(created_at);
-- 创建检索历史表
CREATE TABLE IF NOT EXISTS retrieval_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
query_text TEXT NOT NULL,
query_embedding vector(1536),
results_count INTEGER,
response_text TEXT,
latency_ms INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_retrieval_created ON retrieval_history(created_at);
3.3 向量数据库对比
┌─────────────────────────────────────────────────────────────────┐
│ 向量数据库选型对比 │
├──────────────────┬──────────────────────────────────────────────┤
│ 数据库 │ 特点与适用场景 │
├──────────────────┼──────────────────────────────────────────────┤
│ PostgreSQL │ ✅ 成熟稳定,支持事务和复杂查询 │
│ + pgvector │ ✅ 与现有 PostgreSQL 生态无缝集成 │
│ │ ✅ 适合中小规模(百万级向量) │
│ │ ⚠️ 超大规模性能不如专用向量数据库 │
├──────────────────┼──────────────────────────────────────────────┤
│ Milvus │ ✅ 专为向量搜索设计,性能优秀 │
│ │ ✅ 支持十亿级向量 │
│ │ ✅ 丰富的索引类型和距离度量 │
│ │ ⚠️ 运维复杂度较高 │
├──────────────────┼──────────────────────────────────────────────┤
│ Chroma │ ✅ 轻量级,易于部署 │
│ │ ✅ 适合开发和原型 │
│ │ ⚠️ 生产环境成熟度待验证 │
├──────────────────┼──────────────────────────────────────────────┤
│ Weaviate │ ✅ 内置向量 + 图数据库 │
│ │ ✅ 支持混合搜索(向量 + 关键词) │
│ │ ⚠️ 学习曲线较陡 │
├──────────────────┼──────────────────────────────────────────────┤
│ Redis │ ✅ 超低延迟,适合缓存层 │
│ + RedisSearch │ ✅ 支持向量搜索模块 │
│ │ ⚠️ 内存成本高,不适合大规模存储 │
└──────────────────┴──────────────────────────────────────────────┘
推荐:
• 初创/中小项目:PostgreSQL + pgvector
• 大规模生产:Milvus
• 快速原型:Chroma
四、文档处理与向量化
4.1 文档加载器
// service/DocumentLoaderService.java
package com.example.rag.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
/**
* 文档加载服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DocumentLoaderService {
private final String uploadDir = "./uploads";
/**
* 上传并加载文档
*/
public List<Document> loadDocument(MultipartFile file) throws IOException {
// 1. 保存文件
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
Path filePath = uploadPath.resolve(filename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
log.info("文件已保存:{}", filePath);
// 2. 使用 Tika 加载文档
Resource resource = new UrlResource(filePath.toUri());
TikaDocumentReader reader = new TikaDocumentReader(resource);
// 3. 读取文档内容
List<Document> documents = reader.get();
log.info("加载文档成功,文档数量:{}", documents.size());
// 4. 添加元数据
for (Document doc : documents) {
doc.getMetadata().put("filename", filename);
doc.getMetadata().put("original_filename", file.getOriginalFilename());
doc.getMetadata().put("content_type", file.getContentType());
doc.getMetadata().put("size", file.getSize());
doc.getMetadata().put("upload_time", System.currentTimeMillis());
}
return documents;
}
/**
* 从目录加载多个文档
*/
public List<Document> loadDocumentsFromDirectory(String directoryPath) {
List<Document> allDocuments = new ArrayList<>();
Path directory = Paths.get(directoryPath);
try {
Files.walk(directory)
.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return name.endsWith(".pdf") || name.endsWith(".doc")
|| name.endsWith(".docx") || name.endsWith(".txt")
|| name.endsWith(".md");
})
.forEach(path -> {
try {
Resource resource = new UrlResource(path.toUri());
TikaDocumentReader reader = new TikaDocumentReader(resource);
List<Document> docs = reader.get();
for (Document doc : docs) {
doc.getMetadata().put("filename", path.getFileName().toString());
doc.getMetadata().put("filepath", path.toString());
}
allDocuments.addAll(docs);
log.info("加载文档:{}", path.getFileName());
} catch (Exception e) {
log.error("加载文档失败:{}", path, e);
}
});
} catch (IOException e) {
log.error("遍历目录失败:{}", directoryPath, e);
}
log.info("总共加载文档:{} 个", allDocuments.size());
return allDocuments;
}
}
4.2 文本分块器
// config/TextSplitterConfig.java
package com.example.rag.config;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 文本分块配置
*/
@Configuration
public class TextSplitterConfig {
/**
* 基于 Token 的文本分块器
*/
@Bean
public TextSplitter tokenTextSplitter() {
return new TokenTextSplitter(
500, // chunkSize: 每块最大 token 数
50, // chunkOverlap: 块之间重叠的 token 数
1000, // maxChunkSize: 单个块的最大 token 数
250, // minChunkSize: 单个块的最小 token 数
10000 // maxDocumentLength: 单个文档的最大 token 数
);
}
/**
* 基于字符的文本分块器(备选)
*/
@Bean
public TextSplitter characterTextSplitter() {
return new org.springframework.ai.transformer.splitter.RecursiveCharacterTextSplitter(
500, // chunkSize: 每块最大字符数
50 // chunkOverlap: 块之间重叠的字符数
);
}
}
// service/TextProcessingService.java
package com.example.rag.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 文本处理服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TextProcessingService {
private final TextSplitter textSplitter;
/**
* 对文档进行分块
*/
public List<Document> splitDocuments(List<Document> documents) {
log.info("开始文本分块,文档数量:{}", documents.size());
List<Document> chunks = textSplitter.apply(documents);
log.info("分块完成,块数量:{}", chunks.size());
// 为每个块添加序号
for (int i = 0; i < chunks.size(); i++) {
chunks.get(i).getMetadata().put("chunk_index", i);
chunks.get(i).getMetadata().put("total_chunks", chunks.size());
}
return chunks;
}
/**
* 对单个文档进行分块
*/
public List<Document> splitDocument(Document document) {
return splitDocuments(List.of(document));
}
}
4.3 向量化与存储
// service/VectorStoreService.java
package com.example.rag.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 向量存储服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VectorStoreService {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
/**
* 存储文档(自动向量化)
*/
@Transactional
public void addDocuments(List<Document> documents) {
log.info("开始存储文档,数量:{}", documents.size());
// Spring AI 会自动处理向量化和存储
vectorStore.add(documents);
log.info("文档存储完成");
}
/**
* 存储单个文档
*/
@Transactional
public void addDocument(Document document) {
addDocuments(List.of(document));
}
/**
* 语义搜索
*/
public List<Document> search(String query, int topK) {
log.info("执行语义搜索,query: {}, topK: {}", query, topK);
SearchRequest searchRequest = SearchRequest.query(query)
.withTopK(topK);
List<Document> results = vectorStore.similaritySearch(searchRequest);
log.info("搜索完成,返回 {} 条结果", results.size());
return results;
}
/**
* 带过滤的语义搜索
*/
public List<Document> searchWithFilter(String query, int topK, Map<String, Object> filters) {
log.info("执行带过滤的语义搜索,query: {}, filters: {}", query, filters);
SearchRequest searchRequest = SearchRequest.query(query)
.withTopK(topK);
// 构建过滤表达式
if (filters != null && !filters.isEmpty()) {
Filter.Expression filterExpression = buildFilterExpression(filters);
searchRequest = searchRequest.withFilterExpression(filterExpression);
}
List<Document> results = vectorStore.similaritySearch(searchRequest);
log.info("搜索完成,返回 {} 条结果", results.size());
return results;
}
/**
* 删除文档
*/
@Transactional
public void deleteDocuments(List<String> documentIds) {
log.info("删除文档,IDs: {}", documentIds);
vectorStore.delete(documentIds);
}
/**
* 构建过滤表达式
*/
private Filter.Expression buildFilterExpression(Map<String, Object> filters) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
for (Map.Entry<String, Object> entry : filters.entrySet()) {
builder.eq(entry.getKey(), entry.getValue().toString());
}
return builder.build();
}
/**
* 获取向量维度
*/
public int getEmbeddingDimensions() {
// 测试获取向量维度
EmbeddingResponse response = embeddingModel.embedForResponse(List.of("test"));
return response.getResult().getOutput().length;
}
}
五、RAG 检索与生成
5.1 检索服务
// service/RetrievalService.java
package com.example.rag.service;
import lombok.Data;
import lombok.RequiredArgsConstructor;
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 java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 检索服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RetrievalService {
private final VectorStore vectorStore;
private final int topK = 4;
private final double similarityThreshold = 0.7;
/**
* 检索相关文档
*/
public RetrievalResult retrieve(String query) {
log.info("执行检索,query: {}", query);
long startTime = System.currentTimeMillis();
// 1. 执行相似度搜索
SearchRequest searchRequest = SearchRequest.query(query)
.withTopK(topK);
List<Document> documents = vectorStore.similaritySearch(searchRequest);
// 2. 过滤低相似度结果
List<RetrievedDocument> filteredDocs = documents.stream()
.map(doc -> {
double similarity = calculateSimilarity(doc, query);
return new RetrievedDocument(
doc.getId(),
doc.getContent(),
doc.getMetadata(),
similarity
);
})
.filter(doc -> doc.getSimilarity() >= similarityThreshold)
.collect(Collectors.toList());
long latency = System.currentTimeMillis() - startTime;
log.info("检索完成,耗时:{}ms, 返回:{} 条结果", latency, filteredDocs.size());
return new RetrievalResult(query, filteredDocs, latency);
}
/**
* 计算相似度(简化版本)
*/
private double calculateSimilarity(Document doc, String query) {
// 实际应用中应该计算向量相似度
// 这里简化处理
return 0.85;
}
/**
* 构建增强 Prompt
*/
public String buildEnhancedPrompt(String query, List<RetrievedDocument> documents) {
StringBuilder context = new StringBuilder();
context.append("以下是相关的知识库内容:\n\n");
for (int i = 0; i < documents.size(); i++) {
RetrievedDocument doc = documents.get(i);
context.append(String.format("[来源 %d]\n", i + 1));
context.append(doc.getContent()).append("\n\n");
if (doc.getMetadata().containsKey("filename")) {
context.append(String.format("来源文件:%s\n\n", doc.getMetadata().get("filename")));
}
}
context.append("请根据以上信息回答用户的问题:\n");
context.append("用户问题:").append(query);
return context.toString();
}
@Data
@RequiredArgsConstructor
public static class RetrievalResult {
private final String query;
private final List<RetrievedDocument> documents;
private final long latencyMs;
}
@Data
@RequiredArgsConstructor
public static class RetrievedDocument {
private final String id;
private final String content;
private final Map<String, Object> metadata;
private final double similarity;
}
}
5.2 RAG 聊天服务
// service/RagChatService.java
package com.example.rag.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* RAG 聊天服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RagChatService {
private final ChatClient ragChatClient;
private final RetrievalService retrievalService;
/**
* 基于 RAG 的聊天
*/
public ChatResponse chat(String query, String conversationId) {
log.info("RAG 聊天请求,conversationId: {}, query: {}", conversationId, query);
// 1. 检索相关文档
RetrievalService.RetrievalResult retrievalResult = retrievalService.retrieve(query);
// 2. 构建增强 Prompt
String enhancedPrompt = buildRagPrompt(query, retrievalResult);
// 3. 调用大模型生成回答
String response = ragChatClient.prompt()
.user(enhancedPrompt)
.call()
.content();
log.info("RAG 聊天完成,response length: {}", response.length());
return new ChatResponse(
query,
response,
retrievalResult.getDocuments(),
retrievalResult.getLatencyMs()
);
}
/**
* 带对话历史的 RAG 聊天
*/
public ChatResponse chatWithHistory(String query, List<Message> history, String conversationId) {
log.info("带历史的 RAG 聊天,conversationId: {}, history size: {}", conversationId, history.size());
// 1. 检索相关文档
RetrievalService.RetrievalResult retrievalResult = retrievalService.retrieve(query);
// 2. 构建消息列表
List<Message> messages = new ArrayList<>(history);
// 3. 添加系统提示(包含检索到的上下文)
String systemPrompt = buildSystemPrompt(retrievalResult);
messages.add(0, new Message() {
@Override
public org.springframework.ai.chat.messages.MessageType getMessageType() {
return org.springframework.ai.chat.messages.MessageType.SYSTEM;
}
@Override
public String getContent() {
return systemPrompt;
}
});
// 4. 添加当前用户问题
messages.add(new UserMessage(query));
// 5. 调用大模型
Prompt prompt = new Prompt(messages);
String response = ragChatClient.prompt(prompt)
.call()
.content();
return new ChatResponse(
query,
response,
retrievalResult.getDocuments(),
retrievalResult.getLatencyMs()
);
}
/**
* 构建 RAG Prompt
*/
private String buildRagPrompt(String query, RetrievalService.RetrievalResult retrievalResult) {
PromptTemplate template = new PromptTemplate("""
你是一个智能助手,请基于以下知识库内容回答用户问题。
【知识库内容】
{context}
【回答要求】
1. 优先使用知识库内容回答问题
2. 如果知识库内容不足以回答问题,请诚实告知
3. 引用知识库内容时请注明来源
4. 回答应简洁明了
【用户问题】
{question}
【你的回答】
""");
Map<String, Object> params = new HashMap<>();
params.put("question", query);
params.put("context", buildContext(retrievalResult));
return template.render(params);
}
/**
* 构建系统 Prompt
*/
private String buildSystemPrompt(RetrievalService.RetrievalResult retrievalResult) {
return """
你是一个智能助手,基于知识库内容回答用户问题。
可用信息:
""" + buildContext(retrievalResult);
}
/**
* 构建上下文字符串
*/
private String buildContext(RetrievalService.RetrievalResult retrievalResult) {
StringBuilder context = new StringBuilder();
for (int i = 0; i < retrievalResult.getDocuments().size(); i++) {
RetrievalService.RetrievedDocument doc = retrievalResult.getDocuments().get(i);
context.append(String.format("\n[文档 %d]\n", i + 1));
context.append(doc.getContent());
if (doc.getMetadata().containsKey("filename")) {
context.append(String.format("\n来源:%s", doc.getMetadata().get("filename")));
}
context.append(String.format("\n相似度:%.2f", doc.getSimilarity()));
}
return context.toString();
}
/**
* 聊天响应
*/
public record ChatResponse(
String question,
String answer,
List<RetrievalService.RetrievedDocument> sourceDocuments,
long retrievalLatencyMs
) {}
}
5.3 REST API 控制器
// controller/RagController.java
package com.example.rag.controller;
import com.example.rag.service.RagChatService;
import com.example.rag.service.DocumentLoaderService;
import com.example.rag.service.TextProcessingService;
import com.example.rag.service.VectorStoreService;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* RAG API 控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagController {
private final RagChatService ragChatService;
private final DocumentLoaderService documentLoaderService;
private final TextProcessingService textProcessingService;
private final VectorStoreService vectorStoreService;
/**
* 聊天接口
*/
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
log.info("收到聊天请求,query: {}", request.getQuery());
RagChatService.ChatResponse response = ragChatService.chat(
request.getQuery(),
request.getConversationId()
);
return ResponseEntity.ok(new ChatResponse(
response.question(),
response.answer(),
response.sourceDocuments().stream()
.map(doc -> new SourceDocument(
doc.getId(),
doc.getContent(),
doc.getMetadata(),
doc.getSimilarity()
))
.toList(),
response.retrievalLatencyMs()
));
}
/**
* 上传文档
*/
@PostMapping("/documents/upload")
public ResponseEntity<Map<String, Object>> uploadDocument(
@RequestParam("file") MultipartFile file) throws IOException {
log.info("收到文档上传请求,filename: {}", file.getOriginalFilename());
// 1. 加载文档
List<Document> documents = documentLoaderService.loadDocument(file);
// 2. 文本分块
List<Document> chunks = textProcessingService.splitDocuments(documents);
// 3. 向量化并存储
vectorStoreService.addDocuments(chunks);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "文档处理完成");
result.put("chunks", chunks.size());
return ResponseEntity.ok(result);
}
/**
* 批量导入文档
*/
@PostMapping("/documents/import")
public ResponseEntity<Map<String, Object>> importDocuments(
@RequestParam("directory") String directory) {
log.info("收到批量导入请求,directory: {}", directory);
// 1. 加载文档
List<Document> documents = documentLoaderService.loadDocumentsFromDirectory(directory);
// 2. 文本分块
List<Document> chunks = textProcessingService.splitDocuments(documents);
// 3. 向量化并存储
vectorStoreService.addDocuments(chunks);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "批量导入完成");
result.put("documents", documents.size());
result.put("chunks", chunks.size());
return ResponseEntity.ok(result);
}
/**
* 搜索接口
*/
@GetMapping("/search")
public ResponseEntity<SearchResponse> search(
@RequestParam("query") String query,
@RequestParam(value = "topK", defaultValue = "4") int topK) {
log.info("收到搜索请求,query: {}", query);
// 执行搜索(这里简化处理,实际应该调用检索服务)
List<Document> results = vectorStoreService.search(query, topK);
List<SearchResult> searchResults = results.stream()
.map(doc -> new SearchResult(
doc.getId(),
doc.getContent(),
doc.getMetadata()
))
.toList();
return ResponseEntity.ok(new SearchResponse(query, searchResults));
}
// ==================== DTO 类 ====================
@Data
public static class ChatRequest {
@NotBlank(message = "问题不能为空")
private String query;
private String conversationId;
}
@Data
public static class ChatResponse {
private final String question;
private final String answer;
private final List<SourceDocument> sourceDocuments;
private final long retrievalLatencyMs;
}
@Data
public static class SourceDocument {
private final String id;
private final String content;
private final Map<String, Object> metadata;
private final double similarity;
}
@Data
public static class SearchResponse {
private final String query;
private final List<SearchResult> results;
}
@Data
public static class SearchResult {
private final String id;
private final String content;
private final Map<String, Object> metadata;
}
}
六、完整实战案例:企业知识库系统
6.1 项目结构
spring-ai-rag-demo/
├── src/main/java/com/example/rag/
│ ├── RagApplication.java # 启动类
│ ├── config/
│ │ ├── RagConfig.java # RAG 配置
│ │ ├── TextSplitterConfig.java # 分块配置
│ │ └── RedisConfig.java # Redis 配置
│ ├── controller/
│ │ ├── RagController.java # RAG API
│ │ ├── DocumentController.java # 文档管理 API
│ │ └── ConversationController.java # 对话管理 API
│ ├── service/
│ │ ├── RagChatService.java # RAG 聊天服务
│ │ ├── RetrievalService.java # 检索服务
│ │ ├── VectorStoreService.java # 向量存储服务
│ │ ├── DocumentLoaderService.java # 文档加载服务
│ │ ├── TextProcessingService.java # 文本处理服务
│ │ └── ConversationService.java # 对话历史服务
│ ├── entity/
│ │ ├── DocumentMetadata.java # 文档元数据实体
│ │ └── Conversation.java # 对话实体
│ ├── repository/
│ │ ├── DocumentMetadataRepository.java
│ │ └── ConversationRepository.java
│ ├── dto/
│ │ ├── request/
│ │ └── response/
│ └── util/
│ └── EmbeddingUtils.java # 向量化辅助工具
├── src/main/resources/
│ ├── application.yml
│ ├── application-dev.yml
│ └── application-prod.yml
├── src/test/java/
├── docker/
│ ├── docker-compose.yml
│ └── pgvector/
│ └── init.sql
├── uploads/ # 文档上传目录
├── pom.xml
└── README.md
6.2 Docker Compose 配置
# docker/docker-compose.yml
version: '3.8'
services:
# PostgreSQL + pgvector
postgres:
image: pgvector/pgvector:pg16
container_name: rag-postgres
environment:
POSTGRES_DB: rag_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres123
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./pgvector/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis
redis:
image: redis:7-alpine
container_name: rag-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Spring Boot 应用
app:
build: ..
container_name: rag-app
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: docker
OPENAI_API_KEY: ${OPENAI_API_KEY}
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/rag_db
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: postgres123
SPRING_DATA_REDIS_HOST: redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./uploads:/app/uploads
volumes:
postgres_data:
redis_data:
6.3 对话历史管理
// service/ConversationService.java
package com.example.rag.service;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 对话历史服务(使用 Redis 存储)
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ConversationService {
private final RedisTemplate<String, Object> redisTemplate;
private final long ttlHours = 24; // 对话历史保留 24 小时
/**
* 添加对话消息
*/
public void addMessage(String conversationId, Message message) {
String key = buildKey(conversationId);
List<Message> messages = getHistory(conversationId);
messages.add(message);
// 限制历史消息数量
if (messages.size() > 20) {
messages = messages.subList(messages.size() - 20, messages.size());
}
redisTemplate.opsForList().setRange(key, 0, messages);
redisTemplate.expire(key, ttlHours, TimeUnit.HOURS);
log.debug("添加对话消息,conversationId: {}, total: {}", conversationId, messages.size());
}
/**
* 获取对话历史
*/
public List<Message> getHistory(String conversationId) {
String key = buildKey(conversationId);
List<Object> messages = redisTemplate.opsForList().range(key, 0, -1);
if (messages == null || messages.isEmpty()) {
return new ArrayList<>();
}
return messages.stream()
.map(msg -> (Message) msg)
.toList();
}
/**
* 清除对话历史
*/
public void clearHistory(String conversationId) {
String key = buildKey(conversationId);
redisTemplate.delete(key);
log.info("清除对话历史,conversationId: {}", conversationId);
}
/**
* 构建用户对话消息
*/
public Message createUserMessage(String content) {
return new UserMessage(content);
}
/**
* 构建助手对话消息
*/
public Message createAssistantMessage(String content) {
return new AssistantMessage(content);
}
private String buildKey(String conversationId) {
return "conversation:" + conversationId;
}
@Data
public static class ConversationMessage {
private String role;
private String content;
private long timestamp;
}
}
6.4 文档管理 API
// controller/DocumentController.java
package com.example.rag.controller;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 文档管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/documents")
@RequiredArgsConstructor
public class DocumentController {
private final VectorStore vectorStore;
/**
* 上传单个文档
*/
@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadDocument(
@RequestParam("file") MultipartFile file) {
log.info("上传文档:{}", file.getOriginalFilename());
// 处理文档上传逻辑
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("filename", file.getOriginalFilename());
result.put("size", file.getSize());
return ResponseEntity.ok(result);
}
/**
* 删除文档
*/
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> deleteDocument(
@PathVariable String id) {
log.info("删除文档:{}", id);
vectorStore.delete(List.of(id));
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "文档已删除");
return ResponseEntity.ok(result);
}
/**
* 列出所有文档
*/
@GetMapping("/list")
public ResponseEntity<List<DocumentInfo>> listDocuments() {
// 实现文档列表查询
return ResponseEntity.ok(List.of());
}
@Data
public static class DocumentInfo {
private String id;
private String filename;
private long size;
private String contentType;
private long createdAt;
}
}
七、性能优化与最佳实践
7.1 检索性能优化
// config/RetrievalOptimizationConfig.java
package com.example.rag.config;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 检索优化配置
*/
@Configuration
public class RetrievalOptimizationConfig {
/**
* 优化策略 1:混合搜索(向量 + 关键词)
*/
public SearchRequest buildHybridSearch(String query, int topK) {
return SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.7);
}
/**
* 优化策略 2:元数据过滤
*/
public SearchRequest buildFilteredSearch(String query, String docType) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
return SearchRequest.query(query)
.withTopK(10)
.withFilterExpression(builder.eq("doc_type", docType).build());
}
/**
* 优化策略 3:多路召回
*/
// 使用不同的检索策略,然后合并结果
}
7.2 缓存优化
// service/CachedRetrievalService.java
package com.example.rag.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 带缓存的检索服务
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CachedRetrievalService {
private final RetrievalService retrievalService;
private final RedisTemplate<String, Object> redisTemplate;
private final long cacheTtlMinutes = 30;
/**
* 带缓存的检索
*/
public RetrievalService.RetrievalResult retrieveWithCache(String query) {
String cacheKey = "retrieval:" + hashQuery(query);
// 1. 尝试从缓存获取
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
log.info("缓存命中,query: {}", query);
return (RetrievalService.RetrievalResult) cached;
}
// 2. 执行检索
RetrievalService.RetrievalResult result = retrievalService.retrieve(query);
// 3. 存入缓存
redisTemplate.opsForValue().set(cacheKey, result, cacheTtlMinutes, TimeUnit.MINUTES);
return result;
}
private String hashQuery(String query) {
return Integer.toHexString(query.hashCode());
}
}
7.3 监控与日志
// aspect/RetrievalMetricsAspect.java
package com.example.rag.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 检索指标监控
*/
@Slf4j
@Aspect
@Component
public class RetrievalMetricsAspect {
@Around("execution(* com.example.rag.service.RetrievalService.retrieve(..))")
public Object measureRetrieval(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long latency = System.currentTimeMillis() - startTime;
log.info("检索指标 - 耗时:{}ms, 参数:{}", latency, joinPoint.getArgs()[0]);
// 可以发送到监控系统(Prometheus、Micrometer 等)
return result;
} catch (Exception e) {
log.error("检索异常", e);
throw e;
}
}
}
7.4 最佳实践清单
┌─────────────────────────────────────────────────────────────────┐
│ RAG 系统最佳实践 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 文档处理: │
│ □ 选择合适的分块大小(500-1000 tokens) │
│ □ 设置合理的重叠(10-20%) │
│ □ 保留文档元数据(来源、时间、类型) │
│ □ 异步处理大文档上传 │
│ │
│ 向量化: │
│ □ 选择适合中文的 Embedding 模型 │
│ □ 统一向量维度(1536 或 768) │
│ □ 批量处理提高吞吐量 │
│ │
│ 检索优化: │
│ □ 设置合理的 Top-K(3-5) │
│ □ 使用相似度阈值过滤 │
│ □ 实现检索结果缓存 │
│ □ 考虑混合搜索(向量 + 关键词) │
│ │
│ Prompt 工程: │
│ □ 清晰的系统提示 │
│ □ 明确的回答要求 │
│ □ 包含来源引用 │
│ □ 处理知识不足的情况 │
│ │
│ 性能优化: │
│ □ 向量数据库索引(HNSW) │
│ □ Redis 缓存检索结果 │
│ □ 异步文档处理 │
│ □ 连接池优化 │
│ │
│ 安全与监控: │
│ □ API 认证与授权 │
│ □ 输入验证与过滤 │
│ □ 检索日志记录 │
│ □ 性能指标监控 │
│ □ 错误告警机制 │
│ │
└─────────────────────────────────────────────────────────────────┘
八、常见问题与解决方案
Q1: 检索结果不相关怎么办?
解决方案:
- 调整 Embedding 模型(尝试不同的模型)
- 优化文本分块策略(调整 chunk size)
- 增加元数据过滤
- 使用混合搜索(向量 + 关键词)
- 调整相似度阈值
Q2: 回答质量不高?
解决方案:
- 优化 Prompt 模板
- 增加检索文档数量(Top-K)
- 改进文档质量
- 调整温度参数(temperature)
- 添加 Few-Shot 示例
Q3: 响应速度慢?
解决方案:
- 启用检索结果缓存
- 优化向量数据库索引
- 减少 Top-K 数量
- 使用更快的 Embedding 模型
- 异步处理文档入库
Q4: 如何处理多轮对话?
解决方案:
// 使用对话历史增强检索
public String buildQueryWithContext(String currentQuery, List<Message> history) {
// 从历史中提取关键信息
// 重写查询以包含上下文
return enhancedQuery;
}
九、总结
9.1 核心要点回顾
- RAG 架构:检索 + 增强 + 生成,解决 LLM 局限性
- Spring AI:统一的 API 抽象,简化 AI 应用开发
- 向量数据库:pgvector 适合中小规模,Milvus 适合大规模
- 文档处理:加载 → 分块 → 向量化 → 存储
- 检索优化:缓存、索引、混合搜索
- 生产实践:监控、安全、性能优化
9.2 后续扩展方向
- 📌 多模态 RAG(支持图片、表格)
- 📌 Graph RAG(知识图谱增强)
- 📌 Agentic RAG(Agent 自主检索)
- 📌 流式 RAG(实时数据检索)