AI RAG实战指南:从基础检索到自适应反思

前言:RAG 不是"检索 + 生成"那么简单

很多人对 RAG 的理解是"切分文档 → 向量化 → 检索 → 拼 prompt → 生成"。听起来确实不复杂,但真正落地会发现:切分策略、检索方式、上下文组装、幻觉控制……每一环都是坑。

RAG 的核心目标:让模型基于真实文档回答,避免胡编乱造。但做到这一点,需要在效果、性能、成本之间反复权衡。

一、RAG 的演进路线

graph LR A[单体RAG<br/>切分+向量+生成] --> B[混合检索RAG<br/>向量+关键词+重排] B --> C[模块化RAG<br/>路由+检索+组装] C --> D[高级RAG<br/>多跳/分层/图] D --> E[自适应RAG<br/>反思+纠错]
阶段核心特点适用场景
单体 RAG一条龙,简单直接原型验证、简单文档
混合检索 RAG多路召回 + 重排序生产环境基础方案
模块化 RAG模块解耦,灵活替换复杂业务、需要迭代
多跳/分层/图 RAG跨文档推理、结构化检索复杂问题、技术文档
自适应 RAG自我反思、动态调整高准确度场景

二、文档切分:第一个大坑

2.1 固定长度切分的问题

最简单的切分按固定字符数:

def split_documents(docs, chunk_size=512, overlap=50):
    chunks = []
    for doc in docs:
        for i in range(0, len(doc), chunk_size - overlap):
            chunks.append(doc[i:i + chunk_size])
    return chunks

问题:

  • 把一个完整的技术方案切到两段里,每段只有一半信息
  • 检索时要么匹配不上,要么匹配上了但不完整

2.2 递归字符分割(推荐)

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""]
)

关键经验值:

  • chunk_size:400-800 之间,中文 500-800 字、英文 800-1200 词
  • chunk_overlap:50-100,保证语义边界不硬切断
  • separators 顺序很重要:优先用大的自然边界(段落、句子),最后才是字符

2.3 特殊内容处理

表格处理: PDF 的表格被切得面目全非时,需要专门的表格识别逻辑,遇到表格整块保留。

代码块: 不要在代码块中间切分,保持代码完整性。

三、Embedding 模型选择

3.1 模型对比

模型维度优势适用场景
OpenAI text-embedding-3-small1536速度快,性价比高通用场景
OpenAI text-embedding-3-large3072精度高高准确度需求
BAAI/bge-large-zh-v1.51024中文效果好中文为主
sentence-transformers/all-MiniLM-L6-v2384轻量、快资源受限

3.2 批量向量化

import time

def batch_embeddings(texts, batch_size=10):
    """批量生成向量,处理速率限制"""
    embeddings = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        try:
            response = client.embeddings.create(
                input=batch,
                model="text-embedding-3-small"
            )
            embeddings.extend([np.array(d.embedding) for d in response.data])
        except Exception as e:
            print(f"Batch {i} failed: {e}")
            time.sleep(5)
        time.sleep(0.1)  # 避免触发速率限制
    return embeddings

四、向量数据库

4.1 选型对比

数据库优势劣势适用场景
ChromaDB本地部署简单,Python 友好规模受限小项目、开发
FAISS高效、Meta 出品不是完整数据库嵌入式集成
Milvus功能强大、可扩展部署重大规模生产
Pinecone云服务省事数据要外传不敏感数据
QdrantRust 写、性能好生态较新性能敏感场景

4.2 FAISS 索引类型

import faiss
import numpy as np

# 假设有 N 个 1024 维向量
embeddings = np.array([doc.embedding for doc in documents]).astype('float32')

# IndexFlatL2:精确搜索,简单但慢
index_flat = faiss.IndexFlatL2(1024)
index_flat.add(embeddings)

# IndexIVFFlat:聚类后搜索,更快
n_clusters = 1000  # 一般是文档数的 sqrt
index_ivf = faiss.IndexIVFFlat(faiss.IndexFlatL2(1024), 1024, n_clusters)
index_ivf.train(embeddings)
index_ivf.add(embeddings)
index_ivf.nprobe = 20  # 查询时探查的簇数

