从 0 跑通一个最小 RAG Demo:文档切分、向量检索与答案生成

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。你应该看到:

  1. 切分结果:5 句话被切成了 3-5 个块(取决于你的文本长度和分割符)

  2. 前两个问题:模型从知识库中检索到了相关片段并给出了正确答案

  3. 第三个问题("今天天气怎么样"):模型应该返回"不包含足够的信息",而不是编造天气

如果第三个问题模型还是编造了答案,说明提示词约束不够强。把"如果上下文不包含答案"改成"禁止使用你训练数据中的任何信息 "再试-5。

下一步可以做什么

跑通这个 Demo 后,你可以按需扩展:

  • 支持 PDF/网页 :把 TextLoader 换成 PyPDFLoader 或 WebBaseLoader

  • 换向量库:Chroma 适合本地,FAISS 更快,Milvus/Weaviate 适合生产

  • 增加对话记忆 :让模型能理解"他/她/它"指代的是谁-2

  • 加 Reranker :先用向量检索召回 10 条,再用交叉编码器精排,能显著提升第一屏答案的质量-9

但不要一开始就搞这些。先把最小闭环跑通,确认检索到的内容确实和问题相关,再逐步优化。

相关推荐
资深技术分享员1 分钟前
Geejing WebBuilder 数据库连接配置与在线 SQL 工具,运维的左右手
运维·数据库·sql·低代码
ao-weilai28 分钟前
MySQL数据库:表操作
数据库·mysql·adb
宵时待雨37 分钟前
MySQL数据库1:数据库基础
服务器·数据库·mysql
做运维的阿瑞1 小时前
一个 NULL 让整列姓名消失?MySQL 字符串与日期函数复盘
数据库·sql·mysql
程序猿乐锅1 小时前
【黑马点评 | 第十一篇】关注 Feed 流实现
java·数据库·redis·分布式·后端·缓存·maven
phltxy1 小时前
C 语言文件操作:让数据从内存走向持久化
c语言·网络·数据库
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-69-DM9主备集群部署
linux·运维·服务器·数据库·sql·学习
jnrjian2 小时前
Connect Microsoft Tools to Oracle Databases powerbi 之类的oracle 客户端
数据库·microsoft·oracle
北京恒星科通刘军2 小时前
灾害监测预警系统中,应急疏散广播的“应急叫应”实际送达率与有效性研究
网络·数据库·人工智能
ShineWinsu2 小时前
对于Redis:List类型的解析
数据库·c++·redis·链表·缓存·面试·list