乐于分享
好东西不私藏

AI笔记五:RAG(检索增强生成)

AI笔记五:RAG(检索增强生成)

本篇代码使用python语言,实现的最简单的RAG。

共3910字,欢迎评论区和私信交流。

一、认识RAG

1. 为什么长上下文窗口(如 1M Token)不能取代 RAG?

很多人觉得:“既然模型能看 100 万字,直接把资料全塞进去不就行了?” 答案是不行。

  • 成本与延迟:塞入 100 万字,每次问答成本高昂,且首字响应极慢。
  • 注意力分散:大量研究表明,长文本中间的信息被模型遗忘或忽略的概率极高。
  • 权限与可控性:RAG 可以结合数据库权限,实现“张三只能查 A 文档,李四只能查 B 文档”。

2. RAG标准定义(2026)

  • RAG 不是“查资料”,而是“可控的知识路由 + 增强推理系统”。

核心公式:RAG = 领域知识访问 + 上下文工程 + LLM 推理


二、构建本地 RAG

极简三层架构(教学/入门版)

  1. 数据层(Data)
     数据源 + 预处理 + 分块 + Embedding + 向量库
  2. 检索层(Retrieval)
     查询向量 → 检索 → 重排 → 上下文
  3. 生成层(Generation)
     Prompt + LLM + 后处理

目标:不依赖任何云服务,不用装复杂的数据库,用纯 Python 实现最基础的 RAG。

1. pdf文档解析

pdf本地解析是 PyMuPDF (fitz)

# pip install PyMuPDFimport fitzdef load_pdf_local(file_path):    """高质量的本地 PDF 文本提取"""    doc = fitz.open(file_path)    text = ""    for page in doc:        # 提取带有格式的文本,比 pdfminer 准确率高很多        text += page.get_text("text") + "\n"    return texttext = load_pdf_local("your_document.pdf")

2. 文本切分

传统按固定字数切分会把一句话腰斩。现在标准做法是递归字符切分(优先按段落、换行、句号切,不够再按字数切)。

# pip install langchain-text-splittersfrom langchain_text_splitters import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter(    chunk_size=500,      # 每块最大长度    chunk_overlap=100,   # 块之间保留 100 字重叠(防止上下文断裂)    separators=["\n\n", "\n", "。", "!", "?", " ", ""])chunks = splitter.split_text(text)

3. 本地向量化

在本地加载开源模型 BGE-M3

# pip install sentence-transformersfrom sentence_transformers import SentenceTransformer# 首次运行会自动下载模型(约 2GB),后续走本地缓存embedder = SentenceTransformer('BAAI/bge-m3')# 将文本块转为向量chunk_vectors = embedder.encode(chunks, normalize_embeddings=True)

4. 向量检索:使用 FAISS

不需要装数据库,几行代码搞定内存级极速检索。

# pip install faiss-cpuimport faissimport numpy as np# 1. 构建索引 (d=1024 是 bge-m3 的维度)dimension = chunk_vectors.shape[1]index = faiss.IndexFlatIP(dimension) # 内积检索(因为向量已归一化,等价于余弦相似度)index.add(np.array(chunk_vectors).astype('float32'))# 2. 检索函数def search_faiss(query, top_k=3):    query_vec = embedder.encode([query], normalize_embeddings=True)    distances, indices = index.search(np.array(query_vec).astype('float32'), top_k)    # 返回最相关的文本块    return [chunks[i] for i in indices[0]]# 测试检索results = search_faiss("Llama 2 有多少参数?")

5. 组装 RAG Pipeline

from openai import OpenAI# 这里以兼容 OpenAI 格式的智谱 API 或本地 Ollama 为例client = OpenAI(api_key="your_key", base_url="your_base_url")def ask_rag(question):    # 1. 检索    context = search_faiss(question, top_k=3)    context_str = "\n---\n".join(context)    # 2. 构建 Prompt (Context Engineering)    prompt = f"""你是一个严谨的问答助手。请仅根据以下【已知信息】回答问题。    如果信息不足,请直接说“我不知道”。    【已知信息】    {context_str}    【用户问题】    {question}"""    # 3. 调用大模型 (使用最新的标准 API)    response = client.chat.completions.create(        model="glm-4-flash", # 或 gpt-4o-mini 等快速模型        messages=[{"role": "user", "content": prompt}],        temperature=0    )    return response.choices[0].message.contentprint(ask_rag("Llama 2 有多少参数?"))

✅ 至此,你已经拥有了一个完全本地化、最简单、最基础的 RAG 系统!


三、攻克工程痛点 ——> 优化检索

基础 RAG 在实际应用中会立刻暴露两个致命弱点。

痛点 1:字面匹配的盲区 ——> 混合检索