索引参数经验:

  • 簇数量:文档数的 sqrt 值,比如 10000 篇文档用 100-500 个簇
  • nprobe:10-50,太少漏文档,太多变慢

4.3 索引定期重建

索引不重建,检索效果会越来越差:

def rebuild_index():
    # 备份
    backup_dir = f"./chroma_db_backup_{datetime.now().strftime('%Y%m%d')}"
    shutil.copytree("./chroma_db", backup_dir)

    # 获取所有数据并重建
    collection = client.get_collection("knowledge_base")
    all_data = collection.get(include=['documents', 'metadatas', 'embeddings'])

    client.delete_collection("knowledge_base")
    collection = client.create_collection("knowledge_base")
    collection.add(
        documents=all_data['documents'],
        metadatas=all_data['metadatas'],
        embeddings=all_data['embeddings'],
        ids=[f"doc_{i}" for i in range(len(all_data['documents']))]
    )

五、混合检索

5.1 为什么需要混合检索

纯向量检索的问题:

  • 用户搜具体技术术语、错误代码、版本号时效果差
  • 这些内容字面本身就是信息,不需要语义理解

纯关键词检索的问题:

  • “性能调优"和"性能优化"匹配不上
  • 无法理解语义关系

5.2 实现方案

from langchain.retrievers import BM25Retriever, EnsembleRetriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
from langchain_community.document_compressors.cross_encoder import CrossEncoderReranker

def create_hybrid_retriever(vectorstore, documents, top_k=10):
    # 向量检索
    vector_retriever = vectorstore.as_retriever(search_kwargs={"k": top_k * 2})

    # BM25 关键词检索
    bm25_retriever = BM25Retriever.from_documents(documents)
    bm25_retriever.k = top_k * 2

    # 组合两种检索(权重可调)
    ensemble_retriever = EnsembleRetriever(
        retrievers=[vector_retriever, bm25_retriever],
        weights=[0.7, 0.3]  # 语义权重高一些
    )

    # 交叉编码器重排序
    cross_encoder = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")
    compressor = CrossEncoderReranker(compressor=cross_encoder, top_n=top_k)

    return ContextualCompressionRetriever(
        base_compressor=compressor,
        base_retriever=ensemble_retriever
    )

5.3 融合权重调优

初始权重一般给语义检索 0.6、关键词检索 0.4。通过 A/B 测试和用户反馈调整,很多团队最终稳定在 0.7/0.3。

关键:简单问题不用重排序,检测到问题复杂时才启用。 重排序会把几百毫秒的检索变成几秒。

六、生成层:如何让模型不胡说

6.1 Prompt 约束

prompt = """你是一个专业的知识库助手。请基于以下上下文回答问题。

上下文:
{context}

问题:{question}

请用中文回答,并在回答中用【来源:文档名】的形式标注信息来源。
如果上下文中没有相关信息,请直接说"我没有找到相关信息",不要编造答案。

答案:"""

6.2 带引用的 QA 链

from langchain.chains import RetrievalQAWithSourcesChain

chain = RetrievalQAWithSourcesChain.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
    chain_type_kwargs={"prompt": prompt_template}
)

result = chain.invoke({"question": "公司的休假政策是什么?"})
print(result["answer"])
print("来源:", result["source_documents"])

带引用的好处:用户能追溯到原文,信任度更高。

七、模块化 RAG

7.1 单体 RAG 的局限

单体 RAG 把所有步骤串起来:

问题 → 向量检索 → 上下文组装 → LLM 生成

问题:

  • 检索方式单一,无法根据问题调整
  • 想 A/B 测试不同检索策略,得复制整个系统
  • 改一个模块要重构整个流程

7.2 模块化架构

graph TB A[用户问题] --> B{路由模块} B -->|事实查询| C1[混合检索] B -->|分析查询| C2[向量检索] B -->|创造查询| C3[知识检索] C1 --> D[重排序] C2 --> D C3 --> D D --> E[上下文组装] E --> F[生成]

