news 2026/8/10 10:33:44

从项目中学习大模型开发(二)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从项目中学习大模型开发(二)

前言:本文系统介绍了 RAG(检索增强生成)技术的核心概念、实现流程与实战经验。首先阐述了 RAG 的定义、价值及其与微调的对比,并梳理了从 Naive RAG 到 Agentic RAG 的演进路径。随后,详细讲解了 LangChain 中文档加载(Document 对象、各类加载器及 DirectoryLoader 批量加载)与文档分块(分块原因、策略、核心参数及中文优化技巧)两大核心环节。最后,通过 loader.py 完整代码解析和实战踩坑经验,提供了从理论到实践的完整指南,帮助读者构建高效、可靠的 RAG 应用。

一、RAG介绍

1.1 什么是RAG?

RAG(检索增强生成)是一种结合检索和生成两种方法的技术。它通过先检索相关的文档,用检索出来的信息对提示词增强,再使用大模型生成答案。

RAG的本质:RAG = 大模型LLM + 外部数据

1.2 为什么需要 RAG?

大语言模型(LLM)虽然强大,但直接使用依旧有三个绕不开的痛点:

时效性 :训练数据有截止日期,在你不使用联网搜索的情况下,大模型是无法询问最新的时事的。

知识覆盖度 :虽然大模型的训练数据集非常庞大,但仍可能无法涵盖所有领域的知识或特定领域的深度信息。

幻觉问题:大模型在某些情况(提问方式不对,模型知识欠缺)下给出的回答很可能是错误的,或者是虚构的甚至是故意欺骗的信息,即一本正经胡说八道。

1.3 RAG vs 微调

下面将以表格的形式,从适用场景、数据成本、更新成本、可解释性、幻觉控制和典型项目这六个方面,将RAG和微调进行对比。

对比维度RAG微调(Fine-tuning)
适用场景知识频繁更新、需引用来源、私有文档问答改变模型风格/格式/能力、领域术语内化
数据成本原文即可,无需标注需要高质量的指令
更新成本增删文档即可,几乎零成本重新训练,成本高
可解释性强(可追溯来源文档)弱(知识融在权重里)
幻觉控制好(有上下文约束)一般
典型项目课程助手、企业知识库、客服代码生成、特定文体写作

1.4 Naive RAG 的三步流水线(索引→检索→生成)

Naive RAG(朴素 RAG)是 RAG 的最基础形态,固定为三步:

[索引阶段] [检索阶段] [生成阶段] 文档 → 切分 → 问题向量化 → 问题 + 上下文 → Embedding → 相似度匹配 → 拼 Prompt → 存入向量库 → 取 Top-K 块 → LLM 生成答案

索引阶段(Indexing) :把原始文档切分成小块,每块用 Embedding 模型转成向量,存入向量数据库。这是离线一次性完成的。
检索阶段(Retrieval) :用户提问时,把问题也转成向量,在向量库里找最相似的 K 个文本块。
生成阶段(Generation) :把检索到的文本块作为上下文,和原问题一起塞进 Prompt,让 LLM 基于上下文回答。

本章会重点讲解索引阶段中的文档加载和分块。

1.5 RAG 演进路径

RAG 技术已经演进了三代,了解这个路径有助于你认清本项目的位置:

阶段别名标志性组件
RAG v0Naive RAG切分 + 向量 + Top-K + Prompt
RAG v1Advanced RAG+ 查询改写 / 重排序 / 上下文压缩 / 混合检索
RAG v2Modular RAG+ 查询路由 / 图索引 / Agent 编排 / 自我评估
RAG v3Agentic RAGLLM 自主决定何时检索、检索什么、如何验证

本项目属于Naive RAG,只是进行了一些轻量优化,即分块器加了中文分割符,以及检索时使用了MMR算法。

二、文档加载

2.1 Document 对象是什么

在 LangChain 的世界里,所有加载器返回的不是字符串,不是字典,而是统一的 Document 对象 。理解它是理解整个加载体系的前提。

class Document: page_content: str # 文档的文本内容 metadata: dict # 文档的元数据(来源、页码等)

