ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RAG 数据导入与解析:从 txt 和 Markdown 到向量库的完整链路

RAG 数据导入与解析:从 txt 和 Markdown 到向量库的完整链路 RAG 系统落地时最容易被低估的环节不是向量检索也不是大模型选型而是数据导入与解析。很多人一上来就调 embedding 模型、搭向量库结果发现检索出来的内容驴唇不对马嘴回头一查原始文档在解析阶段就已经碎成了渣。我做过好几个 RAG 项目踩过最多的坑几乎都集中在文档加载和结构化处理这一段。这篇就从最基础的 txt 和 Markdown 入手把通用文本和结构化文本的导入解析逻辑讲透后面再延伸到 PDF、Word、HTML 等格式就有了参照系。1. 为什么 txt 和 Markdown 是 RAG 数据管道的起点1.1 纯文本是检验解析链路的最小闭环很多人觉得 txt 太简单不值得花时间。但我的经验恰恰相反txt 是验证整条 RAG 数据管道是否通畅的最佳试验品。它没有复杂的格式嵌套没有二进制编码问题没有表格和图片的干扰如果连 txt 导入后检索效果都不好那问题一定出在切分策略或 embedding 环节而不是解析器本身。从工程角度看先用 txt 跑通加载 → 切分 → 向量化 → 检索 → 生成这条完整链路相当于给自己建立了一个基线。后面接入 Markdown、PDF 时任何效果波动都可以和这个基线对比快速定位是解析引入的噪声还是检索本身的瓶颈。LangChain 的Document对象是整个数据管道的基本单元它只有两个核心字段page_content存放文本内容metadata存放来源、页码、标题等附加信息。理解这个结构非常关键因为后续所有的切分、过滤、检索都围绕它展开。txt 加载器返回的就是最纯粹的Document列表没有多余的元数据干扰适合用来观察管道的基础行为。1.2 Markdown 是结构化文本的天然试验场Markdown 在 RAG 场景里的地位被严重低估了。它用极轻量的语法表达了标题层级、列表、代码块、表格、引用等结构信息而这些结构恰恰是切分策略最需要的语义边界信号。举个例子一篇技术文档用##分隔不同主题用###分隔子主题。如果切分器能识别这些标题层级就可以按照语义单元切分而不是机械地按字符数截断。按字符数截断最典型的翻车场景是一个完整的代码示例被从中间切开前半段在 chunk A后半段在 chunk B检索时只召回一半大模型拿到的上下文残缺不全生成的答案自然错漏百出。Markdown 的另一个优势是它的纯文本本质。它不像 PDF 那样需要复杂的布局分析也不像 Word 那样依赖二进制解析库用 Python 直接读取就是完整内容。这意味着解析环节引入的噪声极低可以把精力集中在切分策略的优化上。所以我的建议是txt 用来验证管道通畅性Markdown 用来打磨切分策略这两个跑通了再去碰 PDF 和 Word 会从容很多。1.3 通用文本与结构化文本的边界在哪里这里需要厘清一个概念什么是通用文本什么是结构化文本。通用文本指的是没有显式层级标记的连续文本比如小说、新闻稿、会议记录段落之间只有换行没有标题、列表这些语义标记。结构化文本则带有明确的组织标记比如 Markdown 的标题符号、HTML 的标签、JSON 的键值对。这个区分直接决定了切分策略的选择。通用文本只能依赖段落、句子边界和字符数来切分而结构化文本可以利用其内在的层级结构做语义切分。很多 RAG 教程把这两类文本混在一起讲导致读者不知道什么时候该用RecursiveCharacterTextSplitter什么时候该用MarkdownHeaderTextSplitter。我的做法是先判断文本有没有可识别的结构标记有就用结构化切分器没有就退回通用切分器必要时两者组合使用。2. LangChain Document 与 Loader 的协作机制2.1 Document 对象的字段设计与元数据价值Document对象看起来简单但metadata字段的设计直接决定了后续检索的过滤能力和溯源能力。我在实际项目里会给每个Document至少塞进这几类元数据来源标识文件路径或 URL用于溯源和去重标题层级当前 chunk 所属的标题路径比如第三章 3.2 节 配置说明位置信息在原始文档中的字符偏移或页码时间戳文档的创建或修改时间用于时效性过滤这些元数据在检索阶段能发挥大作用。比如用户问的是最新版本的配置方法你就可以在检索时加一个时间过滤条件只召回最近半年的文档。又比如用户问的是某个具体章节的内容你可以用标题路径做精确匹配。没有元数据的Document就是一坨没有出处的文本检索效果和可维护性都会大打折扣。需要特别注意的是元数据的值必须是可序列化的基本类型字符串、数字、布尔值不能塞入复杂的嵌套对象。我见过有人把整个解析配置字典塞进 metadata结果在向量库写入时报序列化错误。正确的做法是把关键信息扁平化比如把{config: {model: gpt, temp: 0.7}}拆成config_model和config_temp两个独立字段。2.2 Loader 的职责边界只做加载不做切分这是新手最容易混淆的一点Loader 只负责把原始文件读成Document列表不负责切分。切分是 Splitter 的职责。LangChain 把这两个环节拆开是有道理的因为加载和切分的策略是正交的同一个 PDF 文件你可以用不同的切分策略处理同一种切分策略也可以应用到不同格式的文件上。以TextLoader为例它的核心参数只有几个from langchain_community.document_loaders import TextLoader loader TextLoader( file_path./docs/example.txt, encodingutf-8, autodetect_encodingTrue ) documents loader.load()encoding参数看似不起眼实则是中文场景下最常见的翻车点。Windows 系统默认用 GBK 编码保存 txt如果你不指定utf-8读出来的就是乱码。autodetect_encodingTrue可以让 LangChain 尝试自动检测编码但这个检测不是百分百准确尤其是短文本。我的建议是能明确指定编码就明确指定不要依赖自动检测。如果文档来源混杂可以在加载前用chardet库先探测一遍编码。load()和lazy_load()的区别也值得说一下。load()一次性把所有文档读进内存返回一个列表lazy_load()返回一个生成器逐个产出Document。处理大批量文件时lazy_load()能显著降低内存占用。我处理过一个包含上万个小文件的知识库用load()直接把内存打满换成lazy_load()后内存占用降到了原来的十分之一。2.3 目录级批量加载与文件过滤策略实际项目里很少只加载单个文件更多是加载整个目录。DirectoryLoader就是干这个的它支持用 glob 模式过滤文件类型from langchain_community.document_loaders import DirectoryLoader, TextLoader loader DirectoryLoader( path./knowledge_base, glob**/*.md, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, recursiveTrue, show_progressTrue, use_multithreadingTrue, max_concurrency4 ) documents loader.load()这里有几个参数值得展开说。glob**/*.md中的**表示递归匹配所有子目录如果只写*.md就只匹配当前目录。loader_kwargs用来给底层的TextLoader传参编码设置就是通过它传进去的。use_multithreadingTrue配合max_concurrency可以并行加载但要注意并行加载时如果底层 loader 不是线程安全的可能会出问题。TextLoader是线程安全的可以放心开多线程但某些依赖外部服务的 loader 就不一定了。还有一个坑DirectoryLoader默认遇到加载失败的文件会直接抛异常导致整个批量加载中断。生产环境里更稳妥的做法是设置silent_errorsTrue让加载失败的文件被跳过并记录日志而不是让整个任务崩掉。当然跳过之后要记得检查日志看看是哪些文件出了问题不能放任不管。3. 通用文本切分的参数计算与实测调优3.1 chunk_size 与 chunk_overlap 的取值逻辑切分参数没有万能值但有一套推导逻辑。chunk_size决定了每个 chunk 的最大字符数chunk_overlap决定了相邻 chunk 之间的重叠字符数。这两个参数直接影响检索粒度和上下文完整性。先看chunk_size。它主要受两个因素制约embedding 模型的最大输入长度和检索精度。大多数 embedding 模型支持 512 个 token 左右的输入换算成中文大约是 300 到 400 个汉字。如果chunk_size设得太大超出模型输入限制的部分会被截断等于白切设得太小一个完整的语义单元被切碎检索时召回的是残缺片段。我的经验值是中文文本 chunk_size 设在 300 到 500 字符之间英文文本设在 500 到 1000 字符之间。这个范围是在检索精度和上下文完整性之间取的平衡。具体到某个项目还要看文档的段落平均长度。如果文档段落普遍很短比如 FAQ 问答对chunk_size 可以设小一点让每个 chunk 正好容纳一个完整问答如果段落很长比如技术白皮书chunk_size 就要相应放大。再看chunk_overlap。它的作用是防止语义在切分边界处断裂。比如一句话正好被切在中间有了 overlap前后两个 chunk 都能包含这句话的完整内容。overlap 的常见取值是chunk_size的 10% 到 20%。设得太小起不到保护作用设得太大则会导致大量冗余内容被重复索引浪费存储空间还降低检索效率。我一般从 15% 起步然后根据实测效果微调。文本类型chunk_size 建议值chunk_overlap 建议值说明中文技术文档40060段落较长需要较大 chunk中文 FAQ20030问答对短小chunk 宜小英文技术文档800120英文 token 密度低于中文中英混合50080折中取值兼顾两种语言3.2 RecursiveCharacterTextSplitter 的分隔符优先级RecursiveCharacterTextSplitter是通用文本切分的首选它的核心思路是按分隔符优先级从高到低依次尝试切分直到每个 chunk 都小于 chunk_size。默认的分隔符列表是[\n\n, \n, , ]对应段落、行、空格、字符四个层级。这个优先级设计的逻辑是优先在语义边界处切分。段落边界\n\n是最强的语义边界其次是行边界\n再次是空格最后才是不管语义直接按字符切。中文场景下需要调整这个列表因为中文句子之间用句号、问号、感叹号分隔而不是空格from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap60, separators[\n\n, \n, 。, , , , , , ], length_functionlen, is_separator_regexFalse ) chunks splitter.split_documents(documents)把中文标点加入分隔符列表后切分器会优先在句子边界处断开而不是在句子中间硬切。这个改动对中文检索效果的提升非常明显我实测下来召回内容的可读性提升了一个档次。length_function参数默认是len也就是按字符数计算长度。如果你用的是按 token 计费的 embedding 模型可以换成 token 计数函数让 chunk_size 直接对应 token 数。但要注意token 计数需要加载 tokenizer会增加一点计算开销。3.3 切分效果的验证方法与常见问题切分完不能直接往向量库里灌必须先验证效果。我常用的验证方法有三种第一种是抽样目测。随机抽 10 到 20 个 chunk看看它们是否语义完整、有没有被从中间切断。这个方法最直接能发现大部分明显问题。第二种是边界检查。专门看那些正好在 chunk_size 附近被切开的 chunk检查切分点是否落在合理的位置。如果发现大量 chunk 在句子中间断开说明分隔符列表需要调整。第三种是检索回测。准备一批典型问题跑一遍检索看召回的 chunk 是否包含答案。这个方法最接近真实使用场景但需要先有向量库和检索链路。常见的切分问题有这么几类chunk 过短导致语义不完整通常是 chunk_size 设得太小或者分隔符过于激进chunk 过长导致检索精度下降通常是 chunk_size 设得太大大量重复内容通常是 chunk_overlap 设得太大。遇到这些问题按先调 chunk_size再调 overlap最后调分隔符的顺序排查基本都能解决。提示切分参数调优是个迭代过程不要指望一次调好。建议把每次调整的参数和对应的检索效果记录下来形成自己的参数经验库。4. Markdown 结构化切分的层级利用4.1 MarkdownHeaderTextSplitter 的工作原理MarkdownHeaderTextSplitter的思路和通用切分器完全不同它不按字符数切而是按 Markdown 的标题层级切。你告诉它哪些标题级别需要保留它就在这些标题处断开并把标题内容写入每个 chunk 的 metadata。from langchain.text_splitter import MarkdownHeaderTextSplitter headers_to_split_on [ (#, h1), (##, h2), (###, h3), ] markdown_splitter MarkdownHeaderTextSplitter( headers_to_split_onheaders_to_split_on, strip_headersFalse ) with open(./docs/guide.md, r, encodingutf-8) as f: md_text f.read() md_chunks markdown_splitter.split_text(md_text)headers_to_split_on是一个元组列表每个元组包含标题符号和对应的 metadata 键名。strip_headersFalse表示保留标题文本在 chunk 内容里设为True则会把标题从内容中移除、只保留在 metadata 里。我的建议是保留标题因为标题本身携带了重要的语义信息对 embedding 有正向帮助。切分后每个 chunk 的 metadata 会长这样{ h1: RAG 数据导入指南, h2: Markdown 结构化切分, h3: 标题层级利用 }这个 metadata 结构非常有用。检索时你可以根据标题路径做过滤比如只检索某个章节下的内容也可以在生成答案时把标题路径作为上下文提示给大模型帮助它理解内容的归属。4.2 标题层级与 chunk 粒度的映射关系MarkdownHeaderTextSplitter有一个需要注意的行为它只在标题处切分不控制 chunk 大小。如果某个##标题下的内容特别长切出来的 chunk 就会很大可能超出 embedding 模型的输入限制。解决这个问题需要两步走先用MarkdownHeaderTextSplitter按标题切分再用RecursiveCharacterTextSplitter对过大的 chunk 做二次切分。LangChain 官方推荐的做法是from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap60, separators[\n\n, \n, 。, , ] ) final_chunks text_splitter.split_documents(md_chunks)这样既保留了标题层级的语义边界又保证了每个 chunk 的大小可控。二次切分时md_chunks里每个 chunk 的 metadata 会被继承到切分后的子 chunk 上标题路径信息不会丢失。标题层级和 chunk 粒度的映射关系可以这样理解#一级标题通常对应文档主题粒度太粗一般不用来切分##二级标题对应主要章节是切分的主力层级###三级标题对应子章节适合内容较细的文档。如果文档层级很深切到###就够了再往下切会导致 chunk 过碎。4.3 代码块、表格、引用块的特殊处理Markdown 里的代码块、表格、引用块是切分时的雷区。这些结构对完整性要求很高一旦被切开就失去意义。代码块用三个反引号包裹RecursiveCharacterTextSplitter默认的分隔符列表里没有反引号所以它可能会在代码块中间断开。解决办法是在分隔符列表里加入代码块标记或者用正则表达式先把代码块提取出来单独处理。我通常的做法是在切分前用正则把代码块替换成占位符切分完再把代码块填回去这样能保证代码块的完整性。表格的问题类似。Markdown 表格用|分隔单元格用---分隔表头和表体。如果表格被切开检索出来的就是残缺的表格大模型无法正确理解。对于表格我建议把整个表格作为一个独立的 chunk不要切分。如果表格特别大可以按行切分但每一行都要带上表头信息否则数据就失去了列的含义。引用块用标记通常是对正文的补充说明。引用块被切开的影响相对小一些但最好也保持完整。可以在分隔符列表里加入\n来识别引用块边界。import re def protect_code_blocks(text): code_blocks [] pattern r[\s\S]*? def replace(match): code_blocks.append(match.group(0)) return f__CODE_BLOCK_{len(code_blocks)-1}__ protected_text re.sub(pattern, replace, text) return protected_text, code_blocks def restore_code_blocks(text, code_blocks): for i, block in enumerate(code_blocks): text text.replace(f__CODE_BLOCK_{i}__, block) return text这段代码展示了代码块保护的基本思路。实际使用时占位符要足够独特避免和正文内容冲突。切分完成后再把占位符替换回原始代码块。5. 从加载到入库的完整链路实操5.1 环境准备与依赖安装先把环境搭起来。Python 版本建议 3.9 以上LangChain 的版本迭代很快不同版本 API 有差异建议锁定版本pip install langchain0.1.0 pip install langchain-community0.0.10 pip install chromadb0.4.22 pip install sentence-transformers2.2.2这里选了 Chroma 作为向量库因为它轻量、本地可跑、和 LangChain 集成度高适合做原型验证。embedding 模型用sentence-transformers的本地模型不依赖外部 API离线也能跑。如果追求更好的中文效果可以换成BAAI/bge-large-zh-v1.5这类中文优化的模型。注意LangChain 的包结构在 0.1 版本后做了拆分很多 loader 和 splitter 从主包移到了langchain-community。如果导入报错先检查是不是包路径变了。5.2 加载、切分、向量化的串联代码把前面讲的环节串起来形成一个完整的处理函数from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import ( RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter ) from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma def build_knowledge_base(source_dir, persist_dir): # 第一步加载所有 Markdown 文件 loader DirectoryLoader( pathsource_dir, glob**/*.md, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, recursiveTrue, silent_errorsTrue, show_progressTrue ) raw_docs loader.load() print(f加载了 {len(raw_docs)} 个文档) # 第二步按标题层级切分 headers_to_split_on [ (#, h1), (##, h2), (###, h3), ] md_splitter MarkdownHeaderTextSplitter( headers_to_split_onheaders_to_split_on, strip_headersFalse ) md_chunks [] for doc in raw_docs: chunks md_splitter.split_text(doc.page_content) for chunk in chunks: chunk.metadata[source] doc.metadata.get(source, unknown) md_chunks.extend(chunks) print(f标题切分后得到 {len(md_chunks)} 个片段) # 第三步二次切分控制大小 text_splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap60, separators[\n\n, \n, 。, , , , , , ] ) final_chunks text_splitter.split_documents(md_chunks) print(f二次切分后得到 {len(final_chunks)} 个片段) # 第四步向量化并入库 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True} ) vectorstore Chroma.from_documents( documentsfinal_chunks, embeddingembeddings, persist_directorypersist_dir ) vectorstore.persist() print(f向量库已持久化到 {persist_dir}) return vectorstore vectorstore build_knowledge_base(./knowledge_base, ./chroma_db)这段代码把加载、切分、向量化、入库四个环节串成了一条流水线。每一步都有打印输出方便观察中间结果。实际项目里可以把这些打印换成日志记录便于排查问题。5.3 入库后的检索验证与效果评估向量库建好后必须做检索验证。准备几个典型问题看召回的 chunk 是否包含答案def test_retrieval(vectorstore, query, k3): results vectorstore.similarity_search_with_score(query, kk) for i, (doc, score) in enumerate(results): print(f--- 结果 {i1} (相似度: {score:.4f}) ---) print(f来源: {doc.metadata.get(source, unknown)}) print(f标题路径: {doc.metadata.get(h1, )} {doc.metadata.get(h2, )} {doc.metadata.get(h3, )}) print(f内容: {doc.page_content[:200]}...) print() test_retrieval(vectorstore, Markdown 代码块怎么处理)评估检索效果时我关注三个指标召回率相关 chunk 有没有被召回、准确率召回的 chunk 有多少是相关的、排序质量最相关的 chunk 是不是排在前面。如果召回率低说明切分或 embedding 有问题如果准确率低说明 chunk 里混入了太多无关内容如果排序质量差说明 embedding 模型对这类语义的区分度不够。相似度分数也值得关注。Chroma 默认用余弦距离分数越低表示越相似。如果所有结果的分数都很接近说明 embedding 模型没能很好地区分这些内容可能需要换模型或者调整切分粒度。6. 实操中踩过的坑与经验沉淀6.1 编码问题导致的乱码与内容丢失编码问题是中文 RAG 项目里最高频的坑。我遇到过一次一批从 Windows 系统导出的 txt 文件用TextLoader默认参数加载后中文全部变成乱码。原因是这些文件用 GBK 编码保存而TextLoader默认用 UTF-8 读取。解决办法有两种一是加载时显式指定encodinggbk二是用chardet先探测编码再加载。第二种更通用import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(10000) result chardet.detect(raw) return result[encoding] encoding detect_encoding(./docs/example.txt) loader TextLoader(./docs/example.txt, encodingencoding)chardet的探测不是百分百准确尤其是短文本。所以我的做法是探测结果作为参考如果探测置信度低于 0.8就手动检查一下。另外有些文件可能混合了多种编码这种情况只能人工处理没有通用解法。还有一个隐蔽的坑BOM 头。有些 UTF-8 文件开头带有 BOM 标记读取后会在内容最前面多出一个不可见字符。这个字符会干扰 embedding导致检索效果下降。解决办法是用utf-8-sig编码读取Python 会自动去掉 BOM 头。6.2 切分边界处的语义断裂修复语义断裂是切分环节最头疼的问题。我遇到过一个典型案例一份 API 文档里某个接口的参数说明被切成了两个 chunk第一个 chunk 只有参数名第二个 chunk 只有参数说明检索时只召回其中一个大模型拿到的信息不完整生成的答案就漏了参数。修复这类问题我总结了几个手段。第一个是增大 chunk_overlap让相邻 chunk 有更多重叠内容降低断裂概率。第二个是优化分隔符列表把文档中常见的语义边界符号加进去。第三个是在 chunk 内容前拼接标题路径让每个 chunk 都带上上下文信息def enrich_chunk_with_context(chunk): h1 chunk.metadata.get(h1, ) h2 chunk.metadata.get(h2, ) h3 chunk.metadata.get(h3, ) context_parts [p for p in [h1, h2, h3] if p] if context_parts: context .join(context_parts) chunk.page_content f[{context}]\n{chunk.page_content} return chunk final_chunks [enrich_chunk_with_context(c) for c in final_chunks]这个做法相当于给每个 chunk 加了一个上下文标签embedding 时会把这个标签一起编码进去检索时就能利用标题信息做语义匹配。实测下来这个改动对检索准确率的提升很明显尤其是当用户的问题涉及具体章节时。6.3 大批量文件处理时的内存与性能优化处理大批量文件时内存和性能是两个绕不开的问题。我处理过一个包含 5 万多个 Markdown 文件的知识库一开始用load()一次性加载内存直接飙到 8GB 以上机器差点扛不住。优化手段有这么几个。第一用lazy_load()替代load()逐个处理文件内存占用降到几百 MB。第二分批向量化每处理 1000 个 chunk 就写入一次向量库而不是全部处理完再一次性写入。第三用多进程而非多线程做 CPU 密集型的切分和向量化Python 的 GIL 会限制多线程在 CPU 密集型任务上的表现。def batch_process(source_dir, batch_size1000): loader DirectoryLoader( pathsource_dir, glob**/*.md, loader_clsTextLoader, loader_kwargs{encoding: utf-8}, recursiveTrue, silent_errorsTrue ) batch [] for doc in loader.lazy_load(): chunks process_single_doc(doc) batch.extend(chunks) if len(batch) batch_size: yield batch batch [] if batch: yield batch这个生成器模式让内存占用始终保持在可控范围内。配合向量库的增量写入整个处理过程就变得很稳。提示向量化是计算密集型任务如果机器有 GPU把 embedding 模型放到 GPU 上能提速十倍以上。没有 GPU 的话考虑用更小的模型或者降低向量维度。6.4 元数据丢失与溯源断链的预防元数据丢失是另一个高频问题。最常见的情况是加载时 metadata 里有source字段经过几轮切分后source字段不见了。原因是某些切分器在创建新Document时没有继承原始 metadata。预防这个问题我的做法是在每个处理环节后都检查一遍 metadata 完整性def check_metadata_integrity(chunks, required_keys): missing [] for i, chunk in enumerate(chunks): for key in required_keys: if key not in chunk.metadata: missing.append((i, key)) if missing: print(f发现 {len(missing)} 处元数据缺失) for idx, key in missing[:10]: print(f chunk {idx} 缺少 {key}) else: print(元数据完整性检查通过) return missing check_metadata_integrity(final_chunks, [source, h1])如果发现缺失就在切分后手动补上。比如MarkdownHeaderTextSplitter切分后原始source字段会丢失需要手动从原始Document复制过来。这个补丁看起来麻烦但能保证溯源链路不断后期排查问题时能快速定位到原始文件。溯源能力在 RAG 系统里非常重要。用户问了一个问题系统给出了答案如果用户追问这个答案是从哪来的你得能准确指出源文件和具体位置。没有完整的元数据这个功能就无从谈起。7. 从 txt 和 Markdown 延伸到其他格式的思路txt 和 Markdown 跑通之后接入其他格式就是替换 Loader 和调整切分策略的事。PDF 用PyPDFLoader或PDFPlumberLoaderWord 用Docx2txtLoaderHTML 用UnstructuredHTMLLoader。核心逻辑不变加载成Document按结构切分向量化入库。不同格式的特殊处理点在于PDF 需要处理扫描件 OCR 和表格提取Word 需要处理样式和批注HTML 需要处理标签嵌套和导航栏噪声。这些内容展开讲篇幅太长但只要你把 txt 和 Markdown 这条基础链路吃透了理解这些格式的处理逻辑会快很多。我在实际项目里的体会是数据导入与解析环节投入的时间和最终 RAG 系统的效果成正比。很多人急着调模型、换向量库却在这个基础环节偷工减料结果就是上层怎么调都调不好。把 txt 和 Markdown 这两个最简单的格式做到极致建立起可靠的切分和元数据管理习惯后面的复杂格式才有稳固的地基。
返回列表