上集回顾:老张给AI装上了"第二大脑"(向量库),但用户反馈检索效果不稳定。今天我们来聊聊如何调优RAG的3个关键参数。
一、故事继续:调优之路
周四上午,老张收到了一份详细的反馈报告:
检索质量统计(过去一周): - 一部分查询能精准命中 - 另一部分要么没找到文档,要么找到了但不相关 - 还有少量查询超时 失败原因大致三类: 1. 未找到相关文档(阈值/分块问题) 2. 找到但答案不相关(topK/重排问题) 3. 检索超时(性能问题)老张的诊断:“问题出在三个方面:阈值太高导致漏召回、分块不当影响检索精度、topK设置不合理。”
二、参数一:topK——返回多少结果?
2.1 topK是什么?
SearchRequestrequest=SearchRequest.builder().query(userQuestion).topK(5)// ← 返回最相关的5个文本块.build();老张的类比:“topK就像考试时允许你翻几页书——翻太少可能找不到答案,翻太多容易看花眼。”
2.2 topK的影响
| topK值 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 1-3 | 精准、快速 | 可能遗漏相关信息 | 简单问答 |
| 5-10 | 平衡 | 适中复杂度 | 一般场景 |
| 10+ | 召回率高 | Token消耗大、响应慢 | 复杂查询 |
2.3 实战:动态topK
publicList<Document>smartSearch(Stringquery,intbaseTopK){// 根据问题复杂度动态调整inttopK=query.contains("?")||query.length()>20?baseTopK*2// 复杂问题,多检索:baseTopK;// 简单问题,少检索SearchRequestrequest=SearchRequest.builder().query(query).topK(topK).build();returnvectorStore.similaritySearch(request);}三、参数二:相似度阈值——多相似才算相关?
3.1 阈值是什么?
SearchRequestrequest=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 调参技巧
/** * 智能阈值调整 */publicList<Document>adaptiveSearch(Stringquery){// 尝试不同的阈值,选择最佳效果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());}returnresults;}四、参数三:文本分块策略——怎么切?
4.1 分块策略对比
| 策略 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度 | 每N个token切一刀 | 简单快速 | 可能切断语义 |
| 按段落 | 以换行符为边界 | 保持段落完整 | 段落长短不一 |
| 按句子 | 以句号/问号切分 | 粒度细 | 句子可能太长 |
| 语义切分 | 按内容相关度切分 | 最智能 | 计算开销大 |
4.2 推荐策略:重叠窗口
// Spring AI 1.0:TokenTextSplitter 构造参数为// (chunkSize, minChunkSizeChars, maxChunkSizeChars, overlap, maxNumChunks)TextSplittersplitter=newTokenTextSplitter(500,// 每个 chunk 最大 500 个 token100,// 最小 chunk 字符数800,// 最大 chunk 字符数50,// 重叠 50 个 token(保持上下文连贯)10000// 最大 chunk 数量上限);老张的发现:“重叠50个token是关键——这样相邻的chunk能保持上下文连贯,检索时不会断章取义。”
4.3 元数据增强
// 给每个chunk添加元数据Documentdoc=newDocument("我们的ERP系统支持采购管理...",Map.of("source","产品手册v2.0.pdf","page","15","section","采购管理模块","documentType","product-manual"));// 检索时可以按元数据过滤SearchRequestrequest=SearchRequest.builder().query("采购流程").filterExpression("metadata.documentType == 'product-manual'").topK(5).build();五、综合调优:老张的方案
5.1 三级检索策略
@ServicepublicclassOptimizedKnowledgeService{@AutowiredprivateVectorStorevectorStore;/** * 三级检索策略 */publicStringsmartRagQuery(Stringquestion){// 第一级:宽松检索,确保召回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()));// 构建增强PromptStringcontext=formatContext(finalDocs);Stringprompt=buildPrompt(question,context);// 调用AI生成答案returnchatClient.prompt().user(prompt).call().content();}}5.2 调优思路演示
| 参数 | 调优方向 | 取舍 |
|---|---|---|
| topK | 太小漏召回,太大噪声多 | 业务文档规模小时从 3-5 起步,规模大时 5-10 |
| 相似度阈值 | 太高过滤掉相关文档,太低引入噪声 | 先在测试集上扫一遍 0.5-0.9 的分布再定 |
| 分块策略 | 固定长度 vs 段落 vs 重叠窗口 | 中文建议段落级 + 少量重叠,保语义完整 |
注:以上为教学演示的调优思路,具体最优值因数据分布与 Embedding 模型而异,请在自己的知识库上实测对比。
六、长期优化建议
6.1 A/B测试
// 持续监控和调优@Scheduled(fixedRate=86400000)// 每天publicvoidanalyzeSearchQuality(){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(Stringquery:badQueries){optimizeForQuery(query);}}6.2 知识库更新
// 定期重新索引@Scheduled(cron="0 0 3 * * ?")// 每天凌晨3点publicvoiddailyIndex(){vectorStore.deleteAll();List<Document>allDocs=documentRepository.findAllActive();TextSplittersplitter=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