RAG原理与实现-从零搭建企业知识库
**本文适合谁**:刚接触 RAG 的程序员,或者知道 RAG 是什么但没有亲手跑过完整流程的人。读完本文,你能理解 RAG 的底层逻辑,并跑通一个完整的企业知识问答系统。
RAG 原理与实现:从零搭建企业知识库
本文适合谁:刚接触 RAG 的程序员,或者知道 RAG 是什么但没有亲手跑过完整流程的人。读完本文,你能理解 RAG 的底层逻辑,并跑通一个完整的企业知识问答系统。
为什么 LLM 不能直接回答你的公司问题
在动手写代码之前,先理解问题的本质——为什么需要 RAG。
想象你是一家电商公司,刚上线了 AI 客服。用户问:"我买的电子产品还能退货吗?"
直接用 LLM 回答会发生什么?
它会说:"根据中国消费者权益保护法,一般商品有七天无理由退货权利……" ——然后给出一段通用的法律知识,完全不知道你们公司具体的退货规则:是 7 天还是 15 天、电子产品有没有特殊说明、特价商品能不能退。
这不是 LLM 笨,而是它真的不知道。你的退货政策从来没有出现在任何公开的训练数据里。
这就是 RAG 要解决的问题:让 LLM 在回答之前,先从你的知识库里找到相关信息,然后基于这些真实信息来回答。
RAG 是什么:用一个图书馆比喻
你去图书馆查资料写报告,有两种方式:
闭卷方式:不查任何资料,完全凭记忆写。优点是快,缺点是可能记错,也不知道最新进展。
开卷方式:先去书架找相关书籍,翻到相关章节,边看边写,引用原文。优点是准确、有据可查,缺点是需要先找资料。
RAG 就是让 LLM 做开卷考试。
具体流程:
- 你的知识库是"书架"——提前把公司文档整理好,存进向量数据库
- 用户提问时,先在"书架"里找相关资料(检索)
- 把找到的资料摆到 LLM 面前
- LLM 看着资料回答
RAG 全称 Retrieval-Augmented Generation,检索增强生成。
根据 AWS 的定义:RAG 对 LLM 输出进行优化,使其在生成之前能够引用训练数据来源之外的权威知识库。
三个关键技术概念
理解 RAG 之前,需要先搞清楚三个概念。不用担心,我会用类比来解释。
1. Embedding(向量化):把文字变成坐标
计算机不懂文字的语义,但它能计算数字的距离。
Embedding 模型做的事情是:把一段文字变成一组数字(向量),而且语义相近的文字,变成的向量也相近。
举个例子:
- "苹果手机多少钱" → [0.2, 0.8, -0.3, ...] (1536维数字)
- "iPhone 售价" → [0.18, 0.79, -0.31, ...] (数字非常接近)
- "今天天气真好" → [-0.5, 0.1, 0.7, ...] (数字差别很大)
这样,通过计算向量之间的距离,就能判断两段文字的语义有多相关。
可以想象成:Embedding 把文字"投射"到一个巨大的语义空间里,语义相似的词和句子落在空间里相近的位置。
2. Chunking(分块):把文档切成合适大小的片段
为什么要切?两个原因:
原因一:LLM 有上下文窗口限制,一次能处理的文字有上限。一份 500 页的 PDF 塞不进去。
原因二:检索时不需要整本文档,只需要跟问题最相关的那几段。如果文档太大,相关信息被大量无关内容稀释,反而找不准。
切蛋糕类比:一整个大蛋糕切成小块,吃的时候只取你想吃的那块。太大一块吃不完,太小一块又吃不够。512-1024 个字符(token)通常是合适的"块大小"。
3. 向量相似度检索:在语义空间里找"最近的邻居"
有了向量,怎么找相关文档?
把用户问题也转成向量,然后在向量数据库里找"最近的邻居"——向量距离最小的那些文档块,就是最相关的内容。
最常用的是余弦相似度(cosine similarity):计算两个向量的夹角,夹角越小(越接近 0 度),两个向量越相似。取相似度最高的 top-k 个块返回。
RAG 的完整流程
RAG 分两个阶段:
离线索引阶段(做一次,或知识更新时做)
原始文档 → 加载 → 清洗 → 分块 → 向量化 → 存入向量数据库
在线检索阶段(每次查询都跑一遍)
用户问题 → 向量化 → 相似度检索 → 取 top-k 文档块 → 拼入 Prompt → LLM 生成答案
RAG 完整工作流程——离线构建阶段(文档切割+向量化入库)与在线检索阶段(检索+生成)
动手实现:从零搭建一个 RAG 系统
理论够了,开始动手。我们要搭建一个能回答公司退货政策问题的 RAG 系统。
需要安装的依赖:
pip install langchain langchain-community langchain-openai chromadb tiktoken
如果不想用 OpenAI(需要付费),下面提供 HuggingFace 免费 Embedding 模型的替代方案。
第一步:准备知识库文档
# 实际项目里,这里换成 PyPDFLoader、UnstructuredWordDocumentLoader 等
# 读取真实的 PDF 或 Word 文档
# 我们先用一段示例文本模拟公司退货政策文档
company_policy = """
退货政策(最后更新:2024年3月)
一、电子产品
1.1 自购买之日起 7 天内可申请无理由退货
- 前提:商品保持原包装完好,附件齐全
- 不支持退货情形:包装损坏、有明显使用痕迹、缺少配件
1.2 7天以上、30天以内:仅支持产品质量问题换货
1.3 超过30天:需联系厂商走质保流程
二、服装鞋帽
2.1 自购买之日起 15 天内可申请无理由退货
- 前提:吊牌未剪、未洗涤、未穿着
2.2 促销特价商品:不支持无理由退货,仅支持质量问题换货
2.3 内衣、泳衣类:出于卫生原因,不支持退货
三、食品饮料
3.1 不支持无理由退货
3.2 质量问题:请联系客服,提供照片和购买凭证,3个工作日内处理
四、退货流程
第一步:登录 App,进入"我的 → 我的订单"
第二步:找到目标订单,点击"申请售后"
第三步:选择"退货退款",填写退货原因
第四步:等待客服审核(工作日 24 小时内)
第五步:审核通过后,快递员上门取件
第六步:商品入库验收后,3-5 个工作日退款至原支付账户
五、特殊说明
- 节假日期间退款时间可能延长至 7-10 个工作日
- 大家电类商品退货需联系配送人员上门处理
- 如遇退款纠纷,可通过 400-XXX-XXXX 联系客服中心
"""
第二步:加载文档,进行分块
import os
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
# 把文本包装成 LangChain Document 对象
# 实际项目里,TextLoader/PyPDFLoader 会直接生成这个对象
documents = [Document(
page_content=company_policy,
metadata={"source": "company_policy.txt", "version": "2024-03"}
)]
# RecursiveCharacterTextSplitter 是 LangChain 最常用的分块器
# 它按优先级递归切割:先按段落(\n\n),再按换行(\n),再按句子,最后按字符
splitter = RecursiveCharacterTextSplitter(
chunk_size=400, # 每块最大 400 个字符
chunk_overlap=50, # 相邻块重叠 50 个字符,防止信息在边界处丢失
separators=["\n\n", "\n", "。", ",", " ", ""] # 中文文档加上句号和逗号
)
chunks = splitter.split_documents(documents)
print(f"原文档 {len(documents)} 篇,分块后 {len(chunks)} 个 chunk")
print("\n=== 各 chunk 预览 ===")
for i, chunk in enumerate(chunks):
print(f"\nChunk {i+1}({len(chunk.page_content)} 字符):")
print(chunk.page_content[:80] + "...")
预期输出:
原文档 1 篇,分块后 6 个 chunk
=== 各 chunk 预览 ===
Chunk 1(387 字符):
退货政策(最后更新:2024年3月)
一、电子产品
1.1 自购买之日起 7 天内可申请无理由退货
- 前提:商品保持原包装完好,附件齐全...
Chunk 2(342 字符):
二、服装鞋帽
2.1 自购买之日起 15 天内可申请无理由退货...
...
第三步:向量化并存入 Chroma
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# 方案一:使用 OpenAI Embedding(需要 OPENAI_API_KEY)
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small" # 性价比最高的选择
)
# 方案二:使用 HuggingFace 本地 Embedding(免费,需要下载约 500MB 模型)
# from langchain_community.embeddings import HuggingFaceEmbeddings
# embeddings = HuggingFaceEmbeddings(
# model_name="BAAI/bge-small-zh-v1.5", # 专为中文优化
# model_kwargs={"device": "cpu"}
# )
# 创建向量数据库,把 chunks 向量化后存入
# Chroma 是一个轻量级开源向量数据库,适合开发和原型验证
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 本地持久化,重启程序不丢失数据
)
print(f"向量数据库构建完成,共 {vectorstore._collection.count()} 个向量")
预期输出:
向量数据库构建完成,共 6 个向量
第四步:构建检索问答链
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# 初始化 LLM
llm = ChatOpenAI(
model="gpt-4o-mini", # 也可以换成 DeepSeek
temperature=0, # temperature=0 让回答更确定性,适合知识问答
)
# 自定义 Prompt 模板,明确告诉 LLM 基于提供的文档回答
custom_prompt = PromptTemplate(
input_variables=["context", "question"],
template="""你是一个专业的客服助手,根据以下提供的公司政策文档来回答用户问题。
重要规则:
1. 只根据下面的文档内容回答,不要添加文档中没有的信息
2. 如果文档中没有明确答案,请如实说明"该问题在当前文档中未找到相关规定"
3. 回答要简洁清晰,必要时列出步骤
【相关文档内容】
{context}
【用户问题】
{question}
【回答】"""
)
# 构建 RetrievalQA 链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # "stuff" = 把所有检索结果塞进一个 Prompt(最简单)
retriever=vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3} # 每次检索 top-3 个 chunk
),
chain_type_kwargs={"prompt": custom_prompt},
return_source_documents=True # 返回来源文档,方便调试和展示
)
第五步:测试问答效果
def ask(question: str):
"""测试一个问题"""
print(f"\n{'='*50}")
print(f"问:{question}")
result = qa_chain.invoke({"query": question})
print(f"答:{result['result']}")
print(f"\n来源文档(共 {len(result['source_documents'])} 个 chunk):")
for i, doc in enumerate(result['source_documents']):
print(f" [{i+1}] {doc.page_content[:60]}...")
return result
# 测试问题
ask("我买了衣服,15天了还能退货吗?")
ask("电子产品的无理由退货期是多少天?")
ask("退货需要多久才能退款?")
ask("内衣可以退货吗?")
ask("食品买了可以退货吗?")
预期输出:
==================================================
问:我买了衣服,15天了还能退货吗?
答:根据公司退货政策,服装鞋帽自购买之日起 15 天内可申请无理由退货,前提是吊牌未剪、未洗涤、未穿着。
如果您是在第15天申请退货,还在政策允许范围内,可以操作。但如果已超过15天,则只能在存在质量问题时申请换货,无法申请无理由退货。
来源文档(共 3 个 chunk):
[1] 二、服装鞋帽
2.1 自购买之日起 15 天内可申请无理由退货
[2] 四、退货流程...
[3] 一、电子产品...
==================================================
问:退货需要多久才能退款?
答:根据退货流程说明,商品入库验收后,退款将在 3-5 个工作日内退回原支付账户。
需要注意的是,节假日期间退款时间可能延长至 7-10 个工作日。
来源文档(共 3 个 chunk):
[1] 四、退货流程...
[2] 五、特殊说明...
[3] ...
RAG 系统工作正常:它检索到了相关的政策文档块,LLM 基于文档内容给出了准确的回答,不是通用的泛泛而谈。
常见问题和解决方案
问题一:检索结果和问题不相关
可能的原因:
- Embedding 模型选择不当(中文场景用了英文模型)
- chunk_size 太大,语义信息被稀释
解决方案:
# 中文场景用 BGE 系列 Embedding 模型效果更好
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5")
# 减小 chunk_size,提高语义聚焦
splitter = RecursiveCharacterTextSplitter(chunk_size=256, chunk_overlap=30)
问题二:回答被截断,上下文不完整
可能的原因:chunk 太小,一个完整的规则被切成了两段,每段单独看语焉不详。
解决方案:增大 chunk_size 或使用父子分块(第 02 篇详细讲):
# 增大块大小
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
问题三:模型回答了文档里没有的内容(幻觉)
可能的原因:Prompt 没有强约束模型只能基于文档回答。
解决方案:在 Prompt 里明确指令:
template="""仅根据以下文档内容回答问题。如果文档中没有相关内容,请明确回答"根据现有文档,该问题没有明确规定"。不要使用你自己的知识来补充。
文档内容:
{context}
问题:{question}
回答:"""
问题四:处理 PDF 等复杂格式文档
pip install pypdf unstructured
from langchain_community.document_loaders import PyPDFLoader
# 加载 PDF
loader = PyPDFLoader("/path/to/your/document.pdf")
documents = loader.load()
# 或者加载整个目录的文档
from langchain_community.document_loaders import DirectoryLoader
loader = DirectoryLoader("/path/to/docs", glob="**/*.pdf")
documents = loader.load()
RAG 检索质量调优的关键参数
搭好基础版 RAG 之后,影响效果的几个关键参数:
k(检索数量):每次返回多少个 chunk。k=3 是常见起点,太少可能漏掉关键信息,太多则占用上下文窗口,LLM 注意力分散。
# k 值调优
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 默认,大多数场景
retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) # 文档量大时可以增加
retriever = vectorstore.as_retriever(search_kwargs={"k": 1}) # 问题非常精确时可以减少
score_threshold(相似度阈值):只返回相似度超过阈值的结果,过滤不相关内容。
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.7, "k": 5} # 只返回相似度 > 0.7 的结果
)
chunk_size 和 chunk_overlap:这是影响 RAG 效果最大的参数,第 02 篇专门讲分块策略。
一个生产可用版本的结构
上面的示例是最简化版本,生产环境需要补充几个关键能力:
class ProductionRAG:
"""生产就绪的 RAG 系统骨架"""
def __init__(self, vectorstore, llm):
self.vectorstore = vectorstore
self.llm = llm
self.qa_chain = self._build_chain()
def _build_chain(self):
return RetrievalQA.from_chain_type(
llm=self.llm,
retriever=self.vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 3}
),
return_source_documents=True
)
def add_documents(self, new_docs: list[Document]):
"""增量添加文档(不需要重建整个索引)"""
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(new_docs)
self.vectorstore.add_documents(chunks)
print(f"已添加 {len(chunks)} 个新文档块")
def ask(self, question: str, user_id: str = None) -> dict:
"""带日志和错误处理的问答"""
try:
result = self.qa_chain.invoke({"query": question})
# 生产环境:记录问答日志,用于后续质量评估
# log_qa(user_id, question, result, timestamp=datetime.now())
return {
"answer": result["result"],
"sources": [doc.metadata for doc in result["source_documents"]],
"success": True
}
except Exception as e:
# 生产环境:告警通知、降级回答
return {
"answer": "抱歉,系统暂时无法回答您的问题,请联系人工客服。",
"error": str(e),
"success": False
}
小结
RAG 的核心逻辑非常简单:在回答之前先查资料,基于资料来回答。
实现上分两个阶段:
- 离线:加载文档 → 分块 → 向量化 → 存入向量数据库
- 在线:问题向量化 → 检索相似块 → 拼入 Prompt → LLM 生成答案
这个最简版本能解决大多数企业知识问答场景。但要做到生产级别,还有很多可以优化的地方:分块策略(第 02 篇)、Embedding 选型(第 03 篇)、向量数据库选型(第 04 篇)、混合检索与重排序(第 05 篇)……
先跑通这个最简版本,再逐步优化。