PAGE 06 · 中级

🔍 RAG 检索增强生成

Retrieval-Augmented Generation — 让大语言模型基于外部知识精准回答,
从文档加载到向量检索的完整实战指南

🤔 一、为什么需要 RAG?

大语言模型(LLM)能力强大,但存在三个根本性缺陷,严重制约了其在生产环境中的应用。RAG(Retrieval-Augmented Generation,检索增强生成)正是为解决这些问题而生。

📅 知识过时

LLM 的训练数据有截止日期。例如 GPT-4 的训练数据截止到 2024 年,无法回答此后发生的事件。对于需要实时数据的场景(如新闻、股价、公司内部文档),直接使用 LLM 会产生过时甚至错误的信息

关键缺陷
🎭 幻觉问题(Hallucination)

LLM 会"自信地编造"看似合理但完全虚构的信息。研究表明,GPT-4 在专业问答中的幻觉率可达 3-5%[1]。在医疗、法律、金融等高风险场景中,一个幻觉回答可能造成严重后果。

关键缺陷
🔒 无法访问私有数据

企业内部的知识库、技术文档、客户数据、研发笔记等都属于私有数据。LLM 从未见过这些数据,自然无法基于它们回答问题。而把这些数据直接塞进 Prompt 又受到上下文窗口大小的限制。

关键缺陷

微调 vs RAG vs 检索:三种方案对比

💡 核心洞察:这三者并非互斥,实际生产中常常组合使用。但 RAG 通常是解决问题的第一步,因为它的实现成本最低、见效最快。

维度 🔄 微调(Fine-tuning) 🔍 检索(Retrieval) 🎯 RAG(检索 + 生成)
原理 用领域数据重新训练模型参数 从知识库中检索相关文档 先检索再注入上下文生成回答
知识更新 需重新训练,成本高 更新文档即可,实时 更新文档即可,实时
幻觉控制 有改善但不根本 信息准确,但无自然语言生成 回答有据可查,可溯源
实现成本 高(GPU、数据准备、训练) 中低
数据需求 大量标注数据(千条+) 原始文档即可 原始文档即可
可解释性 黑箱 返回原始文档片段 返回原始文档片段 + 生成回答
适用场景 改变模型风格/能力 精确事实查询 知识密集型问答、企业知识库

RAG 的核心思想

🎯 一句话概括:检索(Retrieval) + 生成(Generation) = 更准确的回答

当用户提出问题时,系统先从知识库中检索与问题最相关的文档片段,然后把这些片段作为上下文提供给 LLM,让 LLM 基于这些真实信息来生成回答。

👤
用户提问
"公司年假政策是什么?"
📚
检索知识库
从向量数据库中找到相关文档
🤖
LLM 生成回答
基于检索到的上下文精准回答
返回结果 + 来源
用户得到准确答案并可追溯原文

⚙️ 二、RAG 的完整流程

一个完整的 RAG 系统分为两个阶段:离线索引阶段(文档→分块→Embedding→存储)和在线查询阶段(用户查询→检索→生成)。下面逐一拆解。

📄 文档加载(Document Loading)

第一步是将各种来源和格式的原始文档加载为统一的文本格式。LangChain 提供了丰富的 Document Loader 生态,支持几乎所有常见格式。

支持的格式
格式Loader说明
PDFPyPDFLoader / PDFMinerLoader支持文本和表格提取
Word (.docx)Docx2txtLoader / UnstructuredWordDocumentLoader保留段落结构
网页 (HTML)WebBaseLoader / AsyncWebBaseLoader支持爬取和解析
MarkdownUnstructuredMarkdownLoader保留标题层级
CSV / ExcelCSVLoader / UnstructuredExcelLoader逐行或按块加载
Notion / ConfluenceNotionDirectoryLoader / ConfluenceLoader直接连接知识管理平台
# LangChain 文档加载示例
from langchain_community.document_loaders import (
    PyPDFLoader,
    TextLoader,
    WebBaseLoader,
    Docx2txtLoader,
)

# 加载 PDF
pdf_loader = PyPDFLoader("company_policy.pdf")
pdf_docs = pdf_loader.load()  # 每页一个 Document