7.3 核心模块

1. 问题路由模块:

def route_query(question: str) -> QueryType:
    """根据问题类型决定路由"""
    prompt = f"""
    分析以下问题的类型:
    问题:{question}

    类型说明:
    - factual: 事实性查询,需要精确信息
    - analytical: 分析性查询,需要综合多个信息
    - creative: 创造性查询,需要发散思维

    返回类型和置信度(0-1)。
    """
    result = llm.predict(prompt, response_model=QueryType)
    return result

2. 检索模块(多策略):

class BaseRetriever(ABC):
    @abstractmethod
    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        pass

class HybridRetriever(BaseRetriever):
    def __init__(self, retrievers: list, weights: list):
        self.retrievers = retrievers
        self.weights = weights

    def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
        all_results = []
        for retriever, weight in zip(self.retrievers, self.weights):
            results = retriever.retrieve(query, top_k=top_k)
            for result in results:
                result['score'] *= weight
            all_results.extend(results)
        return self.merge_results(all_results)[:top_k * 2]

3. 重排序模块:

class Reranker:
    def __init__(self, model_name="BAAI/bge-reranker-base"):
        from sentence_transformers import CrossEncoder
        self.model = CrossEncoder(model_name)

    def rerank(self, query, documents, top_k=5):
        pairs = [(query, doc['content']) for doc in documents]
        scores = self.model.predict(pairs)

        for doc, score in zip(documents, scores):
            doc['rerank_score'] = score

        documents.sort(key=lambda x: x['rerank_score'], reverse=True)
        return documents[:top_k]

4. 上下文组装模块:

class ContextBuilder:
    def __init__(self, max_tokens=3000):
        self.max_tokens = max_tokens

    def build(self, query, documents):
        # 按相关性排序
        documents = sorted(documents, key=lambda x: x.get('rerank_score', 0), reverse=True)

        # 前 3 个文档完整保留
        context_parts = []
        current_tokens = 0

        for doc in documents[:3]:
            content = f"## 来源:{doc.get('source', '未知')}\n{doc['content']}\n"
            doc_tokens = len(content) // 4
            if current_tokens + doc_tokens > self.max_tokens * 0.8:
                break
            context_parts.append(content)
            current_tokens += doc_tokens

        # 剩余文档只保留摘要
        for doc in documents[3:]:
            if current_tokens >= self.max_tokens:
                break
            summary = self._summarize(doc['content'])
            context_parts.append(f"## 来源:{doc.get('source', '未知')}\n{summary}\n")
            current_tokens += len(summary) // 4

        return "\n".join(context_parts)

7.4 模块化效果对比

指标单体 RAG模块化 RAG
准确率72%85%
平均延迟1.2s1.5s
Token 使用25001800
开发效率基线+40%

八、知识图谱 RAG

8.1 为什么需要图谱

向量检索的三种尴尬情况:

  1. 找得到但连不上:每个片段都对,但拼在一起说不通
  2. 答非所问:语义相似但方向不对
  3. 缺乏上下文:不知道配置会和别的什么冲突

文档本来有内在关系——组件依赖、调用链、故障传导——但传统检索把这些结构全丢了。

8.2 实体关系提取

def extract_entities_relations(chunk_text):
    prompt = f"""
从以下文本中提取实体和关系,按照给定的类型约束输出 JSON。

文本:{chunk_text}

实体类型:服务、组件、配置、错误、API、产品、版本
关系类型:调用、依赖、导致、修复、包含、兼容

输出格式:
{{
  "entities": [{{"text": "...", "type": "..."}}],
  "relations": [{{"source": "...", "target": "...", "relation": "..."}}]
}}
"""
    response = llm.generate(prompt, temperature=0.1)  # 低温度保证稳定
    return parse_json(response)

关键经验:

  • 限制实体和关系类型,避免类型爆炸
  • 温度设 0.1,稳定性比创造性重要
  • 人工审查前 20 个结果,迭代基线

