Skip to content

第10章:RAG(检索增强生成)

你想让 Agent 知道公司内部文档的内容,但这些文档加起来几千页,塞不进上下文窗口。你想让 Agent 回答关于最新政策的问题,但训练数据截止日期早于这些政策发布。这就是检索增强生成(RAG,Retrieval-Augmented Generation)要解决的问题:让模型在推理时动态获取它"不知道"的外部知识。

但 RAG 本身也在进化。最早的 RAG 是被动的——用户问,系统查,模型答,一气呵成,不管查到的东西够不够用。Agentic RAG 是主动的——Agent 自己决定查什么、怎么查、查完了够不够、要不要继续查。这两种模式在架构上有本质差异,但很多团队在构建的时候并没有意识到自己选择了哪条路。

这一章先把 RAG 的基础打牢,再讲清楚每个工程决策背后的权衡,最后解释 Agentic RAG 带来的架构变化。

10.1 RAG 基础架构:Indexing → Retrieval → Generation

核心直觉

RAG 的本质是:在模型生成答案之前,先从外部知识库里找到相关内容塞进上下文。

三阶段流水线

RAG 的标准流程分三个阶段,这个架构在 Lewis et al. (2020) 的原始论文中提出,至今仍是基础框架 [1]:

Indexing(索引构建),离线完成:

  1. 把原始文档(PDF、Markdown、数据库记录、代码等)切成固定大小的块(Chunk)
  2. 用 Embedding 模型把每个块转换成向量,捕捉语义信息
  3. 把向量和原始文本存进向量数据库

Retrieval(检索),在线完成:

  1. 用同一个 Embedding 模型把用户查询转成向量
  2. 在向量数据库里做相似度搜索(通常是余弦相似度或内积),找到最相关的 Top-K 个块
  3. 把这些块的原始文本取出来

Generation(生成),在线完成:

  1. 把检索到的内容和用户原始问题拼在一起,构造 Prompt
  2. 发给 LLM 生成最终答案

这个流程的优点很明显:知识库可以随时更新(重新索引即可),不需要重新训练模型;可以支持超出单次上下文窗口的大型知识库;提供的信息可以溯源(来自哪个文档哪段话)。

什么时候可以不用 RAG

在上 RAG 之前,先问一个问题:知识库有多大?

Anthropic 在 Contextual Retrieval 的工程文章里提到 [2]:如果知识库小于 20 万 token(大约 500 页),直接把整个知识库塞进上下文可能是更简单的选择——结合 Prompt Caching 技术(截至 2025 年中,Claude API 支持),可以把缓存命中时的延迟降低超过 2 倍,成本降低最多 90%。

这个判断成立的条件:知识库稳定、查询频繁、对延迟不敏感。对大多数企业知识库场景(几千到几十万个文档),RAG 仍然是唯一可行的路线。

10.2 关键技术决策

Chunking 策略:最容易被忽视的决策

核心直觉

块切得太大,相似度搜索精度下降;切得太小,单个块缺乏上下文,模型看不懂。这是 RAG 最常见的第一个坑。

三种主流策略

固定长度分块:按字符数或 token 数量切分,可以设置重叠窗口防止信息在块边界被截断。实现最简单,但完全不考虑语义边界——可能把一个完整的段落或代码函数从中间切开。

递归字符分块(Recursive Character Splitting):按 "\n\n" → "\n" → "." → " " 的优先级递归分割,尽量在语义边界处切分。LangChain 的默认分块策略用的就是这种方式,是目前最常见的实践选择。

语义分块(Semantic Chunking):用 Embedding 相似度检测段落之间的语义断点,相似度骤降的位置就是块边界。理论上最准确,但计算开销更高,且效果依赖 Embedding 模型的质量。

python
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 如果你想按 token 数控制块大小,用 tiktoken 版本
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    model_name="gpt-4",
    chunk_size=512,       # token 数量
    chunk_overlap=50,     # token 重叠窗口,防止边界信息丢失
    separators=["\n\n", "\n", ".", " ", ""]
)

chunks = splitter.split_documents(documents)

