RAG 检索增强生成完全指南:从 Naive 到生产级 Advanced RAG

RAG(Retrieval-Augmented Generation,检索增强生成)是 2023 年以来 AI 工程里被讨论最多、踩坑也最多的技术。它让 LLM 能"查资料后再回答",几乎是企业落地 AI 的事实标准。但很多团队的 RAG 上线后效果不好 —— 不是因为 LLM 不行,而是 RAG 的每一个工程细节都能让效果腰斩。这篇文章把 RAG 的完整流程讲透,从最朴素的 Naive RAG 一直走到生产级的 Advanced RAG。

RAG 解决什么问题

LLM 直接回答有三个根本局限:

  • 知识截止日期:训练数据有 cutoff,之后的事件不知道。
  • 私有知识不知道:公司内部文档、特定行业数据,LLM 没见过。
  • 幻觉:不知道时也会编,且编得很自信。

RAG 的思路极其直接:先从知识库里检索相关内容,再让 LLM 基于这些内容回答。LLM 不再凭"记忆"作答,而是基于"给定的参考资料"作答。这同时解决了三个问题:时效性、私有性、可溯源。

Naive RAG:最朴素的流程

# 索引阶段(离线)
1. 加载文档(PDF / Word / Markdown / 网页 / 数据库)
2. 切分成 chunks
3. 用 embedding 模型把每个 chunk 转成向量
4. 存进向量数据库

# 查询阶段(在线)
1. 用户提问
2. 把问题 embed
3. 向量库找最相似的 top-k chunks
4. 把 chunks + 原问题塞进 prompt,让 LLM 回答

用 LangChain 写出来不到 20 行:

from langchain_community.document_loaders import DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import RetrievalQA

# 索引
docs = DirectoryLoader("./knowledge", glob="**/*.md").load()
chunks = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50).split_documents(docs)
vectorstore = Chroma.from_documents(chunks, OpenAIEmbeddings())

# 查询
qa = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-4"),
    retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),
)
answer = qa.invoke({"query": "如何配置 nginx HTTPS?"})

能跑,但远不够生产用。下面我们看每一步可以怎么优化。

第一关:文档加载与清洗

看似最不起眼的一步,实际是 RAG 质量的根基。

常见格式与坑

  • PDF:布局复杂(双栏、表格、图片)。PyMuPDFPyPDF2 准确。带表格的用 UnstructuredTabula 单独提取表格。
  • Word / PPT:用 python-docx / python-pptx,但格式信息很容易丢。
  • HTML:先用 BeautifulSoup 去掉导航 / 广告 / 脚注,只留正文。
  • 扫描件 / 图片:OCR(Tesseract / 阿里 RPA / Azure Document Intelligence)。

清洗

import re

def clean(text):
    text = re.sub(r'\s+', ' ', text)              # 合并空白
    text = re.sub(r'(?:页\s*\d+\s*/\s*\d+)', '', text)   # 去页码
    text = re.sub(r'\[\d+\]', '', text)            # 去脚注引用
    return text.strip()

第二关:Chunking(切分)

切太小 → 缺上下文;切太大 → embedding 模糊、检索不准。这是 RAG 最常被低估的关键步骤。

固定大小切分

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 中文约 500 字
    chunk_overlap=50,    # 相邻 chunk 重叠 50 字,避免边界丢信息
    separators=["\n\n", "\n", "。", "!", "?", " "],  # 优先按段落、句号切
)
chunks = splitter.split_text(text)

语义切分

检测相邻句子的 embedding 距离,距离大的地方才切 —— 自动按"话题边界"切。LangChain 的 SemanticChunker、LlamaIndex 的 SemanticSplitterNodeParser 都做这件事。

结构化切分

Markdown / HTML / 代码文档有天然结构(标题、章节、函数)。按结构切,每个 chunk 加上它的层级标签:

# 一段 chunk 的实际内容会是:
{
    "text": "...具体段落内容...",
    "metadata": {
        "doc_id": "manual-001",
        "section": "3.2.1 HTTPS 配置",
        "parent_section": "3 安全设置",
        "page": 42,
    }
}

这些 metadata 在检索时非常有用 —— 你可以"只在某一章节内检索",或者让 LLM 看到 chunk 来自哪里。

给 chunk 加摘要 / 父子结构

高级做法:每个 chunk 配一段它的"父级摘要"。检索时匹配小 chunk(精准),但 LLM 看的是 chunk + 父级摘要(全局上下文)。这种"Parent-Child Chunking" 或 Small-to-Big 模式是生产 RAG 的标配。

第三关:Embedding 与检索

(上一篇文章已经详细讲过 embedding,这里只列 RAG 相关要点)

检索策略

  • 纯向量检索:简单,但对术语精确匹配差。"GPT-4.1" 这种专有名词,向量可能不如关键词准。
  • BM25 检索:Elasticsearch 经典算法,对精确匹配好,但不理解同义词。
  • 混合检索(Hybrid):同时跑向量 + BM25,用 RRF(Reciprocal Rank Fusion)合并结果。生产 RAG 几乎都用这种。