# 加载网页
web_loader = WebBaseLoader("https://docs.example.com/api")
web_docs = web_loader.load()

# 加载 Markdown
md_loader = TextLoader("README.md", encoding="utf-8")
md_docs = md_loader.load()

print(f"共加载 {len(pdf_docs)} 个文档块")
# 每个 Document: page_content + metadataPython

✂️ 文本分块(Text Splitting)

LLM 的上下文窗口有限(如 GPT-4o 为 128K tokens),且 Embedding 模型也有长度限制(通常 512-8192 tokens)。更关键的是,如果检索粒度太大,返回的结果会包含大量无关信息,干扰生成质量

为什么要分块?

三大原因:

  • 模型限制:Embedding 模型有最大输入长度(如 OpenAI ada-002 限制 8191 tokens)
  • 检索精度:小块文本语义更聚焦,向量相似度搜索更精准
  • 上下文效率:只注入最相关的片段,而非整篇文档,节省 Token 费用
分块策略对比
策略方法优点缺点
固定大小 按字符/Token 数切分 简单、可预测、输出均匀 可能截断语义完整性
按段落/标题 利用 Markdown/HTML 结构 保持语义完整 段落长度不一,需设上限
语义分块 用 Embedding 相似度判断切分点 语义边界最自然 计算成本高,需额外 Embedding
递归分块 先按大分隔符切,再递归细分 兼顾结构与粒度 参数调优稍复杂
最佳实践:分块大小与重叠

📐 推荐参数:

  • 块大小(chunk_size):500 ~ 1500 字符(约 100-300 tokens),这是经验最优区间
  • 重叠(chunk_overlap):块大小的 10%-20%(如 chunk_size=1000 则 overlap=100-200)
  • 重叠的目的:防止关键信息被截断在两个块的边界上
# LangChain 文本分块
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 推荐配置:递归分块,保持语义完整
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,       # 每块最大 1000 字符
    chunk_overlap=200,     # 重叠 200 字符(防止语义断裂)
    length_function=len,
    separators=["\n\n", "\n", ".", " ", ""],  # 优先按段落切分
)

chunks = text_splitter.split_documents(pdf_docs)
print(f"共分出 {len(chunks)} 个文本块")
print("平均块长度:", sum(len(c.page_content) for c in chunks) / len(chunks))Python

🔢 Embedding(向量化)

Embedding 是 RAG 系统的核心技术。它将文本映射为高维向量(通常 768-3072 维),使得语义相似的文本在向量空间中的距离更近

🧠 直觉理解:想象一个巨大的坐标系,每个维度代表一种语义特征。"

"猫和狗睡觉" 与 "小猫在窝里打呼噜" 的向量会非常接近,而 "猫和狗睡觉" 与 "公司股票涨跌" 的向量会相距很远。

主流 Embedding 模型对比
模型维度最大长度特点
OpenAI text-embedding-3-large 3072 8191 tokens 当前最强商用模型,MTEB 榜首
OpenAI text-embedding-3-small 1536 8191 tokens 性价比高,适合大多数场景
BGE-large-zh-v1.5 1024 512 tokens 中文场景表现优异,开源免费
Cohere embed-v3 1024 512 tokens 多语言支持好,支持重排序
GTE-Qwen2 3584 32768 tokens 阿里开源,长文本支持好
# Embedding 示例
from langchain_openai import OpenAIEmbeddings

# 使用 OpenAI 最新的 text-embedding-3-small
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    dimensions=1536  # 可降至 512/1024 以节省存储
)

# 将文本向量化
vector = embeddings.embed_query("什么是机器学习?")
print(f"向量维度: {len(vector)}")  # 输出: 1536

# 批量向量化
vectors = embeddings.embed_documents([
    "机器学习是人工智能的一个分支",
    "深度学习使用神经网络处理数据",
    "今天的天气真不错",
])

# 语义相似的文本,余弦相似度更高
import numpy as np
sim = np.dot(vectors[0], vectors[1]) / (
    np.linalg.norm(vectors[0]) * np.linalg.norm(vectors[1])
)
print(f"相似度: {sim:.4f}")  # 接近 0.85(高相似度)Python

🗄️ 向量数据库存储

