上集回顾:老张给AI装上了"第二大脑"(向量库),但用户反馈检索效果不稳定。今天我们来聊聊如何调优RAG的3个关键参数。
一、故事继续:调优之路
周四上午,老张收到了一份详细的反馈报告:
检索质量统计(过去一周):
- 一部分查询能精准命中
- 另一部分要么没找到文档,要么找到了但不相关
- 还有少量查询超时
失败原因大致三类:
1. 未找到相关文档(阈值/分块问题)
2. 找到但答案不相关(topK/重排问题)
3. 检索超时(性能问题)
老张的诊断:"问题出在三个方面:阈值太高导致漏召回、分块不当影响检索精度、topK设置不合理。"
二、参数一:topK------返回多少结果?
2.1 topK是什么?
java
SearchRequest request = SearchRequest.builder()
.query(userQuestion)
.topK(5) // ← 返回最相关的5个文本块
.build();
老张的类比:"topK就像考试时允许你翻几页书------翻太少可能找不到答案,翻太多容易看花眼。"
2.2 topK的影响
| topK值 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 1-3 | 精准、快速 | 可能遗漏相关信息 | 简单问答 |
| 5-10 | 平衡 | 适中复杂度 | 一般场景 |
| 10+ | 召回率高 | Token消耗大、响应慢 | 复杂查询 |
2.3 实战:动态topK
java
public List<Document> smartSearch(String query, int baseTopK) {
// 根据问题复杂度动态调整
int topK = query.contains("?") || query.length() > 20
? baseTopK * 2 // 复杂问题,多检索
: baseTopK; // 简单问题,少检索
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(topK)
.build();
return vectorStore.similaritySearch(request);
}
三、参数二:相似度阈值------多相似才算相关?
3.1 阈值是什么?
java
SearchRequest request = SearchRequest.builder()
.query(userQuestion)
.similarityThreshold(0.7) // ← 只有相似度≥0.7的才返回
.build();
老张的理解:"阈值就像招聘的分数线------太高了招不到人,太低了鱼龙混杂。"
3.2 阈值的影响
| 阈值 | 效果 | 风险 |
|---|---|---|
| 0.9+ | 极度精准 | 召回率低,很多相关文档被过滤 |
| 0.7-0.9 | 平衡 | 推荐范围 |
| 0.5-0.7 | 高召回 | 可能返回不相关结果 |
| <0.5 | 几乎全召回 | 噪声大,干扰生成 |
3.3 调参技巧
java
/**
* 智能阈值调整
*/
public List<Document> adaptiveSearch(String query) {
// 尝试不同的阈值,选择最佳效果
List<Document> results = vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(10)
.similarityThreshold(0.6) // 先放宽
.build()
);
// 如果结果太少,再放宽
if (results.size() < 3) {
results = vectorStore.similaritySearch(
SearchRequest.builder()
.query(query)
.topK(15)
.similarityThreshold(0.5)
.build()
);
}
return results;
}
四、参数三:文本分块策略------怎么切?
4.1 分块策略对比
| 策略 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度 | 每N个token切一刀 | 简单快速 | 可能切断语义 |
| 按段落 | 以换行符为边界 | 保持段落完整 | 段落长短不一 |
| 按句子 | 以句号/问号切分 | 粒度细 | 句子可能太长 |
| 语义切分 | 按内容相关度切分 | 最智能 | 计算开销大 |
4.2 推荐策略:重叠窗口
java
// Spring AI 1.0:TokenTextSplitter 构造参数为
// (chunkSize, minChunkSizeChars, maxChunkSizeChars, overlap, maxNumChunks)
TextSplitter splitter = new TokenTextSplitter(
500, // 每个 chunk 最大 500 个 token
100, // 最小 chunk 字符数
800, // 最大 chunk 字符数
50, // 重叠 50 个 token(保持上下文连贯)
10000 // 最大 chunk 数量上限
);
老张的发现:"重叠50个token是关键------这样相邻的chunk能保持上下文连贯,检索时不会断章取义。"
4.3 元数据增强
java
// 给每个chunk添加元数据
Document doc = new Document(
"我们的ERP系统支持采购管理...",
Map.of(
"source", "产品手册v2.0.pdf",
"page", "15",
"section", "采购管理模块",
"documentType", "product-manual"
)
);
// 检索时可以按元数据过滤
SearchRequest request = SearchRequest.builder()
.query("采购流程")
.filterExpression("metadata.documentType == 'product-manual'")
.topK(5)
.build();
五、综合调优:老张的方案
5.1 三级检索策略
java
@Service
public class OptimizedKnowledgeService {
@Autowired
private VectorStore vectorStore;
/**
* 三级检索策略
*/
public String smartRagQuery(String question) {
// 第一级:宽松检索,确保召回
List<Document> candidates = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(10)
.similarityThreshold(0.5)
.build()
);
if (candidates.isEmpty()) {
return "抱歉,我在知识库中没找到相关信息,请尝试换个问法或联系人工客服。";
}
// 第二级:按阈值排序,取高质量结果
List<Document> highQuality = candidates.stream()
.filter(doc -> doc.getMetadata().getScore() >= 0.7)
.limit(5)
.collect(Collectors.toList());
// 第三级:如果高质量结果不足,用全部结果
List<Document> finalDocs = highQuality.size() >= 3
? highQuality
: candidates.subList(0, Math.min(5, candidates.size()));
// 构建增强Prompt
String context = formatContext(finalDocs);
String prompt = buildPrompt(question, context);
// 调用AI生成答案
return chatClient.prompt().user(prompt).call().content();
}
}
5.2 调优思路演示
| 参数 | 调优方向 | 取舍 |
|---|---|---|
| topK | 太小漏召回,太大噪声多 | 业务文档规模小时从 3-5 起步,规模大时 5-10 |
| 相似度阈值 | 太高过滤掉相关文档,太低引入噪声 | 先在测试集上扫一遍 0.5-0.9 的分布再定 |
| 分块策略 | 固定长度 vs 段落 vs 重叠窗口 | 中文建议段落级 + 少量重叠,保语义完整 |
注:以上为教学演示的调优思路,具体最优值因数据分布与 Embedding 模型而异,请在自己的知识库上实测对比。
六、长期优化建议
6.1 A/B测试
java
// 持续监控和调优
@Scheduled(fixedRate = 86400000) // 每天
public void analyzeSearchQuality() {
List<SearchLog> logs = searchLogRepository.findLastWeek();
// 分析低质量查询
List<String> badQueries = logs.stream()
.filter(log -> log.getScore() < 0.6)
.map(SearchLog::getQuery)
.distinct()
.limit(50)
.collect(Collectors.toList());
// 针对性优化
for (String query : badQueries) {
optimizeForQuery(query);
}
}
6.2 知识库更新
java
// 定期重新索引
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点
public void dailyIndex() {
vectorStore.deleteAll();
List<Document> allDocs = documentRepository.findAllActive();
TextSplitter splitter = createOptimizedSplitter();
List<Document> chunks = splitter.apply(allDocs);
vectorStore.add(chunks);
log.info("知识库重建完成,共 {} 个文本块", chunks.size());
}
七、下一集预告
老张的RAG系统终于稳定了,但张姐又提了新需求:
"能不能让用户直接上传PDF,自动就能问答,不用我们手动配置?"
第7集《给AI装上工具箱:@Tool注解实战》将讲述如何让AI具备调用工具的能力,实现从"问答"到"干活"的跨越。
📦 完整代码
GitHub分支:https://github.com/yinclei/SpringAITutorial/chapter06-rag-tuning
💬 互动环节
老兵问答:
你在RAG项目中遇到的最大挑战是什么?
A. 检索召回率低
B. 答案质量不稳定
C. 系统性能瓶颈
D. 部署运维复杂
下周一晚9点,我们继续!
作者:Java老兵搞AI | 关注我,看Java如何玩转AI