def rrf_merge(rankings, k=60):
    """Reciprocal Rank Fusion: 多个排序融合"""
    scores = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking):
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

vector_results = vector_db.search(query_emb, top_k=20)
bm25_results = es.search(query, top_k=20)
merged = rrf_merge([vector_results, bm25_results])[:10]

Reranker:让结果更准

检索召回 20 个 chunk,但前 5 个不一定真的最相关。再用一个交叉编码器(Cross-Encoder)给"query × doc 对"打分重排:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-large")

candidates = retrieve(query, top_k=20)
pairs = [(query, c.text) for c in candidates]
scores = reranker.predict(pairs)
reranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:5]

Reranker 比 Bi-Encoder(embedding)精度高,但慢得多 —— 所以只用在"召回完之后的精排"环节。这是把 RAG 准确率从 60% 提到 85%+ 最有效的一步。

第四关:Prompt 与生成

RAG_PROMPT = """
你是一名技术问答助手。请仅基于下面的"参考资料"回答用户问题。
如果参考资料里没有答案,回答"我在已有资料中找不到这个信息"。
回答时请引用资料编号,例如 [1] [2]。

参考资料:
{context}

用户问题:{question}

回答:
"""

def format_context(chunks):
    return "\n\n".join(f"[{i+1}] {c.text} (来源:{c.metadata.get('source')})"
                       for i, c in enumerate(chunks))

prompt = RAG_PROMPT.format(
    context=format_context(retrieved_chunks),
    question=user_question,
)
answer = llm.generate(prompt)

几个关键设计点:

  • "仅基于参考资料":强约束,降低幻觉。
  • "找不到就说找不到":避免编造。
  • 引用编号:让答案可溯源,用户能看哪段来自哪。
  • 来源信息:每个 chunk 带 source(文件名 / URL / 章节),让用户能跳到原文。

Query Rewriting:让查询更好被检索

用户的问题往往口语化,直接 embed 不准。先用 LLM 改写一下:

REWRITE_PROMPT = """
把下面的用户问题改写成 3 个不同表达的检索查询,以更好地匹配文档。
原问题:{question}
输出 3 个查询,每行一个。
"""

queries = llm.generate(REWRITE_PROMPT.format(question=user_q)).split("\n")
# ["nginx 怎么配置 HTTPS", "nginx SSL 证书设置", "nginx TLS 启用方法"]

# 分别检索,合并结果
all_chunks = []
for q in queries:
    all_chunks.extend(retrieve(q, top_k=10))
all_chunks = dedupe(all_chunks)

这个技巧叫 Multi-Query Retrieval,有时能让召回率提升 30%。

HyDE:让 LLM 先编一个"假答案"

更激进的查询改写:让 LLM 先"假装回答"问题(可能是错的),然后用"假答案"去检索 —— 答案的 embedding 通常比问题的 embedding 更接近真实文档(都是陈述句):

HYDE_PROMPT = "请简短回答这个问题(不超过 100 字):{question}"
hypothetical = llm.generate(HYDE_PROMPT.format(question=user_q))
chunks = vector_db.search(embed(hypothetical), top_k=10)

对 LLM 真不知道的领域,HyDE 反而不太行(假答案太离谱)。一般做兜底:既检索原问题又检索 HyDE 答案,RRF 合并。

Self-Query:让 LLM 提取过滤条件

用户问"2024 年发布的关于 React 的文章" —— "2024 年" 应该作为元数据过滤而不是语义检索。Self-Query 让 LLM 把自然语言分解成"语义查询 + 结构化过滤":

{
    "query": "React",
    "filter": {"year": {"$eq": 2024}, "topic": {"$eq": "React"}}
}

然后向量库做"过滤 + 语义检索"的组合查询。Pinecone、Milvus、pgvector 都支持元数据过滤。

Agentic RAG:让 LLM 自己决定要不要检索

简单问题不需要检索,复杂问题可能要多次检索。Agentic RAG 让 LLM 自己决定:

tools = [
    {"name": "search_knowledge", "description": "搜索内部文档"},
    {"name": "search_web", "description": "搜索互联网"},
]

# LLM 决定要不要、用哪个工具
response = llm.with_tools(tools).invoke(user_q)
if response.tool_calls:
    for call in response.tool_calls:
        result = execute_tool(call.name, call.args)
        # 把结果喂回 LLM,可能再循环

这就是 ReAct / Self-RAG / CRAG(Corrective RAG)等高级模式的基本思路。最适合那些"问题复杂、需要多步推理"的场景。

评估 RAG 效果

RAG 评估比单独评 LLM 难,因为有"检索"和"生成"两个环节都可能错。常用指标:

  • Hit Rate / Recall@k:正确答案是不是在 top-k 召回里?
  • MRR(Mean Reciprocal Rank):正确答案排第几?
  • Faithfulness:回答内容是否完全基于检索到的资料?
  • Answer Relevance:回答和问题是否相关?
  • Context Recall / Precision:检索到的资料是否完整 / 是否有冗余?

