RAG 知识库优化:别让 AI 一本正经地胡说八道

Posted on 五 08 5月 2026 in Journal

Abstract RAG 知识库优化:别让 AI 一本正经地胡说八道
Authors Walter Fan
Category Journal
Status v1.0
Updated 2026-05-10
License CC-BY-NC-ND 4.0

引言:RAG 最怕一本正经地错

你有没有遇到过这样的场景:

花了两周搭了一套 RAG 系统,接上公司知识库,兴冲冲演示给老板看。老板随口问了一句:"我们 Q1 的营收是多少?" 系统很快回答了一个数字,语气坚定,格式漂亮。问题是,数字是错的。

这不是段子,这是无数 RAG 项目的真实写照。

RAG (Retrieval-Augmented Generation,检索增强生成) 的原理不复杂:先从知识库里找材料,再让 LLM 基于材料回答。听起来很像开卷考试。

可是开卷考试也会翻错页、抄错段,甚至没看书就开始发挥。RAG 也一样——分块不当、检索不准、上下文塞太多、Prompt 没约束、引用没溯源、上线后不评估,每一项都能把"知识库助手"变成"知识库造谣机"。

我把 RAG 优化拆成四件事:

数据准备  →  检索优化  →  生成约束  →  评估监控

这四件事做扎实,RAG 才有资格谈"可用"。否则 Demo 再漂亮,也只是一个会说漂亮话的概率玩具。

一、RAG 架构回顾

先把基本流程摆出来。流程不复杂,复杂的是每一步都可能埋坑。

@startuml

top to bottom direction
skinparam defaultTextAlignment center
skinparam shadowing false
skinparam rectangle {
    RoundCorner 15
}

rectangle "用户提问" as User

rectangle "Query 理解\n& 改写" as QueryRewrite
rectangle "检索/召回\n(Retrieval)" as Retrieval
rectangle "重排序\n(Reranking)" as Reranking
rectangle "上下文组装\n& Prompt" as PromptAssembly
rectangle "LLM 生成\n(Generation)" as Generation
rectangle "后处理 &\n引用溯源" as PostProcess

User --> QueryRewrite
QueryRewrite --> Retrieval
Retrieval --> Reranking
Reranking --> PromptAssembly
PromptAssembly --> Generation
Generation --> PostProcess

@enduml

每个环节都有优化空间,也都有翻车机会。RAG 的麻烦就在这里:它不是一个单点模型问题,而是一条链路问题。链路上任何一环松了,最后都会体现在答案质量上。

二、数据准备:垃圾进,垃圾出

先把材料收拾干净

1. 分块策略是基石

分块是 RAG 里最容易被低估、却最影响体验的环节。很多系统不是模型不行,是把知识切碎的时候就已经切坏了。

固定长度分块省事,但它不关心句子、段落、标题和上下文。就像切菜只看尺子不看菜,最后切出来能不能下锅,全凭运气。

更靠谱的做法是按语义边界切:

# 固定长度分块——省事但粗暴
chunks = [text[i:i+512] for i in range(0, len(text), 512)]

# 按语义边界分块——多花几行代码,少踩很多坑
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=100,
    separators=["\n## ", "\n### ", "\n\n", "\n", "。", ".", " "],
    length_function=len
)
chunks = splitter.split_text(text)

先按这几条原则做:

原则 说明
语义完整性 一个 chunk 应该包含一个完整的语义单元
适当重叠 10-20% 的重叠率,避免上下文断裂
保留元数据 每个 chunk 附带来源文档、章节、页码
大小适中 通常 200-800 tokens,太小缺上下文,太大噪声多

2. 文档预处理别偷懒

PDF 解析、网页抓取、Office 文档导入,看起来都是"把文档变成文本",实际效果差很多。页眉页脚、水印、目录、乱码、断行、表格错位,这些都会进入检索链路。

脏数据进了向量库,不会因为套了一层 AI 就自动变干净。

def preprocess_document(doc):
    doc = remove_headers_footers(doc)
    tables = extract_tables(doc)
    images = extract_and_describe_images(doc)
    metadata = extract_metadata(doc)
    return doc, tables, images, metadata

