RAG 的本质是给大模型“开卷考试”:先把你的文档切成小块存进向量库,用户提问时检索出最相关的几块,连同问题一起交给模型生成答案。下面从零开始,用最少的依赖跑通完整流程。
第一步:理解 RAG 的两个阶段
在动手之前,先明确你在做什么:
阶段一(离线):把你的文档加载 → 切分 → 向量化 → 存入向量数据库。这步只需要跑一次。
阶段二(在线):用户提问 → 把问题向量化 → 在库里搜索最相似的 N 个文本块 → 拼成提示词 → 交给大模型生成答案-1。
第二步:安装依赖
只需要四个核心库。建议用清华镜像加速:
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:
RAG 是检索增强生成的缩写。 它的核心思想是在回答问题前先从外部知识库检索相关信息。 向量数据库用于存储文本的向量表示。 ChromaDB 是一个轻量级的向量数据库,适合本地开发。 嵌入模型负责把文本转换成高维向量。第四步:编写完整代码
创建一个rag_demo.py文件,把下面的代码复制进去。每一步我都加了注释,你可以直接运行。
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=512tokens,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
但不要一开始就搞这些。先把最小闭环跑通,确认检索到的内容确实和问题相关,再逐步优化。