Naive RAG 讲解
目录+
RAG,全称 Retrieval-Augmented Generation(检索增强生成),是过去两年大模型应用落地里最重要的工程方案之一。 如果把大模型比作一个"很聪明但记忆固定的学生",那么 RAG 做的事情就是:先去外部资料里找答案,再带着资料回答问题。
而 Naive RAG,可以理解为最基础、最原始、也是最容易落地的一代 RAG 方案。它的结构并不复杂,但已经足够解决很多实际问题,比如企业知识库问答、内部文档检索、客服机器人、私有数据接入大模型等。
一、为什么需要 RAG
大语言模型能力很强,但存在三类核心局限。
1. 时效性有限
模型知识来自训练数据,而训练数据有截止时间。最新政策、财报、内部 SOP、个人知识库文档——这些如果没被放进训练数据,模型就无法直接回答。
2. 知识覆盖不完整
即使训练了海量语料,也不可能覆盖所有垂直领域和每家企业的内部资料。医疗、法律、金融等高门槛领域,以及内部文档、合同、产品手册、私有接口说明——模型可能"懂一点",但不够深、不够准。
3. 幻觉问题
大模型在不知道答案时,仍可能生成一个看起来很像答案的内容——语气自信,内容却可能是错的。其根本原因是生成模型在概率分布下会"补全最像答案的文本"。

当问题依赖最新知识、外部知识、私有知识时,仅靠模型参数往往不够。
二、什么是 RAG:微调 vs RAG
面对"知识更新慢"和"容易幻觉"的问题,常见有两条路:微调(Fine-Tuning) 和 RAG。
微调
在通用大模型基础上用特定领域数据继续训练,让模型更适应某类任务或领域。适合解决语气风格统一、输出格式稳定、特定任务能力增强等问题。

但也存在明显代价:数据准备成本高、训练和迭代成本高、知识更新不灵活,不适合频繁变动的知识库场景。
RAG
RAG 不把新知识塞进模型参数,而是给模型外挂一个"可检索知识库":先从外部知识库找相关内容,拼进提示词,再让 LLM 基于上下文生成答案。

RAG 拆成三个词理解:
- Retrieval(检索):用户提问后,去外部知识源(企业文档、产品手册、FAQ、论文等)找到相关片段。在 Naive RAG 中,常见的做法是把文档切分后向量化,存入向量数据库,再通过向量检索匹配。
- Augmented(增强):增强的对象不是模型,而是 Prompt。把检索到的相关片段和用户问题拼在一起,让模型在"带资料"的前提下回答。
- Generation(生成):把增强后的提示词喂给 LLM,输出答案。
本质一句话:RAG = LLM + 外部知识检索,也就是"让模型先翻书,再回答"。


相比微调,RAG 的优点是成本更低、上线更快、知识更新更方便,更适合企业文档和私有数据接入。在多数知识库问答场景里,RAG 往往是第一选择。
三、Naive RAG 的核心流程
Naive RAG 虽然叫"Naive",但流程已很完整。工程上分为两个阶段:离线阶段和在线阶段。后续所有进阶 RAG 的变化,本质上都是在这两个阶段上做增强(索引更好、检索更准、生成更稳)。
1. 离线阶段:构建知识库
在系统正式服务之前,把原始文档加工成"可检索"的知识库。
a. 数据清洗
原始文档常有 HTML 标签、页眉页脚、空白字符、重复内容、敏感信息等噪音。清洗的目标是明确:哪些内容值得进知识库,哪些应该被丢掉。企业场景还可能涉及权限对齐、归属关系梳理、业务字段补全等。
b. 文档分块
RAG 不会把整篇文档直接塞给模型或向量库,而是先拆成小块。原因有三:
- 模型上下文窗口和向量模型输入长度都有限
- 整篇文档过长会稀释语义,噪音太多会影响检索精度
- 很多问题只对应文档中的一小段,分块越合理,命中正确答案的概率越高
分块的目标不是机械切开,而是切成既足够短、又尽量保留语义完整的小单元。
c. 向量化
每个文本块通过 Embedding(向量)模型转成向量,让"语义相近"的内容在向量空间里靠得更近,使文本可以进行相似度计算。
d. 存储
向量及其原始文本、元数据存入向量数据库。向量数据库的核心价值是高效检索:提供相似度搜索、Top-K 返回、元数据过滤和高并发能力。
2. 在线阶段:回答用户问题
流程:用户提问 → 问题向量化 → 向量检索 → 拼接提示词 → LLM 生成答案。
检索结果的质量直接决定最终回答质量。