3. 不要只建一个大索引

不要把所有内容一股脑塞进一个向量索引。文档、章节、句子、表格、图片,信息形态不同,检索方式也该不同。

一种常见做法是父子文档索引 (Parent-Child):

文档层 (Document)  →  摘要索引 (用于粗召回)
  │
  ├── 段落层 (Section)  →  主索引 (用于精确检索)
  │     │
  │     └── 句子层 (Sentence)  →  细粒度索引 (用于精确匹配)
  │
  └── 表格/图片  →  结构化索引 (单独处理)

容易踩的坑

坑 1:不洗数据,直接灌

PDF 原样解析,页眉页脚、目录、版权声明全进了向量库。检索时这些噪声频繁被召回,严重拉低质量。典型情况是:用户问一个具体流程,top-5 里却混进两条页眉里的 "Confidential - Do Not Distribute"。

清洗数据可以按四步走。不要一开始就上"全家桶",先把最脏、最重复、最影响检索的东西干掉:

步骤 具体怎么做 常用工具
去模板噪声 统计每页重复出现的文本,删除页眉页脚、水印、版权声明、导航菜单、重复目录;网页内容先做正文抽取,别把侧边栏和广告一起塞进去 PyMuPDF / pdfplumbertrafilaturareadability-lxmlBeautifulSoup
修版式问题 合并异常断行,修复乱码和全半角混用,去掉多余空格,恢复项目符号;PDF 里跨页断开的句子要重新拼起来 ftfy、正则、unstructuredDoclingMarkItDown
保留结构信息 把标题层级、表格、代码块、图片说明、来源页码保存到 metadata;表格不要粗暴拍平成一坨文本,最好转成 Markdown 表格或结构化 JSON pandascamelot / tabulamarkdownify
抽样验收 随机抽几十个 chunk,看语义是否完整、来源是否清楚、噪声是否重复;再用几条典型问题做 smoke test,看召回结果是不是"看起来就靠谱" 自己写脚本、pytest、简单的检索评测集

坑 2:分块大小一刀切

所有文档统一用 512 tokens 分块。FAQ 类文档需要小块 (100-200),技术手册需要大块 (500-800),一刀切顾此失彼。

分块策略也可以做成一张配置表,不要把所有文档都塞进同一个 splitter:

文档类型 具体怎么做 常用工具
FAQ / 问答 一问一答尽量保持在同一个 chunk,chunk 可以小一点,通常 100-200 tokens 就够;不要把多个无关问题硬拼在一起 RecursiveCharacterTextSplitter、自定义 Q/A parser
技术手册 / 设计文档 按标题层级、段落和代码块切,保留 10-20% overlap;代码块不要从中间切断 MarkdownHeaderTextSplitterRecursiveCharacterTextSplitter
长 PDF / 规章制度 先按章节切,再在章节内按段落切;每个 chunk 带上章节名、页码、版本号 LangChain splitters、LlamaIndex node parser
表格 / 配置项 不要按 token 硬切,优先按行、按字段或按业务实体切;必要时转成结构化 JSON 单独入库 pandascamelot / tabula、自定义 parser

坑 3:忽略文档之间的关系

公司制度文档 A 引用了文档 B 的条款,但分块后这种引用关系丢失了。用户问到相关问题,系统只能给出片面回答。

文档关系要在入库时就显式保存,否则检索阶段很难凭空猜出来:

关系类型 具体怎么做 常用工具
引用关系 解析"见第 X 条"、"参考文档 B"、URL 链接、附件名,把被引用文档 ID 写进 metadata 正则、BeautifulSoup、自定义 link extractor
层级关系 保存 document -> section -> chunk 的父子结构;召回子 chunk 后,可以把父章节一起带出来补上下文 LlamaIndex Parent-Child retriever、LangChain Parent Document Retriever
版本关系 保存文档版本、发布日期、失效日期;检索时优先召回最新版,避免旧政策压过新政策 metadata filter、向量库过滤条件
主题关系 给文档打业务标签,比如产品线、部门、系统、模块;用户问题先缩小范围,再做向量检索 embedding 聚类、人工标签、spaCy / KeyBERT