将所有文本块的 Embedding 向量存入专门的向量数据库(Vector Database)。查询时,通过计算查询向量与存储向量的距离,快速找到最相似的文档块。

⚡ ANN(近似最近邻)算法:暴力搜索需要比较所有向量,时间复杂度 O(n)。向量数据库使用 ANN 算法(如 HNSW、IVF),在牺牲极小精度的情况下将搜索加速到 O(log n),支持亿级向量的毫秒级查询。

主流向量数据库对比
数据库类型特点适用场景
Chroma 开源 轻量、嵌入式、API 简洁、与 LangChain 深度集成 原型开发、本地测试、中小规模
FAISS 开源 Meta 开源、C++ 底层极快、纯库无服务 高性能本地方案、研究实验
Pinecone 云服务 全托管、自动扩展、无需运维 不想管基础设施的生产环境
Milvus 开源 分布式架构、支持十亿级向量、云原生 大规模生产部署
Weaviate 开源 内置向量化、混合搜索(向量+关键词) 需要混合搜索的生产环境
Qdrant 开源 Rust 编写、过滤功能强、API 优雅 需要元数据过滤的场景
# 使用 Chroma(最简单的方案)
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# 创建向量数据库(自动 Embedding + 存储)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"  # 持久化到本地
)

# 查询:返回最相关的 3 个文档块
results = vectorstore.similarity_search(
    "公司的年假制度是什么?",
    k=3
)

for i, doc in enumerate(results):
    print(f"\n--- 结果 {i+1} ---")
    print(doc.page_content[:200])  # 打印前 200 字Python

🔎 检索(Retrieval)

检索是 RAG 系统的质量瓶颈——即使生成能力再强,如果检索不到正确的上下文,回答也不可能是准确的。

相似度度量方法
方法公式特点
余弦相似度 最常用 cos(A,B) = (A·B) / (|A|×|B|) 只关注方向不关注长度,归一化后效果稳定
欧氏距离 d = ||A - B||₂ 同时考虑方向和长度,对绝对距离敏感
点积 A · B 计算最快,但要求向量已归一化
多路召回 + 重排序(Reranking)

🚀 高级检索策略:为了提高召回率和精度,实际生产中常采用"宽召回 + 精排序"的两阶段策略:

  1. 多路召回:同时使用向量搜索 + BM25 关键词搜索,取并集
  2. 重排序(Reranking):用 Cross-Encoder 模型对召回结果进行精确评分排序
# 多路召回 + Reranking 示例
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# 1️⃣ 向量检索器
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# 2️⃣ BM25 关键词检索器
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10

# 3️⃣ 多路召回(合并去重)
ensemble = EnsembleRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    weights=[0.6, 0.4]  # 向量搜索权重更高
)

# 4️⃣ Cross-Encoder 重排序
reranker = CrossEncoderReranker(
    model=HuggingFaceCrossEncoder("BAAI/bge-reranker-large"),
    top_k=5  # 最终只取 top 5
)

# 5️⃣ 组合:多路召回 → 重排序
compression_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=ensemble
)

results = compression_retriever.invoke("公司年假政策?")Python

💬 生成(Generation)

最后一步:将检索到的文档片段作为上下文(Context)注入到 Prompt 中,让 LLM 基于这些真实信息生成回答。

🔑 关键设计:Prompt 模板的质量直接影响最终回答效果。好的 RAG Prompt 应包含:

  • 角色设定:"你是一个专业的知识库助手"
  • 上下文注入:明确的分隔符标记检索到的内容
  • 约束条件:"仅基于提供的上下文回答,如果上下文中没有相关信息请说'我不知道'"
  • 来源引用:"在回答后标注信息来源"
# LangChain RAG 生成链
from langchain_openai import ChatOpenAI
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

# 定义 Prompt 模板
system_prompt = """你是一个专业的知识库助手。请仅基于以下提供的上下文来回答用户问题。

规则:
1. 优先使用上下文中的信息回答
2. 如果上下文中没有相关信息,明确说"根据现有知识库,我无法回答这个问题"
3. 回答时引用来源(标注文档标题和章节)
4. 用清晰、专业的语言回答

上下文:
{context}"""

prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{input}"),
])

# 创建 LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 构建 RAG 链:检索 → 文档注入 → 生成
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)

