RAG 的本质是给大模型"开卷考试":先把你的文档切成小块存进向量库,用户提问时检索出最相关的几块,连同问题一起交给模型生成答案。下面从零开始,用最少的依赖跑通完整流程。
第一步:理解 RAG 的两个阶段
在动手之前,先明确你在做什么:
阶段一(离线) :把你的文档加载 → 切分 → 向量化 → 存入向量数据库。这步只需要跑一次。
阶段二(在线) :用户提问 → 把问题向量化 → 在库里搜索最相似的 N 个文本块 → 拼成提示词 → 交给大模型生成答案-1。
第二步:安装依赖
只需要四个核心库。建议用清华镜像加速:
python
pip install langchain langchain-openai langchain-community langchain-text-splitters chromadb -i https://pypi.tuna.tsinghua.edu.cn/simple
这些库的分工:langchain 负责串联流程,langchain-openai 提供嵌入和对话模型接口,chromadb 是向量数据库,langchain-text-splitters 负责文档切分-7。
第三步:准备你的知识库文档
创建一个简单的文本文件作为知识库。比如 knowledge.txt:
python
RAG 是检索增强生成的缩写。
它的核心思想是在回答问题前先从外部知识库检索相关信息。
向量数据库用于存储文本的向量表示。
ChromaDB 是一个轻量级的向量数据库,适合本地开发。
嵌入模型负责把文本转换成高维向量。
第四步:编写完整代码
创建一个 rag_demo.py 文件,把下面的代码复制进去。每一步我都加了注释,你可以直接运行。
python
import os
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# ========== 0. 配置 API Key ==========
# 你需要一个支持 OpenAI 接口的 API Key
# 如果使用阿里云百炼,把 base_url 改成 dashscope 的地址
os.environ["OPENAI_API_KEY"] = "你的 API Key"
os.environ["OPENAI_BASE_URL"] = "https://api.openai.com/v1" # 或替换为你的代理地址
# ========== 1. 加载文档 ==========
loader = TextLoader("knowledge.txt", encoding="utf-8")
docs = loader.load()
print(f"加载了 {len(docs)} 个文档")
# ========== 2. 切分文档 ==========
# chunk_size=200 表示每个文本块约 200 个字符
# chunk_overlap=50 表示相邻块之间有 50 字符重叠,避免语义断裂
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", " ", ""]
)
splits = text_splitter.split_documents(docs)
print(f"切分成了 {len(splits)} 个块")
for i, split in enumerate(splits):
print(f" 块 {i}: {split.page_content[:50]}...")
# ========== 3. 向量化并存入向量库 ==========
# 使用 OpenAI 的嵌入模型将文本块转为向量,存入 Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=splits,
embedding=embeddings,
persist_directory="./chroma_db" # 持久化,下次不用重新嵌入
)
print(f"向量库构建完成,共 {vectorstore._collection.count()} 条向量")
# ========== 4. 创建检索器 ==========
# 搜索时返回最相似的 2 个文本块
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# ========== 5. 构建 RAG 提示词 ==========
# 核心规则:只允许基于提供的上下文回答,不能编造
prompt_template = """仅根据以下提供的上下文回答问题。如果上下文不包含答案,请直接说"提供的文档不包含足够的信息来回答此问题"。
上下文:
{context}
问题:{question}
答案:"""
prompt = ChatPromptTemplate.from_template(prompt_template)
# ========== 6. 初始化大模型 ==========
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ========== 7. 串联成 RAG 链 ==========
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# ========== 8. 测试 ==========
questions = [
"RAG 是什么?",
"ChromaDB 是什么?",
"今天天气怎么样?" # 这个问题知识库里没有,测试模型的边界处理
]
for q in questions:
print(f"\n问题:{q}")
answer = rag_chain.invoke(q)
print(f"答案:{answer}")
第五步:理解每个关键环节的"为什么"
文档切分:chunk_size 和 chunk_overlap 怎么定
切分是 RAG 中最容易被忽视但影响巨大的环节。块太小,检索到的片段缺乏上下文;块太大,嵌入向量会"稀释"关键信息,检索精度下降-3-13。
经验起点 :chunk_size=512 tokens,chunk_overlap=10%-15%(约 50-80 tokens)-8。中文场景下可以先用 chunk_size=200-300 字符,因为中文字符的信息密度更高。
RecursiveCharacterTextSplitter 的聪明之处在于:它按优先级尝试分割符(先段落、再换行、再句号),尽量保证每个块在语义边界处断开-7。
嵌入模型:选哪个
中文场景首选 bge-large-zh-v1.5 ,它在 MTEB 中文榜单上表现最好,开源可本地部署,1024 维的向量维度是性价比最优的选择-4-9。如果追求便捷,text-embedding-3-small 的 1536 维英文效果够用,成本也低-4。
关键认知 :Embedding 模型决定了检索的"天花板"。换一个更好的嵌入模型,往往是投入产出比最高的优化手段-9。
提示词:防止幻觉的核心规则
RAG 提示词必须包含基础约束 :明确声明模型只能使用提供的上下文,并定义"不知道"时的回退行为-5。
上面的模板用了最直接的表达:"仅根据以下提供的上下文回答问题。如果上下文不包含答案,请直接说......"这种命令式语言 比"可以考虑使用上下文"有效得多-10。
第六步:跑起来,然后观察输出
运行 python rag_demo.py。你应该看到:
-
切分结果:5 句话被切成了 3-5 个块(取决于你的文本长度和分割符)
-
前两个问题:模型从知识库中检索到了相关片段并给出了正确答案
-
第三个问题("今天天气怎么样"):模型应该返回"不包含足够的信息",而不是编造天气
如果第三个问题模型还是编造了答案,说明提示词约束不够强。把"如果上下文不包含答案"改成"禁止使用你训练数据中的任何信息 "再试-5。
下一步可以做什么
跑通这个 Demo 后,你可以按需扩展:
-
支持 PDF/网页 :把
TextLoader换成PyPDFLoader或WebBaseLoader -
换向量库:Chroma 适合本地,FAISS 更快,Milvus/Weaviate 适合生产
-
增加对话记忆 :让模型能理解"他/她/它"指代的是谁-2
-
加 Reranker :先用向量检索召回 10 条,再用交叉编码器精排,能显著提升第一屏答案的质量-9
但不要一开始就搞这些。先把最小闭环跑通,确认检索到的内容确实和问题相关,再逐步优化。