三、检索优化:找到对的内容

先别急着让模型回答

1. 混合检索

很多 RAG 项目一开始只上向量检索,觉得语义相似就够了。实际并不够。

向量检索擅长找"意思接近"的内容,BM25 这类关键词检索擅长打中精确词。有人问 "ISO 27001",有人问 "报销制度 v2.1",这种问题如果只靠语义相似,很容易把路走偏。

所以更稳妥的方式是两条路一起走:

from typing import List

def hybrid_search(query: str, top_k: int = 10) -> List[dict]:
    vector_results = vector_store.similarity_search(query, k=top_k * 2)
    bm25_results = bm25_index.search(query, k=top_k * 2)

    # Reciprocal Rank Fusion
    fused = reciprocal_rank_fusion(
        [vector_results, bm25_results],
        weights=[0.6, 0.4]
    )
    return fused[:top_k]


def reciprocal_rank_fusion(result_lists, weights=None, k=60):
    scores = {}
    if weights is None:
        weights = [1.0] * len(result_lists)

    for results, weight in zip(result_lists, weights):
        for rank, doc in enumerate(results):
            doc_id = doc["id"]
            if doc_id not in scores:
                scores[doc_id] = 0
            scores[doc_id] += weight * (1.0 / (k + rank + 1))

    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

2. Query 改写

用户的问题是面向人的,不一定适合检索系统。人会省略上下文,会用简称,会问得很口语。检索系统可没那么善解人意。

可以把一个原始问题改写成几个检索友好的 query:

def query_rewrite(original_query: str, llm) -> List[str]:
    prompt = f"""请将以下用户问题改写为3个不同角度的检索查询:

    原始问题:{original_query}

    要求:
    1. 一个保持原意但更精确的版本
    2. 一个使用同义词/近义词的版本  
    3. 一个更宽泛的版本
    """
    queries = llm.generate(prompt)
    return [original_query] + queries

3. Reranking 往往最划算

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")

def rerank(query: str, candidates: List[str], top_k: int = 5):
    pairs = [[query, doc] for doc in candidates]
    scores = reranker.predict(pairs)

    ranked = sorted(
        zip(candidates, scores), 
        key=lambda x: x[1], 
        reverse=True
    )
    return ranked[:top_k]

经验法则:先粗召回 20-50 条,再用 Reranker 精排到 3-5 条。很多时候这一步比盲目换大模型更划算。

如果前面用了 Hybrid Retrieval,通常会先分别跑两路召回:

BM25 / 关键词检索   →  擅长命中专有名词、编号、错误码
向量检索 / Dense    →  擅长命中语义相近的表达

两路结果合并时,一个常用办法是 RRF(Reciprocal Rank Fusion)。它不直接比较 BM25 分数和向量相似度,因为这两个分数不是一个量纲;它只看排名:

RRF_score(d) = Σ 1 / (k + rank_i(d))

其中 d 是某个文档或 chunk,rank_i(d) 是它在第 i 路检索结果里的排名,k 是平滑参数,常见取值是 60。一个 chunk 如果在 BM25 和向量检索里都排得靠前,RRF 分数就会更高;如果只在一路里偶然靠前,分数就不会太夸张。

这招朴素,但很实用。它像开会时听两个人投票:关键词检索说"这个很像",向量检索也说"这个也像",那就优先拿出来给 reranker 精排。

4. 元数据过滤

不要什么问题都去全库里搜。用户问财务制度,就先限定部门;问最新政策,就过滤更新时间;问某个产品线,就缩到对应文档集合。

不花哨,但很管用。

results = vector_store.similarity_search(
    query,
    k=10,
    filter={
        "department": "finance",
        "doc_type": "policy",
        "updated_after": "2025-01-01"
    }
)

容易踩的坑

坑 4:只用向量检索

用户问 "ISO 27001 认证流程",向量检索返回一堆关于"认证"和"流程"的无关内容——语义相似但主题不同。加上 BM25 关键词匹配后,"ISO 27001" 这种关键字才能被精确命中。