工具:RAGAS(自动用 LLM 评估)、TruLensDeepEval。生产 RAG 必须把这些指标做进 CI,模型 / chunk size / 检索器换一次就跑一遍评估。

RAG 的常见坑

坑 1:Chunk 不带上下文。 "本章讨论 X" 这段切出来后,LLM 不知道 X 是啥。修复:每个 chunk 前面加文档/章节标题作为前缀。

坑 2:检索召回 0 个相关结果但还硬答。 检索分数都很低时,直接返回"我不知道"比强答更好。设阈值:相似度 < 0.5 时直接告知用户。

坑 3:用户问题包含多个子问题。 "React 和 Vue 有什么区别,以及哪个更适合大型项目?" —— 单次检索 + 单次生成搞不定。要么拆问题分别检索,要么用 Agentic RAG 多轮处理。

坑 4:文档更新没同步到向量库。 知识库改了内容但向量库还是旧 embedding,答案陈旧。要做增量索引 + 版本管理(每个 chunk 带 doc_version,文档更新时删旧加新)。

坑 5:幻觉来自"似是而非"的检索。 检索召回了不相关但有关键词的 chunk,LLM 强行用它编答案。修复:strong system prompt + reranker + 阈值过滤。

RAG vs 长上下文 vs 微调

2024 年 Claude / Gemini 支持 1M+ 上下文,有人喊"RAG 已死"。实际:

  • RAG:成本低,知识可控,可追溯,可增量更新。但召回质量靠功夫。
  • 长上下文:简单粗暴,把所有资料塞进去。但贵(token 多)、慢(每次都全量处理)、且实测 "lost in the middle"(中间内容效果差)。
  • 微调:把知识"烧进"模型,效果好但成本高、知识更新难、还可能影响通用能力。

实际生产里三者通常组合:核心通用知识用微调,实时/动态知识用 RAG,极少数极长上下文用长 context。不是替代关系,是互补。

多模态 RAG:不只是文本

真实文档常含图、表、公式。把它们也纳入检索:

  • 图片:用 CLIP / SigLIP 把图片 embed 到同一个空间,用户文字查询也能匹配到图。
  • 表格:用 GPT-4V 或 Qwen-VL 把表格转成结构化数据 + 摘要,分别索引。
  • 公式:LaTeX 公式 → 自然语言描述 → embed。

Graph RAG:用知识图谱代替向量

Microsoft 2024 年开源的 GraphRAG 提出:对企业知识库,先用 LLM 抽取"实体 + 关系"构建知识图谱,查询时基于图谱做局部子图召回。优势:

  • 能回答"关于 X 的所有事"这种问题(向量 RAG 因为 chunk 分散答不好)。
  • 跨文档关联(同一个实体在不同文档里的信息能聚合)。

代价:索引成本高(每个 chunk 都要让 LLM 抽实体),且更新麻烦。适合"知识结构稳定 + 需要全局视角"的场景。

常见的"RAG 看着没问题但效果差"的原因

线上 RAG 上线后,排查问题的固定路线:

  1. 检索没召到正确文档:看 top-10 里有没有正确文档。没有 → 改 chunking / 加 reranker / 混合检索。
  2. 正确文档在 top-10 但排很后:加 reranker,或者调 chunk 让正确内容更聚焦。
  3. 正确文档排第一但 LLM 没用:看 prompt 是否强调"基于参考资料",看 LLM 是否倾向于编造。
  4. 问题本身超出知识库:加 fallback —— 检索分数低于阈值时回答"不知道"。

这种"分阶段诊断"方法比"换个 LLM 看看"高效得多。每个阶段都有明确的可测量指标和明确的优化手段。

写在最后

RAG 的精髓在于"把对的资料给 LLM"—— LLM 本身的智能不变,但有了好的资料,它的表现就像换了一个领域专家。整个 RAG 流程里,LLM 只占最后一步,前面"怎么切、怎么存、怎么检索、怎么重排、怎么 prompt"才是工程上的难点,也是效果差异的来源。

给一个工程心得:RAG 不是装好就完事,是要持续优化的系统。上线第一版用 Naive RAG 跑起来,然后用真实用户问题 / 评估集去定位"哪一步在出错",一步步加 Reranker、调 chunk、做混合检索、加 Query Rewriting。每个改进都用 RAGAS 测一遍,有数据再上。这种"测量驱动的迭代"是 RAG 工程的灵魂 —— 没有它,你永远不知道改对了还是改坏了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Embedding 与向量数据库完全指南:语义搜索的工程实现

2026-5-15 15:54:03

技术教程

Prompt Engineering 完全指南:从 Zero-shot 到 Function Calling 的实战技巧

2026-5-15 15:54:03

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索