基于《大规模语言模型:从理论到实践(第2版)》第9章 检索增强生成
爆款小标题:RAG 不是「搜一下再问模型」:原书第9章框架与组件拆解
为什么这一节重要
大模型会「一本正经地胡说八道」——即幻觉——尤其当问题涉及最新信息、专业领域或内部知识时。检索增强生成(RAG) 的思路是:在生成前先用检索从外部知识库(文档、数据库)里取出相关片段,再把片段与用户问题一起交给模型,让模型依据检索结果生成答案,从而减少幻觉、注入领域与时效知识。但 RAG 不是简单「先搜一下再问模型」:文档如何切块、如何建索引、检索到哪些内容、如何拼进 prompt、以及模型是否真的「依据」检索内容,每一步都会影响最终效果。本节基于原书第 9 章,把 RAG 的完整链路与关键组件讲清,为后续优化策略与评估打基础。
学习目标
学完本节,你将能够:
- 说出完整流程:从「原始文档」到「最终答案」的每一步(文档处理→切块→索引→查询时检索→与 query 拼接→生成),并指出可能引入误差的环节。
- 理解检索与生成的分工:说明检索负责「提供依据」、生成负责「组织答案」;二者配合如何缓解幻觉与时效性问题。
- 识别关键组件:列出原书第 9 章给出的 RAG 系统组件(文档加载与切块、嵌入模型、向量库/检索器、重排序、提示构建、生成模型),并能说明每类组件的选型会影响什么。
一、RAG 的定义与价值(原书第 9 章)
定义:RAG(Retrieval-Augmented Generation)指在生成式模型中,先通过检索从外部语料或知识库中获取与当前查询相关的文档或片段,再将检索结果与查询一起作为输入交给模型,由模型基于这些内容生成回答。原书第 9 章强调,RAG 的核心是「用检索约束与增强生成」,而不是让模型仅凭参数记忆生成。
价值:
- 减少幻觉:当答案需要依赖事实、数据或政策时,检索提供「可追溯的依据」,模型在 prompt 中被要求基于这些依据回答,可显著降低无中生有。
- 注入领域与时效知识:模型参数中的知识有截止日期与领域边界;通过检索可以接入最新文档、内部手册、法规等,扩展模型的有效知识范围。
- 可解释与可审计:检索到的片段可作为「引用来源」,便于用户与合规审计核对答案依据。
因此,对「必须正确」「必须最新」或「必须可追溯」的场景,RAG 常是首选方案之一。
二、从文档到答案:完整流程拆解(原书第 9 章)
离线阶段:文档处理与索引
- 文档加载:从存储中读取原始文档(PDF、Word、HTML、Markdown 等),解析为纯文本或带结构的文本。
- 切块(Chunking):将长文档切成较小单元(如 256/512 token、按段落或按句),以便检索时返回「与问题最相关的一段」而非整本书。切块策略(块大小、是否重叠、是否按语义边界)会直接影响召回质量与上下文长度,需与业务文档类型匹配(见 4.2、4.3 节)。
- 向量化与索引:用嵌入模型把每个块转成向量,存入向量库(或同时建关键词索引做混合检索)。查询时用同一嵌入模型把用户问题转成向量,在向量库中做相似度检索(如余弦、内积),得到 Top-K 个最相关块。
在线阶段:检索与生成
- 检索:用户发起查询后,将 query 向量化,在索引中检索出 Top-K 个相关块(可配合关键词检索、过滤条件等)。
- 重排序(可选):对 Top-K 用更强的模型(如 Cross-Encoder)或规则做重排,选出最相关的若干条再送入生成,以节省上下文并提升依据质量。
- 提示构建:把「检索到的文本」与「用户问题」按约定格式拼成 prompt,例如:「根据以下参考内容回答问题。参考内容:… 用户问题:… 请仅依据参考内容回答。」
- 生成:将完整 prompt 送入大模型,得到生成结果;可选做引用标注(如标出依据的块)、后处理或人工抽检。
可能引入误差的环节:文档解析错误、切块割裂语义、嵌入或检索不相关、重排序漏掉关键块、prompt 设计不当导致模型忽略检索内容、或模型仍自行发挥产生幻觉。工程上需要针对每个环节做监控与优化(见 4.2 节)。
三、关键组件与选型影响(原书第 9 章)
- 文档加载与切块:不同格式需要不同解析器;切块大小与重叠影响召回粒度与噪声,需结合业务调参。
- 嵌入模型:决定「查询与文档」在向量空间中的表示质量;可选通用嵌入(如 OpenAI、BGE)或领域微调,中英文场景需考虑多语言模型(见 4.3 节)。
- 向量库/检索器:小规模可用精确检索,大规模常用近似最近邻(ANN,如 HNSW);选型影响延迟与召回精度。
- 重排序:可选;能明显提升「送入生成的块」的质量,但增加延迟与成本。
- 提示构建与生成模型:prompt 中是否明确「仅依据参考内容」「若参考中无则说不知道」等,会直接影响模型是否依赖检索;生成模型可与检索/嵌入分离选型,便于成本与效果平衡。
四、案例:从零搭建 RAG 的 6 步实现
以下为完整可运行代码。依赖:pip install sentence-transformers faiss-cpu。设置 OPENAI_API_KEY 可接入真实 LLM;不设置则使用 mock 回答。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""完整可运行的 RAG 实现:加载→切块→向量化→检索→提示构建→生成"""
import os
import hashlib
from typing import List, Tuple
def load_and_chunk_texts(texts: List[str], chunk_size: int = 256, chunk_overlap: int = 50) -> List[dict]:
chunks = []
for doc_idx, text in enumerate(texts):
words = text.replace("\n", " ").split()
for i in range(0, len(words), chunk_size - chunk_overlap):
chunk_words = words[i : i + chunk_size]
chunk_text = " ".join(chunk_words)
if len(chunk_text.strip()) < 10:
continue
chunk_id = hashlib.md5(chunk_text.encode()).hexdigest()[:12]
chunks.append({"text": chunk_text, "chunk_id": chunk_id, "doc_id": doc_idx})
return chunks
def build_index(chunks: List[dict], model_name: str = "paraphrase-multilingual-MiniLM-L12-v2"):
from sentence_transformers import SentenceTransformer
import faiss
model = SentenceTransformer(model_name)
texts = [c["text"] for c in chunks]
embeddings = model.encode(texts, normalize_embeddings=True).astype("float32")
dim = embeddings.shape[1]
index = faiss.IndexFlatIP(dim)
index.add(embeddings)
return model, index, chunks
def retrieve(query: str, model, index, chunks: List[dict], top_k: int = 5) -> List[Tuple[str, float]]:
import faiss
q_emb = model.encode([query], normalize_embeddings=True).astype("float32")
scores, ids = index.search(q_emb, top_k)
return [(chunks[idx]["text"], float(scores[0][i])) for i, idx in enumerate(ids[0]) if idx >= 0]
def build_prompt(query: str, retrieved: List[Tuple[str, float]]) -> str:
context = "\n\n".join([f"[{i+1}] {t}" for i, (t, _) in enumerate(retrieved)])
return f"""根据以下参考内容回答问题。若参考中无相关信息,请明确说明「根据提供的内容无法回答」。
参考内容:\n{context}\n用户问题:{query}\n请仅依据上述参考内容回答。"""
def generate(prompt: str) -> str:
if os.getenv("OPENAI_API_KEY"):
try:
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1"))
r = client.chat.completions.create(model=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"), messages=[{"role": "user", "content": prompt}], max_tokens=512)
return r.choices[0].message.content.strip()
except Exception as e:
return f"[API 调用失败: {e}]"
for line in prompt.split("\n"):
if line.strip().startswith("[1]") and len(line) > 10:
return f"(模拟回答){line[3:].strip()[:200]}..."
return "根据提供的内容无法回答。"
# 主流程
if __name__ == "__main__":
docs = ["本公司退货政策:商品签收后7天内可无理由退货,需保持商品完好。", "远程办公政策:每周可远程办公2天,需提前在系统中申请。"]
chunks = load_and_chunk_texts(docs, chunk_size=64, chunk_overlap=10)
model, index, chunks = build_index(chunks)
query = "请问退货政策是什么?"
retrieved = retrieve(query, model, index, chunks, top_k=3)
prompt = build_prompt(query, retrieved)
answer = generate(prompt)
print(answer)
步骤 1:文档加载与解析
- 使用
PyMuPDF(PDF)、python-docx(Word)、BeautifulSoup(HTML)等解析器读取原始文件。 - 输出:每条记录为
{doc_id, chunk_id, text, metadata},其中 metadata 可含来源、页码等,便于后续引用。 - 注意:PDF 表格需用
pdfplumber或camelot单独提取,避免表格结构丢失。
步骤 2:切块(Chunking)
- 推荐:
RecursiveCharacterTextSplitter(LangChain)或自实现按段落/句子的切分。 - 参数:
chunk_size=512(token 或字符)、chunk_overlap=50,重叠避免边界割裂。 - 技术文档可按
\n\n或##先分块再按长度切;表格按行或按块保留表头。
步骤 3:向量化与建索引
- 嵌入模型:如
BAAI/bge-large-zh-v1.5(中文)、BAAI/bge-m3(多语言)。 - 调用:
model.encode(texts, normalize_embeddings=True),得到 L2 归一化向量。 - 向量库:小规模用
FAISS,百万级用Milvus或Qdrant;写入时指定collection、dim、index_type="HNSW"等。
步骤 4:检索
- 查询向量化:同一嵌入模型对用户 query 编码。
- 检索:
vector_db.search(query_embedding, top_k=5),返回(doc_id, score)列表。 - 可选:同时跑 BM25,用 RRF 融合两路结果(见 4.2 节)。
步骤 5:提示构建
- 模板示例:
根据以下参考内容回答问题。若参考中无相关信息,请明确说明「根据提供的内容无法回答」。 参考内容: [1] {chunk_1_text} [2] {chunk_2_text} ... 用户问题:{query} 请仅依据上述参考内容回答。 - 将检索到的 chunk 按
[1]、[2]编号,便于模型引用与溯源。
步骤 6:生成与引用
- 调用 LLM API 或本地模型,传入完整 prompt。
- 后处理:若模型输出含
[1]、[2]等引用标记,可渲染为可点击链接;或做引用正确性校验(见 4.2 评估)。
五、工程实战要点
1. 切块策略要与文档类型匹配
技术文档可按小节/段落切;问答对可按「一问一答」切;长表格需考虑保留表头与结构。块过大则噪声多、块过小则语义碎,建议用 4.2 节的评估指标做迭代。
2. 检索与生成模型可分离选型
检索可用轻量嵌入模型 + 向量库;生成可用 API 或自部署 LLM。这样可以在「检索精度」与「生成成本」之间分别优化,并便于做 A/B 测试。
3. 明确「无依据」时的行为
在 prompt 中约定:若检索结果中无相关信息,模型应明确说「根据提供的内容无法回答」而非猜测,以减少幻觉。
六、常见误区与避坑指南
误区一:认为「有检索就没幻觉」
检索错、或模型忽略检索内容时仍会幻觉。避坑:要同时评估「检索相关性」与「生成是否依据检索」;可用引用、人工抽检或模型打分做监控。
误区二:块切得越大越好
块过大容易带入无关信息、占满上下文。避坑:用召回率、答案相关性等指标调优块大小与重叠(见 4.2 节)。
误区三:忽略 prompt 设计
若 prompt 未强调「仅依据参考内容」,模型可能仍以参数记忆为主。避坑:在 prompt 中显式约束「只根据上述参考回答」「若参考中没有则说明」。
七、小结与衔接
本节基于原书第 9 章梳理了 RAG 的定义与价值、从文档到答案的完整流程(离线索引 + 在线检索与生成)、以及各关键组件的选型影响;并指出了可能引入误差的环节与工程要点。下一节将讲 RAG 的优化策略与评估指标:查询改写、混合检索、重排序、HyDE,以及检索层与生成层的评估方法(原书第 9 章)。
课后思考题
- 画出你理解的 RAG 数据流(从原始文档到最终答案),并标出「可能引入误差」的环节。
- 若用户问题需要跨多段文档的信息综合,仅「单轮检索 + 一次生成」可能有什么不足?可结合「多轮检索」或「分步推理」简要说明。