混合检索不要只喊口号,可以按问题类型拆:

场景 具体怎么做 常用工具
有明确关键词 对产品名、标准号、错误码、人名、工单号走 BM25 或精确匹配,先保证关键字不丢 Elasticsearch / OpenSearchPostgreSQL full-text search
语义描述类问题 用户说的是"怎么申请权限"、"系统为什么变慢"这类自然语言问题,用向量检索找相近语义 FAISSMilvusQdrantpgvector
两者都有 BM25 和向量检索各召回一批,再用 RRF 或加权分数合并,避免一边独大 Reciprocal Rank Fusion、Elasticsearch hybrid search、LlamaIndex retrievers
带结构条件 先用 metadata 过滤部门、产品线、版本、时间,再做混合检索,别在全库里大海捞针 向量库 metadata filter、where 条件、业务标签

坑 5:不做 Reranking

向量检索的 top-10 结果中,真正相关的可能排在第 5-8 位。不做重排序直接取 top-3,大概率丢失关键信息。

Reranking 的核心是:召回阶段宁可多捞一点,排序阶段再精挑细选

场景 具体怎么做 常用工具
通用文本重排 先召回 20-50 条,再用 reranker 精排到 3-5 条;这一步通常比盲目增大 top_k 更有效 BAAI/bge-reranker-largeCohere RerankJina Reranker
中文知识库 选中文或多语言 reranker,不要默认拿英文 cross-encoder 硬套 bge-reranker-v2-m3bge-reranker-large
专业领域文档 准备一小批真实问答对,用人工标注的相关性样本评估 reranker;效果不够再考虑微调 sentence-transformers CrossEncoder、评测集、pytest
防止结果太单一 top-5 里不要全是同一章节的近似 chunk,可以加 MMR 或按文档去重 MMR、按 doc_id 去重、自定义 post-rank 规则

坑 6:Embedding 模型选错

用英文 Embedding 模型处理中文知识库,或者用通用模型处理专业领域文档。要选与语言和领域匹配的模型,必要时做 fine-tuning。

选模型别只看榜单,先看你的语料和问题长什么样:

场景 推荐模型 注意点
中文通用 BAAI/bge-large-zh-v1.5 适合中文知识库的基线模型,先用它跑一版评测再说
英文通用 text-embedding-3-large 效果好,但要考虑 API 成本、数据出境和隐私要求
多语言 BAAI/bge-m3 中英混合、跨语言检索时更稳,适合国际化文档
代码 voyage-code-3 代码检索不要只靠自然语言 embedding,最好保留函数名、类名、文件路径等 metadata
垂直领域 通用模型 + 小评测集,必要时再 fine-tuning 先做 50-100 条真实查询评测,别上来就训练模型

四、生成优化:让 LLM 少发挥

规则要写清楚

1. Prompt 不是装饰,是边界

RAG 里的 Prompt 不是为了让回答更"优雅",是给模型划边界:哪些能答,哪些不能答,引用怎么给,资料不够时怎么说。

尤其是企业知识库,宁可回答"根据现有资料无法回答",也不要编一个听起来像真的答案。编出来的答案如果被人当真,后果比不回答严重得多。

RAG_SYSTEM_PROMPT = """你是一个专业的知识库问答助手。请严格基于以下检索到的参考资料回答用户问题。

## 规则
1. **只基于参考资料回答**,不要使用你的训练知识
2. 如果参考资料不足以回答问题,明确说"根据现有资料无法回答"
3. 回答中引用来源,格式为 [来源: 文档名称]
4. 如果多个来源有矛盾,指出差异并说明各自来源
5. 保持回答简洁、结构化

## 参考资料
{context}

## 用户问题
{question}
"""

2. 上下文不是越多越好

调 RAG 时有个常见冲动:怕漏信息,那就多塞点上下文。听起来合理,实际很危险。上下文越多,噪声也越多,模型越容易抓不住重点。