四、Top-K 与 Top-P
Top-K
把检索结果按相似度排序后,取前 K 条(如 Top-3 取最相关的 3 条)。这是最直观、最常用的做法。
Top-P
不是固定取多少条,而是按累计相似度阈值来取。例如相似度依次为 0.9、0.5、0.4、0.3、0.2,累计阈值设为 2,则累加至 0.9+0.5+0.4+0.3=2.1,取前 4 条。更灵活,但业务场景中 Top-K 仍更常用,因为更稳定、更容易调试。
五、常见分块策略
分块是 Naive RAG 最关键的环节之一——分块做差了,向量化、检索、生成都会一起变差。
1. 固定长度分块
按固定字符数或 Token 数切分。
- 优点:实现简单、速度快、存储和检索效率高
- 缺点:容易把完整语义硬切断,上下文衔接可能丢失
def split_by_fixed_char_count(text, count):
text = ("自然语言处理(NLP),作为计算机科学、人工智能与语言学的交融之地,致力于赋予计算机解析和处理人类语言的能力。"
"在这个领域,机器学习发挥着至关重要的作用。利用多样的算法,机器得以分析、领会乃至创造我们所理解的语言。"
"从机器翻译到情感分析,从自动摘要到实体识别,NLP的应用已遍布各个领域。随着深度学习技术的飞速进步,"
"NLP的精确度与效能均实现了巨大飞跃。如今,部分尖端的NLP系统甚至能够处理复杂的语言理解任务,"
"如问答系统、语音识别和对话系统等。NLP的研究推进不仅优化了人机交流,也对提升机器的自主性和智能水平起到了关键作用。")
# 假设我们按照每100个字符来切分文本
chunks = split_by_fixed_char_count(text, 100)
for i, chunk in enumerate(chunks):
print(f"块{i} - 长度{len(chunk)} - 内容: {chunk}")
2. 固定长度 + 重叠窗口
每个块长度固定,相邻块之间保留部分重叠内容(overlap),以保住跨块语义连续性。生产环境里 overlap 常设为块大小的 10%~20%。
- 优点:比纯固定分块更稳,能减少语义断裂
- 缺点:数据冗余增加、存储成本变高,也可能带入更多噪音
# chunk_size表示块的字符个数,stride为步长,滑动窗口的大小为chunk_size-stride
# overlapping window 重叠窗口
def sliding_window_chunks(text, count, stride):
# 列表推导式
return [text[i: i+count] for i in range(0, len(text), count - stride)]
text = ("自然语言处理(NLP),作为计算机科学、人工智能与语言学的交融之地,致力于赋予计算机解析和处理人类语言的能力。"
"在这个领域,机器学习发挥着至关重要的作用。利用多样的算法,机器得以分析、领会乃至创造我们所理解的语言。"
"从机器翻译到情感分析,从自动摘要到实体识别,NLP的应用已遍布各个领域。随着深度学习技术的飞速进步,"
"NLP的精确度与效能均实现了巨大飞跃。如今,部分尖端的NLP系统甚至能够处理复杂的语言理解任务,"
"如问答系统、语音识别和对话系统等。NLP的研究推进不仅优化了人机交流,也对提升机器的自主性和智能水平起到了关键作用。")
chunks = sliding_window_chunks(text, 100, 20)
for i, chunk in enumerate(chunks):
print(f"块 {i} - 长度{len(chunk)},内容: {chunk}")
3. 按句子分块
尤其适合中文场景——一句话往往已是较完整的语义单元。
- 优点:语义自然,阅读和解释性更好
- 缺点:块可能太短、容易缺失上下文,对复杂答案不够友好
# 正则表达式
import re
text = ("自然语言处理(NLP),作为计算机科学、人工智能与语言学的交融之地,致力于赋予计算机解析和处理人类语言的能力。"
"在这个领域,机器学习发挥着至关重要的作用。利用多样的算法,机器得以分析、领会乃至创造我们所理解的语言。"
"从机器翻译到情感分析,从自动摘要到实体识别,NLP的应用已遍布各个领域。随着深度学习技术的飞速进步,"
"NLP的精确度与效能均实现了巨大飞跃!如今,部分尖端的NLP系统甚至能够处理复杂的语言理解任务,"
"如问答系统、语音识别和对话系统等。NLP的研究推进不仅优化了人机交流,也对提升机器的自主性和智能水平起到了关键作用。")
'''
re.split(r'[。?!]', text) 是使用正则表达式对字符串 text 进行分割的操作。
re.split(pattern, string):这是 Python 的 re 模块中的一个函数,用于根据正则表达式 pattern 将字符串 string 分割成多个部分。
r'[。?!]':这是一个正则表达式模式,用于匹配中文句子结束的标点符号。
re.split 函数会返回一个列表,其中包含分割后的子字符串和匹配到的标点符号
'''
sentences = re.split(r'[。?!]', text)
print(sentences)
print('-' * 100)
for i, chunk in enumerate(sentences):
print(f"块 {i + 1} - 长度{len(chunk)},内容: {chunk}")
4. 递归分块
LangChain 中常见的方案(如 RecursiveCharacterTextSplitter)。核心思想:优先按更自然的分隔符切分,如果块仍太大就继续按更细粒度分隔符再切,最后尽量合并成接近目标长度的块。相比机械固定切分,更适合真实文档。
# 利用langchain的RecursiveCharacterTextSplitter
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = ("自然语言处理(NLP),作为计算机科学、人工智能与语言学的交融之地,致力于赋予计算机解析和处理人类语言的能力。"
"在这个领域,机器学习发挥着至关重要的作用。利用多样的算法,机器得以分析、领会乃至创造我们所理解的语言。"
"从机器翻译到情感分析,从自动摘要到实体识别,NLP的应用已遍布各个领域。随着深度学习技术的飞速进步,"
"NLP的精确度与效能均实现了巨大飞跃。如今,部分尖端的NLP系统甚至能够处理复杂的语言理解任务,"
"如问答系统、语音识别和对话系统等。NLP的研究推进不仅优化了人机交流,也对提升机器的自主性和智能水平起到了关键作用。")
splitter = RecursiveCharacterTextSplitter(
chunk_size=20, # 分割长度
chunk_overlap=5, # 重叠长度 /重叠窗口大小
# separators=["\n\n", "\n", " ", ""],
separators = ["\n\n", "\n", "。", ","] )
chunks = splitter.split_text(text)
for i, chunk in enumerate(chunks):
print(f"块 {i + 1} - 长度{len(chunk)},内容: {chunk}")
'''
推荐使用:RecursiveCharacterTextSplitter + Overlap
注意参数:
- chunk_size: 一般300~500 tokens(或字符)
- chunk_overlap: 一般为chunk_size的5%-20%(防止上下文断裂)
- separators: 从高到低设置合适的分隔符,例如:["\n\n", "\n", ".", " "]
注意事项:
- 避免切片太小:小于 100 tokens 的 chunk 容易导致语义不完整。
- 避免切片太大:超过模型最大上下文, 会导致无法嵌入。
- 动态调整参数:针对不同类型文档调整 chunk_size 和 overlap。
'''
六、优点与局限
优点
- 成本低、上线快:不需要训练模型,适合快速验证业务
- 知识更新方便:文档更新后重新索引即可,无需重新训练
- 易于接入私有数据:企业内部资料、产品手册、客服文档可直接接入
- 可解释性更强:回答来自哪些检索片段可以展示给用户,更容易追溯依据
局限
- 检索未必精准:向量相似不等于业务相关,有时检索到的是"像"而不是"对"
- 分块质量高度影响效果:块太大噪音多,块太小上下文不够
- 缺少复杂推理:擅长"找到相关资料",但跨文档整合、多跳检索等复杂推理场景容易吃力
- 噪声干扰:检索结果带噪音时,模型可能被误导生成不准确的答案
因此后来出现了 Hybrid Search、Re-ranking、Query Rewrite、Multi-hop RAG、Graph RAG、Agentic RAG 等进阶方案,本质都是在修补 Naive RAG 这些短板。
七、总结
Naive RAG 的本质,是把外部知识切碎、编码、存储,在用户提问时取回最相关的碎片,交给大模型组织成答案。
它不是让模型"学会了新知识",而是让模型在回答时"临时查到了新知识"。这个差别非常关键:
- 微调:把知识写进脑子里
- RAG:给模型配一个随时能翻的资料库
它的核心流程是:清洗分块 → 向量化 → 存入向量库 → 用户提问时检索 → 拼入 Prompt → LLM 生成答案。它对所有 RAG 系统定义了一个今天仍然成立的基本范式:先检索,再生成。
对于绝大多数企业知识库、私有问答、文档助手场景来说,Naive RAG 往往就是第一步,也是最值得先跑通的一步。
Naive RAG的核心步骤
1. 索引化(离线阶段):
文档 -> 分块 -> 向量化 -> 存储
2. 检索:
用户提问 -> 向量化 -> 检索数据库 -> 得到 TOP-K 个相关文档块
3. 增强生成:
增强提示词(原始问题 + 相关文档块) -> LLM -> 生成答案