如果直接实例化 RecursiveCharacterTextSplitter(...)chunk_size 默认按字符数计量,而不是 token 数。下面这张表为了便于比较,统一按 token 口径讨论块大小。

块大小的工程权衡

块大小检索精度上下文完整性适用场景
小(≤256 token)高(相似度集中)低(缺乏上下文)精确匹配、FAQ
中(256-512 token)通用场景
大(≥512 token)低(向量更模糊)高(保留上下文)长篇分析、技术文档

没有一个万能的块大小,只有适合你的查询模式的块大小。如果查询是"合同第三条的具体内容",小块更合适;如果查询是"总结这篇报告的核心论点",大块更合适。

Embedding 模型选择

核心直觉

Embedding 模型决定了向量能多精准地捕捉语义,直接影响检索质量的上限。

选型的核心维度是:领域相关性评估分数。通用 Embedding 模型(如 OpenAI text-embedding-3-large、Voyage、Google Gemini Text 004)在大多数场景下表现不错,但对于代码、医疗、法律等垂直领域,专门训练的领域 Embedding 模型通常显著优于通用模型。

Anthropic 在 Contextual Retrieval 的测试中 [2](截至 2024 年 9 月)发现:Voyage 和 Gemini Text 004 的 Embedding 在跨 Codebase、Fiction、学术论文等多种知识域的检索任务中表现最好。这个结论会随模型迭代发生变化,在选型时建议用自己的实际数据集做对比评估,而不是直接套用排行榜。

MTEB(Massive Text Embedding Benchmark)[3] 是目前覆盖最全面的 Embedding 评估基准,包含 56 个数据集和 8 类任务,是选型时最主要的参考来源。

向量数据库选型

不同向量数据库的核心差异不只是"能存多少向量"。生产选型至少看五个维度:规模(支持多少向量)、搜索类型(纯向量/混合搜索)、过滤能力(metadata filter 是否高效)、部署方式(本地/托管/混合)、运维成本(索引构建、备份、扩容、冷数据成本)。

数据库定位优势适用场景
Chroma轻量级本地零配置,Python 原生,适合快速原型开发阶段、小规模知识库
Qdrant开源生产级向量搜索Rust 实现,payload filter 强,支持本地、云和私有部署 [8]需要自托管、权限过滤、多租户隔离的生产 RAG
Milvus高性能分布式支持十亿级向量,功能完整超大规模检索、团队有专门基础设施能力
Pinecone全托管 SaaS无需自运维,稳定性好不想运维基础设施的团队
Weaviate混合搜索向量 + BM25 原生支持,GraphQL API需要混合搜索的场景
pgvectorPostgreSQL 扩展复用已有 PG 基础设施,SQL 查询已有 PG 且规模不大的场景
TurbopufferServerless 向量 + 全文搜索基于对象存储,强调低成本、大规模和混合检索 [9]大规模文档检索、成本敏感、希望减少运维

一个常见的工程选择路径:开发阶段用 Chroma 快速跑通逻辑;生产阶段如果需要自托管和强过滤,优先评估 Qdrant / Milvus;如果团队不想运维,评估 Pinecone / Weaviate Cloud / Turbopuffer;如果数据本来就在 PostgreSQL 且规模不大,pgvector 是最低迁移成本方案。

这里不要把某个时间点的社区热度写成永久结论。向量数据库生态变化非常快,价格、混合搜索能力、过滤性能、托管成熟度都在持续变化。写作或选型时,应该用自己的文档量、查询模式、权限模型和延迟预算做小型 benchmark。

10.3 Advanced RAG:让检索更准

基础 RAG 流水线在实际使用中会暴露一个核心问题:检索召回率不稳定。用户的查询措辞不精确,向量搜索可能找不到最相关的内容;文档块缺乏上下文,语义向量无法准确表示其含义;查询需要跨多个文档拼凑答案,单次检索搞不定。

Advanced RAG 是一组技术的统称,各自针对上述问题的某个维度。

Hybrid Search:向量 + 关键词混合检索

核心直觉

语义向量擅长理解"意思相近",但会遗漏精确的关键词匹配;BM25 擅长精确词匹配,但不懂语义。两者结合,覆盖对方的盲区。

