通过测试用例完整掌握 RAG 流程
本章对应 Test09_RAGTutorial,通过一系列测试用例带你从 0 到 1 搭建
一个完整的 RAG(Retrieval-Augmented Generation,检索增强生成)流程。
EmbeddingModel 进行文本向量化SimpleVectorStoretext-embedding-v2)传统 AI:
用户:"公司2023年营收是多少?"
AI:"抱歉,我不知道..." ❌
RAG:
1. 从知识库检索相关文档
2. 找到:"2023年营收10亿元"
3. AI 基于文档回答:"根据资料,2023年营收10亿元" ✅创建文件:src/main/java/com/example/chatbot/util/VectorDistanceUtils.java
queryVector 与每个文档向量做相似度/距离计算,再按分数排序取 TopK。cos(a, b) = (Σ(ai * bi)) / (sqrt(Σ(ai^2)) * sqrt(Σ(bi^2)))
含义:比较“方向”是否一致;越接近 1 越相似(常见范围 [-1, 1],Embedding 场景多落在 [0, 1])。d(a, b) = sqrt( Σ( (ai - bi)^2 ) )
含义:比较“空间距离”;越小越相似(0 表示两个向量完全相同)。dotProduct 对应 Σ(ai*bi),norm 对应 sqrt(Σ(ai^2));欧氏距离就是先累加平方差再开方。package com.example.chatbot.util;
/**
* 向量距离计算工具类
*/
public class VectorDistanceUtils {
/**
* 计算余弦相似度,值越接近1越相似
*/
public static double cosineSimilarity(float[] vector1, float[] vector2) {
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
for (int i = 0; i < vector1.length; i++) {
dotProduct += vector1[i] * vector2[i];
norm1 += vector1[i] * vector1[i];
norm2 += vector2[i] * vector2[i];
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
/**
* 计算欧氏距离,值越小越相似
*/
public static double euclideanDistance(float[] vector1, float[] vector2) {
double sum = 0.0;
for (int i = 0; i < vector1.length; i++) {
double diff = vector1[i] - vector2[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
}mvn compile
mvn test-compile| 方法名称 | 数学公式 | 计算例子 (a=[1,2,3], b=[4,5,6]) | 适用场景 |
|---|---|---|---|
| 余弦相似度 | cos(a,b) = Σ(ai×bi) / (√Σ(ai²) × √Σ(bi²)) | (1×4+2×5+3×6) / (√14 × √77) = 32 / (√1078) ≈ 0.974 | 文本相似度、RAG检索 |
| 欧氏距离 | d(a,b) = √Σ(ai - bi)² | √[(1-4)² + (2-5)² + (3-6)²] = √[9+9+9] = √27 ≈ 5.196 | 聚类分析、图像检索 |
| 曼哈顿距离 | d(a,b) = Σ|ai - bi| | |1-4| + |2-5| + |3-6| = 3 + 3 + 3 = 9 | 特征选择、城市距离 |
| 切比雪夫距离 | d(a,b) = max|ai - bi| | max(|1-4|, |2-5|, |3-6|) = max(3, 3, 3) = 3 | 最坏情况分析 |
| 点积 | dot(a,b) = Σ(ai × bi) | 1×4 + 2×5 + 3×6 = 4 + 10 + 18 = 32 | 快速计算、神经网络 |
| 皮尔逊相关系数 | corr = Σ((xi-μx)(yi-μy)) / (√Σ(xi-μx)² × √Σ(yi-μy)²) | μx=2, μy=5 → 计算得 1.0 (完全正相关) | 时间序列、金融数据 |
| 杰卡德相似度 | J(A,B) = |A∩B| / |A∪B| | 二值化后计算重叠比例 | 二值向量、推荐系统 |
| 汉明距离 | d(a,b) = Σ(ai ≠ bi) | 计算不同位置的数量 | 二进制向量、错误检测 |
创建文件:src/test/java/com/example/chatbot/knowledge/Test09_RAGTutorial.java
package com.example.chatbot.knowledge;
import com.example.chatbot.util.VectorDistanceUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.*;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(classes = com.example.chatbot.AiChatbotApplication.class)
@DisplayName("知识点9:RAG 完整教程")
public class Test09_RAGTutorial {
@Autowired
private EmbeddingModel embeddingModel;
@Autowired
private ChatModel chatModel;
// 简单的内存向量库
static class SimpleVectorStore {
private final List<VectorDocument> documents = new ArrayList<>();
static class VectorDocument {
String id;
String content;
float[] vector;
Map<String, Object> metadata = new HashMap<>();
VectorDocument(String id, String content, float[] vector) {
this.id = id;
this.content = content;
this.vector = vector;
}
}
void add(String id, String content, float[] vector) {
documents.add(new VectorDocument(id, content, vector));
}
List<VectorDocument> search(float[] queryVector, int topK) {
return documents.stream()
.map(doc -> {
double similarity = VectorDistanceUtils.cosineSimilarity(
queryVector, doc.vector
);
doc.metadata.put("similarity", similarity);
return doc;
})
.sorted((a, b) -> Double.compare(
(Double) b.metadata.get("similarity"),
(Double) a.metadata.get("similarity")
))
.limit(topK)
.collect(Collectors.toList());
}
int size() {
return documents.size();
}
}
}/**
* 测试 9.1:验证 EmbeddingModel 注入
*/
@Test
@DisplayName("9.1 EmbeddingModel 注入")
public void testEmbeddingModelInjection() {
System.out.println("\n========== 测试:EmbeddingModel 注入 ==========");
assertNotNull(embeddingModel, "EmbeddingModel 应该被注入");
System.out.println("✅ EmbeddingModel 注入成功");
System.out.println("📝 类型: " + embeddingModel.getClass().getSimpleName());
System.out.println("✅ 测试通过");
}mvn test -Dtest=Test09_RAGTutorial#testEmbeddingModelInjection/**
* 测试 9.2:文本向量化
*/
@Test
@DisplayName("9.2 文本向量化")
public void testEmbedding() {
System.out.println("\n=== 测试向量化 ===");
String text = "Spring AI 是一个强大的框架";
System.out.println("📝 原始文本: " + text);
System.out.println("\n🔄 向量化中...");
var response = embeddingModel.embedForResponse(List.of(text));
float[] vector = response.getResults().get(0).getOutput();
System.out.println("✅ 向量化完成");
System.out.println("📊 向量维度: " + vector.length);
System.out.println("📊 前5个值: " + java.util.Arrays.toString(
java.util.Arrays.copyOfRange(vector, 0, Math.min(5, vector.length))
));
assertTrue(vector.length > 0, "向量不应为空");
assertEquals(1536, vector.length, "通义千问的向量维度应该是1536");
System.out.println("✅ 向量化测试通过");
}mvn test -Dtest=Test09_RAGTutorial#testEmbedding/**
* 测试 9.3:相似度计算
*/
@Test
@DisplayName("9.3 相似度计算")
public void testSimilarity() {
System.out.println("\n=== 测试相似度计算 ===");
String text1 = "我喜欢编程";
String text2 = "我热爱写代码";
String text3 = "今天天气真好";
System.out.println("📝 文本1: " + text1);
System.out.println("📝 文本2: " + text2);
System.out.println("📝 文本3: " + text3);
System.out.println("\n🔄 向量化中...");
java.util.List<float[]> vectors = embeddingModel.embed(java.util.List.of(text1, text2, text3));
System.out.println("\n📊 相似度计算:");
double sim12 = VectorDistanceUtils.cosineSimilarity(vectors.get(0), vectors.get(1));
double sim13 = VectorDistanceUtils.cosineSimilarity(vectors.get(0), vectors.get(2));
double sim23 = VectorDistanceUtils.cosineSimilarity(vectors.get(1), vectors.get(2));
System.out.println(" 文本1 vs 文本2: " + String.format("%.4f", sim12));
System.out.println(" 文本1 vs 文本3: " + String.format("%.4f", sim13));
System.out.println(" 文本2 vs 文本3: " + String.format("%.4f", sim23));
System.out.println("\n💡 分析:");
assertTrue(sim12 > sim13, "相似的文本(1和2)相似度应该更高");
assertTrue(sim12 > sim23, "相似的文本(1和2)相似度应该更高");
System.out.println(" ✅ 编程相关的文本确实更相似");
System.out.println("✅ 相似度测试通过");
}/**
* 测试 9.4:构建简单向量库
*/
@Test
@DisplayName("9.4 构建简单向量库")
public void testVectorStore() {
System.out.println("\n=== 测试向量库 ===");
java.util.List<String> documents = java.util.List.of(
"Spring AI 是 Spring 官方的 AI 集成框架",
"Spring Boot 简化了 Spring 应用的开发",
"Java 是一种面向对象的编程语言",
"Python 在数据科学领域很流行",
"RAG 是检索增强生成技术"
);
System.out.println("📚 准备知识库文档...");
documents.forEach(doc -> System.out.println(" - " + doc));
System.out.println("\n🔄 向量化文档...");
java.util.List<float[]> vectors = embeddingModel.embed(documents);
System.out.println("✅ 向量化完成,共 " + vectors.size() + " 个向量");
System.out.println("\n💾 存入向量库...");
SimpleVectorStore vectorStore = new SimpleVectorStore();
for (int i = 0; i < documents.size(); i++) {
vectorStore.add("doc-" + i, documents.get(i), vectors.get(i));
}
System.out.println("✅ 存储完成,向量库大小: " + vectorStore.size());
System.out.println("\n🔍 测试检索...");
String query = "什么是 Spring AI?";
System.out.println(" 查询: " + query);
float[] queryVector = embeddingModel.embed(query);
java.util.List<SimpleVectorStore.VectorDocument> results = vectorStore.search(queryVector, 3);
System.out.println("\n📊 検索结果(Top 3):");
for (int i = 0; i < results.size(); i++) {
var doc = results.get(i);
System.out.println(" " + (i + 1) + ". [相似度: " +
String.format("%.4f", doc.metadata.get("similarity")) + "] " +
doc.content);
}
assertTrue(results.get(0).content.contains("Spring AI"),
"最相关的文档应该包含 Spring AI");
System.out.println("\n✅ 向量库测试通过");
}/**
* 测试 9.5:完整 RAG 流程
*/
@Test
@DisplayName("9.5 完整 RAG 流程")
public void testCompleteRAG() {
System.out.println("\n=== 完整 RAG 流程 ===");
System.out.println("【步骤1:构建知识库】");
java.util.List<String> knowledge = java.util.List.of(
"公司成立于2020年,总部位于北京",
"2023年公司营收达到10亿元,同比增长50%",
"公司主要产品是AI助手,服务超过100万用户",
"公司有员工500人,其中研发人员占60%"
);
knowledge.forEach(doc -> System.out.println(" 📄 " + doc));
System.out.println("\n【步骤2:向量化并存储】");
SimpleVectorStore vectorStore = new SimpleVectorStore();
java.util.List<float[]> vectors = embeddingModel.embed(knowledge);
for (int i = 0; i < knowledge.size(); i++) {
vectorStore.add("doc-" + i, knowledge.get(i), vectors.get(i));
}
System.out.println(" ✅ 存储了 " + vectorStore.size() + " 个文档");
System.out.println("\n【步骤3:用户提问】");
String question = "公司2023年的营收是多少?";
System.out.println(" 👤 问题: " + question);
System.out.println("\n【步骤4:检索相关文档】");
float[] questionVector = embeddingModel.embed(question);
java.util.List<SimpleVectorStore.VectorDocument> relevantDocs =
vectorStore.search(questionVector, 2);
System.out.println(" 🔍 找到 " + relevantDocs.size() + " 个相关文档:");
relevantDocs.forEach(doc ->
System.out.println(" - " + doc.content)
);
System.out.println("\n【步骤5:构造增强 Prompt】");
String context = relevantDocs.stream()
.map(doc -> doc.content)
.collect(java.util.stream.Collectors.joining("\n"));
String enhancedPrompt = String.format(
"基于以下信息回答问题:\n\n%s\n\n问题:%s",
context, question
);
System.out.println(" 📝 增强后的 Prompt:\n " + enhancedPrompt.replace("\n", "\n "));
System.out.println("\n【步骤6:AI 生成回答】");
var response = chatModel.call(new org.springframework.ai.chat.prompt.Prompt(enhancedPrompt));
String answer = response.getResult().getOutput().getText();
System.out.println(" 🤖 AI 回答: " + answer);
assertTrue(answer.contains("10亿") || answer.contains("10 亿"),
"回答应该包含营收数据");
System.out.println("\n✅ 完整 RAG 流程测试通过");
}/**
* 测试 9.6:对比有无 RAG
*/
@Test
@DisplayName("9.6 对比有无 RAG")
public void testWithAndWithoutRAG() {
System.out.println("\n=== 对比有无 RAG ===");
java.util.List<String> knowledge = java.util.List.of(
"XYZ 公司成立于2020年",
"XYZ 公司2023年营收10亿元"
);
SimpleVectorStore vectorStore = new SimpleVectorStore();
java.util.List<float[]> vectors = embeddingModel.embed(knowledge);
for (int i = 0; i < knowledge.size(); i++) {
vectorStore.add("doc-" + i, knowledge.get(i), vectors.get(i));
}
String question = "XYZ 公司2023年营收多少?";
System.out.println("\n【测试1:没有 RAG】");
System.out.println("👤 问题: " + question);
var response1 = chatModel.call(new org.springframework.ai.chat.prompt.Prompt(question));
String answer1 = response1.getResult().getOutput().getText();
System.out.println("🤖 回答: " + answer1);
System.out.println("❌ AI 可能不知道答案");
System.out.println("\n【测试2:使用 RAG】");
float[] qVector = embeddingModel.embed(question);
java.util.List<SimpleVectorStore.VectorDocument> docs = vectorStore.search(qVector, 1);
String context = docs.get(0).content;
String enhancedPrompt = String.format(
"根据以下信息回答:%s\n\n问题:%s", context, question
);
var response2 = chatModel.call(new org.springframework.ai.chat.prompt.Prompt(enhancedPrompt));
String answer2 = response2.getResult().getOutput().getText();
System.out.println("🤖 回答: " + answer2);
System.out.println("✅ AI 基于知识库准确回答");
assertTrue(answer2.contains("10亿") || answer2.contains("10 亿"));
System.out.println("\n✅ 对比测试通过");
}
真实项目中通常会把向量存到向量数据库(便于持久化、并发、检索性能与规模扩展)。
当前这套项目里已验证可跑通的组合是:chromadb==0.4.24 + spring-ai-chroma-store-spring-boot-starter:1.0.0-M6,
本地服务监听 localhost:8000,并通过 ChromaDB 的 v1 API 正常读写集合。
chromadb==0.4.24
pip install chromadb==0.4.24
# 启动本地 ChromaDB 服务
chroma run --host localhost --port 8000# 检查 Python 版本(需要 3.7+)
python --version
# 如果没有 Python,先安装 Python 3.7+
# macOS: brew install python
# Ubuntu: sudo apt install python3 python3-pip
# Windows: 从 python.org 下载安装# 当前已验证版本:0.4.24
pip install chromadb==0.4.24
# 验证安装和版本
python -c "import chromadb; print(f'ChromaDB 版本: {chromadb.__version__}')"# 启动本地 ChromaDB 服务
chroma run --host localhost --port 8000
# 验证服务状态
curl http://localhost:8000/api/v1/heartbeat# 检查服务是否运行(v1 API)
curl http://localhost:8000/api/v1/heartbeat
# 预期返回:{"nanosecond heartbeat": 1234567890}
# 或者使用 Python 验证
python -c "
import chromadb
try:
client = chromadb.HttpClient(host='localhost', port=8000)
print('✅ ChromaDB v1 API 连接成功!')
print(f'集合数量: {len(client.list_collections())}')
except Exception as e:
print(f'❌ 连接失败: {e}')
"# 查看进程
ps aux | grep chroma
# 停止服务
pkill -f chroma
# 查看日志
tail -f chroma.log
# 重启服务
pkill -f chroma && chroma run --host localhost --port 8000<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chroma-store-spring-boot-starter</artifactId>
</dependency>0.4.24
1.0.0-M6
http://localhost:8000
v1
The v1 API is deprecated 的兼容性问题。
spring:
ai:
vectorstore:
chroma:
client:
host: http://localhost
port: 8000
collection-name: SpringAiCollection
initialize-schema: true<!-- Spring AI ChromaDB Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-chroma-store-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- 当前已验证:ChromaDB 0.4.24 + Spring AI 1.0.0-M6 -->import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
@Autowired
private VectorStore vectorStore;
@Test
@DisplayName("9.7 ChromaDB 向量库(可选)")
public void testChromaDbVectorStore() {
System.out.println("\n=== 测试 9.7:ChromaDB VectorStore ===");
java.util.List<Document> docs = java.util.List.of(
new Document("Spring AI rocks!!"),
new Document("RAG = 检索 + 增强 Prompt + 生成"),
new Document("向量数据库用于存储 embedding 并做相似度检索")
);
vectorStore.add(docs);
java.util.List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder().query("Spring AI").topK(2).build()
);
System.out.println("🔍 TopK 结果数量: " + results.size());
results.forEach(d -> System.out.println(" - " + d.getText()));
}
// 运行
// mvn test -Dtest=Test09_RAGTutorial#testChromaDbVectorStoreVectorStore 会在引入 starter 后自动装配;
如果你要接入 Chroma Cloud / 鉴权,再按官方文档改成手动创建 ChromaApi 与 ChromaVectorStore Bean。
chromadb==0.4.24,
所以只要保持版本不升级到 1.x,一般不会遇到这个错误;如果你升级了 ChromaDB,则需要重新处理 API 兼容。
pip install chromadb==0.4.24chroma run --host localhost --port 8000spring.ai.vectorstore.chroma
curl http://localhost:8000/api/v1/heartbeat运行完整的 RAG 教程测试类:
mvn test -Dtest=Test09_RAGTutorialTests run: 6, Failures: 0, Errors: 0, Skipped: 0
✅ 所有测试通过!请完成下面练习,巩固 RAG 的“向量检索 + Prompt 增强 + 生成”闭环能力
⚠️ 提示:本章高频点集中在 Embedding、相似度/TopK 检索、Chunk/Overlap、检索结果组织成 Prompt、以及“幻觉/可解释性”。
java.util.List<String> knowledge = java.util.List.of(
"XYZ 公司成立于2020年",
"XYZ 公司2023年营收10亿元",
"XYZ 公司总部位于杭州"
);
SimpleVectorStore store = new SimpleVectorStore();
java.util.List<float[]> vectors = embeddingModel.embed(knowledge);
for (int i = 0; i < knowledge.size(); i++) {
store.add("doc-" + i, knowledge.get(i), vectors.get(i));
}
String query = "XYZ 公司 2023 年的营收是多少?";
float[] q = embeddingModel.embed(query);
java.util.List<SimpleVectorStore.VectorDocument> top = store.search(q, 1);
String top1 = top.get(0).content;
System.out.println("Top1=" + top1);
System.out.println("similarity=" + top.get(0).metadata.get("similarity"));
assertTrue(top1.contains("营收10亿") || top1.contains("营收10 亿元"));static java.util.List<String> split(String text, int chunkSize, int overlapSize) {
java.util.List<String> chunks = new java.util.ArrayList<>();
int start = 0;
while (start < text.length()) {
int end = Math.min(text.length(), start + chunkSize);
chunks.add(text.substring(start, end));
if (end == text.length()) break;
start = Math.max(0, end - overlapSize);
}
return chunks;
}String context = java.util.stream.IntStream.range(0, relevantDocs.size())
.mapToObj(i -> "[" + (i + 1) + "] " + relevantDocs.get(i).content)
.collect(java.util.stream.Collectors.joining("\n"));
String promptText = "你是一个严谨的知识库问答助手。\n" +
"- 只能根据【资料】回答问题\n" +
"- 如果资料不足以回答,请直接说:资料不足,无法回答\n" +
"- 回答时请标注引用的资料编号,例如:[1][2]\n\n" +
"【资料】\n" + context + "\n\n" +
"【问题】\n" + question;
var resp = chatModel.call(new org.springframework.ai.chat.prompt.Prompt(promptText));
String answer = resp.getResult().getOutput().getText();
System.out.println(answer);spring-ai-chroma-store-spring-boot-starter,用 VectorStore 写入 3 条文档并检索 TopK。
docker run -it --rm --name chroma -p 8000:8000 ghcr.io/chroma-core/chroma:1.0.0java.util.List<org.springframework.ai.document.Document> docs = java.util.List.of(
new org.springframework.ai.document.Document("Spring AI rocks!!"),
new org.springframework.ai.document.Document("RAG = 检索 + 增强 Prompt + 生成"),
new org.springframework.ai.document.Document("向量数据库用于存储 embedding 并做相似度检索")
);
vectorStore.add(docs);
java.util.List<org.springframework.ai.document.Document> results = vectorStore.similaritySearch(
org.springframework.ai.vectorstore.SearchRequest.builder().query("Spring AI").topK(2).build()
);
results.forEach(d -> System.out.println(d.getText()));