)
前言本文系统介绍了 RAG检索增强生成技术的核心概念、实现流程与实战经验。首先阐述了 RAG 的定义、价值及其与微调的对比并梳理了从 Naive RAG 到 Agentic RAG 的演进路径。随后详细讲解了 LangChain 中文档加载Document 对象、各类加载器及 DirectoryLoader 批量加载与文档分块分块原因、策略、核心参数及中文优化技巧两大核心环节。最后通过 loader.py 完整代码解析和实战踩坑经验提供了从理论到实践的完整指南帮助读者构建高效、可靠的 RAG 应用。一、RAG介绍1.1 什么是RAGRAG(检索增强生成)是一种结合检索和生成两种方法的技术。它通过先检索相关的文档用检索出来的信息对提示词增强再使用大模型生成答案。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 PromptRAG 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内置最简单读取纯文本内容。 注意必须指定 encodingWord (.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, encodingutf-8) docs loader.load() print(docs[0].page_content) # 整个文件内容作为一个字符串 print(docs[0].metadata) # {source: 笔记.txt}2. Word 加载器Docx2txtLoader和UnstructuredWordDocumentLoaderDocx2txtLoader轻量版依赖 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增强版功能更强能识别段落、标题、列表等结构支持 modeelements 。特点依赖 unstructured python-docxmodeelements 能区分标题、正文、列表metadata 带 category适合结构复杂的 Word 文档、需要保留层次的场景基本用法from langchain_community.document_loaders import UnstructuredWordDocumentLoader 模式一合并为一个 Document默认 loader UnstructuredWordDocumentLoader(课程笔记.docx) docs loader.load() 模式二按元素切分标题、段落、列表项各自独立 loader UnstructuredWordDocumentLoader( 课程笔记.docx, modeelements # 每个元素一个 Document ) docs loader.load() docs[0].metadata 会包含 {category: Title, ...} 等结构信息3. PDF 加载器PyPDFLoader和UnstructuredPDFLoaderPyPDFLoader最常用的 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。特点strategyhi_res 高精度模式能识别表格、图片但需要额外系统依赖modeelements 按元素切分区分 Title/Table/NarrativeText 等依赖较重安装复杂需 poppler、tesseract 等适合学术论文、复杂版面 PDF、含表格的PDF基本用法from langchain_community.document_loaders import UnstructuredPDFLoader 模式一合并为一个 Document loader UnstructuredPDFLoader(复杂课件.pdf) docs loader.load() 模式二按元素切分标题、表格、图片各自独立 loader UnstructuredPDFLoader( 复杂课件.pdf, modeelements, strategyhi_res # 高精度模式能识别表格 ) docs loader.load() docs[0].metadata 包含 {category: Table, page_number: 1, ...}4. PPT 加载器UnstructuredPowerPointLoader特点依赖 unstructured python-pptx可能会把 演讲者备注 也读进来需确认是否符合预期表格内容会被扁平化为文本结构信息部分丢失modeelements 对 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, modeelements ) 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 }两种模式对比modesingle 默认,输出时整个 PPT 一个 Document适用于内容少、结构简单的场景modeelements 输出时每个元素一个 Document 适用于需要精确定位标题/正文/表格的场景5. Markdown 加载器 UnstructuredMarkdownLoaderMarkdown 是 RAG 项目中最常见的格式。虽然可以用 TextLoader 读 .md 但专用加载器能更好地保留结构。基本用法from langchain_community.document_loaders import UnstructuredMarkdownLoader 基本用法 loader UnstructuredMarkdownLoader(example.md) docs loader.load() 进阶按元素切分 (modeelements) 这会将标题、段落、列表项分成独立的 Document loader UnstructuredMarkdownLoader(example.md, modeelements) 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_pathfaq.csv, csv_args{ delimiter: ,, quotechar: , }, # 自定义内容格式 page_content_columnquestion, # 或者自定义处理 # 更好的方式自定义将多列合并 ) 更标准的写法是使用 pandas 处理后加载或在 metadata 中保留其他列JSONLoader (适合 API 数据)特点 解析 JSON 文件支持提取特定字段。用法 需要配合 jq 表达式来定义“什么是文档内容”。基本用法from langchain_community.document_loaders import JSONLoader 假设 data.json 结构为 [{title: ..., content: ...}, ...] loader JSONLoader( file_pathdata.json, jq_schema.[].content, # 提取数组中每个元素的 content 字段作为文本 text_contentTrue, # metadata 指定 title 字段 metadata_funclambda record, metadata: {**metadata, title: record.get(title)} ) docs loader.load()7. 表格加载器 UnstructuredExcelLoaderfrom langchain_community.document_loaders import UnstructuredExcelLoader 加载 Excel默认 modeelements每个单元格是一个 element loader UnstructuredExcelLoader(report.xlsx) docs loader.load() 或者 modesingle将整个表合并为一个文本 loader UnstructuredExcelLoader(report.xlsx, modesingle)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_clsTextLoader, # 3. 用哪个加载器读 loader_kwargs{ # 4. 传给加载器的参数 encoding: utf-8 }, show_progressTrue, # 5. 显示进度条可选 use_multithreadingTrue, # 6. 多线程加速可选 silent_errorsTrue # 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 数切需精确控制 tokenMarkdownHeaderTextSplitter按 Markdown 标题切保留层级纯 Markdown 文档HTMLHeaderTextSplitter按 HTML 标签切网页内容SemanticChunker按 Embedding 相似度动态切高质量需求RecursiveJsonSplitter递归切 JSON结构化数据选型建议不确定用什么 → RecursiveCharacterTextSplitter 通用首选纯 Markdown 文档 → MarkdownHeaderTextSplitter RecursiveCharacterTextSplitter 组合追求极致质量 → SemanticChunker 慢但语义完整代码文档 → 自定义分隔符按函数/类切3.4 三大核心参数详解本项目的分块代码splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap100, 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 默认的 separatorsseparators[\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( pathos.path.join(BASE_DIR, date/notes/), glob**/*.md, loader_clsTextLoader, loader_kwargs{encoding: utf-8} ) 2. Word 加载器 docx_loader DirectoryLoader( pathos.path.join(BASE_DIR, date/notes/), glob**/*.docx, loader_clsDocx2txtLoader, ) 3. PDF 加载器 pdf_loader DirectoryLoader( pathos.path.join(BASE_DIR, date/slides), glob**/*.pdf, loader_clsPyPDFLoader, ) 4. PPT 加载器 pptx_loader DirectoryLoader( pathos.path.join(BASE_DIR, date/slides), glob**/*.pptx, loader_clsUnstructuredPowerPointLoader, ) 5. 纯文本加载器 txt_loader DirectoryLoader( pathos.path.join(BASE_DIR, date/notes/), glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8} )设计要点 按文件类型分别建加载器而不是用一个加载器处理所有格式。因为每种格式的底层解析库不同 loader_cls 必须指定对应的类笔记类md/docx/txt统一从 date/notes/ 加载课件类pdf/pptx从 date/slides/ 加载目录划分清晰glob**/* 的 ** 保证递归扫描子目录只有 TextLoader 传了 encodingutf-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_size800, chunk_overlap100, separators[\n#, \n##, \n###, \n\n, \n, 。, , ., ] ) chunks splitter.split_documents(all_docs)逐行解读 1. chunk_size800 每块最大 800 字符。这个值是针对中文课程笔记的经验值约 200-300 tokens能容纳 2-3 个完整段落2. chunk_overlap100 相邻块重叠 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, encodingutf-8) # 显式指定5.2 相对路径坑现象 在项目根目录运行 python src/app.py 正常但在 src/ 目录运行 python app.py 就报 FileNotFoundError 。原因 相对路径如 date/notes/ 是相对于 当前工作目录 CWD解析的不是相对于脚本文件。CWD 取决于你在哪里执行命令所以会飘。正确写法loader DirectoryLoader(pathdate/notes/, ...) # CWD 一变就找不到错误写法loader DirectoryLoader(pathdate/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先清洗再加载