机制

BM25(Best Matching 25)是一种基于词频和逆文档频率的排名算法。它不依赖神经网络,计算快、可解释,特别适合处理含有产品名、错误码、专有名词的查询。

python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma

# 向量检索器(语义搜索)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# BM25 检索器(关键词搜索)
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 10

# 混合检索:0.5/0.5 权重(可根据场景调整)
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5]
)

Anthropic 的 Contextual Retrieval 测试数据 [2] 显示:单独用 Embedding 的 top-20 检索失败率是 5.7%;加入 BM25 混合检索后,失败率降低,效果明显。(具体数字见 10.3 的 Contextual Embeddings 部分)

什么时候混合检索最有价值:知识库里有大量专有名词、产品型号、错误码、人名、代码符号时,纯向量检索容易漏掉精确匹配,混合检索收益显著。知识库是纯叙述性文本、用户查询都是语义性问题时,收益相对有限。

Re-ranking:重排序提升精度

核心直觉

向量检索找到的 Top-K 结果不一定是最相关的,Re-ranking 用一个更精准但更慢的模型对候选结果重新排序,过滤掉质量差的块。

机制

Re-ranking 的两步流程:

  1. 初步检索:用向量搜索快速召回 Top-N(如 150 个)候选块
  2. 精排:把每个候选块和用户查询一起传给 Re-ranker,计算相关性分数,取 Top-K(如 20 个)

Re-ranker 通常是 Cross-Encoder 架构——它同时接收查询和文档,而不是分别编码,因此能计算更精准的语义匹配,但速度比 Embedding 慢得多(所以只用于精排阶段,而非全量检索)。

Anthropic 的测试 [2] 数据(使用 Cohere Reranker,截至 2024 年 9 月):

  • 基线(Embedding only):top-20 检索失败率 5.7%
    • Contextual Embeddings:失败率 3.7%(降低 35%)
    • Contextual Embeddings + Contextual BM25:失败率 2.9%(降低 49%)
    • Contextual Embeddings + Contextual BM25 + Reranking:失败率 1.9%(降低 67%

这组数字说明:每个技术层都有增量收益,叠加效果最好。代价是延迟和成本同步上升,要根据应用对响应速度的要求权衡取舍。

Query Rewriting 与 HyDE

核心直觉

用户的原始查询经常不是检索的最佳输入——太口语化、太模糊、或者和文档的措辞不匹配。Query Rewriting 的思路是先改写查询,再检索。

两种主流方法

Query Rewriting(查询改写):让 LLM 把用户的口语化查询改写成更适合检索的形式。也可以生成多个改写版本,分别检索后合并结果(Multi-Query Retrieval)。

python
from langchain.retrievers.multi_query import MultiQueryRetriever

# 自动生成多个查询变体,每个变体分别检索,合并结果
retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(),
    llm=llm
)

HyDE(Hypothetical Document Embeddings,假设文档嵌入):Gao et al. (2022) 在论文 "Precise Zero-Shot Dense Retrieval without Relevance Labels" 中提出 [4]。核心思路是:先让 LLM 根据查询生成一个"假设答案"(内容可能不正确,但能捕捉答案的语言风格和语义特征),再用这个假设答案的向量去检索,而不是用原始查询的向量。

用户查询:"Python 中如何处理并发?"

HyDE 生成假设答案:
"Python 中处理并发主要有三种方式:threading 模块适合 I/O 密集型任务...
asyncio 实现协程式并发...multiprocessing 模块用于 CPU 密集型并行..."

用假设答案的向量去检索,会比用原始查询向量找到更相关的文档

HyDE 的逻辑是:查询和文档在向量空间里的分布往往不对齐(查询短而模糊,文档长而详细),假设文档在风格上更接近真实文档,所以用它检索的相似度计算更准确。

HyDE 的限制:如果 LLM 对这个领域完全不了解(如非常专业的行业知识),生成的假设答案可能包含严重幻觉,反而误导检索。在知识库包含大量 LLM 不熟悉内容的场景下,HyDE 的收益会打折扣。

Contextual Retrieval:Anthropic 的工程改进方案

核心直觉