# 执行查询
response = rag_chain.invoke({"input": "公司年假制度是什么?"})
print(response["answer"])Python

🏗️ 三、RAG 架构图

下图展示了完整的 RAG 系统架构,分为离线索引阶段(上方)和在线查询阶段(下方)。

📦 离线索引阶段(Indexing)
📁
原始文档
PDF / Word / HTML / MD
📄
Document Loader
统一格式加载
✂️
Text Splitter
分块 + 重叠
🔢
Embedding
文本 → 向量
🗄️
Vector DB
向量 + 元数据存储
用户请求触发
⚡ 在线查询阶段(Querying)
👤
用户提问
自然语言 Query
🔢
Query Embedding
问题 → 向量
🔎
相似度检索
ANN / 多路召回
📋
Reranking
重排序 Top-K
🤖
LLM 生成
上下文 + Prompt
最终回答 + 来源引用

📊 关键数据流:用户的问题经过相同的 Embedding 模型向量化后,与向量数据库中存储的文档向量计算相似度,召回最相关的文档片段,再注入到 LLM 的 Prompt 中作为上下文。

💡 性能关键:在线查询的延迟主要取决于 Embedding 计算 + 向量检索时间,通常在 50-200ms 之间。LLM 生成是最大的延迟来源,通常 1-5 秒。

🚀 四、Advanced RAG 技巧

基础的 RAG 系统已经能解决很多问题,但面对复杂场景,还需要一些进阶技巧来提升质量。

🔄 Query 改写(Query Rewriting)

用户的原始查询往往措辞不佳、缺少关键信息或过于模糊。通过让 LLM 改写查询,可以显著提升检索质量。

❌ 原始查询

"那个东西怎么用?"

"上次说的那个功能呢?"

✅ 改写后查询

"LangChain 中如何使用 Chroma 向量数据库?"

"LangChain 的 RetrievalQA 链如何配置?"

常见 Query 改写策略
  • Query Expansion(查询扩展):用 LLM 生成多个相关查询,分别检索后合并结果
  • Step-back Prompting(后退提问):将具体问题改为更宽泛的问题(如"法国人口"→"法国的基本国情"),检索更全面的上下文
  • HyDE(假设性文档嵌入):让 LLM 先生成一个假设性回答,再用这个回答去检索(见下文详述)
📑 Parent-Child 分块策略

这是一种解决"检索精度 vs 上下文完整性"矛盾的经典策略。

核心理念:

  • 子块(Child):小块(~200 tokens),用于精确检索
  • 父块(Parent):大块(~1000 tokens),包含更完整的上下文
  • 流程:检索时匹配子块 → 返回对应的父块 → LLM 获得更完整的上下文
# Parent-Child 分块策略
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

# 父块分块器(大块,提供完整上下文)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)

# 子块分块器(小块,用于精准检索)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)

# 父块存储(普通内存存储即可)
store = InMemoryStore()

# 创建 Parent-Child 检索器
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)

retriever.add_documents(docs)

# 检索:匹配子块,但返回的是父块(更完整的上下文)
results = retriever.invoke("年假政策")
print(len(results[0].page_content))  # 比子块大很多Python
🤔 HyDE(Hypothetical Document Embeddings)

由 Liu et al. (2022) 提出[2]。核心思想:让 LLM 先"假装"回答问题,生成一个假设性文档,然后用这个文档的 Embedding 去检索

为什么有效?查询是简短的问题,而文档块是详细的陈述。两者在语义空间中有分布差异(distribution gap)。HyDE 通过生成一个"类似文档"的假设性回答来缩小这个差异,从而提升检索相关性。

用户问题
🤖
LLM 生成假设回答
🔢
Embedding 假设回答
🎯
更精准的检索
# HyDE 示例
from langchain_community.llms import HuggingFaceHub
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# Step 1: 让 LLM 生成假设性文档
hyde_prompt = ChatPromptTemplate.from_template("""请为以下问题写一个详细的假设性回答,
即使你不确定答案也没关系,只需要写一个看起来合理的段落:
问题:{question}
假设性回答:""")

hyde_chain = hyde_prompt | llm | StrOutputParser()