8.3 图谱检索策略

import networkx as nx

def retrieve_context(G, entities, max_hops=2):
    """从问题实体出发,做多跳检索"""
    context_nodes = set(entities)
    for entity in entities:
        if G.has_node(entity):
            for node, distance in nx.single_source_shortest_path_length(
                G, entity, cutoff=max_hops
            ).items():
                context_nodes.add(node)

    subgraph = G.subgraph(context_nodes).copy()
    return subgraph

跳数选择: 2 跳通常足够覆盖大部分场景,3 跳以上噪声明显增加。

8.4 效果对比

指标向量检索图谱 RAG
准确度68%82%
响应时间200-400ms300-500ms
可解释性黑盒可展示推理路径

九、多跳 RAG

9.1 什么是多跳

单跳是查字典,多跳是拼图。

适合多跳的问题:

  • “项目A和项目B在架构设计上有什么共同点和不同点?”
  • “为什么这个文档里提到了X,另一个文档又说是Y?”
  • “这三篇文档中,谁的观点最有说服力?”

9.2 链式推理实现

class ChainHopRAG:
    def __init__(self, vectorstore, max_hops=3):
        self.vectorstore = vectorstore
        self.max_hops = max_hops
        self.llm = ChatOpenAI(temperature=0)

    def run(self, question):
        collected_info = []
        current_queries = [question]

        for hop in range(self.max_hops):
            # 检索当前查询
            for query in current_queries:
                docs = self.vectorstore.similarity_search(query, k=3)
                collected_info.extend(docs)

            # 判断是否继续
            if hop < self.max_hops - 1:
                response = self.llm.invoke(
                    self.query_gen_prompt.format(
                        question=question,
                        collected_info="\n".join([d.page_content for d in collected_info[-6:]])
                    )
                )

                if "不需要进一步检索" in response.content:
                    break

                current_queries = self._extract_queries(response.content)
                if not current_queries:
                    break

        # 生成最终答案
        return self.llm.invoke(
            self.answer_prompt.format(
                question=question,
                collected_info="\n".join([d.page_content for d in collected_info])
            )
        ).content

9.3 多跳的代价

指标单跳 RAG多跳 RAG
跨文档问题准确率42%68%
响应时间1.2s4.5s
成本基线2-3 倍

建议:用路由判断决定是否走多跳。

def should_use_multi_hop(question):
    """判断问题是否适合多跳"""
    keywords = ["对比", "差异", "为什么", "关系", "影响"]
    return any(keyword in question for keyword in keywords)

十、分层 RAG

10.1 平铺 RAG 的痛点

不同问题需要不同粒度的信息:

  • “这个系统的整体架构?” → 需要章节级
  • “Redis 连接池参数?” → 需要段落级、代码级

平铺 RAG 只能返回固定粒度,粒度不匹配是常见问题。

10.2 文档层次结构

文档
├── 章节 (level=2)
│   ├── 小节 (level=3)
│   │   ├── 段落 (level=4)
│   │   └── 代码块
│   └── 小节
└── 章节

10.3 分层检索

def hierarchical_search(query, vector_db, top_k=5):
    query_embedding = embedding_model.encode(query)

    # 1. 判断查询意图
    intent = classify_query_intent(query)

    # 2. 根据意图选择搜索层级
    if intent == "overview":
        search_levels = [2]  # 章节级
    elif intent == "detail":
        search_levels = [4]  # 段落级
    else:
        search_levels = [2, 3, 4]  # 多层级

    # 3. 在目标层级搜索
    results = vector_db.search(
        query_embedding,
        top_k=top_k * 2,
        filter={'level': {'$in': search_levels}}
    )

    # 4. 去重和扩展(必要时补充父节点)
    results = deduplicate_and_expand(results, vector_db)

    return results[:top_k]

10.4 效果对比

指标平铺 RAG分层 RAG
架构类问题准确率62%87%
实现类问题准确率71%85%
平均 token 消耗850620
用户满意度3.4/54.6/5

十一、Self-RAG:自反思检索

11.1 传统 RAG 的局限