传统 RAG 最大的问题是:把文档切成小块后,单个块丢失了它在原文里的上下文——同样一句"公司营收增长了 3%",不知道是哪家公司、哪个季度,单独看毫无意义。Anthropic 的 Contextual Retrieval 就是在切块时主动给每个块加上上下文说明。

机制

Anthropic 在 2024 年 9 月发布的 Contextual Retrieval 方案 [2]:在构建索引时,为每个块自动生成一段简短的上下文描述(50-100 个 token),说明这个块来自哪个文档、讲的是什么背景,然后把这段描述拼接到块的前面,再做 Embedding 和 BM25 索引。

原始块:
"公司营收增长了 3%。"

加上上下文后的块:
"这段内容来自 ACME 公司 2023 年第二季度 SEC 财务报告;上季度营收为 3.14 亿美元。
公司营收增长了 3%。"

用 Claude Haiku 自动生成上下文的 Prompt 模板:

<document> 
{{WHOLE_DOCUMENT}} 
</document> 

这是我们要在整体文档语境中定位的片段:
<chunk> 
{{CHUNK_CONTENT}} 
</chunk> 

请给出一段简短的上下文描述,说明这个片段在整体文档中的位置,
以便提升检索时的准确性。只输出上下文描述本身,不要有其他内容。

成本层面:假设每个块 800 个 token、每个文档 8000 个 token,结合 Prompt Caching(每个文档只处理一次,后续块调用缓存),生成上下文的成本约为每百万文档 token 1.02 美元(截至 2024 年 9 月的 Claude Haiku 定价)。

Multi-hop Retrieval:回答需要多步推理的查询

核心直觉

有些问题的答案不在一个文档块里,需要先找到信息 A,用 A 作为线索找到信息 B,再用 B 找到最终答案——这就是多跳检索。

单次检索解决不了的查询例子:"在 2023 年发布了旗舰芯片的那家公司,其 CEO 的背景是什么?"——需要先检索"哪家公司 2023 年发布了旗舰芯片",再用公司名检索"CEO 是谁",再检索"这位 CEO 的背景"。

Tang & Yang (2024) 的 MultiHop-RAG [5] 研究表明:现有的 RAG 系统在多跳查询上的表现明显差于单跳查询,即使是 GPT-4 在获得了相关证据后,在多跳推理准确率上也有明显下滑。这是当前 RAG 系统的主要工程挑战之一。

实现多跳检索的基本思路:

python
def multi_hop_retrieval(query: str, max_hops: int = 3) -> list[str]:
    """多跳检索:每一跳基于上一跳的结果生成新的子查询"""
    all_contexts = []
    current_query = query
    
    for hop in range(max_hops):
        # 检索当前查询
        contexts = retriever.retrieve(current_query)
        all_contexts.extend(contexts)
        
        # 让 LLM 判断:信息是否足够?如果不够,生成下一跳查询
        next_query = llm.generate_followup_query(
            original_query=query,
            retrieved_contexts=contexts,
            hop_number=hop
        )
        
        if next_query is None:  # 信息已足够
            break
        
        current_query = next_query
    
    return all_contexts

多跳检索在 Agentic RAG 框架中更自然,因为 Agent 本来就具备"检索结果不够→继续查"的判断能力。

10.4 Agentic RAG vs 传统 RAG

两种模式的本质差异

传统 RAG 是被动流水线:用户查询进来,固定步骤走一遍,一个答案出去。整个过程里没有"判断"——不管检索结果好不好、够不够,都直接送给模型生成答案。

Agentic RAG 是主动决策循环:Agent 接到任务,自己决定查哪个数据源、用什么查询策略、检索结果够不够用、需不需要再查一轮。

传统 RAG 的局限

  1. 单次检索的信息量限制:复杂问题可能需要从多个角度、多个文档拼凑答案,一次检索不够
  2. 无法验证检索质量:不管检索到的内容相关性高不高,都直接用
  3. 固定数据源:传统 RAG 通常只有一个向量数据库,而现实中知识分散在多个系统——文档库、数据库、API、代码库

Agentic RAG 的工作方式

把检索能力封装成工具,让 Agent 主动调用:

python
from anthropic import Anthropic

client = Anthropic()

ALLOWED_METRICS = {"revenue", "orders", "active_users"}
ALLOWED_GROUP_BY = {"day", "week", "month"}


def run_readonly_analytics_query(metric: str, start_date: str, end_date: str, group_by: str = "day"):
    """只读分析查询:模型给结构化参数,后端拼参数化 SQL。"""
    if metric not in ALLOWED_METRICS:
        raise ValueError(f"Unsupported metric: {metric}")
    if group_by not in ALLOWED_GROUP_BY:
        raise ValueError(f"Unsupported group_by: {group_by}")

    bucket = {"day": "day", "week": "week", "month": "month"}[group_by]
    sql = f"""
        SELECT date_trunc('{bucket}', order_date) AS bucket, SUM(value) AS metric_value
        FROM analytics.daily_metrics
        WHERE metric_name = :metric
          AND order_date BETWEEN :start_date AND :end_date
        GROUP BY 1
        ORDER BY 1
    """
    params = {
        "metric": metric,
        "start_date": start_date,
        "end_date": end_date,
    }
    return execute_readonly_sql(sql, params=params, timeout_seconds=10)

# 把 RAG 检索包装成工具
tools = [
    {
        "name": "search_knowledge_base",
        "description": "在公司知识库中搜索相关信息。当需要了解公司政策、产品规格、历史记录时使用。",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "搜索查询,尽量使用精确的关键词"
                },
                "num_results": {
                    "type": "integer",
                    "description": "返回结果数量,默认 5",
                    "default": 5
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "query_sales_metrics",
        "description": "查询只读分析数据库中的聚合指标。只能查询 allowlist 内的指标。",
        "input_schema": {
            "type": "object",
            "properties": {
                "metric": {
                    "type": "string",
                    "enum": ["revenue", "orders", "active_users"],
                    "description": "要查询的指标名称"
                },
                "start_date": {
                    "type": "string",
                    "description": "开始日期,格式 YYYY-MM-DD"
                },
                "end_date": {
                    "type": "string",
                    "description": "结束日期,格式 YYYY-MM-DD"
                },
                "group_by": {
                    "type": "string",
                    "enum": ["day", "week", "month"],
                    "description": "聚合粒度,默认按天",
                    "default": "day"
                }
            },
            "required": ["metric", "start_date", "end_date"]
        }
    }
]

def run_agentic_rag(user_question: str) -> str:
    messages = [{"role": "user", "content": user_question}]
    
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )
        
        if response.stop_reason == "end_turn":
            # Agent 决定不再需要更多信息,生成最终答案
            return response.content[0].text
        
        if response.stop_reason == "tool_use":
            # Agent 决定需要检索更多信息
            tool_calls = [b for b in response.content if b.type == "tool_use"]
            tool_results = []
            
            for tool_call in tool_calls:
                if tool_call.name == "search_knowledge_base":
                    result = vector_search(
                        tool_call.input["query"],
                        k=tool_call.input.get("num_results", 5),
                    )
                elif tool_call.name == "query_sales_metrics":
                    result = run_readonly_analytics_query(
                        metric=tool_call.input["metric"],
                        start_date=tool_call.input["start_date"],
                        end_date=tool_call.input["end_date"],
                        group_by=tool_call.input.get("group_by", "day"),
                    )
                
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tool_call.id,
                    "content": result
                })
            
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

工程上不要让 LLM 直接生成并执行任意 SQL。更稳妥的做法是:只暴露结构化参数、后端用 allowlist 构造参数化查询,并配合只读账号、超时限制和审计日志。

Agentic RAG 带来的新能力

动态数据源路由:Agent 可以根据问题类型决定去哪里查。问"产品规格"去文档库,问"上个月的销售数据"去数据库,问"竞争对手动态"去网络搜索。

自适应检索深度:传统 RAG 固定检索一次。Agentic RAG 根据检索质量自动决定是否继续——如果第一次检索的结果置信度低,自动触发补充检索。

检索+推理交织:不是先全部检索再推理,而是检索一步、推理一步、再根据推理结论决定下一步检索方向。这让它能处理需要多步逻辑串联的复杂问题。