# Step 2: 用假设性回答进行检索
question = "Transformer 的自注意力机制如何工作?"
hypothetical_doc = hyde_chain.invoke({"question": question})

# Step 3: 用假设性文档的 Embedding 进行向量检索
results = vectorstore.similarity_search(hypothetical_doc, k=3)Python
🤖 Agentic RAG(智能体化检索)

传统 RAG 对每个问题都执行相同的"检索→生成"流程。Agentic RAG 赋予 AI 自主决策能力:它可以根据问题类型判断是否需要检索、检索哪些知识库、需要多少次检索。

🎯 Agentic RAG 的决策能力:

  • 是否检索:对于常识问题("什么是光合作用?")直接回答,无需检索
  • 从哪检索:根据问题路由到不同的知识库(HR 文档 vs 技术文档 vs 产品文档)
  • 检索几次:如果第一次检索的结果不够,可以再检索补充信息
  • 何时停止:判断已获得足够信息后停止检索,直接生成回答
# Agentic RAG 概念示例(使用 LangGraph 实现)
from langgraph.prebuilt import create_react_agent

# 定义 Agent 可用的工具
tools = [
    search_hr_docs_tool,      # 搜索 HR 知识库
    search_tech_docs_tool,    # 搜索技术文档库
    search_product_docs_tool, # 搜索产品文档库
]

# 创建 Agentic RAG
agent = create_react_agent(llm, tools)

# Agent 会自主决定:需要检索吗?检索哪个库?
result = agent.invoke({
    "messages": ["公司新员工的社保缴纳流程是什么?"]
})

# Agent 的决策过程:
# 1. 思考:这是一个 HR 相关问题 → 选择 search_hr_docs_tool
# 2. 执行检索:"新员工社保缴纳流程"
# 3. 判断检索结果是否充分
# 4. 如果充分 → 基于检索结果生成回答
# 5. 如果不充分 → 换个关键词再次检索Python

📊 五、RAG 的评估

RAG 系统是一个流水线,每个环节都可能出现问题。只有通过系统化的评估,才能找到瓶颈并持续优化。

RAGAS 框架

RAGAS(RAG Assessment)是专门用于评估 RAG 系统的开源框架[3],它从三个维度评估系统质量:

🎯 忠实度(Faithfulness)

问题:生成的回答是否忠实于检索到的上下文?

计算方法:将回答拆分为多个声明,逐一验证每个声明是否能在上下文中找到依据。

目标:尽可能高(≥ 0.8)

最核心指标
🔗 答案相关性(Answer Relevance)

问题:回答是否针对用户的问题?

计算方法:用回答反向生成问题,看生成的问题是否与原始问题语义匹配。

目标:尽可能高(≥ 0.8)

🔎 上下文精确度(Context Precision)

问题:检索到的上下文是否真的相关?

计算方法:检查排名靠前的检索结果中,有多少是真正与问题相关的。

目标:尽可能高(≥ 0.7)

# RAGAS 评估示例
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)

# 准备评估数据
eval_data = {
    "question": ["公司年假政策?", "如何申请调休?"],
    "answer": [answer_1, answer_2],         # RAG 系统生成的回答
    "contexts": [[ctx_1a, ctx_1b], [ctx_2a]], # 检索到的上下文
    "ground_truth": [gt_1, gt_2],           # 标准答案(可选)
}

# 执行评估
results = evaluate(
    eval_data,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)

print(results)
# faithfulness: 0.92  ✅ 高忠实度
# answer_relevancy: 0.88  ✅ 回答相关
# context_precision: 0.75  ⚠️ 检索精度可优化Python

LangSmith 评估

LangSmith 是 LangChain 官方的可观测性平台,提供 RAG 系统的端到端追踪和评估能力。

LangSmith 的核心能力:

  • 追踪(Tracing):记录每一次 RAG 调用的完整链路——从检索到生成,每一步的输入输出都可视化
  • 评估(Evaluation):用 LLM-as-Judge 自动评估回答质量,支持自定义评估标准
  • 数据集管理:管理测试用例,建立回归测试基准
  • 在线监控:生产环境的实时性能监控和异常告警

