ARTICLE DETAIL

资讯详情

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

RAG数据导入解析实战:LangChain Document Loader处理txt与Markdown

RAG数据导入解析实战:LangChain Document Loader处理txt与Markdown RAG 系统里最容易被低估、也最容易在后期把整个项目拖垮的环节不是向量检索也不是重排序而是数据导入与解析。我见过太多团队在 demo 阶段用几个干净的 txt 文件跑通了全流程信心满满地接入真实业务文档结果检索质量断崖式下跌——答案答非所问、关键信息检索不到、模型一本正经地胡说八道。追根溯源问题几乎都出在最上游文档没有被正确地解析成结构化的、语义完整的文本块。这篇内容聚焦 RAG 数据导入链路的第一段路从最朴素的 txt 纯文本到带层级结构的 Markdown怎么用 LangChain 的 Document Loader 体系把它们干净地读进来怎么理解 Document 对象这个贯穿全流程的核心数据结构以及怎么在解析阶段就把后续切分、嵌入、检索的坑提前填掉。如果你正在搭 RAG 知识库、正在被文档读进来就乱码或者标题和正文被切散这类问题折磨或者刚接触 LangChain 想搞清楚 Loader 到底该怎么选这篇可以当作一份可以直接抄作业的实操记录来看。1. 为什么数据导入决定了 RAG 的上限1.1 检索质量的天花板在解析阶段就被定死了很多人对 RAG 的理解停留在把文档切块、向量化、存进向量库、检索时算相似度这条流水线上觉得只要嵌入模型够强、向量库够快效果就不会差。这个认知有个致命漏洞检索的本质是拿问题去匹配文本块如果文本块本身就是残缺的、语义断裂的、格式混乱的再强的嵌入模型也救不回来。举个我实际踩过的例子。早期做一个内部技术文档问答文档是 Word 导出的 txt里面标题和正文之间没有空行长这样第三章 接口鉴权机制 所有对外接口必须携带签名参数签名算法采用 HMAC-SHA256...切分器按固定字符数切正好切在签名算法采用 HMAC-和SHA256之间。检索时用户问签名用什么算法这个块因为被腰斩向量表示已经偏离了原意排名掉到十几名开外模型自然答不出来。后来我把解析阶段改成先识别标题行、再按标题聚合正文同样的问题立刻能命中。这就是解析阶段的价值它决定了每个文本块是不是一个语义自洽的单元。切分器再智能也只能在解析器给出的结构基础上做文章。解析器如果把标题、正文、表格、代码全糊成一坨切分器就是巧妇难为无米之炊。1.2 不同格式的文档解析难度差了一个数量级RAG 项目里常见的文档格式按解析难度大致可以排个序格式结构信息解析难度主要坑点txt几乎无低编码、换行符、无结构Markdown强低语法变体、嵌套结构HTML强中标签噪声、正文提取PDF中高双栏、扫描件、表格Word中中样式丢失、嵌入对象Excel强中多 sheet、合并单元格txt 和 Markdown 是这条链路的起点也是最值得先啃透的。原因很简单它们没有二进制格式的干扰你能把全部精力放在结构识别和语义聚合上。把这两种格式的解析逻辑吃透后面处理 PDF、Word 时你脑子里已经有一套清晰的我要从这份文档里提取什么结构的框架而不是被各种解析库的 API 牵着走。1.3 LangChain 的 Loader 体系解决了什么在没有统一框架之前读 txt 用open()读 Markdown 可能用markdown库读 PDF 用PyPDF2每种格式返回的数据结构都不一样下游切分器要写一堆 if-else 来适配。LangChain 的 Document Loader 体系做了一件很关键的事把所有格式的文档统一抽象成Document对象。这个统一带来的好处是连锁的。切分器只需要认Document嵌入模型只需要认Document向量库写入也只需要认Document。你换一种文档格式上层的切分、嵌入、检索代码一行都不用改。这就是为什么我建议所有 RAG 项目都从 LangChain 的 Loader 体系入手哪怕你后面不用 LangChain 的其他组件光是这个统一抽象就值回票价。2. Document 对象贯穿 RAG 全流程的核心数据结构2.1 Document 到底装了什么在动手写 Loader 之前必须先把Document这个对象吃透因为它是后面所有操作的载体。一个Document对象本质上就两个字段from langchain_core.documents import Document doc Document( page_content这里是文档的正文内容, metadata{ source: docs/guide.md, title: 接口鉴权机制, chapter: 第三章, line_start: 42, } )page_content是字符串装的是这段文本的实际内容后面切分、嵌入、检索比对的全是它。metadata是字典装的是这段文本的身份信息——它来自哪个文件、属于哪一章、在第几行、是什么类型的内容。很多人只重视page_content把metadata当成可有可无的附属品这是大错特错。metadata 在 RAG 里承担着两个关键职责过滤和溯源。过滤指的是检索时可以按 metadata 做前置筛选。比如用户问的是第三章的鉴权机制你可以在向量检索前先用chapter 第三章过滤一遍把候选集从全库缩小到一章检索精度和速度都会明显提升。溯源指的是答案生成后你可以把source、line_start这些信息一并返回给用户告诉他这个答案来自 guide.md 第 42 行用户能自己去核对信任度完全不一样。2.2 metadata 里应该塞什么metadata 的设计没有标准答案但有几类信息我建议尽量在解析阶段就填进去因为越往后越难补来源信息source文件路径或 URL、file_type格式、doc_id唯一标识。这是溯源的基础。结构信息title所属标题、chapter章节、heading_level标题层级、section_path从根到当前的路径如第三章 鉴权 签名算法。这是做结构化检索和过滤的关键。位置信息line_start、line_end、page如果是 PDF。方便定位和展示。类型信息content_type正文、代码、表格、列表。不同类型的内容在检索时可能需要区别对待。提示metadata 里的值尽量用字符串、数字、布尔这类基础类型不要塞复杂对象。因为很多向量库在存储 metadata 时对类型有限制塞了嵌套字典或自定义对象写入时可能直接报错。2.3 一个容易被忽略的细节metadata 会参与检索过滤但不会参与向量计算这点必须说清楚否则容易产生误解。向量检索时嵌入模型只对page_content做编码metadata不参与相似度计算。metadata 的作用是在检索的过滤阶段pre-filter 或 post-filter起作用。这意味着两件事。第一你不能指望把关键信息塞进 metadata 就能被语义检索到该在page_content里的内容一个字都不能少。第二metadata 是给你做精确过滤用的比如按时间范围、按文档类型、按章节筛选这些是向量相似度做不到的。我见过有人为了让检索更准把标题重复塞进page_content的开头这个做法本身没错——标题确实是强语义信号放进正文有助于嵌入模型理解这段内容的主题。但要注意别塞得太生硬否则会稀释正文的语义密度。比较自然的做法是在切分时把所属标题作为上下文拼在块的开头这个后面讲切分时会展开。3. 纯文本 txt 的加载看似简单坑却不少3.1 最基础的 TextLoader 用法LangChain 里读 txt 最直接的就是TextLoaderfrom langchain_community.document_loaders import TextLoader loader TextLoader( file_pathdocs/faq.txt, encodingutf-8, autodetect_encodingTrue, ) documents loader.load()load()返回的是一个Document列表。对于 txt 来说默认情况下整个文件会被读成一个Documentpage_content是全文metadata里只有source。这里有几个参数值得注意。encoding指定编码中文文档务必用utf-8否则很容易读出乱码。autodetect_encodingTrue会让 LangChain 尝试自动检测编码处理来源不明的文件时比较省心但检测本身有开销文件多的时候会拖慢速度所以确定编码的情况下还是显式指定更好。3.2 编码问题中文 txt 的头号杀手中文 txt 的编码问题比想象中普遍。Windows 上很多老文件是 GBK 或 GB2312 编码直接按 utf-8 读会抛UnicodeDecodeError或者读出一堆问号和乱码。我处理过一批从旧系统导出的文档一半是 GBK一半是 utf-8混在一起。处理这类问题的稳妥做法是先探测再读取import chardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read(100000) # 读前 100KB 足够判断 result chardet.detect(raw) return result[encoding], result[confidence] encoding, confidence detect_encoding(docs/legacy.txt) print(f检测到编码: {encoding}, 置信度: {confidence})chardet的检测不是 100% 准确尤其是文件很短的时候。所以我的经验是置信度低于 0.8 的不要盲目相信检测结果要么人工确认要么用errorsreplace兜底读进来再检查有没有大量替换字符。另外检测时只读文件头部就够了读整个大文件纯属浪费。还有一个隐蔽的坑BOM字节顺序标记。有些 utf-8 文件带 BOM读进来后第一个字符会是个看不见的\ufeff它会污染第一个文本块的语义还可能让后续的字符串匹配出问题。处理办法是读的时候用encodingutf-8-sig它会自动吃掉 BOM。3.3 换行符与空白字符的清洗txt 文件的换行符有\nUnix、\r\nWindows、\r老 Mac三种。跨平台收集来的文件混在一起如果不统一后面按行切分时会出问题。统一的做法很简单def normalize_text(text): text text.replace(\r\n, \n).replace(\r, \n) # 把连续 3 个以上的换行压缩成 2 个保留段落分隔 import re text re.sub(r\n{3,}, \n\n, text) # 去掉行尾多余空格 text \n.join(line.rstrip() for line in text.split(\n)) return text这里有个度要把握。清洗的目的是去掉噪声不是把文档压扁。段落之间的空行要保留因为它是切分器判断段落边界的重要依据。但连续七八个空行、每行末尾一堆空格、全角空格混半角空格这些该清就清。我一般会把清洗逻辑单独写成一个函数加载后统一过一遍而不是散落在各个 Loader 里。3.4 大文件的分批加载txt 文件如果特别大比如几百 MB 的日志或语料一次性load()进内存会直接把进程撑爆。这时候要用懒加载loader TextLoader(docs/huge.txt, encodingutf-8) for doc in loader.lazy_load(): # 逐条处理处理完就释放 process(doc)lazy_load()返回的是生成器不会一次性把所有内容读进内存。不过要注意TextLoader的懒加载对单个大文件来说还是一条一条吐的如果单个文件本身就巨大还是得自己按行分块读。这种情况我一般会绕开TextLoader直接手写一个按行读取、按固定行数聚合的加载器控制权更足。4. Markdown 加载把结构信息榨干4.1 为什么 Markdown 是 RAG 的优质输入如果说 txt 是能读就行那 Markdown 就是自带结构金矿。Markdown 用#表示标题层级、用-或1.表示列表、用 包裹代码块、用|表示表格。这些语法标记在渲染时是格式但在解析时是结构信号。对 RAG 来说这些结构信号的价值在于它们天然地划分了语义边界。一个二级标题下的内容通常就是一个完整的语义单元。你顺着标题层级去切分切出来的块大概率是语义自洽的比按固定字符数硬切强太多。所以处理 Markdown 的核心思路不是把标记去掉变成纯文本而是保留并利用标记把结构提取到 metadata 里。标记本身在page_content里可以保留嵌入模型对#这类符号不敏感不会造成干扰但更重要的是把标题、层级这些信息结构化地存下来。4.2 UnstructuredMarkdownLoader 的用法与局限LangChain 里读 Markdown 常用的是UnstructuredMarkdownLoaderfrom langchain_community.document_loaders import UnstructuredMarkdownLoader loader UnstructuredMarkdownLoader( docs/guide.md, modesingle, # 或 elements ) documents loader.load()modesingle会把整个文件读成一个Documentmodeelements会按元素标题、段落、列表项等拆成多个Document每个元素的metadata里会带上category如Title、NarrativeText、ListItem。elements模式看起来很美但实际用起来有几个问题。第一它拆得太碎一个列表的每个条目都成了独立Document语义被切散了。第二它对中文 Markdown 的支持不算完美某些语法变体识别不准。第三它依赖unstructured库这个库本身比较重安装和运行都有成本。我的建议是如果你只是想把 Markdown 读进来用UnstructuredMarkdownLoader的single模式就够了如果你想要精细的结构化解析不如自己写一个基于正则或 Markdown 解析库的加载器控制力更强也更轻量。4.3 自己动手基于标题层级的结构化解析下面这个加载器是我在实际项目里反复打磨过的核心逻辑是逐行扫描遇到标题行就记录当前标题路径遇到正文就归属到最近的标题下。import re from langchain_core.documents import Document HEADING_RE re.compile(r^(#{1,6})\s(.*)$) def load_markdown_structured(file_path): with open(file_path, r, encodingutf-8) as f: lines f.readlines() documents [] # 用栈维护标题路径栈里存 (level, title) heading_stack [] buffer [] buffer_start_line 0 def flush_buffer(end_line): if not buffer: return content .join(buffer).strip() if not content: return # 构造 section_path如 第三章 鉴权 签名算法 section_path .join(t for _, t in heading_stack) documents.append(Document( page_contentcontent, metadata{ source: file_path, section_path: section_path, heading_level: heading_stack[-1][0] if heading_stack else 0, title: heading_stack[-1][1] if heading_stack else , line_start: buffer_start_line, line_end: end_line, content_type: text, } )) for i, line in enumerate(lines): match HEADING_RE.match(line) if match: # 遇到新标题先把之前累积的正文冲出去 flush_buffer(i - 1) buffer [] level len(match.group(1)) title match.group(2).strip() # 弹出层级 当前标题的栈顶元素 while heading_stack and heading_stack[-1][0] level: heading_stack.pop() heading_stack.append((level, title)) buffer_start_line i 1 else: if not buffer: buffer_start_line i buffer.append(line) flush_buffer(len(lines) - 1) return documents这段代码有几个设计点值得说明。用栈维护标题路径是关键。Markdown 的标题是有层级的一个三级标题属于它上面最近的那个二级标题二级标题又属于它上面最近的一级标题。用栈来维护这个当前路径遇到新标题时把层级不低于它的旧标题弹出去就能保证section_path始终正确。这个section_path存进 metadata 后检索时可以按路径过滤也可以拼进page_content增强语义。按标题聚合正文而不是按行或按固定长度切是为了保证每个Document是一个语义单元。一个标题下的内容通常讲的是同一件事聚合在一起语义最完整。记录行号是为了溯源。用户看到答案后想核对原文行号能帮他快速定位。4.4 代码块和表格的特殊处理上面那个加载器把所有非标题行都当正文处理但 Markdown 里的代码块和表格需要区别对待。代码块用 包裹里面的内容可能包含#开头的行比如 Python 注释、Shell 脚本如果被误判成标题整个结构就乱了。所以扫描时要维护一个是否在代码块内的状态in_code_block False for i, line in enumerate(lines): if line.strip().startswith(): in_code_block not in_code_block buffer.append(line) continue if in_code_block: buffer.append(line) continue # 只有在非代码块内才做标题匹配 match HEADING_RE.match(line) ...表格的处理稍微复杂。Markdown 表格是连续的以|开头的行识别出来后可以整块作为一个Documentcontent_type标为table。表格在 RAG 里是个难点因为表格的语义高度依赖行列关系切成纯文本后关系就丢了。一个实用的折中办法是把表格转成键值对式的文本描述比如把| 参数 | 类型 | 说明 | |------|------|------| | name | str | 用户名 |转成参数 name类型 str说明 用户名。这样嵌入模型更容易理解检索时也更容易命中。这个转换逻辑可以放在解析阶段也可以放在切分阶段我倾向于放在解析阶段因为解析阶段最清楚哪些内容是表格。5. 从解析到切分的衔接别让好结构毁在切分上5.1 解析产出的 Document 怎么喂给切分器解析阶段产出的Document列表直接喂给切分器就行from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , ], ) chunks splitter.split_documents(documents)split_documents会自动把原Document的 metadata 复制到每个切出来的子块上。这意味着你在解析阶段辛苦填的section_path、title、line_start都会跟着传下去检索时就能用上。这里有个细节RecursiveCharacterTextSplitter的separators参数对中文很重要。默认的分隔符是英文的[\n\n, \n, , ]中文句子之间没有空格按空格切会切得莫名其妙。加上中文标点。作为分隔符切出来的块更符合中文的语义边界。5.2 把标题拼进块内容增强语义前面提到标题是强语义信号。一个只包含签名算法采用 HMAC-SHA256密钥长度不低于 32 字节的块如果不知道它属于接口鉴权这一章嵌入模型很难把它和鉴权用什么算法这个问题关联起来。解决办法是在切分后把section_path拼到每个块的page_content开头for chunk in chunks: section_path chunk.metadata.get(section_path, ) if section_path: chunk.page_content f【{section_path}】\n{chunk.page_content}这样每个块都自带上下文检索时语义更完整。实测下来这个简单的操作对检索召回率的提升相当明显尤其是文档里有很多相似术语的时候。注意拼接的上下文要简洁用【】这类符号包起来让嵌入模型能区分这是上下文和这是正文。别把整段标题路径原样塞进去太长反而稀释语义。5.3 块大小怎么定没有标准答案但有判断依据chunk_size设多少是 RAG 里被问得最多的问题之一。我的经验是没有万能值但有判断依据。块太小比如 200 字符语义不完整检索时容易命中但答案缺胳膊少腿。块太大比如 2000 字符一个块里混了好几个主题嵌入向量被平均化检索精度下降。500 到 800 字符是中文文档比较常用的区间但具体要看文档类型。技术文档、API 文档这类信息密度高的块可以小一点400 到 600。叙述性的文章、教程块可以大一点800 到 1200。判断标准是一个块读下来是不是在讲同一件事。如果读着读着话题变了说明块太大了如果读完感觉话没说完说明块太小了。chunk_overlap设 10% 到 15% 的chunk_size比较常见作用是防止关键信息正好被切在边界上。但 overlap 不是越大越好太大会导致大量重复内容浪费存储和检索开销。6. 实操中踩过的坑与排查思路6.1 中文乱码的完整排查链路乱码是最高频的问题我把它拆成一条可复现的排查链路。第一步确认文件真实编码。不要相信文件扩展名也不要想当然。用chardet或file -i命令探测得到初步判断。第二步用探测到的编码读一小段打印出来看。如果还是乱码说明探测错了换一个编码再试。中文常见的就 utf-8、gbk、gb18030 这几种gb18030 是 gbk 的超集兼容性更好拿不准时可以先试 gb18030。第三步检查 BOM。如果读出来的第一个字符是\ufeff说明是带 BOM 的 utf-8改用utf-8-sig。第四步如果以上都不行用errorsreplace强行读进来统计替换字符\ufffd的比例。比例低比如低于 1%说明大部分内容没问题可以接受比例高说明编码判断根本错了得回到第一步。这套流程我走过很多遍基本能覆盖 95% 以上的中文乱码场景。6.2 标题识别失败的几种情况自己写 Markdown 解析时标题识别失败通常有这几个原因。一是标题行前面有空格。有些编辑器或导出工具会在#前加空格^#就匹配不到了。正则改成^\s*#{1,6}\s更稳。二是用了 Setext 风格的标题也就是用或---在文字下方划线表示标题。这种语法老但确实存在需要额外识别如果某行全是或-且上一行是非空文本那上一行就是标题。三是标题里混了行内格式比如## **加粗的标题**。这种标题的文本部分带了**存进 metadata 时最好清洗掉否则section_path里会带着一堆星号。四是代码块里的#被误判。这个前面讲过用in_code_block状态位解决。6.3 metadata 丢失的隐蔽问题有时候你会发现明明解析时填了 metadata检索时却取不到。这种情况我遇到过两次原因不同。一次是切分器的问题。某些切分器在split_documents时不会自动继承 metadata需要手动传。解决办法是切分后遍历检查一遍看 metadata 是不是还在。另一次是向量库写入时的问题。某些向量库对 metadata 的字段类型有限制比如只接受字符串和数字遇到列表或 None 会静默丢弃整个 metadata。解决办法是写入前把 metadata 的值统一转成基础类型None 转成空字符串列表转成逗号分隔的字符串。提示解析完成后养成习惯打印几个 Document 的 metadata 看看确认该有的字段都在、类型都对。这个检查花不了几秒钟但能省掉后面几小时的排查。6.4 大文档解析的内存与速度优化文档一多解析就成了瓶颈。几个实用的优化点。能用lazy_load就用lazy_load别一次性把所有 Document 读进内存。解析和后续处理可以做成流式的读一个处理一个。正则表达式提前编译。re.compile一次反复用比每次re.match现编译快很多。文档量大时这个差异很明显。避免在循环里做重复的字符串操作。比如section_path的拼接可以在标题变化时才重算而不是每行都算一遍。如果解析逻辑复杂考虑用多进程并行处理不同文件。解析是 CPU 密集型任务多进程比多线程有效。但要注意如果后续要写入同一个向量库并行写入可能冲突通常是并行解析、串行写入。7. 结构化解析带来的检索收益7.1 按 section_path 过滤把检索范围缩小一个量级结构化解析最直接的收益就是检索时能做精确的元数据过滤。用户问第三章的鉴权机制你可以先用section_path包含第三章来过滤把候选集从全库几万个块缩小到几十个块再在这几十个块里做向量检索。这个操作对精度的提升是立竿见影的。因为向量检索在候选集很大时容易把语义相近但主题无关的块排到前面。加了结构过滤后无关主题的块根本进不了候选集误召回大幅减少。实现上不同向量库的过滤语法不一样但思路一致把section_path作为可过滤字段存进去检索时带上过滤条件。LangChain 的 retriever 大多支持filter参数具体写法查对应向量库的文档。7.2 结构化上下文让答案更完整除了过滤结构化信息还能帮模型生成更完整的答案。当检索到的块带着section_path时你可以把这些路径一并塞进 prompt让模型知道这段内容属于哪个章节生成答案时就能带上上下文而不是干巴巴地复述一段话。比如检索到签名算法采用 HMAC-SHA256如果 prompt 里告诉模型这段属于第三章 接口鉴权机制 签名算法模型回答时就能说在接口鉴权机制的签名算法部分采用的是 HMAC-SHA256答案的完整度和可信度都上了一个台阶。7.3 溯源展示提升用户信任最后是溯源。RAG 系统最怕的就是用户不信任——模型说得头头是道但用户不知道是真是假。如果每个答案都能附上来源文件名、章节、行号用户能一键跳转核对信任度完全不同。这些信息全部来自解析阶段填的 metadata。source给文件名section_path给章节line_start给行号。解析时多花几分钟填好后面展示时就是现成的。这也是我一直强调metadata 要在解析阶段就填全的原因——越往后补成本越高甚至补不回来。8. 把解析逻辑封装成可复用的组件8.1 统一入口一个函数处理多种格式项目里文档格式往往不止一种与其在每个地方写 if-else 判断格式不如封装一个统一入口def load_document(file_path): ext file_path.lower().rsplit(., 1)[-1] if ext txt: return load_txt(file_path) elif ext in (md, markdown): return load_markdown_structured(file_path) else: raise ValueError(f不支持的格式: {ext})这样上层调用只需要传文件路径不用关心格式。新增格式时也只改这一个函数符合开闭原则。8.2 解析配置的集中管理解析涉及不少参数编码、块大小、是否保留代码块、section_path 的分隔符等等。这些参数散落在代码各处会很难维护集中到一个配置对象里更清晰from dataclasses import dataclass dataclass class ParseConfig: encoding: str utf-8 keep_code_block: bool True section_separator: str min_content_length: int 10 # 太短的块直接丢弃配置集中后不同项目、不同文档类型可以用不同的配置切换起来很方便。比如处理英文文档时把section_separator改成 / 处理代码文档时把keep_code_block设为 True。8.3 解析质量的自动化检查解析完不做检查等于闭着眼睛开车。我一般会加几个自动化检查。检查一空块比例。如果超过 10% 的 Document 的page_content是空的或极短说明解析逻辑有问题可能把内容都过滤掉了。检查二metadata 完整率。统计有多少 Document 缺source、缺section_path缺得多的字段说明解析时没填全。检查三编码异常率。统计page_content里替换字符\ufffd的比例高了说明编码有问题。这几个检查写成函数每次解析后跑一遍打印个报告。花不了多少时间但能第一时间发现解析质量问题避免脏数据流到下游。9. 关于解析这件事我的一些真实体会做 RAG 这几年我越来越觉得解析是整个系统里投入产出比最高的环节。它不像模型调优那样有光环也不像向量库选型那样有话题度但它决定了系统的地基。地基没打好上面盖得再漂亮也是危楼。我见过太多团队把精力全砸在换嵌入模型、调检索参数上效果提升却微乎其微最后发现是解析阶段把文档结构全丢了。反过来把解析做扎实哪怕用最普通的嵌入模型和向量库检索质量也能达到可用水平。具体到 txt 和 Markdown 这两种格式我的建议是txt 重点解决编码和清洗Markdown 重点榨取结构信息。txt 没有结构可榨那就把噪声清干净保证内容准确Markdown 有结构那就把标题层级、代码块、表格这些信号全部提取到 metadata 里为后续的过滤和溯源打好基础。还有一个体会是解析逻辑一定要自己写一遍。用现成的 Loader 当然快但现成 Loader 的通用性是以牺牲针对性为代价的。你的文档有什么特点、你想提取什么结构只有你自己最清楚。自己写一遍哪怕只是把现成 Loader 包一层你对数据的理解也会深一个层次后面出问题时排查起来心里有底。最后分享一个小技巧解析阶段产出的 Document别急着往下走先存一份到本地比如用 JSON 序列化后面切分、嵌入、检索出问题时可以随时回到这个中间产物上排查确认问题到底出在解析还是出在下游。这个中间产物就像个存档点能帮你快速定位问题环节省下大量来回调试的时间。
返回列表