向量检索喜欢“语义相近”的词,但在医疗、法律等垂直领域,专有名词差一个字,意思天差地别。 

解决方案:Hybrid Search(混合检索)+ RRF 融合 不要手写复杂的 Elasticsearch 环境,我们在本地用 rank_bm25 库几行代码搞定关键词检索:

# pip install rank_bm25 jiebafrom rank_bm25 import BM25Okapiimport jieba# 1. 构建本地 BM25 索引(对中文进行分词)tokenized_chunks = [list(jieba.cut(chunk)) for chunk in chunks]bm25 = BM25Okapi(tokenized_chunks)def search_bm25(query, top_k=3):    tokenized_query = list(jieba.cut(query))    scores = bm25.get_scores(tokenized_query)    top_indices = np.argsort(scores)[::-1][:top_k]    return [chunks[i] for i in top_indices]

RRF(倒数秩融合)算法: 不要粗暴地把两个分数相加,而是把它们的排名名次进行融合。公式极其优雅且有效:

(k通常取60,rank是文档在某个检索列表中的位置,1表示第一名)

痛点 2: 最佳答案排在后面 ——> 重排序

大模型的上下文窗口有限,你不敢把 Top 50 都塞进去。 

解决方案:Rerank(交叉编码器重排序) 先粗筛 Top 20,再用一个专门的小模型精准打分,选出真正的 Top 3。

# 使用本地 Rerank 模型(无需联网,CPU可跑)from sentence_transformers import CrossEncoderreranker = CrossEncoder('BAAI/bge-reranker-v2-m3', max_length=512)def hybrid_rag_search(query, top_k=20, final_k=3):    # 1. 多路粗召回    vec_results = search_faiss(query, top_k)    bm25_results = search_bm25(query, top_k)    # 简易去重合并    all_candidates = list(set(vec_results + bm25_results))    # 2. Rerank 精排    pairs = [[query, doc] for doc in all_candidates]    scores = reranker.predict(pairs)    # 3. 按分数降序排列,取 final_k    ranked_docs = sorted(zip(scores, all_candidates), key=lambda x: x[0], reverse=True)    return [doc for score, doc in ranked_docs[:final_k]]

💡 工程铁律:检索质量决定了 RAG 的上限,大模型能力只决定能否摸到这个上限。Rerank 是 2026 年 RAG 的必选项,不是可选项。


四、高级 RAG 技巧

单纯的 Query -> 检索 -> 回答 属于“朴素 RAG”。真正的现代 RAG 引入了 Agent 机制。

1. Query Rewrite(查询重写)

用户的提问往往很模糊或有语病。

  • 用户问:“那个新出的支持商用的模型参数多少?”
  • 系统后台自动改写为:
“Llama 2 商业许可版本的模型参数量是多少?”(实现方式:调用一次轻量级 LLM 专门做意图理解和改写,然后再去检索)

2. Multi-Query(多路查询扩展)

针对复杂的用户问题,自动生成多个子问题去检索。

  • 用户问:“对比一下 Llama 2 和 GPT-4 的安全性策略”
  • 系统拆解为:
    • Q1: “Llama 2 的安全性策略”
    • Q2: “GPT-4 的安全性策略”
  • 然后将两路检索结果合并喂给大模型。

3. 多模态 RAG(处理表格和图片的终极方案)

2026 年的标准做法是:直接让视觉大模型(VLM)“看”整页文档。架构思路

  1. 把 PDF 每一页转成高清图片(使用 PyMuPDF)。
  2. 检索时,如果判断问题涉及图表,直接检索图片。
  3. 将图片和问题一起发给多模态大模型(如 GPT-4o、智谱 CogVLM 等)。
# 伪代码示例:VLM 直接读取 PDF 页面import base64def encode_image(image_path):    with open(image_path, "rb") as image_file:        return base64.b64encode(image_file.read()).decode('utf-8')response = client.chat.completions.create(    model="qwen3-vl:8b", # 多模态模型    messages=[{        "role": "user",        "content": [            {"type": "text", "text": "提取这张图表中的关键数据"},            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{encode_image('page_5.png')}"}}        ]    }])

4. GraphRAG(知识图谱增强)

不要迷信 GraphRAG! 传统向量 RAG 能解决 90% 的问题。

对比维度
传统 Vector RAG
GraphRAG
核心原理
相似语义文本
找实体的关联
绝对优势场景
具体事实查询
全局总结查询
构建成本
极低(分钟级)
极高(小时~天)
维护成本
增量更新极快
增量更新极难

建议:只有当你用尽了“混合检索 + Rerank + 查询重写”,依然无法解决跨文档的全局归纳问题时,再去引入 Neo4j 等图数据库尝试 GraphRAG。它是特种兵,不是常规军。


五、生产级开发 ——> 五层RAG架构