问题一:有些问题根本不需要检索

  • “写一个 Python 冒泡排序”
  • “解释什么是递归”

问题二:检索质量无法评估

问题三:无法根据检索结果调整策略

11.2 Self-RAG 核心:反思标记

让 LLM 自己做决策:

  • [Retrieve] / [NoRetrieve]:是否需要检索
  • [Relevant] / [Irrelevant]:文档是否相关
  • [Continue] / [End]:是否继续检索

11.3 判断是否需要检索

def should_retrieve_with_rules(question, llm):
    """带规则的检索判断"""
    # 关键词触发规则
    force_retrieve_keywords = ['最新', '今年', '2024', '2025', '具体数据', '详细']
    no_retrieve_keywords = ['编程', '代码', '算法', '解释', '是什么']

    if any(kw in question for kw in force_retrieve_keywords):
        return True

    if any(kw in question for kw in no_retrieve_keywords):
        return False

    # 其他情况由 LLM 判断
    prompt = f"问题: {question}\n判断是否需要检索外部信息,只回答 yes 或 no。"
    response = llm.generate(prompt)
    return "yes" in response.lower()

11.4 自适应检索

def adaptive_retrieval(question, vector_db, llm, max_iterations=3):
    all_docs = []
    search_terms = [question]

    for iteration in range(max_iterations):
        docs = vector_db.search(search_terms[-1], top_k=5)
        all_docs.extend(docs)

        # 评估检索结果
        evaluation = evaluate_retrieval(question, docs, llm)

        # 质量足够,停止
        if evaluation["relevance"] >= 7 and evaluation["completeness"] >= 7:
            break

        # 相关性低,调整搜索词
        if evaluation["relevance"] < 5:
            search_terms.append(generate_alternative_query(question, llm))
        # 信息不足,扩大范围
        elif evaluation["completeness"] < 7:
            search_terms.append(expand_query(question, llm))

    return remove_duplicates(all_docs)[:10]

11.5 Self-RAG 效果

指标传统 RAGSelf-RAG
答案准确性72%84%
相关性评分6.8/108.2/10
用户满意度65%79%
平均响应时间2.3s3.8s

十二、纠错 RAG(Corrective RAG)

12.1 核心思路

RAG 系统总会产生似是而非的回答。Corrective RAG 的思路:生成后加一层审查,发现问题就修正

flowchart LR A[问题] --> B[检索] B --> C[生成初步回答] C --> D[自我审查] D --> E{通过?} E -- 是 --> F[返回] E -- 否 --> G[重新检索/生成] G --> D

12.2 审查 Prompt 设计

任务:审查以下回答是否准确基于检索到的文档。

原始问题:{question}
检索到的文档:{retrieved_docs}
生成的回答:{generated_answer}

请回答:
1. 回答中是否有与检索文档内容相矛盾的信息?
2. 回答中是否有检索文档完全未提及的内容?
3. 如果有问题,请指出具体是哪些部分,并说明为什么。

如果回答完全基于文档内容,请回答"审查通过"。

关键:审查必须"具体到部分”,不能只给通过/不通过。

12.3 常见坑

坑一:审查模型本身会犯错

审查模型也是概率模型,可能"睁眼说瞎话"。解决:要求逐条说明问题,给出文档原文对比。

坑二:审查太严格,系统不敢回答

有些"问题"其实是合理的推断。文档说"支持 JSON",模型说"支持 JSON 等结构化格式"不算错。解决:明确"语义等价不算问题"。

坑三:修正过程时延大

解决:只对关键问题做完整审查,简单问题走快速通道。

12.4 纠错 RAG 效果

  • 回答中"不支持的特性"这种硬编造减少约 40%
  • 用户反馈"回答不准确"的比例从 15% 降到 8%
  • 平均时延增加 1.5-2 倍

十三、性能优化

13.1 缓存策略

import hashlib
from functools import lru_cache