def build_context(chunks: List[dict], max_tokens: int = 4000) -> str:
    context_parts = []
    current_tokens = 0

    for i, chunk in enumerate(chunks):
        chunk_tokens = count_tokens(chunk["text"])
        if current_tokens + chunk_tokens > max_tokens:
            break

        context_parts.append(
            f"[参考{i+1}] (来源: {chunk['source']}, "
            f"更新: {chunk['date']})\n{chunk['text']}"
        )
        current_tokens += chunk_tokens

    return "\n\n---\n\n".join(context_parts)

3. 引用溯源不是锦上添花

RAG 系统和普通聊天机器人最大的区别,是它该让用户追到来源。答案后面没有引用,用户就只能选择信或不信——这不是知识库系统该有的样子。

CITATION_PROMPT = """回答问题时,请在每个关键信息后标注来源编号。

格式示例:
公司的年假政策规定,入职满1年的员工享有5天年假[1],
满5年的员工享有10天年假[2]。

最后列出参考来源:
[1] 《员工手册v3.2》第四章第二节
[2] 《2025年度假期政策更新》
"""

容易踩的坑

坑 7:不限制 LLM 的"创造力"

Prompt 里没要求"只基于检索内容回答",LLM 就开始脑补,把训练数据里的过时信息混入答案。这就是所谓的幻觉——它不是故意骗你,它是真觉得自己说得对。

生成阶段要把边界写死,尤其是企业知识库,别让模型自由发挥:

风险场景 具体怎么做 常用工具 / 机制
资料不足 明确要求"资料不足就说无法回答",不要让模型凭常识补全 System Prompt、拒答模板
来源混乱 要求每个关键结论都带引用编号,没有引用的句子不输出或标记为不确定 Citation Prompt、后处理校验
多来源冲突 如果多个来源说法不一致,要求模型列出差异,而不是自行裁判 Prompt 规则、conflict detection
输出跑偏 限定回答格式,比如结论、依据、注意事项、来源;复杂场景用 JSON Schema 约束 structured output、Pydantic、Guardrails

坑 8:上下文塞太多

把检索到的 20 个 chunk 全塞进 Prompt,LLM 反而被噪声干扰,抓不住重点。实践中,3-5 个高质量 chunk 往往优于 10+ 个中等质量 chunk

上下文组装要像打包行李:该带的带上,"也许有用"的先放下:

场景 具体怎么做 常用工具 / 机制
chunk 太多 先 rerank,再只取 top 3-5 个高质量 chunk;不要把 top 20 原样塞进 Prompt reranker、top_k 控制
chunk 太长 对长 chunk 做摘要或二次切分,只保留与问题相关的段落 map-reduce summarize、自定义 trimmer
信息重复 同一文档连续命中的多个相似 chunk,按 doc_id 和相似度去重 MMR、dedup by metadata
token 超限 给上下文设置 token budget,超过预算就按相关性和新鲜度裁剪 tiktoken、token counter、context budget

坑 9:忽略 "Lost in the Middle"

LLM 对上下文中间部分的注意力较弱。最匹配的内容应该放在上下文的开头和结尾,不是中间。

上下文排序不是排队买奶茶,最重要的内容不要站在中间被淹没:

场景 具体怎么做 常用工具 / 机制
最相关 chunk 很少 把最高分 chunk 放在上下文开头,必要时在结尾再放一次简短摘要 attention-aware reorder
多个来源都重要 开头放主证据,结尾放补充证据,中间放背景材料 自定义 context assembler
长上下文模型 即使用长上下文,也按相关性排序,不要把原文顺序当成唯一顺序 rerank score、position strategy
需要引用溯源 保留 chunk 编号和来源编号,重排后不要丢失引用关系 source map、citation metadata
def reorder_for_attention(chunks: List[dict]) -> List[dict]:
    """最匹配的放开头和结尾,次匹配的放中间"""
    if len(chunks) <= 2:
        return chunks

    sorted_chunks = sorted(chunks, key=lambda x: x["score"], reverse=True)
    result = []
    left, right = [], []

    for i, chunk in enumerate(sorted_chunks):
        if i % 2 == 0:
            left.append(chunk)
        else:
            right.append(chunk)

    return left + list(reversed(right))

五、评估与监控:别靠感觉上线

