**

一、问题背景:为什么我的RAG系统像个“人工智障”

三个月前,我接手了一个内部知识库问答项目。数据源是约1.2万篇技术文档(Markdown、PDF、Confluence导出混杂),用户提问后系统返回答案。初版架构很朴素:LangChain + OpenAI text-embedding-ada-002 + FAISS + GPT-3.5-turbo。

上线第一周,业务方反馈:“答非所问,经常把不相关的段落拼在一起。”我跑了一个2000条真实问题的评测集,指标惨不忍睹:

  • 检索Top-5召回准确率:62%(人工判断是否包含正确答案)
  • 端到端回答可用率:58%(人工评分≥3/5)
  • 平均响应延迟:2.3s(含LLM生成)

问题集中在三处:chunk切得太粗导致语义稀释、embedding对中文技术术语不敏感、检索结果未做精排。接下来我按顺序做了三组实验。

二、环境与版本

先交代基线环境,避免“版本不一致导致效果不可复现”的坑:

# requirements.txt 关键部分
python==3.10.12
langchain==0.1.16
langchain-community==0.0.34
faiss-cpu==1.8.0
openai==1.30.1
sentence-transformers==2.7.0
FlagEmbedding==1.2.10
torch==2.2.2+cu121
transformers==4.40.0

硬件:单卡 RTX 4090(24GB),CPU 32核,内存128GB。LLM固定用GPT-3.5-turbo-0125(temperature=0),这样对比时只变检索侧,生成侧不变。

三、方案设计:三阶段递进优化

我采用控制变量法,每轮只改一个因素,记录指标:

阶段 Chunk策略 Embedding模型 Rerank Top-5准确率
Baseline 1024字符,无重叠 text-embedding-ada-002 无 62%
V1 256字符,重叠64 text-embedding-ada-002 无 71%
V2 256字符,重叠64 bge-large-zh-v1.5 无 82%
V3 256字符,重叠64 bge-large-zh-v1.5 bge-reranker-large 89%

端到端回答可用率对应为:58% → 66% → 78% → 87%。延迟从2.3s增至2.9s(rerank增加约300ms),可接受。

四、核心实现

4.1 Chunk策略调整(含代码)

原方案用RecursiveCharacterTextSplitter,chunk_size=1024,overlap=0。问题:技术文档中一个段落常包含多个概念,1024字符把“安装步骤”和“配置参数”混在一起,embedding向量被平均化。

改为chunk_size=256,overlap=64,并针对Markdown按标题层级切分:

from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter

def split_docs(docs):
    # 先按Markdown标题粗切
    headers_to_split_on = [
        ("#", "H1"), ("##", "H2"), ("###", "H3"),
    ]
    md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
    md_splits = md_splitter.split_text(docs)

    # 再按字符细切,保证语义单元完整
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=256,
        chunk_overlap=64,
        separators=["\n\n", "\n", "。", ";", ",", " ", ""],
        length_function=len,
    )
    return text_splitter.split_documents(md_splits)

实测:chunk_size从1024降到256后,单块平均token数从380降到95,检索时噪声段落减少。但太小会丢上下文,256+64是本次数据集的甜点。

4.2 Embedding模型切换(含代码)

text-embedding-ada-002对中文技术术语(如“K8s Ingress Controller”、“OAuth2.0授权码模式”)的向量区分度不足。换用BAAI/bge-large-zh-v1.5(中文榜单MTEB领先,维度1024,最大长度512)。

关键点:BGE模型检索时,query需要加指令前缀,passage不加。这是官方推荐做法,能提升1-3个百分点。

from FlagEmbedding import FlagModel

model = FlagModel(
    'BAAI/bge-large-zh-v1.5',
    query_instruction_for_retrieval="为这个句子生成表示以用于检索相关文章:",
    use_fp16=True  # 4090上开启fp16,推理速度提升约40%
)

def embed_query(text):
    return model.encode_queries(text)

def embed_passages(texts):
    return model.encode(texts, batch_size=64, max_length=512)

注意:不要用sentence-transformers直接加载BGE而不加instruction,否则效果会掉。我实测不加instruction时Top-5准确率只有79%,加了之后82%。

4.3 Rerank引入与对比

检索阶段用FAISS做ANN,取Top-20候选,再用bge-reranker-large交叉编码器精排,取Top-5送给LLM。Reranker直接对(query, passage)打分,比向量内积更准,但计算量大,所以只对少量候选做。

from FlagEmbedding import FlagReranker

reranker = FlagReranker('BAAI/bge-reranker-large', use_fp16=True)

def rerank(query, candidates, top_k=5):
    pairs = [[query, c.page_content] for c in candidates]
    scores = reranker.compute_score(pairs, normalize=True)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [c for c, s in ranked[:top_k]]

对比数据(同一2000条评测集):

配置 Top-5准确率 MRR@10 延迟(检索+rerank)
无rerank,向量Top-5 82% 0.71 45ms
向量Top-20 + rerank Top-5 89% 0.84 340ms

Rerank把“向量相似但语义无关”的段落过滤掉了。典型case:query“如何重置管理员密码”,向量检索会把“管理员权限说明”排前面,reranker则把“重置密码步骤”提到第一。

五、踩坑与优化

坑1:FAISS索引未归一化。 BGE输出向量需L2归一化后再用内积,否则余弦相似度计算错误。我一开始忘了,召回率掉8%。修正:faiss.normalize_L2(embeddings)。

坑2:Reranker batch_size过大导致OOM。 4090 24GB上,bge-reranker-large batch_size=32时显存飙到22GB,容易崩。改成16,稳定在14GB。

坑3:Chunk重叠导致重复检索。 overlap=64时,相邻块有重复内容,Top-5里可能出现同一段落的两个切片。加了一个简单的去重:按内容前50字符哈希去重。

坑4:PDF解析乱码。 部分PDF用PyPDF2提取后中文乱码,换成pdfplumber并设置laparams,问题解决。

六、效果数据与总结

最终线上灰度两周,日均请求1.2万次:

  • Top-5检索准确率:89%(+27pt)
  • 端到端回答可用率:87%(+29pt)
  • P99延迟:3.1s(+0.8s,用户可接受)
  • 人工badcase率:从12%降到4%

回看整个优化,性价比最高的是embedding模型切换(+11pt,成本几乎为零),其次是rerank(+7pt,延迟+300ms),chunk调整(+9pt,但需重新索引)。如果资源有限,优先换中文embedding,再加rerank。

RAG没有银弹,每个数据集的最优chunk_size和模型都不同。建议搭建一个200-500条的评测集,每改一个变量就跑一次,用数据说话。下一步我准备试试bge-m3多向量检索和query改写,有结果再写一篇。