⚠️ 评估最佳实践:

  1. 先评检索,再评生成:如果检索质量差(Context Precision 低),先优化检索,再评估生成
  2. 建立黄金数据集:准备 50-100 个有标准答案的问答对,用于回归测试
  3. 关注端到端指标:最终目标是回答质量,不要过度优化单一环节

💻 六、Python 完整代码示例

下面使用 LangChain + Chroma 实现一个完整的 RAG 系统,涵盖从文档加载到生成回答的全流程。

📋 前置依赖:

pip install langchain langchain-openai langchain-chroma chromadb pypdf

并设置环境变量:export OPENAI_API_KEY="your-api-key"

"""
完整的 RAG 系统 — LangChain + Chroma
功能:文档加载 → 分块 → Embedding → 存储 → 检索 → 生成
"""

import os
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_chroma import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage, AIMessage

# ═══════════════════════════════════════════════
# 1. 配置
# ═══════════════════════════════════════════════
os.environ["OPENAI_API_KEY"] = "your-api-key"

CHROMA_DIR = "./chroma_rag_db"       # 向量数据库持久化目录
CHUNK_SIZE = 1000                     # 分块大小
CHUNK_OVERLAP = 200                  # 分块重叠
TOP_K = 5                            # 检索返回的文档数量

# ═══════════════════════════════════════════════
# 2. 初始化 Embedding 和 LLM
# ═══════════════════════════════════════════════
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    dimensions=1536,
)
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# ═══════════════════════════════════════════════
# 3. 文档加载与分块
# ═══════════════════════════════════════════════
def load_and_split(file_paths):
    """加载多个文件并分块"""
    all_docs = []

    for path in file_paths:
        if path.endswith(".pdf"):
            loader = PyPDFLoader(path)
        else:
            loader = TextLoader(path, encoding="utf-8")

        docs = loader.load()
        # 给每个文档块添加来源元数据
        for doc in docs:
            doc.metadata["source"] = path

        all_docs.extend(docs)

    # 递归分块:优先按段落 → 换行 → 句子 → 单词
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP,
        separators=["\n\n", "\n", ".", " ", ""],
    )

    chunks = splitter.split_documents(all_docs)
    print(f"✅ 加载了 {len(file_paths)} 个文件,"
               "生成 {len(chunks)} 个文本块")
    return chunks

# ═══════════════════════════════════════════════
# 4. 构建向量数据库
# ═══════════════════════════════════════════════
def build_vectorstore(chunks):
    """将文本块 Embedding 并存入 Chroma"""
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=CHROMA_DIR,
    )
    print(f"✅ 向量数据库已构建,共 {len(chunks)} 条记录")
    return vectorstore

# ═══════════════════════════════════════════════
# 5. 构建 RAG 链
# ═══════════════════════════════════════════════
def create_rag_chain(vectorstore):
    """创建检索增强生成链"""
    # 定义检索器
    retriever = vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": TOP_K},
    )

    # 定义 Prompt 模板
    system_prompt = """你是一个专业的知识库问答助手。

请基于以下提供的【上下文】来回答用户的问题。

规则:
1. 仅使用上下文中的信息,不要编造答案
2. 如果上下文中没有相关信息,请明确说明
3. 在回答中引用信息来源
4. 使用清晰、结构化的语言

【上下文】
{context}"""

    prompt = ChatPromptTemplate.from_messages([
        ("system", system_prompt),
        ("human", "{input}"),
    ])

    # 构建 RAG 链
    qa_chain = create_stuff_documents_chain(llm, prompt)
    rag_chain = create_retrieval_chain(retriever, qa_chain)

    return rag_chain

# ═══════════════════════════════════════════════
# 6. 交互式问答
# ═══════════════════════════════════════════════
def chat(rag_chain):
    """交互式 RAG 问答"""
    print("\n🔍 RAG 问答系统已就绪!(输入 'quit' 退出)\n")
    while True:
        question = input("👤 你: ").strip()
        if question.lower() in ("quit", "exit", "q"):
            break
        if not question:
            continue

        response = rag_chain.invoke({"input": question})
        print(f"\n🤖 助手: {response['answer']}")

        # 显示来源
        if response.get("context"):
            sources = set()
            for doc in response["context"]:
                sources.add(doc.metadata.get("source", "unknown"))
            print(f"📚 来源: {', '.join(sources)}")
        print()