Demo 好看不等于系统可用

1. 先有评估数据集

RAG 项目最容易犯的错,是 Demo 能跑就上线。Demo 里问的十个问题,往往都是开发者自己挑的——怎么挑怎么准。真用户的问题一来,表达方式、背景信息、边界条件全变了。

所以要先有一套评估数据集。不需要一开始很完美,但至少要覆盖常见场景、关键业务和容易出错的问题。

eval_dataset = [
    {
        "question": "公司的报销流程是什么?",
        "expected_answer": "提交申请→主管审批→财务审核→打款",
        "expected_sources": ["报销制度v2.1"],
        "category": "policy"
    },
    # ... 至少 50-100 条覆盖不同场景
]

2. 检索和生成分开评估

RAG 答错了,不一定是模型生成错,也可能是检索没召回;检索召回了,也可能是重排序丢了;上下文都对,也可能是 Prompt 没约束住。

所以评估指标要拆开看:

class RAGEvaluator:
    def evaluate(self, question, generated, expected, retrieved_docs):
        return {
            # 检索质量
            "retrieval_precision": self.calc_precision(retrieved_docs, expected_sources),
            "retrieval_recall": self.calc_recall(retrieved_docs, expected_sources),

            # 生成质量
            "answer_relevance": self.llm_judge_relevance(question, generated),
            "faithfulness": self.llm_judge_faithfulness(generated, retrieved_docs),
            "correctness": self.llm_judge_correctness(generated, expected),

            # 实用指标
            "has_citation": bool(re.search(r'\[.*?\]', generated)),
            "response_length": len(generated),
            "latency_ms": self.last_latency
        }

3. 线上要盯这些数

指标 目标 告警阈值
检索召回率 > 85% < 70%
答案准确率 > 80% < 65%
幻觉率 < 5% > 15%
用户满意度 (好评率) > 75% < 60%
P95 延迟 < 5s > 10s
"无法回答"率 < 20% > 40%

容易踩的坑

坑 10:没评估就上线

"Demo 看着挺好的,上线吧!"——这是 RAG 项目挂掉的头号原因。没有系统评估,你不知道系统在哪些场景下会出错,上线就是盲人骑瞎马。

上线前至少做一轮小而硬的评估,不求完美,但要能暴露问题:

评估对象 具体怎么做 常用工具 / 机制
检索质量 准备 50-100 条真实问题,标注期望来源,计算 recall、precision、MRR RAGASTruLens、自定义 pytest
生成质量 检查答案是否基于来源、是否答到问题、是否有幻觉 LLM-as-judge、人工抽检
引用质量 验证引用是否存在、是否支持对应结论,别只看有没有 [1] citation checker、自定义脚本
线上风险 用边界问题、过期政策、冲突文档做回归测试 regression test set、CI job

坑 11:只评估一次

知识库在更新,用户问法在变,模型在迭代。评估应该是持续的,不是一次性的。

RAG 的评估要接进日常流水线,不然一次评估只能证明"当时没坏":

变化来源 具体怎么做 常用工具 / 机制
文档更新 每次知识库重建索引后跑一遍核心评测集,观察召回率和答案准确率是否下降 CI/CD、scheduled eval
模型升级 Embedding、reranker、LLM 版本变化时做 A/B 对比,不要凭感觉切换 experiment tracking、A/B test
用户问法变化 定期抽样线上问题,把高频问法加入评测集 query log sampling
指标漂移 监控幻觉率、无法回答率、差评率、P95 延迟,超过阈值就回滚或降级 dashboard、alerting

坑 12:不看用户反馈

用户点了"差评"但没人分析原因。每一个差评都在告诉你系统哪里不对——可能是检索不准,可能是分块不当,也可能是 Prompt 有漏洞。这些信息不用花钱买,但很多团队就是不看。

用户反馈不是客服噪声,是最便宜的线上评测数据:

反馈类型 具体怎么做 常用工具 / 机制
点赞 / 点踩 点踩必须记录问题、答案、召回 chunk、模型版本,方便复盘 feedback log、trace ID
用户改写问题 用户连续追问或换问法,说明第一次没答好;把这些 query 加进评测集 conversation log、query mining
人工纠错 允许用户标注"正确来源"或"正确答案",沉淀成训练和评估样本 review queue、labeling workflow
高频差评主题 按部门、产品线、文档类型聚合差评,定位到底是数据问题、检索问题还是生成问题 analytics dashboard、issue tracker

六、进阶技巧

1. Agentic RAG

基础 RAG 跑稳之后,可以考虑 Agentic RAG:让 LLM 先分析问题,再决定检索策略。适合复杂问题,但也会增加延迟、成本和不可控性。不要一上来就用它解决所有问题——先把基础链路做到 80 分再说。

def agentic_rag(question: str):
    # Step 1: LLM 分析问题,决定检索策略
    plan = llm.generate(f"""分析这个问题需要什么信息:
    问题:{question}

    输出:
    - 需要检索的子问题列表
    - 每个子问题的检索策略 (向量/关键词/结构化查询)
    """)

    # Step 2: 执行多轮检索
    all_contexts = []
    for sub_query in plan.sub_queries:
        results = search(sub_query)
        all_contexts.extend(results)

    # Step 3: 判断信息是否充分
    if llm.judge_sufficient(question, all_contexts):
        return llm.generate_answer(question, all_contexts)
    else:
        additional = llm.generate_followup_queries(question, all_contexts)
        # ... 继续检索

2. 知识图谱增强

对于实体关系很强的知识库,可以抽取实体和关系,构建知识图谱作为向量检索的补充。

组织架构、产品依赖、权限关系、合同条款——这些内容只靠向量相似度往往不够。

向量检索 → 找到相关段落
    +
知识图谱 → 找到关联实体和关系
    ↓
更完整的上下文

3. 缓存

RAG 的成本不低,延迟也不低。高频问题、稳定知识库、固定答案,都可以缓存。但缓存一定要带失效策略,否则知识库更新了,系统还在回答旧答案。

import hashlib

class RAGCache:
    def __init__(self, ttl=3600):
        self.cache = {}
        self.ttl = ttl

    def get_or_compute(self, query, search_fn, generate_fn):
        key = hashlib.md5(query.encode()).hexdigest()

        if key in self.cache and not self.is_expired(key):
            return self.cache[key]

        results = search_fn(query)
        answer = generate_fn(query, results)

        self.cache[key] = {"answer": answer, "sources": results}
        return self.cache[key]

七、检查清单

□ 数据准备
  □ 文档清洗 (去噪声、格式统一)
  □ 语义分块 (非固定长度)
  □ 分块重叠 (10-20%)
  □ 元数据保留 (来源、日期、分类)
  □ 多层索引架构

□ 检索优化
  □ 混合检索 (向量 + BM25)
  □ Query 改写与扩展
  □ Reranking 重排序
  □ 元数据过滤
  □ 匹配的 Embedding 模型

□ 生成优化
  □ 严格的 Prompt 约束
  □ 上下文数量控制 (3-5 个)
  □ 注意力友好的排列顺序
  □ 引用溯源
  □ 兜底策略 (无法回答时的处理)

□ 评估监控
  □ 评估数据集 (50-100 条+)
  □ 多维度指标 (检索+生成+实用)
  □ 持续评估 pipeline
  □ 用户反馈收集与分析
  □ 线上监控告警

最后说一句不中听但有用的话:RAG 不是一个项目,是一个产品。

项目可以交付,产品要持续运营。知识库会更新,用户问法会变,模型会升级,业务规则也在变。你不能指望一次上线,从此岁月静好。

如果只记住一件事:RAG 的质量不只取决于 LLM,而取决于整条链路。数据要干净,检索要准,生成要有边界,答案要能溯源,评估要持续跑。

无他,少一点玄学,多一点工程。

参考资料

  1. RAG Optimization Best Practices - LangChain Blog
  2. Twelve RAG Pain Points and Solutions
  3. Lost in the Middle: How Language Models Use Long Contexts
  4. BAAI BGE Embedding Models
  5. Reciprocal Rank Fusion

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。