当前生产级 RAG 开发,主流分为 5 层标准架构数据源层 → 数据处理层 → 索引存储层 → 检索层 → 生成增强层

1. 数据源层(Data Source Layer)

所有外部知识的入口,决定 RAG 知识覆盖范围与质量上限。

常见数据源类型

  • 非结构化
    (最常用) PDF、Word、Markdown、Txt、网页、邮件、聊天记录、手册、论文、音视频转写文本
  • 半结构化
     Excel/CSV、JSON、XML、HTML、Wiki、API 返回
  • 结构化
     关系库(MySQL/PostgreSQL)、NoSQL(MongoDB)、图数据库(Neo4j)
  • 多模态(2026 主流扩展)
     图片 OCR、表格、扫描件、音视频(ASR 转文本)、CAD/图纸元数据

企业常用来源

  • 内部:Wiki、OA、工单、合同、产品文档、研报、历史对话
  • 外部:行业白皮书、法规、公开数据集、权威网站、合作伙伴文档

2. 数据处理层(Data Processing / ETL Layer)

把“脏数据”变成可检索的干净文本块。 

核心模块:

  • 文档解析
     PDF/Word/Excel/HTML 提取文本、表格、标题、结构;去页眉页脚/广告/噪声
  • 清洗
     去重、去乱码、去空行、标准化格式、过滤短文本
  • 分块(Chunking)
     按语义/固定长度/标题层级切分(512–1024 token 主流),加重叠窗口防语义断裂
  • 元数据提取
     来源、日期、作者、文档类型、页码、版本、权限标签(用于过滤/溯源)
  • 查询预处理(在线)
     查询改写、扩展、纠错、分类、意图识别

工具 LlamaParse、PyPDF2、Unstructured、LangChain、 RecursiveCharacterTextSplitter

3. 索引存储层(Index & Storage Layer)

向量+全文双索引是工业标配。

核心组件:

  • Embedding 模型
    (向量化) 中文:BGE-base-zh、m3e、text-embedding-ada-002 作用:把文本块转为高维向量(768/1024 维),捕捉语义
  • 向量数据库(Vector DB)
     Milvus、FAISS、Chroma、PGVector、Qdrant、Weaviate 存储:向量 + 原文块 + 元数据 能力:余弦相似度/欧氏距离快速检索 Top-K
  • 全文索引(Sparse)
     BM25、Elasticsearch 作用:补全向量检索短板——精确词/型号/编号/术语强匹配
  • 混合存储
     Elasticsearch 向量库 + 关系库(存元数据/原文)

4. 检索层(Retrieval Layer)——> RAG 核心

从海量库中精准召回相关片段,工业界普遍是 多路召回 + 重排 架构。

标准子模块(2026)

  1. 查询向量化
     问题 → Embedding 向量
  2. 多路并行召回(Recall)
    • 稠密向量检索(Dense):语义相似
    • 稀疏检索(Sparse/BM25):关键词精确匹配
    • 元数据过滤:按日期/权限/来源/版本过滤
    • (进阶)图检索:实体关系召回(GraphRAG)
  3. 融合(Fusion)
     多路结果加权融合(RRF 排序融合)
  4. 重排序(Rerank)
     用 Cross-Encoder 模型(BGE-reranker、Cohere rerank)对候选精排
  5. 上下文压缩/提纯
     提取与问题最相关的句子,压缩冗余

工业检索架构(大厂标准)

召回层 → 融合层 → 精排层

5. 生成增强层(Generation & Augmentation Layer)

用检索结果生成可靠、可溯源回答。 核心模块

  • Prompt 构建
     问题 + Top-N 相关块 + 系统提示(约束格式/禁止幻觉/引用来源)
  • LLM 生成
     GPT、通义千问、文心一言、Llama 3、Qwen
  • 后处理
     答案校验、引用标注(页码/来源)、格式美化、去幻觉检测
  • (进阶)评估与反馈
     实时打分(相关性、忠实度)、bad case 回流优化

🚨 附:RAG 调试 Checklist 指南

当 RAG 系统表现拉胯时,不要盲目换更大的大模型,先按照以下漏斗排查:

  1. 查数据源(占比 60% 的锅)
    :打印出被切分的 chunks,肉眼看看是不是把一句话切成了两半?表格是不是乱码了?
  2. 查检索层(占比 30% 的锅)
    :打印出检索回来的 Top 3 文本,里面到底有没有答案?如果没有,说明切分或检索算法有问题;如果有,大模型却没答对,进入第 3 步。
  3. 查 Prompt 与模型(占比 10% 的锅)
    :把问题和包含答案的文本直接丢给大模型(不用 RAG 流程),它能答对吗?如果答不对,说明是 Prompt 写得烂,或者你用的模型太弱。