Document属性详解:

page_content :文档的纯文本内容,后续会被切分、向量化、检索。

metadata :元数据字典,常见字段有:source :文件路径(最有用,RAG 回答时可以引用来源)page :页码(PDF 加载器会自动填充)total_pages :总页数自定义字段(如课程阶段、章节标题)。

为什么要把元数据和内容绑在一起?因为在 RAG 的生成阶段,我们不仅要告诉模型"答案在这段文字里",还要能告诉用户"这段文字来自哪个文件的哪一页"。本项目中也有用到,后续大家就能看到。

2.2 LangChain文档加载器

LangChain 提供了上百种加载器。会详细介绍其中常用的的加载器。

2.2.1 常用加载器速查表

数据类型推荐加载器底层依赖适用场景与特点
Markdown(.md)UnstructuredMarkdownLoaderunstructured通用推荐,能解析标题、列表等结构,比 TextLoader 更智能。
纯文本 (.txt)TextLoader内置最简单,读取纯文本内容。 注意:必须指定 encoding
Word (.docx)Docx2txtLoaderdocx2txt提取纯文本,轻量,适合无需保留格式的场景
Word (.docx)UnstructuredWordDocumentLoaderunstructured功能更强大,能识别段落、标题、列表等结构
PDF (.pdf)PyPDFLoaderpypdf最常用, 按页切分 ,自带 page 元数据。
PDF (.pdf)UnstructuredPDFLoaderunstructured适合复杂 PDF,能识别表格和元素。
PPT (.pptx)UnstructuredPowerPointLoaderunstructured解析 PPT,支持按 elements 模式提取标题、正文。
Excel (.xlsx)UnstructuredExcelLoaderunstructured解析 Excel,将每个单元格或表格转换为 Document。
CSVCSVLoader内置将每一行转为一个 Document,适合问答对、数据集。
JSONJSONLoader内置解析 JSON,支持 jq 语法提取特定字段,适合 API 返回数据。
HTMLUnstructuredHTMLLoaderunstructured从 HTML 中提取干净文本,去除标签。
目录批量DirectoryLoader内置核心工具,自动遍历目录 + 匹配文件 + 调用上述加载器。

2.2.2 各加载器详细用法与示例代码

1. 纯文本加载器:TextLoader加载器

最简单的加载器,把整个文件读成一个 Document 。

基本用法:

from langchain_community.document_loaders import TextLoader 基本用法(必须显式指定 encoding,否则 Windows 下中文乱码) loader = TextLoader("笔记.txt", encoding="utf-8") docs = loader.load() print(docs[0].page_content) # 整个文件内容作为一个字符串 print(docs[0].metadata) # {'source': '笔记.txt'}

2. Word 加载器:Docx2txtLoader和UnstructuredWordDocumentLoader

Docx2txtLoader(轻量版):依赖 docx2txt 库,提取纯文本,丢失格式。

特点:

  • 整个文档一个 Document,不按页/段切分
  • 只提取文本,丢失表格、图片、加粗等格式
  • 依赖轻量,安装简单: pip install docx2txt
  • 适合不需要保留格式的场景

基本用法

from langchain_community.document_loaders import Docx2txtLoader loader = Docx2txtLoader("课程笔记.docx") docs = loader.load() print(docs[0].page_content) # Word 中所有文本拼接成一个字符串 print(docs[0].metadata) # {'source': '课程笔记.docx'}

UnstructuredWordDocumentLoader(增强版):功能更强,能识别段落、标题、列表等结构,支持 mode="elements" 。

特点:

  • 依赖 unstructured + python-docx
  • mode="elements" 能区分标题、正文、列表,metadata 带 category
  • 适合:结构复杂的 Word 文档、需要保留层次的场景

基本用法:

from langchain_community.document_loaders import UnstructuredWordDocumentLoader 模式一:合并为一个 Document(默认) loader = UnstructuredWordDocumentLoader("课程笔记.docx") docs = loader.load() 模式二:按元素切分(标题、段落、列表项各自独立) loader = UnstructuredWordDocumentLoader( "课程笔记.docx", mode="elements" # 每个元素一个 Document ) docs = loader.load() docs[0].metadata 会包含 {"category": "Title", ...} 等结构信息

3. PDF 加载器:PyPDFLoader和UnstructuredPDFLoader

PyPDFLoader:最常用的 PDF 加载器, 按页切分 ,每页一个 Document。

特点:

  • 按页切分 是最大优势,RAG 回答时能精确引用"第几页"
  • metadata 自动带 page 和 total_pages ,对来源引用很有用
  • 依赖 pypdf ,安装简单
  • 缺点:复杂版面(多栏、表格)提取效果一般,页眉页脚会被当正文

基本用法

from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("Ollama 安装教程.pdf") docs = loader.load() 一页一个 Document print(len(docs)) # 文档页数,如 5 print(docs[0].page_content) # 第 1 页的文本内容 print(docs[0].metadata) { 'source': 'Ollama 安装教程.pdf', 'page': 0, # 页码(从 0 开始) 'total_pages': 5 # 总页数 }

UnstructuredPDFLoader:能识别表格、标题、列表等结构,适合复杂 PDF。

特点:

  • strategy="hi_res" 高精度模式能识别表格、图片,但需要额外系统依赖
  • mode="elements" 按元素切分,区分 Title/Table/NarrativeText 等
  • 依赖较重,安装复杂(需 poppler、tesseract 等)
  • 适合:学术论文、复杂版面 PDF、含表格的PDF

基本用法:

from langchain_community.document_loaders import UnstructuredPDFLoader 模式一:合并为一个 Document loader = UnstructuredPDFLoader("复杂课件.pdf") docs = loader.load() 模式二:按元素切分(标题、表格、图片各自独立) loader = UnstructuredPDFLoader( "复杂课件.pdf", mode="elements", strategy="hi_res" # 高精度模式,能识别表格 ) docs = loader.load() docs[0].metadata 包含 {"category": "Table", "page_number": 1, ...}

4. PPT 加载器:UnstructuredPowerPointLoader

特点:

  • 依赖 unstructured + python-pptx
  • 可能会把 演讲者备注 也读进来,需确认是否符合预期
  • 表格内容会被扁平化为文本,结构信息部分丢失
  • mode="elements" 对 RAG 更友好,能按元素类型做差异化处理

基本用法:

from langchain_community.document_loaders import UnstructuredPowerPointLoader 模式一:合并(默认)—— 整个 PPT 合成一个 Document loader = UnstructuredPowerPointLoader("AI大模型初识.pptx") docs = loader.load() print(len(docs)) # 1 print(docs[0].page_content) # 所有幻灯片文本拼接 模式二:按元素切分 —— 标题、正文、表格各自独立 loader = UnstructuredPowerPointLoader( "AI大模型初识.pptx", mode="elements" ) docs = loader.load() print(len(docs)) # 元素数量(通常 = 幻灯片数 × 每片元素数) print(docs[0].page_content) # 第一个元素的内容(通常是标题) print(docs[0].metadata) { 'source': 'AI大模型初识.pptx', 'category': 'Title', # 元素类型:Title/NarrativeText/Table 'page_number': 1, # 幻灯片页码 'filetype': 'application/vnd...presentationml.presentation' }

两种模式对比:

  • mode="single" (默认),输出时整个 PPT 一个 Document,适用于内容少、结构简单的场景
  • mode="elements" ,输出时每个元素一个 Document ,适用于需要精确定位标题/正文/表格的场景

5. Markdown 加载器: UnstructuredMarkdownLoader

Markdown 是 RAG 项目中最常见的格式。虽然可以用 TextLoader 读 .md ,但专用加载器能更好地保留结构。

基本用法:

from langchain_community.document_loaders import UnstructuredMarkdownLoader 基本用法 loader = UnstructuredMarkdownLoader("example.md") docs = loader.load() 进阶:按元素切分 (mode="elements") 这会将标题、段落、列表项分成独立的 Document loader = UnstructuredMarkdownLoader("example.md", mode="elements") docs = loader.load() 此时 docs[0].metadata 会包含 {"category": "Title", ...}

6. 结构化数据加载器: CSVLoader & JSONLoader

当知识库是结构化数据(如 FAQ 对、产品列表)时,使用这些加载器能直接将数据转为问答对,极大提升检索效率。

CSVLoader (适合 FAQ / 数据集)

  • 特点 :将 CSV 的 每一行 转换为一个 Document 。
  • 用法 :指定哪一列作为 page_content 。

基本用法:

from langchain_community.document_loaders import CSVLoader 假设有一个 faq.csv,列名是 "question", "answer" 将 "question" 和 "answer" 拼接成内容 loader = CSVLoader( file_path="faq.csv", csv_args={ "delimiter": ",", "quotechar": '"', }, # 自定义内容格式 page_content_column="question", # 或者自定义处理 # 更好的方式:自定义将多列合并 ) 更标准的写法是使用 pandas 处理后加载,或在 metadata 中保留其他列

JSONLoader (适合 API 数据)

  • 特点 :解析 JSON 文件,支持提取特定字段。
  • 用法 :需要配合 jq 表达式来定义“什么是文档内容”。

基本用法:

from langchain_community.document_loaders import JSONLoader 假设 data.json 结构为 [{"title": "...", "content": "..."}, ...] loader = JSONLoader( file_path="data.json", jq_schema=".[].content", # 提取数组中每个元素的 content 字段作为文本 text_content=True, # metadata 指定 title 字段 metadata_func=lambda record, metadata: {**metadata, "title": record.get("title")} ) docs = loader.load()

7. 表格加载器: UnstructuredExcelLoader

from langchain_community.document_loaders import UnstructuredExcelLoader 加载 Excel,默认 mode="elements",每个单元格是一个 element loader = UnstructuredExcelLoader("report.xlsx") docs = loader.load() 或者 mode="single",将整个表合并为一个文本 loader = UnstructuredExcelLoader("report.xlsx", mode="single")

8. HTML 加载器: UnstructuredHTMLLoader

从网页下载的 HTML 文件直接喂给 LLM 会充满噪声,HTML 加载器负责清洗。

from langchain_community.document_loaders import UnstructuredHTMLLoader 自动去除 HTML 标签,提取纯文本 loader = UnstructuredHTMLLoader("webpage.html") docs = loader.load()

9. 目录批量加载器:DirectoryLoader

这是本项目的 核心加载器 , loader.py 用它一次性加载了全部课程资料。上面所有加载器都可以作为它的 loader_cls 。

完整用法

from langchain_community.document_loaders import DirectoryLoader, TextLoader loader = DirectoryLoader( path="./date/notes/", # 1. 扫描目录 glob="**/*.md", # 2. 文件匹配规则 loader_cls=TextLoader, # 3. 用哪个加载器读 loader_kwargs={ # 4. 传给加载器的参数 "encoding": "utf-8" }, show_progress=True, # 5. 显示进度条(可选) use_multithreading=True, # 6. 多线程加速(可选) silent_errors=True # 7. 出错时跳过而非中断(可选) ) docs = loader.load()

参数详解:

参数作用
path扫描根目录
glob文件匹配模式
loader_cls用哪个加载器读文件
loader_kwargs透传给loader_cls的参数
show_progress显示加载进度条
use_multithreading多线程并行加载
silent_errors出错跳过不中断

glob 模式详解:

"*.md" # 只匹配当前目录下的md文件(不递归子目录) "**/*.md" # 递归匹配所有子目录下的md文件(本项目用) "**/*.{md,txt}" # 同时匹配md和txt "**/*.pdf" # 递归匹配所有PDF "**/L/**/*.md" # 只匹配L目录下的md

三、文档分块

3.1 为什么要分块?

我们在加载完文档后并不能直接使用,必须要先进行分块,主要有以下三个原因:

原因一:Embedding 模型有长度上限,Embedding 模型是有 token 上限(通常为8192 tokens)。一篇 50 页的 PDF 全文塞进去会直接报错。

原因二:长文本会"稀释"语义,降低检索精度 Embedding 的原理是把文本压缩成一个固定维度的向量。文本越长,向量里承载的语义越"平均化",检索时匹配度下降。比如一篇同时讲"RAG"和"LangChain"的长文,向量化后既不像专门讲 RAG 的,也不像专门讲 LangChain 的,用户问任何一个主题都匹配不精确。

原因三:控制 LLM 上下文成本 检索到的上下文要塞进 Prompt 给 LLM。块太大,一次塞不了几块,还浪费 token;块太小,又缺乏完整语义。分块是平衡检索精度和生成质量的关键杠杆。

一句话总结 :分块策略决定了 RAG 效果的天花板,做得差的话,后面的模型再强也救不回来。

3.2 分块方式总览

常见的分块方式有四种:

分块方式原理优点缺点
按字符固定字符数切分简单、可控可能切断词语/句子
按Token固定token数切分精确控制模型输入需要tokenizer依赖
按语义用Embedding计算语义边界语义完整性最好计算成本高、慢
按结构按 Markdown 标题/代码块/段落保留文档结构依赖文档格式规范

本项目用的是 按字符 + 按结构 的混合方式,这也是 LangChain 最推荐的通用方案。

3.3 LangChain中常用的分块器

LangChain 提供了多种分块器,选对分块器是 RAG 调优的第一步:

分块器切分策略适用场景
CharacterTextSplitter按单一分隔符(如 \n\n )切,再按字符数补切简单场景
RecursiveCharacterTextSplitter按分隔符优先级递归切分通用首选
TokenTextSplitter按 token 数切需精确控制 token
MarkdownHeaderTextSplitter按 Markdown 标题切,保留层级纯 Markdown 文档
HTMLHeaderTextSplitter按 HTML 标签切网页内容
SemanticChunker按 Embedding 相似度动态切高质量需求
RecursiveJsonSplitter递归切 JSON结构化数据

选型建议:

  • 不确定用什么 → RecursiveCharacterTextSplitter (通用首选)
  • 纯 Markdown 文档 → MarkdownHeaderTextSplitter + RecursiveCharacterTextSplitter 组合
  • 追求极致质量 → SemanticChunker (慢但语义完整)
  • 代码文档 → 自定义分隔符按函数/类切

3.4 三大核心参数详解

本项目的分块代码:

splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=100, separators=["\n#", "\n##", "\n###", "\n\n", "\n", "。", ";", ".", ""] )

代码中的三个参数是 RAG 调优的核心,我会逐个进行讲解:

参数一: chunk_size (块大小)

含义 :每个文本块的最大字符数。

取值经验 :

  • 太小(如 100-200):语义被切碎,一个知识点跨多个块,检索时容易漏
  • 太大(如 1500-2000):语义被稀释,且一次塞不了几块进上下文
  • 中文场景经验值 :500-1000 字符。本项目取 800,约 200-300 tokens,对课程笔记(段落式表达)是合理选择

调参建议 :不同领域最佳值不同。代码文档可以小(按函数块,300-500),长篇说明文可以大(800-1200)。建议用评估集对比不同值的效果。

参数二: chunk_overlap (重叠区)

含义 :相邻两个块之间的重叠字符数。

为什么需要重叠 :假设有一句话"RAG 的三步是索引、检索、生成",如果刚好在第 8 个字符切分,前一块是"RAG 的三步是索引",后一块是"、检索、生成",两块都不完整。加了 overlap 后,前一块会多包含后续 100 个字符,保证关键信息不在边界丢失。

取值经验 :一般为 chunk_size 的 10%-20%。本项目 100/800 = 12.5% ,在合理范围。

代价 :overlap 会增加总块数和存储成本,但换来的检索精度提升通常值得。

参数三: separators (分隔符优先级列表)

这是 RecursiveCharacterTextSplitter 的灵魂 ,也是它比 CharacterTextSplitter 强大的根本原因。

工作原理 :不是一个分隔符切到底,而是 按优先级从高到低递归尝试 :

切分流程: 1. 尝试用最高优先级 "\n#"(一级标题)切分 → 如果每段都 ≤ chunk_size,完成 → 否则进入第 2 步 2. 对超长段,用 "\n##"(二级标题)继续切 3. 再用 "\n###"(三级标题) 4. 再用 "\n\n"(段落) 5. 再用 "\n"(换行) 6. 再用 "。"(中文句号) 7. 再用 ";"(中文分号) 8. 再用 "."(英文句号) 9. 最后兜底 ""(按字符硬切)

本项目的 separators 设计:

separators=["\n#", "\n##", "\n###", "\n\n", "\n", "。", ";", ".", ""]
  • 前三个 "\n#" , "\n##" , "\n###" : 按 Markdown 标题切 ,优先保证章节完整性。课程笔记多是 Markdown,这样切能让一块内容属于同一章节
  • "\n\n" : 按段落切 ,非 Markdown 文档也能用
  • "\n" : 按行切 ,进一步细分
  • "。" ";" : 中文句号和分号 ,这是中文场景的关键改进(LangChain 默认 separators 没有中文标点)
  • "." :英文句号,兼容英文内容
  • "" :兜底,实在切不开就按字符硬切

3.5 中文场景的分隔符优化技巧

LangChain 默认的 separators:

separators=["\n\n", "\n", " ", ""] # 默认值,只有英文/通用分隔符

默认配置切中文时,会跳过 "。" ";" 这些中文句末标点,直接按空格(中文几乎不用空格)或字符硬切。结果就是一句话会被从中间切断,"RAG 是检索增强生成" 可能被切成 "RAG 是检索增" + "强生成"。

本项目在 "\n" 和 "." 之间插入 "。" 和 ";" ,这样中文句子会在句号 。 处优先切分,保证每块包含完整的句子,大幅提升检索精度。

separators=["\n#", "\n##", "\n###", "\n\n", "\n", "。", ";", ".", ""]

除此之外,如果文档里有大量列表项,可以加入 "-" 、 "1." 、 "2." 等列表分隔符;如果是代码文档,可以加入 "def " 、 "class " 、 "function " 等代码结构分隔符。

四、loader.py 完整代码解析

4.1 文档加载部分解析

现在把上面的知识串起来,逐段解析本项目的 loader.py 。

第一部分:导入与路径定位

from langchain_community.document_loaders import ( TextLoader, DirectoryLoader, Docx2txtLoader, PyPDFLoader, UnstructuredPowerPointLoader ) from langchain_text_splitters import RecursiveCharacterTextSplitter import os 获取项目根目录的绝对路径 BASE_DIR = os.path.join(os.path.dirname(file), "..")

设计要点 :

  • BASE_DIR 用 __file__ + os.path.dirname + ".." 拼出项目根目录的绝对路径。这样无论从哪个目录启动脚本,路径都不会错
  • 导入语句把用到的 5 个加载器一次性引入, RecursiveCharacterTextSplitter 来自 langchain_text_splitters (注意是 text_splitters 不是 community)

第二部分:5 个 DirectoryLoader 的设计

# 1. Markdown 加载器 md_loader = DirectoryLoader( path=os.path.join(BASE_DIR, "date/notes/"), glob="**/*.md", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"} ) 2. Word 加载器 docx_loader = DirectoryLoader( path=os.path.join(BASE_DIR, "date/notes/"), glob="**/*.docx", loader_cls=Docx2txtLoader, ) 3. PDF 加载器 pdf_loader = DirectoryLoader( path=os.path.join(BASE_DIR, "date/slides"), glob="**/*.pdf", loader_cls=PyPDFLoader, ) 4. PPT 加载器 pptx_loader = DirectoryLoader( path=os.path.join(BASE_DIR, "date/slides"), glob="**/*.pptx", loader_cls=UnstructuredPowerPointLoader, ) 5. 纯文本加载器 txt_loader = DirectoryLoader( path=os.path.join(BASE_DIR, "date/notes/"), glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"} )

设计要点 :

  • 按文件类型分别建加载器,而不是用一个加载器处理所有格式。因为每种格式的底层解析库不同, loader_cls 必须指定对应的类
  • 笔记类(md/docx/txt)统一从 date/notes/ 加载,课件类(pdf/pptx)从 date/slides/ 加载,目录划分清晰
  • glob="**/*" 的 ** 保证递归扫描子目录
  • 只有 TextLoader 传了 encoding="utf-8" ,因为 Docx2txtLoader 和 PyPDFLoader 内部已处理编码

第三部分:合并加载

all_docs = md_loader.load() + docx_loader.load() + pdf_loader.load() + pptx_loader.load() + txt_loader.load()

设计要点 :用 + 把 5 个列表拼接成一个 List[Document] 。此时 all_docs 里每个 Document 是一个文件(PDF 是一页)的完整内容。

4.2 文档分块部分解析

splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=100, separators=["\n#", "\n##", "\n###", "\n\n", "\n", "。", ";", ".", ""] ) chunks = splitter.split_documents(all_docs)

逐行解读 :

1. chunk_size=800 :每块最大 800 字符。这个值是针对中文课程笔记的经验值,约 200-300 tokens,能容纳 2-3 个完整段落

2. chunk_overlap=100 :相邻块重叠 100 字符,约为 chunk_size 的 12.5%。保证跨块的关键信息不丢失

3. separators 列表 :分两层理解

  • 结构层 : "\n#" , "\n##" , "\n###" 按 Markdown 标题切,保证章节完整性
  • 段落层 : "\n\n" , "\n" 按段落和行切
  • 句子层 : "。" , ";" , "." 按中英文句末标点切,保证句子完整
  • 兜底层 : "" 按字符硬切,确保一定能切到 chunk_size 以下

4. split_documents(all_docs) :注意调用的是 split_documents 而不是 split_text 。前者接收 List[Document] , 会保留每个 Document 的 metadata (source、page 等),切分后的每个小块都继承父文档的元数据。这对 RAG 的来源引用至关重要

执行完后, chunks 是一个 List[Document] ,每个元素是一个约 800 字符的文本块,带着来源元数据。这个 chunks 会被 vectorstore.py#L11 导入,用于构建向量库。

4.3 模块化设计解析

设计一:模块级代码,导入即执行

# 在 vectorstore.py 中 from src.loader import chunks # 这一行会触发 loader.py 的全部加载和分块逻辑

all_docs = ... 和 chunks = ... 是模块级代码(不在函数里),导入 loader.py 时立即执行。

优点:下游模块(vectorstore、rag_chain)只需一行 import 就能拿到处理好的 chunks,不用关心加载细节, 关注点分离 做得很好。

缺点:加载大量文档时启动慢。本项目文件少,影响可忽略;生产环境若有上千文件,应改为懒加载或缓存机制。

设计二:全局 chunks 导出模式

chunks 作为模块级变量,天然是单例。整个项目共享同一份分块结果,避免重复加载。配合 vectorstore.py 的"已存在则加载、不存在则构建"逻辑,首次构建后向量库会持久化,后续启动连加载分块都跳过了。

设计三:绝对路径定位

BASE_DIR = os.path.join(os.path.dirname(__file__), "..")

这是 Python 定位项目资源的标准写法。 __file__ 是当前文件的路径, os.path.dirname 取所在目录( src/ ), ".." 回到上一级(项目根目录)。无论从哪里执行 python -m src.app ,路径都正确。

五、实战踩坑与优化经验

5.1 中文编码坑

现象 :加载 .md 或 .txt 文件时,中文变成乱码,或者直接抛 UnicodeDecodeError 。

原因 :Windows 系统默认编码是 GBK,而 Markdown/文本文件通常是 UTF-8 编码。TextLoader 不传 encoding 参数时,会用系统默认编码读取。

错误写法:

loader = TextLoader("笔记.md") # Windows 下大概率乱码

正确写法:

loader = TextLoader("笔记.md", encoding="utf-8") # 显式指定

5.2 相对路径坑

现象 :在项目根目录运行 python src/app.py 正常,但在 src/ 目录运行 python app.py 就报 FileNotFoundError 。

原因 :相对路径(如 "date/notes/" )是相对于 当前工作目录 (CWD)解析的,不是相对于脚本文件。CWD 取决于你在哪里执行命令,所以会飘。

正确写法:

loader = DirectoryLoader(path="date/notes/", ...) # CWD 一变就找不到

错误写法:

loader = DirectoryLoader(path="date/notes/", ...) # CWD 一变就找不到

5.3 chunk_size 调参经验

现象 :RAG 回答不准,要么答非所问,要么信息不全。

排查思路 :chunk_size 直接影响检索精度,建议用同一组测试问题对比不同值的效果:

chunk_size检索表现生成表现适用场景
200召回多但碎片化,语义不完整上下文零散,模型难综合代码片段、QA 对
800(本项目)平衡,单块含 2-3 段上下文连贯,模型好理解通用、课程笔记
1500召回少但单块信息密集上下文长,可能稀释重点长篇说明文

没有万能值, 必须用评估集对比 。建议准备 10-20 个典型问题,对比不同 chunk_size 下的回答质量。

5.4 PDF 页眉页脚污染问题

现象 :检索到的上下文里混入"第 3 页 / 共 20 页"等页眉页脚,干扰模型理解。

原因 : PyPDFLoader 会把页眉页脚也作为正文提取,这些内容向量化后会污染语义。

解决方案 (本项目未做,可作为优化方向):

  • 加载后用正则清洗 metadata 和 page_content
  • 或换用 UnstructuredPDFLoader ,它能区分页眉页脚
  • 或用 PDF 预处理库(如 pdfplumber)先清洗再加载
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/10 10:33:40

从零构建社交趋势研究智能体:基于Dify的工程实践指南

在实际 AI 应用开发领域,构建一个能够理解、分析并响应复杂社会现象的智能体,正从实验室概念走向工程实践。NoimosAI 发布的 Social Agent 社交趋势研究智能体,正是这一趋势下的一个具体案例。它并非一个简单的聊天机器人,而是旨在…

作者头像 李华
网站建设 2026/8/10 10:27:08

OpenClaw开源AI智能体框架从爆火到遇冷的技术复盘与启示

1. 从爆火到沉寂:OpenClaw的“过山车”之旅最近在几个技术社区和AI开发者群里,一个话题被反复提及,但语气已经从几个月前的兴奋变成了如今的唏嘘:“OpenClaw好像彻底凉了。” 作为一个从它刚开源就上手折腾,并且一度在…

作者头像 李华
网站建设 2026/8/10 10:25:55

Shieldstral 1.0 3B:轻量高效的多模态大模型安全过滤实战指南

在部署和微调大语言模型时,内容安全过滤一直是个棘手的问题。传统的安全分类器要么效果不佳,要么体积庞大、计算成本高昂,难以在资源受限的环境下部署。最近,Mistral AI 开源了 Shieldstral 1.0 3B ,一个仅30亿参数的…

作者头像 李华
网站建设 2026/8/10 10:25:38

RAG优化10个实战技巧,从50%到90%准确率的提升路径

摘要:RAG系统10个优化技巧实战,从查询改写、混合检索、重排序到上下文压缩,将检索准确率从50%提升到90%,每个技巧附代码示例与效果对比 RAG优化10个实战技巧,从50%到90%准确率的提升路径 做RAG系统,从能跑到好用,中间有很多优化工作。 很多人卡在"能跑但效果一般…

作者头像 李华
网站建设 2026/8/10 10:23:54

Windows Sysinternals工具集:系统管理与安全分析实战指南

1. Windows Sysinternals 工具集概述Windows Sysinternals 是微软官方提供的一套系统工具集合,由 Mark Russinovich 和 Bryce Cogswell 在1996年创立,后被微软收购并持续维护更新。这套工具集堪称Windows系统管理员的"瑞士军刀",包…

作者头像 李华
网站建设 2026/8/10 10:22:11

给 AI Agent 装一台虚拟电脑:Cloudflare Computer 实战解析

当你的 Agent 不再只是"调用 API",而是能真正跑命令、写文件、编译代码——这就是 Cloudflare Computer 在做的事。 读完本文你将了解:操作步骤 | 技术原理 | 架构设计 | 适用场景🎯 这个项目解决什么问题? AI Agent 开…

作者头像 李华