之前公司项目用到了rag,最近系统梳理检索增强生成技术的一些知识点,本期是第二期,以上内容将总结到新专栏里面,欢迎大家批评指正与讨论。
一、背景
RAG 项目里数据准备和数据处理是整套体系的根基,核心目的就是做好数据清洗,有效提升整体数据质量。整套流程整体包含:数据收集、文本处理、数据清洗、文本分割、索引构建这几大核心环节,也是落地搭建 RAG 应用最基础也最关键的实操技能。
其中,RAG 系统70% 效果上限由数据质量决定,很多时候大家纠结模型选型、检索算法,但如果原始数据全是噪声(比如带广告的网页、格式混乱的 PDF),再强的模型也会 “巧妇难为无米之炊”。干净合规、语义完整的原始文本才是底层核心根基,数据处理环节多花 10% 时间,后续效果能提升 30% 以上。
二、数据源收集
主流分为三种形式:本地 TXT 文本读取、PDF 文档解析、网页内容抓取。
1. TXT 文本数据收集
想要读取本地 TXT 文件,直接用 Python 原生语法就能实现,常用open打开文件搭配read读取内容。通过open函数打开指定路径下的文件,开发中都会搭配 with语句使用,最大好处就是文件使用完毕之后可以自动关闭释放资源 ,避免内存占用溢出。调用file.read()方法能够把整个文件里的全部内容,以字符串形式返回出来,拿到字符串之后我们就能自由做二次处理,也可以直接打印预览内容。
个人理解:TXT 是最 “干净” 的数据源,没有格式冗余,适合作为知识库基础素材,但实际项目中很少纯用 TXT—— 大多是其他格式转换来的,所以读取时一定要注意编码问题,尤其是 Windows 系统下的 GBK 编码文件,很容易踩坑。
# 标准安全读取本地TXT文件(兼容多编码)
def read_local_txt(file_path: str, encode: str = "utf-8") -> str:
"""
功能:本地离线TXT知识库文本读取,兼容utf-8/gbk编码
:param file_path: 文件绝对/相对路径
:param encode: 文件编码,中文场景默认utf-8,Windows文件可试gbk
:return: 拼接完成的全文字符串
"""
try:
# with上下文管理器:自动触发资源开闭,程序异常也能安全关闭文件
with open(file_path, "r", encoding=encode) as file:
# read()一次性读取全文内容,仅适合小体积文本文件(<100MB)
full_content = file.read()
return full_content
except UnicodeDecodeError:
# 编码错误时自动尝试gbk编码
with open(file_path, "r", encoding="gbk") as file:
full_content = file.read()
return full_content
# 调用演示
if __name__ == "__main__":
# 传入本地文本路径
txt_content = read_local_txt("知识库文本.txt")
# 截取前500字符做内容预览,方便调试查看
print(txt_content[:500])
⚠️踩坑提醒大体积日志、百万字长文档禁止使用
read()全量读取,极易造成内存溢出,生产环境优先用 **readline()逐行迭代读取 **,并分批次处理。
2. PDF 文档数据收集
针对 PDF 格式的资料文档,我们可以借助专业解析工具,把 PDF 文档内容解析转换为结构化数组数据,拆分之后每一条文档数据,都能单独存放页面正文内容,以及带有页面编号的元数据信息,方便后续精准定位内容来源。
PDF 是 RAG 项目最常用的数据源(企业手册、技术文档大多是 PDF),但也是 “坑最多” 的 —— 纯文字 PDF、扫描件 PDF、加密 PDF 完全是三种处理逻辑,项目初期一定要先做 PDF 类型排查,否则会白忙活一场。
from langchain_community.document_loaders import PyPDFLoader
def load_pdf_data(pdf_path: str):
"""
功能:批量解析纯文字PDF文档,自动按页码拆分文档对象
:param pdf_path: PDF文件本地存储路径
:return: List[Document] LangChain标准文档对象列表
内置核心属性:
page_content:存储单页PDF正文文本
metadata:存储页码(page)、文件来源(source)等附加信息
"""
# 初始化PDF加载器
pdf_loader = PyPDFLoader(pdf_path)
# 执行加载,自动分页解析
pdf_docs = pdf_loader.load()
return pdf_docs
# 调用示例
pdf_result = load_pdf_data("技术文档.pdf")
print(f"当前PDF总页数:{len(pdf_result)}")
# 截取第一页前300字符预览
print("第一页正文内容:", pdf_result[0].page_content[:300])
📌深度拓展: 商用加密 PDF、图片扫描件 PDF 无法用此方案解析:加密 PDF 需要先解密(可用
PyPDF2解密),扫描件 PDF 需要搭配PaddleOCR等 OCR 工具做图文识别,识别后转成 TXT 再处理 —— 纯文字 PDF 是 RAG 知识库最优数据源,解析效率和质量都最高。
3. 网页抓取数据收集
日常爬取网页知识库、博客、教程类内容,是 RAG 数据源很重要的来源,我们可以借助 Python 专用爬虫库完成网页内容解析,适配大模型场景做网页内容提取。
- requests 库:一款简洁好用的 HTTP 请求库,核心作用就是发送各类 HTTP 网络请求,最常用GET 请求获取网页静态内容,也支持 POST 等请求方式。
- BeautifulSoup:专门用来解析网页 HTML 源码,能够把 requests 获取到的网页内容,转换成 Python 可识别遍历的对象结构,我们可以依靠标签、ID、类名等规则,精准筛选提取自己需要的有效数据,还能遍历页面内所有标签完成数据抓取。
- AsyncHtmlLoader:属于 LangChain 内置的轻量化网页抓取工具,底层依靠异步请求实现抓取,适合简单轻量的批量网页爬取,直接传入网页 URL 地址,调用加载方法就能直接拿到解析完成的网页内容。
- FireCrawlLoader:更智能的网页抓取工具,能够自动过滤网页弹窗、广告、侧边栏等冗余内容,只保留核心正文内容,适配知识库数据源采集。
网页抓取适合补充 “公开知识库”(比如技术博客、官方文档),但一定要注意合规性 —— 不要爬取付费内容、隐私数据,爬取前先看 robots.txt 协议。另外,异步抓取虽然快,但不要无限制并发,容易被目标网站封 IP,建议加延迟(
asyncio.sleep)控制频率。
# 1. requests + BeautifulSoup 基础静态网页抓取(原生爬虫)
import requests
from bs4 import BeautifulSoup
def simple_crawl_web(url: str):
"""
基础静态网页抓取,适合无复杂反爬的公开文档页面
:param url: 目标网页链接
:return: 网页标题、网页纯文本内容
"""
# 配置请求头,伪装浏览器访问,规避基础反爬策略
headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
# 发起GET请求,设置10秒超时防止卡死
res = requests.get(url, headers=headers, timeout=10)
# 统一设置编码,解决中文乱码问题(优先用网页自带编码)
res.encoding = res.apparent_encoding or "utf-8"
# 调用内置解析器解析HTML结构
soup = BeautifulSoup(res.text, "html.parser")
# 提取纯净网页标题
title = soup.title.get_text(strip=True) if soup.title else "无标题"
return title, soup.text
# 2. LangChain异步网页抓取(批量并发爬取)
import asyncio
from langchain_community.document_loaders import AsyncHtmlLoader
def async_crawl_urls(url_list: list, delay: float = 1.0):
"""
异步批量抓取网页链接,带延迟防封IP
优势:IO密集型场景下,执行效率远超同步串行抓取
:param url_list: 待抓取网页链接列表
:param delay: 每个请求延迟时间(秒),防反爬
:return: 解析完成的文档对象列表
"""
async_loader = AsyncHtmlLoader(url_list)
# 自定义异步加载逻辑,添加延迟
async def load_with_delay():
tasks = []
for url in url_list:
tasks.append(async_loader._load_single_url(url))
await asyncio.sleep(delay)
return await asyncio.gather(*tasks)
web_docs = asyncio.run(load_with_delay())
return web_docs
# 3. 智能纯净正文抓取(企业级首选)
from langchain_community.document_loaders import FireCrawlLoader
def smart_crawl_content(target_url: str):
"""
智能网页内容抓取,自动剔除导航栏、广告、评论区等无效内容
:param target_url: 目标网页链接
:return: 仅保留核心正文的纯净文档数据
"""
crawl_loader = FireCrawlLoader(url=target_url, mode="scrape")
pure_docs = crawl_loader.load()
return pure_docs
三、文本处理与数据清洗
一方面去除无效噪声内容,另一方面减少后续调用大模型产生的Token 消耗,从根源上提升数据整体质量。
数据清洗不是 “越多越好”,而是 “精准去噪”—— 比如有些特殊符号(像数学公式里的符号)对技术文档很重要,不能盲目删除;停用词过滤也要分场景,医疗、法律等专业领域,有些看似 “虚词” 可能是关键限定词,要灵活调整词库。
1. 去除特殊字符与标点符号
日常文本里存在大量无意义特殊符号、杂乱标点,我们可以直接使用 Python 当中的正则表达式完成批量清除,英文文本还可以单独编写专属正则规则精准清洗。
import re
def clear_special_symbol(raw_text: str, keep_symbols: list = None) -> str:
"""
全局文本净化函数(支持保留关键符号)
功能:剔除乱码、特殊符号、多余空行与无效空格
过滤规则:默认保留 中文+英文+数字+常规空格,可自定义保留符号
:param raw_text: 未清洗原始文本
:param keep_symbols: 需要保留的特殊符号列表(如["+", "=", "×"])
:return: 净化完成后的干净文本
"""
# 构建保留字符集
keep_str = "".join(keep_symbols) if keep_symbols else ""
rule = re.compile(r"[^\u4e00-\u9fa5a-zA-Z0-9\s" + re.escape(keep_str) + "]")
# 替换所有匹配到的特殊字符为空
clean_text = rule.sub("", raw_text)
# 合并连续多个空格为单个标准空格,去除首尾空格
clean_text = re.sub(r"\s+", " ", clean_text).strip()
return clean_text
# 调用示例:保留数学符号(适合技术文档)
clean_text = clear_special_symbol(raw_text, keep_symbols=["+", "-", "×", "÷", "=", "<", ">"])
2. 去除 HTML 标签与无关冗余元素
网页抓取下来的原始内容会携带大量 HTML 标签,这类标签没有任何实际语义,不仅会增加数据噪声,还会大幅拉高Token 使用成本。行业内常用 HTML2Text 工具完成标签剥离,还能和网页加载器搭配联动使用,一键净化网页文本。
from langchain_community.document_transformers import Html2TextTransformer
# 全局初始化HTML标签清洗转换器
html_cleaner = Html2TextTransformer()
# 批量对爬虫获取的文档列表执行标签清洗
# web_docs 为上方异步爬虫抓取得到的原始网页文档对象
pure_web_docs = html_cleaner.transform_documents(web_docs)
🧠深度原理:一个普通 HTML 页面标签占比可达30% 以上,清洗后可直接降低 30% 左右 Token 计费成本—— 我之前做一个 10 万篇网页的知识库,清洗前 Token 预估成本 2 万 +,清洗后只需要 1.4 万 +,长期私有化部署场景收益极高。
3. 统一文本大小写格式
英文文本存在大小写混杂的情况,会影响后续分词、关键词匹配效果,直接使用字符串 lower() 方法统一转为小写即可,完成文本格式标准化。
个人理解:统一大小写看似简单,但对检索精度影响很大 —— 比如 “BERT” 和 “bert” 在关键词索引中会被当成两个词,统一格式后能避免这种无效区分,提升检索召回率。
4. 去除停用词
像汉语里的的、是、在、了、个、还这类没有实际语义的虚词都属于停用词,对检索和问答没有任何作用。我们可以提前整理一份停用词词库,编写过滤函数,遍历文本内容,只保留有实际含义的实词,剔除全部停用词汇。
# 自定义中文常用停用词库(可根据业务场景扩展/删减)
# 生产环境建议加载哈工大/百度开源完整版停用词表(约2000+词)
stop_word_list = {
"的", "是", "在", "了", "和", "吗", "呢", "啊", "就", "还",
"这", "那", "个", "我", "你", "他", "它", "我们", "你们", "他们"
}
def filter_stop_words(text: str, custom_stop_words: set = None, keep_words: set = None) -> str:
"""
文本级停用词过滤(支持自定义词库和保留词)
:param text: 待过滤原始中文文本
:param custom_stop_words: 自定义停用词库(覆盖默认词库)
:param keep_words: 需要保留的词(即使在停用词库中)
:return: 剔除无用词汇后的精简文本
"""
# 合并停用词库
final_stop_words = custom_stop_words if custom_stop_words else stop_word_list
# 合并保留词库
final_keep_words = keep_words if keep_words else set()
# 将文本拆分为词汇列表(这里简化为按空格分割,实际建议先用jieba分词)
word_list = jieba.lcut(text)
# 过滤逻辑:不在停用词库 或 在保留词库中
valid_words = [
word for word in word_list
if word not in final_stop_words or word in final_keep_words
]
# 重新拼接为完整字符串返回
return "".join(valid_words)
5. 文本分词处理
英文文本可以直接依靠空格完成分割,中文没有天然分隔符,必须依靠分词工具处理。最常用的就是
-
jieba 分词,也是目前主流的中文分词工具,能够把连贯中文语句切分成独立有意义的词语。jieba 一共支持三种分词模式:精准模式、全模式、搜索引擎模式,适配不同业务场景。
-
TF-IDF 算法,全称词频逆文档频率,是信息检索、文本挖掘里非常经典的加权计算方式,主要用来判定单个词语在整篇文档、整个数据集当中的重要程度,常用来做关键词提取、文档相似度计算,Python 中可以借助
scikit-learn库快速实现相关计算。分词是文本处理的 “基础操作”,但直接影响后续检索效果 —— 比如 “自然语言处理” 这个词,精准模式会拆成 “自然语言处理”(一个词),全模式会拆成 “自然、语言、处理、自然语言、语言处理”(多个词)。做 RAG 检索时,建议用 “精准模式为主,搜索引擎模式为辅”,既保证核心语义完整,又能覆盖部分长尾关键词。
# 1. jieba三种模式分词实战(带自定义词典)
import jieba
# 加载自定义词典(解决专业术语拆分错误问题)
jieba.load_userdict("custom_dict.txt") # 格式:专业术语 词频 词性
sentence = "我喜欢学习自然语言处理与RAG检索技术"
# 精准模式(日常知识库首选,拆分语义最贴合原文)
print("精准模式:", jieba.lcut(sentence)) # 输出:['我', '喜欢', '学习', '自然语言处理', '与', 'RAG', '检索技术']
# 全模式分词(拆分所有组合词汇,适合大规模关键词挖掘)
print("全模式:", jieba.lcut(sentence, cut_all=True))
# 搜索引擎模式(偏向搜索业务,长短词汇兼顾适配检索场景)
print("搜索引擎模式:", jieba.lcut_for_search(sentence))
# 2. TF-IDF关键词权重计算(优化版:去除停用词后计算)
from sklearn.feature_extraction.text import TfidfVectorizer
def extract_keywords(text_corpus, top_k=5):
"""
提取文本语料核心关键词(结合停用词过滤)
:param text_corpus: 文本列表
:param top_k: 返回Top K关键词
:return: 关键词列表
"""
# 初始化TF-IDF向量转换器(内置停用词过滤)
tfidf_model = TfidfVectorizer(stop_words=list(stop_word_list))
# 拟合语料并完成向量化计算
tfidf_result = tfidf_model.fit_transform(text_corpus)
# 获取关键词及其权重
keywords = tfidf_model.get_feature_names_out()
weights = tfidf_result.toarray().sum(axis=0)
# 按权重排序,返回Top K
keyword_weight = list(zip(keywords, weights))
keyword_weight.sort(key=lambda x: x[1], reverse=True)
return [kw[0] for kw in keyword_weight[:top_k]]
# 调用示例
text_corpus = [
"RAG是检索增强生成技术",
"自然语言处理是大模型核心基础",
"RAG技术依赖检索与生成两大模块"
]
print("核心关键词:", extract_keywords(text_corpus))
💡行业经验:中文单字 Token 消耗远高于英文,同等字符长度下中文 Token 数量约为英文3 倍,分块、嵌入向量时中文必须缩小块大小适配 —— 比如英文分块设 1000 字符,中文建议设 500-600 字符,避免单块 Token 超标。
四、四大文本分块策略详解
完成数据清洗之后,长篇幅完整文档无法直接用于检索匹配,必须进行文本分割拆分,把长文档切割成大小适中的文本块。目前主流分为四种分割方式:固定大小分割、自定义分隔符分割、递归分块、基于文档逻辑分块。
个人理解:文本分块的核心是 “平衡语义完整性和检索效率”—— 块太大,检索时匹配精度低(比如一个块包含 10 个主题);块太小,会丢失上下文(比如一个技术步骤被拆成两半)。工业界现在几乎都用递归分块,就是因为它能在 “大小” 和 “语义” 之间找到最佳平衡点。
1. 固定大小分块-固定token大小
原理和电脑磁盘分区思路类似,统一按照设定好的固定长度对文本进行切割。如果初次分割之后,文本内容达不到我们需要的大小与结构标准,就搭配不同分隔符二次分割,反复调用分割逻辑,直到切分出符合要求的文本块为止。
from langchain_text_splitters import CharacterTextSplitter
# 初始化固定长度文本分割器
fixed_splitter = CharacterTextSplitter(
chunk_size=500, # 单个文本块最大字符数,中文建议设置300-500
chunk_overlap=60, # 块与块重叠字符数,用于解决上下文内容截断丢失问题
separator="\n" # 优先使用换行符作为基础分割依据
)
# 对清洗完成后的纯净文本执行批量分块
fixed_chunk_list = fixed_splitter.split_text(clean_text)
2. 自定义分隔符分块-比如换行符\n和,
依靠换行符、章节标记、自定义特殊符号作为分割依据,适合格式统一、自带分段标记的文档,适配规则化手册、接口开发文档等标准化文本。
个人理解:这种分块方式适合 “高度结构化” 的文档,比如 API 接口文档里的 “### 接口名称”“### 请求参数”,用分隔符分块能精准拆分每个接口的说明,检索时能直接定位到目标接口,效率很高。
3. 递归分块
递归分块会提前设定好优先级分割符列表,按照段落、换行、句子、词语的顺序逐层完成切割。它不会死板按照固定字符数量强行截断文本,而是依托预设分割符逐层递归拆分,最大程度保留文档原本的语句结构和上下文语义,能够很好解决固定分块容易截断完整语义的问题,也是工业界现在最主流的分块方案。
from langchain_text_splitters import RecursiveCharacterTextSplitter
"""
递归分割核心执行逻辑:严格按照设定优先级依次尝试分割符
优先级顺序:大段落换行(\n\n)→ 单行换行(\n)→ 句号(。)→ 逗号(,)→ 空格( )
"""
recur_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 单块最大字符数
chunk_overlap=50, # 重叠字符数(建议为chunk_size的10%-15%)
separators=["\n\n", "\n", "。", ",", " "]
)
# 执行递归分块操作
recur_chunk_list = recur_splitter.split_text(clean_text)
4. 基于文档逻辑分块-比如标题
按照文档本身天然的结构进行分割,比如按照文章段落、内容小节进行拆分,能够完整保留内容的组织逻辑,维持文本上下文连贯性。这种方式特别适配Markdown 格式文档,依托一级标题、二级标题、列表标记完成分块,优势是拆分出来的文本语义完整连贯,检索召回的内容贴合原文上下文;缺点是极度依赖文档规范排版,格式混乱的文档拆分效果很差。
from langchain_text_splitters import MarkdownHeaderTextSplitter
# 自定义markdown文档层级分割匹配规则
header_rule = [("#", "一级标题"), ("##", "二级标题"), ("###", "三级标题")]
# 初始化markdown专属标题分割器
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=header_rule)
# 对markdown格式原文进行结构化分块
md_chunk_result = md_splitter.split_text(markdown_raw_text)
📊 分块策略选型对比
| 分块方式 | 优点 | 缺点 | 适用业务场景 | 个人推荐度 |
|---|---|---|---|---|
| 固定分块 | 简单易配置、拆分速度快 | 易截断完整语义、上下文容易断裂 | 无格式纯文本、日志类文档 | ⭐⭐⭐ |
| 递归分块 | 语义保留效果最好、容错性高 | 配置逻辑略复杂 | 通用知识库、技术文档(行业首选) | ⭐⭐⭐⭐⭐ |
| 分隔符分块 | 拆分范围精准可控 | 强依赖固定文本格式 | 接口手册、标准化流程文档 | ⭐⭐⭐⭐ |
| 逻辑标题分块 | 文档结构完整性最强 | 仅适配规范排版 MD 文档 | 技术博客、官方结构化文档 | ⭐⭐⭐⭐ |
五、五大索引构建与业务选型
文本分块全部完成之后,最后一步就是搭建索引,依靠索引实现海量文本块的快速检索匹配,行业内常用索引类型一共五类:
- 列表索引:最简单的索引,本质是文本块列表,检索时遍历匹配,适合小体量数据(<1000 块),优点是零配置,缺点是速度慢。
- 关键词表索引:基于 TF-IDF、BM25 等算法构建关键词 - 文本块映射,检索时匹配关键词,优点是速度快、精准度高,缺点是不支持语义模糊匹配。
- 向量索引(项目最常用) :将文本块转为向量,通过向量相似度匹配检索,支持语义理解(比如 “怎么报差旅费” 和 “差旅费报销流程” 能匹配到),优点是泛化能力强,缺点是需要嵌入模型、占用内存略高。
- 树索引:基于二叉树、B 树等数据结构构建,适合范围查询,优点是查询效率稳定,缺点是不适合语义检索。
- 文档摘要索引:先提取每个文本块的摘要,再基于摘要构建索引,检索时先匹配摘要再定位原文,优点是减少无效匹配,缺点是增加摘要生成成本。
实际项目中很少用单一索引,几乎都是 “混合索引”—— 比如先用关键词索引做粗筛(快速过滤掉 90% 不相关文本块),再用向量索引做精排(从 10% 候选中找到最匹配的),这样既保证速度,又保证精度。 另外,向量索引选型要结合数据量:小数据量(<10 万块)用 FAISS 足够,大数据量(>100 万块)建议用 Milvus、Zilliz 等专业向量数据库,支持分布式部署和动态扩容。
from langchain.vectorstores import FAISS
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.indexes import VectorstoreIndexCreator
from rank_bm25 import BM25Okapi
# 1. 向量索引构建(FAISS本地版,适合小中型知识库)
def build_faiss_index(chunks):
"""
构建FAISS向量索引,支持本地离线使用
:param chunks: 文本分块列表
:return: 向量索引对象
"""
# 初始化轻量级开源嵌入模型(平衡速度和精度)
embedding_model = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# 构建向量索引
vector_index = FAISS.from_texts(chunks, embedding_model)
return vector_index
# 2. 关键词索引构建(BM25算法,粗筛首选)
def build_bm25_index(chunks):
"""
构建BM25关键词索引,用于快速粗筛
:param chunks: 文本分块列表
:return: BM25索引对象、分词后的语料
"""
# 分词处理(中文用jieba)
tokenized_corpus = [jieba.lcut(chunk) for chunk in chunks]
# 构建BM25索引
bm25_index = BM25Okapi(tokenized_corpus)
return bm25_index, tokenized_corpus
# 3. 混合索引检索(粗筛+精排)
def hybrid_search(query, bm25_index, tokenized_corpus, vector_index, top_k=10):
"""
混合检索:BM25粗筛 → 向量精排
:param query: 用户查询
:param bm25_index: BM25索引对象
:param tokenized_corpus: 分词语料
:param vector_index: 向量索引对象
:param top_k: 最终返回结果数
:return: 排序后的匹配结果
"""
# 1. BM25粗筛:获取Top30候选
query_tokens = jieba.lcut(query)
bm25_scores = bm25_index.get_scores(query_tokens)
# 取Top30索引
top30_indices = sorted(range(len(bm25_scores)), key=lambda i: bm25_scores[i], reverse=True)[:30]
top30_chunks = [tokenized_corpus[i] for i in top30_indices]
# 2. 向量精排:对Top30候选做语义匹配
vector_results = vector_index.similarity_search_with_score(query, k=top_k)
# 提取结果和分数
results = [(doc.page_content, score) for doc, score in vector_results]
# 按相似度分数排序(降序)
results.sort(key=lambda x: x[1], reverse=True)
return results
# 调用示例
chunks = [chunk.page_content for chunk in recur_chunk_list]
bm25_index, tokenized_corpus = build_bm25_index(chunks)
vector_index = build_faiss_index(chunks)
search_results = hybrid_search("自然语言处理学习方法", bm25_index, tokenized_corpus, vector_index)
六、实战踩坑记录(亲身经历)
坑 1:PDF 解析乱码 / 空白
-
现象:用 PyPDFLoader 解析 PDF 后,page_content 全是乱码或空白。
-
原因:PDF 是扫描件(图片格式)或加密 PDF,PyPDFLoader 只能解析纯文字 PDF。
-
解决方案:① 加密 PDF 先用
PyPDF2.PdfReader.decrypt("密码")解密;② 扫描件 PDF 用 PaddleOCR 识别:from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch") result = ocr.ocr("扫描件.pdf", cls=True) # 提取文本 pdf_text = "\n".join([line[1][0] for line in result[0]]) -
个人总结:项目初期一定要做 PDF 类型检测,纯文字和扫描件分开处理,避免白忙活。
坑 2:文本分块后检索不到相关内容
- 现象:用户查询明明和文档相关,但检索结果完全不匹配。
- 原因:分块时重叠率设置为 0,导致核心语义被截断(比如 “差旅费报销流程” 被拆成 “差旅费” 和 “报销流程” 两个块)。
- 解决方案:重叠率设置为块大小的 10%-15%(比如 chunk_size=500,chunk_overlap=50-75),确保核心语义不被截断。
- 个人总结:重叠率不是 “浪费空间”,而是 “语义保险”,尤其是技术文档、步骤类文本,必须保留重叠部分。
坑 3:向量索引检索速度越来越慢
-
现象:知识库文本块增加到 10 万 + 后,单次检索从 100ms 涨到 1 秒以上。
-
原因:FAISS 默认用暴力搜索(Brute-force),数据量越大速度越慢。
-
解决方案:给 FAISS 添加索引优化(IVF_FLAT 或 HNSW):
python
运行
# 优化FAISS索引(适合10万+文本块) vector_index = FAISS.from_texts(chunks, embedding_model) # 添加IVF_FLAT索引(平衡速度和精度) vector_index = vector_index.serialize_to_bytes() vector_index = FAISS.deserialize_from_bytes(vector_index) vector_index.train(chunks_embeddings) # 提前训练索引 -
个人总结:小数据量用默认配置,大数据量必须做索引优化,或直接迁移到 Milvus 分布式向量库。
坑 4:停用词过滤后丢失关键信息
-
现象:医疗知识库中,“的” 被当成停用词过滤,导致 “高血压的治疗方案” 变成 “高血压治疗方案”,检索时无法匹配 “高血压的治疗”。
-
原因:通用停用词库不适合专业领域,有些 “虚词” 在特定场景是关键限定词。
-
解决方案:① 构建行业专属停用词库;② 对关键领域文本,减少停用词过滤范围:
# 医疗领域停用词库(精简版) medical_stop_words = {"啊", "呢", "吗", "哦", "呀"} # 只过滤纯语气词 -
个人总结:停用词库要 “因地制宜”,不要盲目使用通用词库,尤其是专业领域。
坑 5:网页抓取被封 IP
-
现象:批量爬取网页时,突然无法访问目标网站,报错 403 Forbidden。
-
原因:并发请求过快,被目标网站识别为爬虫,封了 IP。
-
解决方案:① 加请求延迟(1-2 秒 / 次);② 使用代理 IP 池;③ 伪装更真实的请求头:
# 更真实的请求头(模拟Chrome浏览器) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Referer": "https://www.google.com/" # 模拟从谷歌跳转 } -
个人总结:爬虫要 “温柔”,遵守网站反爬规则,避免过度请求,否则可能涉及法律风险。
七、学习疑点留存
- 递归分块完整执行逻辑与实际代码实操还没有完全吃透(比如分隔符优先级如何影响最终分块结果,还需要多做测试);
- 文本分割当中
transformer 0~500相关用法和具体含义暂时没有弄懂; - HTML2Text 网页清洗工具和各类网页加载器联动搭配使用的完整流程还不够熟练(比如如何处理动态渲染的网页内容)。
八、深度工程思考与进阶优化
- 重叠率工程设计chunk_overlap 重叠值不是越大越好,通用场景设置为块大小 10%~15% 即可,过高会造成数据冗余、索引体积膨胀(比如 10 万文本块,重叠率 50% 会导致索引体积翻倍)。
- 增量数据更新企业知识库不会一次性构建完成,需要实现增量爬取→增量清洗→增量分块→增量建索引:
- 用数据库记录已爬取 URL / 文件路径,避免重复抓取;
- 向量索引支持增量添加(FAISS 的
add_texts方法),无需全量重建。
- 数据合规过滤清洗阶段新增敏感词过滤、隐私信息脱敏(比如身份证号、手机号用正则匹配替换),适配企业内部知识库安全规范 —— 我之前做金融行业 RAG 项目,这一步是必做的,否则会有数据泄露风险。
- 多语言兼容中英文混合文本,拆分策略优先以中文分句规则为主(中文用 “。” 分句,英文用 “.” 分句),避免英文单词被强行拆分(比如 “Python” 被拆成 “Pyth” 和 “on”)。
- 模型选型优化嵌入模型不是越大越好:① 小知识库(<1 万文本块)用
all-MiniLM-L6-v2(速度快、占用内存小);② 大知识库(>10 万文本块)用all-mpnet-base-v2(精度更高);③ 中文场景优先用text2vec-base-chinese(对中文语义理解更准)。
九、学习总结与落地建议
- 从 “小知识库” 起步(比如 100 篇文档),先跑通完整流程,再逐步扩大数据量;
- 优先用 LangChain 现成工具(比如 Loader、Splitter),不要重复造轮子,聚焦核心逻辑;
- 多记录踩坑经历和参数配置(比如分块大小、重叠率、模型选型),形成自己的 “实战手册”。