常见误区:认为 Agentic RAG 一定比传统 RAG 好。对于简单的单次问答("这个产品的重量是多少"),传统 RAG 响应更快、成本更低、行为更可预测。Agentic RAG 的额外复杂性只在任务真的需要多步推理或多源检索时才值得。OpenAI 和 Anthropic 的一致建议:从最简单的方案开始,只在必要时增加复杂度。

10.5 RAG 评估体系

为什么 RAG 评估比想象的难

RAG 系统有两个地方可能出问题:检索(找到的内容不相关)和生成(找到了相关内容但答错了)。这两个阶段需要分开评估,才能知道优化方向在哪里——是换向量模型,还是改 Prompt,还是优化分块策略。

三个核心指标

Faithfulness(忠实度):模型的回答中,有多少比例的陈述能从检索到的上下文中找到支撑?

$$\text{Faithfulness} = \frac{\text{上下文中有支撑的陈述数}}{\text{回答中的总陈述数}}$$

这个指标衡量的是"模型有没有编造上下文里没有的内容",即幻觉程度。即使检索结果很好,如果模型没有老实依据上下文回答,Faithfulness 就会低。

Answer Relevance(答案相关性):回答有多好地响应了用户的问题?

衡量方式:让另一个 LLM 根据回答反向生成"这个回答可能在回答什么问题",然后计算这些反向生成的问题与原始问题的向量相似度。答案越切题,相似度越高。

Context Recall(上下文召回率):所有需要回答这个问题的信息,有多少比例被检索出来了?

$$\text{Context Recall} = \frac{\text{被检索到的必要信息片段数}}{\text{回答问题所需的总信息片段数}}$$

这个指标需要有标注好的 Ground Truth 答案(知道哪些信息是回答这个问题必需的),所以在实践中通常需要构建评估数据集。

评估工具

Ragas(截至 2026 年初):专门为 RAG 评估设计的开源框架 [6],支持 Faithfulness、Answer Relevance、Context Precision、Context Recall 等核心指标,并支持与 LangChain、LlamaIndex 直接集成。它的 API 迭代比较快,下面示例按当前稳定版的 class-based 写法展示:

python
from datasets import Dataset
from ragas import evaluate
from ragas.llms import llm_factory
from ragas.metrics import ContextRecall, Faithfulness, ResponseRelevancy

llm = llm_factory("gpt-4o-mini")

# 准备评估数据
eval_data = {
    "question": ["Python 中的 GIL 是什么?", ...],
    "answer": ["GIL 是全局解释器锁...", ...],
    "contexts": [["Python 的 GIL(Global Interpreter Lock)...", ...], ...],
    "ground_truth": ["Python 的全局解释器锁(GIL)是一种互斥锁...", ...]
}

dataset = Dataset.from_dict(eval_data)

result = evaluate(
    dataset=dataset,
    metrics=[
        Faithfulness(llm=llm),
        ResponseRelevancy(llm=llm),
        ContextRecall(llm=llm),
    ],
)

print(result)
# {'faithfulness': 0.86, 'answer_relevancy': 0.92, 'context_recall': 0.78}

DeepEval:另一个常用的 LLM 应用评估框架,支持 RAG 评估、单元测试风格的集成,有更丰富的自定义指标接口。

实践建议

评估数据集的质量决定评估结果的价值。构建评估集的成本往往被低估——需要准备有代表性的真实查询、对应的标准答案、以及哪些文档片段是回答这个问题必需的。如果没有高质量的评估数据集,任何指标数字都是没有意义的。

评估驱动的迭代

RAG 的核心工程挑战:改了一个组件(比如换了分块策略),不知道是变好还是变坏。正确的做法:

  1. 先建立评估数据集(至少 50-100 个有代表性的查询-答案对)
  2. 确定基线(当前系统在每个指标上的分数)
  3. 每次改动后跑评估,看各指标变化
  4. 追踪改动和指标变化的关系,而不是凭感觉判断