# 答案缓存
def query_with_cache(question):
    cache_key = hashlib.md5(question.encode()).hexdigest()

    if cache_key in cache:
        print("命中缓存")
        return cache[cache_key]

    result = chain.invoke({"question": question})
    cache[cache_key] = result
    return result

13.2 性能优化手段

优化点效果
换更快的 embedding 模型速度提升 30%
调整 FAISS 簇数和 nprobe延迟降一半
用 ONNX Runtime 加速推理编码从 80ms 降到 30ms
缓存热点查询命中率 15-20%
简单问题不走 LLM大幅降延迟

十四、RAG 路由策略

不同问题用不同 RAG 策略:

def route_rag_strategy(question):
    """根据问题类型选择 RAG 策略"""
    # 简单事实查询:单体 RAG
    if is_simple_factual(question):
        return "monolithic"

    # 需要跨文档推理:多跳 RAG
    if any(kw in question for kw in ["对比", "差异", "为什么", "关系"]):
        return "multi_hop"

    # 需要理解实体关系:图谱 RAG
    if any(kw in question for kw in ["调用链", "依赖", "导致", "影响"]):
        return "graph"

    # 默认:混合检索 RAG
    return "hybrid"

十五、效果评估

15.1 评估方法

用户反馈: 搜索结果页加"是否有帮助"按钮。

A/B 测试: 同时上线新旧系统,对比点击率、停留时间。

抽样检查: 人工抽样检查检索结果的相关性和覆盖度。

15.2 关键指标

指标说明
准确率答案是否正确
召回率是否找到了所有相关文档
相关性检索结果与问题的相关程度
完整性信息是否足够回答问题
响应时间端到端延迟
Token 消耗每次查询的成本

十六、踩坑总结

坑一:切分太碎或太粗

太碎导致上下文不够,太粗导致检索不准。用递归切分 + 自然边界。

坑二:向量维度盲目追求高

768 维的模型在很多数据集上已经够用,1024 维以上边际收益递减,反而索引构建和查询都变慢。

坑三:跨语言对齐不完美

中文查询"索引优化"和英文文档"index tuning"的相似度不如预期。解决:在元数据中记录语言类型,检索时优先同语言。

坑四:重排序增加延迟

重排序让检索从几百毫秒变几秒。解决:简单问题不走重排序,复杂问题才启用。

坑五:纯语义检索的盲点

具体技术术语、错误代码、版本号这些内容,字面匹配更准。解决:混合检索。

坑六:模型幻觉防不住

再好的检索也可能被模型编造。解决:严格 prompt 约束 + 引用标注 + 纠错 RAG。

十七、技术选型建议

基于实践经验,技术选型建议:

  1. 快速原型: 单体 RAG(LangChain + ChromaDB)
  2. 生产基础: 混合检索 RAG(向量 + BM25 + 重排序)
  3. 复杂业务: 模块化 RAG(路由 + 多检索器 + 组装)
  4. 跨文档推理: 多跳 RAG
  5. 结构化关系: 知识图谱 RAG
  6. 高准确度: Self-RAG / 纠错 RAG

重要:不是所有场景都需要最复杂的方案。 先把单体 RAG 做扎实,根据真实问题逐步升级。

十八、写在最后

RAG 不是一劳永逸的架构,它得跟着场景调。

几个实在的体会:

  1. 效果和性能永远在打架,按业务取舍
  2. 用户反馈比闭门调参管用
  3. 持续迭代比一次搭完美流水线重要
  4. 简单方案先做到极致,再考虑复杂方案
  5. 合适比先进重要

RAG 的核心不是技术多先进,而是真的能解决用户"找信息"的问题。先盖住 80% 的日常查询,剩下 20% 慢慢迭代就行。


本文整合了 21 篇 RAG 检索增强生成相关文章,涵盖文档切分、向量数据库、混合检索、模块化 RAG、知识图谱 RAG、多跳 RAG、分层 RAG、Self-RAG、纠错 RAG、性能优化等核心技术。

版权声明: 本文首发于 指尖魔法屋-AI RAG实战指南:从基础检索到自适应反思https://blog.thinkmoon.cn/post/ai-rag-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!