# ═══════════════════════════════════════════════
# 主程序
# ═══════════════════════════════════════════════
if __name__ == "__main__":
    # Step 1: 加载文档
    files = ["company_handbook.pdf", "hr_policy.md", "tech_docs.txt"]
    chunks = load_and_split(files)

    # Step 2: 构建向量数据库
    vectorstore = build_vectorstore(chunks)

    # Step 3: 创建 RAG 链
    rag_chain = create_rag_chain(vectorstore)

    # Step 4: 开始交互
    chat(rag_chain)Python · complete_rag.py

🏢 七、案例:企业知识库问答系统

场景描述

某中型科技公司(500+ 员工)面临知识管理困境:

实现步骤

数据收集与整理

从各平台导出文档:Confluence 全量导出 → PDF 格式;Google Docs → Markdown;内部 Wiki → HTML。最终收集到1,200+ 文档,约 50 万字

文档预处理与分块

使用 RecursiveCharacterTextSplitter,chunk_size=800,overlap=150。特殊处理表格数据:先转 Markdown 表格再分块,保持行列结构完整。

Embedding 与存储

选用 BGE-large-zh-v1.5(中文表现最佳的开源模型),向量存入 Milvus(支持十亿级向量)。每条记录额外存储元数据:来源文档名、章节标题、部门标签、最后更新时间。

检索优化

实现多路召回:向量搜索(权重 0.6)+ BM25 关键词搜索(权重 0.4),用 BAAI/bge-reranker-large 重排序。添加元数据过滤:根据用户所属部门自动筛选相关文档。

集成与部署

通过企业微信/飞书 Webhook 接入,员工在群聊中 @机器人 即可提问。回答附带原文链接,方便查看完整上下文。

效果对比

❌ 无 RAG(纯 LLM)

问:公司年假有几天?新员工第一年怎么算?

答:根据中国劳动法规定,职工累计工作已满1年不满10年的,年休假5天...

(回答的是通用法律条文,不是公司实际政策)

✅ 有 RAG

问:公司年假有几天?新员工第一年怎么算?

答:根据《员工手册 2025 版》第三章第三节:正式员工享有 10-15 天年假(按工龄计算)。入职第一年的新员工,按入职当月至年底的剩余月份折算年假天数。例如 7 月入职的新员工,当年可享受 5 天年假。

📎 来源:员工手册2025.pdf - 第三章第3节

指标纯 LLMRAG 系统提升
回答准确率52%89%+71%
回答可溯源率0%95%+95%
幻觉率18%3%-83%
员工满意度3.2/54.5/5+41%
平均响应时间2.1s2.8s-0.7s(检索开销)

💡 关键经验总结:

  1. 数据质量决定上限:花在文档清洗和结构化上的时间是最值得的投入
  2. 分块策略影响巨大:不同类型的文档(政策 vs 技术文档)可能需要不同的分块参数
  3. 元数据过滤很关键:给文档打好标签(部门、日期、类型),可以大幅提升检索精度
  4. 持续优化循环:用 RAGAS 评估 → 找到薄弱环节 → 优化 → 再评估

📚 参考文献与延伸阅读

  1. Lewis, P., Perez, E., Piktus, A., et al. (2020). "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks." NeurIPS 2020. arXiv:2005.11401
  2. Liu, N.F., Lin, K., Hewitt, J., et al. (2022). "Lost in the Middle: How Language Models Use Long Contexts." arXiv:2307.03172 / Gao, Y., Xiong, Y., et al. (2022). "Precise Zero-Shot Dense Retrieval without Relevance Labels." ACL 2023. (HyDE)
  3. Es, S., James, J., Espinosa-Anke, L., Schockaert, S. (2024). "RAGAS: Automated Evaluation of Retrieval Augmented Generation." EACL 2024. arXiv:2309.15217
  4. LangChain RAG 文档. python.langchain.com/docs/tutorials/rag/
  5. LlamaIndex RAG 文档. docs.llamaindex.ai
  6. Gao, Y., Xiong, Y., et al. (2024). "Retrieval-Augmented Generation for Large Language Models: A Survey." arXiv:2312.10997. arXiv:2312.10997