RAG 入门笔记:从 0 搭起一个最基础的 RAG

这篇主要讲什么

这一份 PDF 对应的是 Part 1 ~ Part 4,重点不是炫技,而是先把 RAG 最基本的工作流搭起来。

如果用一句大白话来讲,RAG 做的事情就是:

先去资料库里找内容,再拿找到的内容去回答问题。

它解决的问题也很直接:大模型本身记忆不稳定、知识会过期、容易一本正经地胡说。RAG 的思路不是让模型“硬背”,而是让模型“边查边答”。

RAG 最基础的四步

1. 先准备资料

文档先加载进来,比如网页、PDF、数据库里的文本。

这里我自己的理解是:资料质量比模型版本更重要。因为后面检索和回答,都是建立在“你喂进去的资料值不值得信”这个前提上。

2. 再把资料切成小块

原文里用了 RecursiveCharacterTextSplitter 这种切分器。它的核心想法不复杂:

不要把整篇长文一股脑塞进去,要切成更容易命中的小段。

为什么要切?

  • 太长的文本不容易准确匹配问题
  • 太大的块会混入很多无关信息
  • 太小的块又会丢失上下文

所以切分不是越细越好,而是要在“信息完整”和“检索精度”之间找平衡。

我个人觉得,很多人一上来就纠结模型选哪个,反而忽视了切分。实际上,切分策略常常比换一个更贵的模型更影响结果。

3. 把文本变成向量,放进向量库

这一步的意思是:

把文字变成一种机器更方便比较相似度的数字表示。

以后用户来提问,也会把问题转成向量,再去找“最像”的文本块。

原文提到了余弦相似度。可以把它简单理解成:

两个句子意思越接近,向量方向越接近。

不需要把这件事想得太数学。对于使用者来说,重点只有一个:向量检索不是看字面一模一样,而是看语义像不像。

4. 检索后再生成答案

这是最典型的 RAG 结构:

  • 用户提问
  • 检索器找相关片段
  • 把片段和问题一起交给模型
  • 模型根据上下文生成答案

这一步很关键,因为它决定了模型是在“空想”,还是在“看材料说话”。

这份内容的核心思想

我觉得核心思想只有一句:

不要指望模型自己什么都知道,要给它一个能随时查资料的外脑。

这也是 RAG 的出发点。它不是为了替代模型,而是为了弥补模型知识不稳定、上下文有限、可追溯性弱这些天然缺点。

我自己的理解

我会把基础版 RAG 理解成一个“开卷考试系统”。

  • 文档库,就是考试资料
  • 检索器,就是翻书动作
  • 大模型,就是整理答案的人

如果书没整理好,翻书动作就会很慢、很乱、很容易翻错页。最后答案自然不会稳定。

所以基础版 RAG 真正的重点不是“把链子跑通”,而是下面这三件事:

  • 文档是否干净
  • 切分是否合理
  • 检索到的内容是否真的和问题有关

初学时最容易踩的坑

  • 以为能跑通,就等于效果好
  • 切分块太大,导致检索结果又长又散
  • 只看模型回答,不看它到底检索到了什么
  • 资料本身就不准确,却怪模型回答差

学完这篇后应该记住什么

如果只记一件事,我建议记这个:

RAG 不是一个单点技术,而是一条流程。流程里最前面的资料处理做得差,后面的模型再强也救不回来。

一句总结

基础版 RAG 的本质,不是“让模型更聪明”,而是“让模型先找到对的资料,再基于资料回答”。这一步看起来朴素,但后面所有高级技巧,其实都建立在这个地基上。

核心代码实现

方法 1:最基础的 RAG 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1. 读取文档
docs = load_documents(source)

# 2. 切分文档
splits = split_documents(docs, chunk_size=1000, chunk_overlap=200)

# 3. 向量化并写入向量库
vectorstore = build_vectorstore(splits, embedding_model)

# 4. 创建检索器
retriever = vectorstore.as_retriever(k=4)

# 5. 用户提问时先检索
question = "什么是 task decomposition?"
context_docs = retriever.invoke(question)

# 6. 把上下文和问题一起交给模型
prompt = """
请根据下面资料回答问题,不要脱离资料乱答:
{context}

问题:{question}
"""
answer = llm.invoke(prompt.format(
context=format_docs(context_docs),
question=question
))

方法 2:文本切分

1
2
3
4
5
def split_documents(docs, chunk_size=1000, chunk_overlap=200):
chunks = []
for doc in docs:
chunks.extend(recursive_split(doc, chunk_size, chunk_overlap))
return chunks

方法 3:向量检索

1
2
3
4
def retrieve(question):
query_vec = embedding_model.embed_query(question)
results = vectorstore.similarity_search_by_vector(query_vec, k=4)
return results

方法 4:检索后生成

1
2
3
4
5
6
def rag_answer(question):
docs = retrieve(question)
context = "\n\n".join(doc.page_content for doc in docs)
return llm.invoke(
f"只根据以下上下文回答问题:\n{context}\n\n问题:{question}"
)