RAG系统调优的血泪史:从50%到90%准确率的进化
一个从零搭建RAG系统的开发者,分享如何从"勉强能用"优化到"真正好用"的实战经验。没有理论堆砌,全是踩坑心得。
去年接了个活,给一家企业做内部知识库问答系统。客户要求不高:"能让员工问公司的规章制度就行"。
我一开始觉得这活简单——用现成的RAG框架,接个大模型,几天就能搞定。结果做了才发现,RAG的水深得离谱。
从最初的50%准确率(员工试了一圈都说"还不如自己翻文档"),到最后的90%(客户主动问"能不能扩展到其他业务"),中间踩的坑够写本书了。
第一版:能跑,但不好用
我的第一版架构很标准:
跑起来之后,员工反馈:"经常答非所问,问A它答B,要不就是编造不存在的政策"。
我做了个测试,拿了100个真实问题,手动评估答案质量。结果只有52个回答是准确或者基本准确的。
这还叫RAG?这不就是个随机回答生成器吗。
第一个优化:文档切块的方式不对
我一开始的切块策略是:固定长度,每块500字,重叠50字。
这看着很标准,但问题很大。因为我的文档是企业规章制度,里面有大量像这样的内容:
第十条 员工请假须提前3个工作日提交申请,
特殊情况可酌情放宽。
第十一条 请假审批流程:
(一)3天以内,部门主管审批;
(二)3天以上,分管领导审批。
按500字切的话,第十条和第十一条可能被分到两块里。当用户问"请假要提前多久",检索到的可能只有第十条前半部分,关键信息丢了。
**改进方案**:我改成了按段落和章节边界来切。先识别文档的自然分段(通过换行、编号、标题等),然后在这些边界处切。每块尽量保持逻辑完整性。
改完之后,准确率从52%提到了68%。
第二个优化:向量检索不够用
向量检索的问题是:它找的是"相似",不是"相关"。
用户问"请假的审批流程是什么",向量检索可能找到的是另一条也带"流程"字样的规章,但和请假没关系。
**改进方案**:加了关键词检索作为补充。具体做法是:
2. 用BM25做关键词检索
3. 把向量检索和BM25的结果合并,按加权分数排序
def hybrid_search(query, top_k=5):
# 向量检索
vec_results = vector_store.search(query, k=top_k * 2)
# 关键词检索
kw_results = bm25_store.search(query, k=top_k * 2)
# 合并去重
all_docs = {}
for doc, score in vec_results:
all_docs[doc.id] = {'doc': doc, 'vec_score': score, 'kw_score': 0}
for doc, score in kw_results:
if doc.id in all_docs:
all_docs[doc.id]['kw_score'] = score
else:
all_docs[doc.id] = {'doc': doc, 'vec_score': 0, 'kw_score': score}
# 加权计算最终分数
for item in all_docs.values():
item['final_score'] = 0.6 * item['vec_score'] + 0.4 * item['kw_score']
# 排序返回
sorted_docs = sorted(all_docs.values(), key=lambda x: x['final_score'], reverse=True)
return [(item['doc'], item['final_score']) for item in sorted_docs[:top_k]]
改完之后,准确率从68%提到了78%。
第三个优化:重排序才是关键
混合检索解决了"找得不够准"的问题,但还有个更隐蔽的问题:最相关的文档不一定排在最前面。
比如用户问"出差报销的标准",混合检索可能把"出差管理规定"排在第一,"财务报销标准"排在第二。但实际上,用户要的答案在第二个文档里。
**改进方案**:加了一个重排序模型。具体是用一个cross-encoder模型,把问题和检索到的文档一起喂进去,让它重新打分排序。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('BAAI/bge-reranker-large')
def rerank(query, docs, top_k=5):
pairs = [[query, doc.content] for doc in docs]
scores = reranker.predict(pairs)
ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)
return [doc for doc, _ in ranked[:top_k]]
这一步改动最小,但效果最好。准确率从78%直接跳到88%。
第四个优化:提示词不是小事
前面的优化都在"找"这个环节,但"答"这个环节也有问题。
我一开始的提示词是:
请根据以下参考文档回答问题:
{context}
问题:{query}
这个提示词太简单了,模型经常"发挥"——加上自己的理解、引用不存在的条文,甚至编造答案。
**改进方案**:改成了更严格的提示词:
你是企业知识库助手,必须严格基于提供的参考文档回答问题。
规则:
2. 如果文档中没有答案,明确回答"根据现有资料无法回答该问题"
3. 回答时标注信息来源,格式为"根据XX条..."
4. 不要猜测或推断,即使你觉得合理
参考文档:
{context}
问题:{query}
请先判断参考文档中是否有足够信息回答该问题,然后再作答。
改完之后,准确率从88%提到了92%。
最终效果
做了这四轮优化之后,系统终于到了"好用"的程度。
员工反馈从"还不如自己翻文档"变成了"问一句能直接用"。客户也追加了需求,要把系统扩展到更多业务场景。
踩坑心得
回头看,RAG系统的优化不是一招鲜,而是各个环节都要打磨:
2. **检索环节**:单靠向量不够,得混合检索
3. **排序环节**:重排序能解决相似和相关的区别
4. **生成环节**:提示词要够严格,限制模型的"发挥"
每一个环节都有坑,但每一个坑都有解。关键是别想着一步到位,一层一层优化,总会调出来的。
VkingAI