HN 社区上有开发者分享过这样的踩坑经验(2024 年):换了一个"更好"的 Embedding 模型,Context Recall 从 0.72 提升到 0.85,但 Faithfulness 从 0.91 下降到 0.78——整体 RAG 质量反而下降了。如果没有评估框架,光用肉眼 review 几个问题的输出,不可能发现这种系统性退步。

10.6 知识库构建与维护工程

RAG 的工作量大多在检索之前

很多教程把 RAG 讲成"切片、embedding、入库、检索"四步。但真正落地企业知识库时,最耗时间的往往不是向量搜索,而是知识库本身的工程管线:

每一步都会影响最终质量。PDF 解析错了,后面再好的 Embedding 也救不回来;元数据缺失,权限过滤和溯源都会变得脆弱;切片策略没有版本化,回滚和评估就会失去基线。

ETL:从原始文档到可检索知识

企业知识库通常不是干净的 Markdown 文件,而是混杂的 PDF、网页、Confluence、飞书文档、数据库记录、工单、代码仓库和邮件。ETL 管线要处理:

  • 抽取:从 PDF、HTML、数据库、SaaS API 拉取原始内容。
  • 清洗:去掉导航栏、页眉页脚、重复版权声明、乱码、无意义 OCR 噪声。
  • 规范化:统一标题层级、表格表示、代码块、引用、日期和作者信息。
  • 切片:按语义边界切分,并保留章节路径、文档 ID、版本、租户、权限标签。
  • 入库:写入向量库、全文索引和原文存储,保证每个 chunk 能追溯到来源。

建议把 chunk 当成有生命周期的数据对象,而不是一段临时文本:

json
{
  "chunk_id": "policy_2026_04_001#p12_c03",
  "document_id": "policy_2026_04_001",
  "document_version": "2026-04-10",
  "tenant_id": "acme",
  "acl": ["hr", "legal"],
  "source_url": "https://docs.example.com/policy/001",
  "section_path": ["员工手册", "休假制度", "病假"],
  "text": "..."
}

更新、删除和版本管理

知识库不是一次性导入就结束。生产系统必须回答几个问题:

文档更新时怎么办?

不要只追加新 chunk。需要识别旧版本,标记过期,重建受影响的 chunk 和 embedding。否则检索结果里会同时出现旧政策和新政策。

文档删除时怎么办?

删除必须传播到向量库、全文索引、缓存和评估样本。很多 RAG 泄露事故不是模型"知道太多",而是删除流程没有清理旧索引。

切片策略变化时怎么办?

切片策略本身要版本化。chunk_size=512 改成 chunk_size=1024 后,评估基线不再可比。正确做法是保留 pipeline version,让每次检索结果能追溯到当时的索引构建策略。

增量索引怎么做?

用文档 hash 或更新时间判断哪些文档需要重建。对大型知识库,全量重建成本太高;但只看更新时间也不够,因为上游系统可能保留旧时间戳。更稳的做法是记录内容 hash、解析器版本、切片器版本和 embedding 模型版本。

多租户权限隔离

RAG 的权限问题必须尽量在检索前解决,而不是检索后再删结果。

检索后过滤有两个风险:

  1. Top-K 先被无权限文档占满,过滤后剩下的结果太少,召回率下降。
  2. 如果中间日志、trace 或模型上下文里已经出现无权限片段,就已经越界了。

更安全的设计是:查询时把 tenant_id、用户组、文档 ACL 作为过滤条件送到检索层,让向量库只在用户可访问的集合内搜索。对于权限复杂的企业系统,通常还需要把权限快照写入 chunk 元数据,并在文档权限变化时触发重建或重新标记。

知识库质量监控

RAG 系统上线后,需要持续监控知识库本身:

  • OCR 失败率:扫描件、表格、图片型 PDF 是否解析成乱码。
  • 重复率:同一文档多次导入会污染检索。
  • 过期率:过期政策是否还被检索到。
  • 低召回查询:用户反复问但检索不到有效内容的问题。
  • 无引用回答:模型给出答案但没有可靠来源。

这部分工作不够炫,但决定 RAG 能不能长期稳定。很多"模型幻觉"其实不是模型问题,而是知识库工程没做好。

常见误区

