← 返回博客
2026-08-01 13:00:01

RAG系统实战:从文档切片到检索优化,一次完整的工程复盘

RAG系统实战:从文档切片到检索优化,一次完整的工程复盘

1. 一个真实场景:合同审查的“最后一公里”问题

某金融科技公司需要处理年均超10万份的采购合同、保密协议和合规文件。此前基于关键词的ES检索方案,在“限制赔偿责任条款”这类高度语义化查询上,召回率不足40%。工程师们决定引入RAG(检索增强生成)架构,但很快发现:真正的瓶颈不在模型,而在文档进入向量库之前的“预处理”和查询进入模型之前的“再处理”

2. 文档切片:不是简单的按字数切

最直接的教训是:按固定512 token切片,等于人为制造语义断层。一份合同中的“赔偿条款”可能横跨第3页到第5页,若被硬切成三段,检索时只能命中其中一段,LLM生成答案时必然丢失上下文。

工程方案是采用结构化感知切片

def semantic_chunk(text, max_tokens=800):
    # 先用章节标题定位边界
    sections = re.split(r'(第[一二三四五六七八九十百千]+条\s*[\u4e00-\u9fa5]+)', text)
    chunks = []
    for section in sections:
        if not section.strip():
            continue
        if len(section) < max_tokens:
            chunks.append(section)
        else:
            # 超过阈值,按句子边界二次切分
            sentences = re.split(r'(?<=[。;])', section)
            temp = ""
            for sent in sentences:
                if len(temp) + len(sent) < max_tokens:
                    temp += sent
                else:
                    chunks.append(temp)
                    temp = sent
    return chunks

关键点:切片后必须保留元数据。后续检索到的切片,才能通过元数据回溯到原始文档的精确位置,实现“引用溯源”。踩坑记录:最初未保留章节路径,导致LLM回答“根据第X条”时无法定位原文,审计不通过。

3. 检索优化:从“向量相似”到“混合召回+重排”

初版系统只用embedding余弦相似度,结果:查询“违约金上限”时,返回了“赔偿计算方式”的切片,但漏掉了合同中更关键的“限额赔偿条款”。原因是纯向量检索对数字和专有名词不敏感

解决方案是混合检索架构

from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer, CrossEncoder

# BM25召回
bm25 = BM25Okapi(tokenized_corpus)
bm25_docs = bm25.get_top_n(query_tokens, corpus, n=20)

# 向量召回
embedder = SentenceTransformer('BAAI/bge-large-zh-v1.5')
vec_docs = embedder.encode(corpus, query, top_k=20)

# RRF融合
def rrf(rank_list, k=60):
    score = {}
    for doc_id, rank in enumerate(rank_list):
        score[doc_id] = score.get(doc_id, 0) + 1/(k+rank)
    return sorted(score.items(), key=lambda x: -x[1])[:50]

# 重排
reranker = CrossEncoder('BAAI/bge-reranker-base')
rerank_scores = reranker.predict([(query, doc) for doc in top50])
final_docs = [doc for _, doc in sorted(zip(rerank_scores, top50), reverse=True)][:5]

关键点:重排模型是性价比最高的优化点。只加一个cross-encoder,检索精度从62%提升到81%(内部测试集,1000条真实合同查询)。踩坑:RRF的k值不是越大越好,实测k=60比k=100稳定性更好,且BM25的词干化处理不适合中文,需使用jieba分词。

4. 生成环节:上下文压缩与幻觉抑制

检索到Top 5切片后,直接拼接送入LLM,常导致两个问题:超长上下文稀释关键信息模型“自由发挥”编造条款

工程做法是先压缩、后生成

compress_prompt = f"""提取以下合同片段中与'{query}'相关的条款原文,保留数字和百分比,删除无关描述。若无关,输出'无'。
片段1: {...} 片段2: {...}"""

response = llm.chat(compress_prompt)
# 将压缩结果作为最终生成的上下文

关键点:压缩步骤额外增加一次LLM调用,但显著降低幻觉率(从12%降至3%)。踩坑:压缩时务必保留原文的条款编号,否则后续引用失效。实测中,使用GLM-4-Flash做压缩,单次调用成本约0.002元,可接受。

5. 性能与成本:缓存与异步流水线

初版系统每次查询调用3次LLM(压缩+生成+可能的重试),p95延迟达8秒。优化后降至2.1秒:

关键点:缓存命中率在合同审查场景约30%(相同或相似条款反复查询),直接节省30%的LLM调用成本。

6. 踩坑总结与下一步行动

核心收获:

下一步行动建议:将检索评估(Retrieval Evaluation)接入CI/CD,每次调整切片或检索参数时,自动跑一遍1000条测试集,防止回归。推荐使用Ragas框架或自建评估集。

对于正在构建RAG系统的团队,建议按此顺序落地:先跑通结构化切片与混合检索,再评估是否需要重排,最后再考虑压缩与缓存。不要一开始就追求完美,先用最小闭环验证检索质量,再逐步优化生成环节。

本文关键词:RAG、混合检索、RRF、重排、上下文压缩