"Context 越多越好":把 Top-K 设置得越大,就能覆盖越多信息,答案越准——这是错的。上下文过多会稀释真正相关的信息,Anthropic 的测试里 top-20 比 top-10 和 top-5 更好,但这不意味着 top-100 会更好。模型在上下文很长时会出现"中间盲区"(Lost in the Middle)问题——对上下文开头和结尾的信息更敏感,中间的容易被忽略。

"向量相似度等于语义相关":向量空间里很近,不代表真的相关。Embedding 模型捕捉的是语义模式,但在特定领域(法律条文、代码、数字密集的报告)里,Embedding 相似度可能完全跑偏。混合搜索、领域专用 Embedding、评估驱动调优,都是应对这个问题的手段。

"RAG 解决了幻觉问题":RAG 减少了幻觉,但没有消除它。模型仍然可能忽略上下文里的信息、拼接上下文时出错、对超出上下文的问题仍然"自由发挥"。Faithfulness 评估是追踪这个问题的必要工具。

"先上 Agentic RAG 再说":对于清晰的单轮问答场景,Agentic RAG 的额外延迟和成本没有任何收益。先用传统 RAG 跑通,评估出具体的质量短板,再决定哪里需要 Agentic 能力。

"RAG 就是向量数据库选型":向量库只是其中一层。文档 ETL、权限隔离、增量索引、版本管理和质量监控,往往比换一个向量库更影响最终效果。


面试高频题

Q:Agentic RAG 与传统 RAG 的核心区别?

参考答案框架:

  • 传统 RAG:固定的被动流水线,用户查询 → 检索 → 生成,一次完成,无判断
  • Agentic RAG:Agent 主动决策检索策略——查哪个数据源、查几次、当前结果够不够、是否需要补充检索
  • 核心差异:主动性(Agent 驱动 vs 流水线触发)、多步推理(能处理需要串联多步信息的查询)、多源适配(能跨多个异构数据源)

加分点:能说清楚 Agentic RAG 不是万能的——对简单查询,它的额外成本和延迟没有价值;能结合具体产品场景说哪种模式更合适。

Q:如何评估 RAG 系统的质量?

参考答案框架:

  • 分检索和生成两个阶段独立评估,才能知道问题出在哪
  • 三个核心指标:Faithfulness(生成忠实度,防幻觉)、Answer Relevance(答案相关性)、Context Recall(检索召回率)
  • 工具:Ragas、DeepEval
  • 前提是有高质量评估数据集——没有好的数据集,指标数字没有意义
  • 关键实践:建立基线,每次改动后对比指标变化,不要只靠人眼 review
  • 生产系统还要评估知识库工程:文档解析质量、索引更新延迟、权限过滤是否前置、过期文档是否被召回

加分点:能说清楚 Faithfulness 和 Context Recall 的计算逻辑;能提到评估数据集构建的成本和重要性;能说出"提升 Recall 可能降低 Faithfulness"这类指标之间的权衡。


参考资料

[1] Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks - Lewis et al., 2020, NeurIPS (https://arxiv.org/abs/2005.11401)

[2] Introducing Contextual Retrieval - Anthropic Engineering Blog, Sep 2024 (https://www.anthropic.com/engineering/contextual-retrieval)

[3] MTEB: Massive Text Embedding Benchmark - Muennighoff et al., 2022 (https://arxiv.org/abs/2210.07316)

[4] Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE) - Gao et al., 2022 (https://arxiv.org/abs/2212.10496)

[5] MultiHop-RAG: Benchmarking Retrieval-Augmented Generation for Multi-Hop Queries - Tang & Yang, 2024 (https://arxiv.org/abs/2401.15391)

[6] Ragas: Automated Evaluation of RAG Pipelines - Ragas Documentation (https://docs.ragas.io/en/stable/)

[7] Retrieval-Augmented Generation for Large Language Models: A Survey - Gao et al., 2023 (https://arxiv.org/abs/2312.10997)

[8] Qdrant Documentation. What is Qdrant? https://qdrant.tech/documentation/overview/what-is-qdrant/

[9] Turbopuffer Documentation. Vector Search Guide. https://turbopuffer.com/docs/vector