
做 RAG 项目我见过太多团队把精力全砸在模型选型和 prompt 调优上结果数据导入与解析这一层随便拿个文本切分器糊弄过去上线后检索召回质量一塌糊涂。RAG 的检索上限从来不是模型决定的而是你喂进去的知识库文本决定的。这篇是“RAG 数据导入与解析全攻略”系列的第一篇重点聊最基础也最通用的两类输入纯 txt 文本以及转换后的 Markdown 文本同时把结构化解的思路讲透。适合正在搭知识库、准备做 RAG 落地却被各种格式文件搞得头疼的开发者读完你至少能搞定一条从“乱七八糟的 txt”到“结构化可检索 Markdown”的完整管线。1. RAG 数据导入先想清楚解析要解决什么问题1.1 解析质量决定检索上限很多人以为 RAG 的链路是“文档 - 切块 - embedding - 检索 - 生成”真正上手才发现最耗时间的是最前面那一步把各种格式的文件变成干净、可切分、带语义的文本。embedding 模型质量固然重要但如果源文本本身就是乱码、错位、表格被拆烂、代码片段残缺后面做再多优化都补不回来。我自己的体会是解析这个环节决定了检索质量的“天花板”。切块策略和检索调优只是尽量去接近这个天花板永远不可能超越它。所以做数据导入时优先想的不是“用什么 splitter”而是“这份文档最终应该以什么形态进入向量库”。这个形态想清楚了后续每一步都有据可依。1.2 为什么从 txt 和 Markdown 切入真实项目里会遇到 PDF、Word、HTML、CSV、JSON、扫描件为什么系列第一篇只讲 txt 和 Markdown原因有三点。第一通用性最强。txt 是所有格式里最底层的存在任何系统都能导出很多老旧知识库、日志、爬虫抓取的正文最终落地的都是 txt。第二解析可控性最高。PDF 有排版、字体、多栏的问题Word 有样式、批注、嵌套表格的问题而 txt 就是纯字符流你唯一要处理的就是编码、换行、空格和特殊字符。第三Markdown 是 RAG 场景里性价比极高的中间格式。它比 txt 多了一层轻量级结构标记又不像 HTML 那样充满噪音标签切分时可以借助标题、列表、代码块这些结构来做精准切块是目前本地知识库工具里公认最好用的文本形态之一。1.3 先定目标你要的是“能搜到”还是“能回答好”做数据导入前先问自己一个问题知识库面向的是关键词检索、语义检索还是问答式生成这决定了你要不要做结构化解。如果只是做关键词检索那么把 txt 原样切块就能用顶多处理一下编码和乱码。但如果要做问答式 RAG文本里隐含的结构信息就格外重要。举个例子一篇产品文档里有“注意事项本设备不防水”如果这一段被切在某个 chunk 的末尾embedding 时很容易被其他内容稀释检索“设备能泡水吗”时就召回不到。而如果解析阶段就把“注意事项”识别成标题让这段内容成为一个独立小节召回率和准确率都会明显提升。所谓结构化解就是把这个隐含结构显式地提取出来让机器能利用它。2. txt 文本解析通用文本的清洗与切分实操2.1 编码问题第一道坎txt 文件最坑的就是编码。Windows 下常见 GBK/GB18030macOS 和 Linux 下常见 UTF-8老系统还可能产生 UTF-8 with BOM。直接open(path, r, encodingutf-8)读文件遇到 GBK 文件立刻抛异常更麻烦的是有些文件里夹杂着非法字节根本不报错读出来全是乱码。我常用的方案是先用charset-normalizer或chardet检测编码再取一个置信度阈值做兜底。实测下来charset-normalizer比chardet快不少而且对短文本的识别更准。要注意的是检测前先读文件的前 64KB不要读全文件否则大文件会非常慢。检测逻辑大概是from charset_normalizer import from_bytes def detect_encoding(path): with open(path, rb) as f: raw f.read(65536) result from_bytes(raw).best() return result.encoding if result else utf-8如果检测出来的编码是utf-8-sig说明带 BOM写入时记得用encodingutf-8-sig或直接把开头的\ufeff剥掉否则第一个字符会成为乱码。这个坑看起来很初级但在实际项目中非常常见尤其是批量导入几百个 txt 时跑完才发现有一部分文件第一个字是错的排查起来很费时间。2.2 内容清洗哪些该动哪些千万别动编码问题处理完后就是清洗。我的原则是只处理“影响结构和检索的噪音”不要做激进清洗。该处理的包括连续空白符压缩、控制字符删除、Windows 的\r\n统一转成\n、全角空格转半角。这些操作能显著减少后续切分时的异常行为。不该处理的包括下划线、星号、反引号、井号、方括号这类 Markdown 标记符号。很多人想把网页正文里的广告语、版权声明、重复导航去掉这个想法没错但建议用规则匹配而不是靠简单替换否则很容易误删正文。我踩过的坑是清洗时用正则re.sub(r[*_#], , text)把“看似噪音”的符号全删了结果把技术文档里的**加粗**、#include全搞坏了后面转 Markdown 时结构全丢。正确做法是清洗和结构提取分两步走第一步只做字符级清理第二步再做结构识别。前者改的是“字面”后者改的是“语义”混在一起必然出问题。另外还要注意处理不可见字符。比如从 PDF 或网页复制出来的文本里常有零宽空格\u200b和软连字符\u00ad它们不会显示但会严重影响 embedding 效果让相似文本的向量距离变远。清洗时可以把它们纳入控制字符一并去掉。2.3 chunk 切分参数size、overlap、策略怎么选txt 文本解析的核心产出是 chunk切分参数的坑最多。chunk_size 一般不建议拍脑袋定。我通常结合两个因素embedding 模型的输入上限以及语料的自然语义单元长度。OpenAI 的 text-embedding-3-small 支持 8191 token但你在切分时根本不用顶到上限因为 chunk 越长向量越容易被“平均化”导致语义稀释。经验值在 300 到 800 token 之间比较稳妥。中文场景尤其要注意token 数和字符数不是一回事1 个中文字符大概对应 0.6 到 1 个 token如果按字符数切 500实际 token 数可能接近 500 甚至更多。我一般先按“字符数 x 0.75”估算 token 数再反推 chunk_size。overlap 的作用是防止语义被切断。比如两个相邻 chunk 的交集处恰好是一句话的主语和谓语没有 overlap 就会导致其中一个 chunk 语义不完整。但 overlap 不是越大越好过大的 overlap 会让相邻 chunk 高度相似浪费向量库容量检索时还会召回一堆重复内容。文本类数据 overlap 建议在 chunk_size 的 10% 到 20%比如 500 的 chunk 配 50 到 80 的 overlap。至于切分策略纯 txt 最常见的是按固定长度切。但更推荐先用空行把文档拆成“自然段落组”再对超长段落做二次拆分。固定长度切的致命问题是会把标题和正文硬生生拆开后续检索时一个 chunk 里只留下孤零零的标题语义完全不可用。段落优先的方式虽然代码复杂一点但切出来的 chunk 结构明显更合理。2.4 一个可以直接抄走的 txt 预处理脚本下面这个脚本覆盖了编码检测、清洗、段落切分三个步骤我日常处理 txt 类数据时就是在这个基础上改的。import re from charset_normalizer import from_bytes def clean_text(text: str) - str: text text.replace(\r\n, \n).replace(\r, \n) text re.sub(r[\x00-\x08\x0b-\x0c\x0e-\x1f\u200b\u00ad], , text) text re.sub(r[ \t], , text) text re.sub(r\n{3,}, \n\n, text) return text.strip() def split_into_chunks(text: str, chunk_size: int 800, overlap: int 80): paragraphs [p.strip() for p in re.split(r\n\s*\n, text) if p.strip()] chunks [] buffer for para in paragraphs: para_len len(para) if para_len chunk_size: if len(buffer) para_len 1 chunk_size and buffer: chunks.append(buffer.strip()) buffer buffer[-overlap:] if overlap else buffer (\n\n para) if buffer else para else: if buffer: chunks.append(buffer.strip()) buffer start 0 while start para_len: end min(start chunk_size, para_len) chunks.append(para[start:end].strip()) start max(0, end - overlap) if buffer: chunks.append(buffer.strip()) return chunks注意这里用的是字符数切分如果你要更准确可以接入 tokenizer 按 token 切。但字符级切分的好处是简单可控对中英文混排的容忍度也高实测下来在 500 字符左右的小 chunk 下效果和按 token 切没有明显差异。3. 从 txt 到 Markdown结构化转换的完整拆解3.1 Markdown 凭什么更适合 RAG如果说 txt 是“裸文本”那 Markdown 就是“带骨骼的文本”。RAG 场景里Markdown 的优势有三个层面。第一结构标记可以直接变成检索的锚点。标题层级#、##天然对应文档的目录层级切分时可以把“标题 内容”作为一个语义单元表格和代码块也有明确的起止标记不会被误切。第二Markdown 语法是纯文本任何 embedding 模型都能直接处理不需要额外的解析器这比 HTML 的标签噪音、PDF 的坐标信息要友好得多。第三Markdown 有广泛的生态支持各大本地知识库工具、Notion、Obsidian、GitHub 都原生支持后续做知识管理、版本对比、双链引用都方便。但要注意Markdown 不是银弹。它没有把 PDF 里的版式、页眉页脚、图片位置信息带出来也没有解决扫描件 OCR 的问题。它的价值是“在纯文本和重型结构化之间取了一个很好的平衡点”所以系列第一篇选它做中转格式。3.2 标题识别、列表与表格的转换规则从 txt 转 Markdown核心不是写一个转换函数而是定一套“识别规则”。不同来源的 txt 结构差异极大规则写得越通用后续维护成本越低。标题识别是最重要的一步。没有标记的 txt 文本里标题通常表现为“独立成行 字数较少 跟前后内容有明显空行间隔”。当然这只是启发式规则做不到 100% 准确。实际操作时我会先做一轮“候选标题检测”统计每一行的长度、是否以数字/序号开头、是否与上下行构成“标题-正文”的关系再人工校验一轮规则。如果源文本本身有一定格式比如“第一章”“1.1”“3.2.1 概述”这类编号那直接正则匹配编号模式准确率会高很多。列表的识别比较微妙。txt 里的列表往往是用-、*、1.、1开头的行但*也可能是加粗标记1.也可能是正常句子的开头。我的处理原则是凡是能跟上下文组成多行连续模式连续两行以上都以相同类型前缀开头才识别为列表单行的前缀不做转换保留原样。这样能大幅降低误判率。表格的识别与转换在 RAG 数据导入里尤其重要因为表格数据的结构信息一旦丢失检索“这个产品支持哪些协议”时基本召回不到答案。txt 里的表格通常是以制表符或连续空格分隔的文本块转换思路是先以“行内是否包含多个分隔符”来判断是否是表格行再把连续的表头行和分隔行映射成 Markdown 表格语法。这里我没有用复杂的机器学习方案而是固定规则加几轮正则修正毕竟你的目标是给 RAG 用不是做一个通用格式转换器。下面是一个简单的表格识别与转换核心逻辑def is_table_row(line: str) - bool: sep_count line.count(\t) spaces_parts len(re.split(r {2,}, line.strip())) return sep_count 2 or spaces_parts 3 def convert_table_block(lines: list[str]) - str | None: if not lines or not all(is_table_row(line) for line in lines): return None headers [c.strip() for c in re.split(r\t| {2,}, lines[0].strip())] md_lines [| | .join(headers) |, | |.join([ --- ] * len(headers)) |] for line in lines[1:]: cells [c.strip() for c in re.split(r\t| {2,}, line.strip())] md_lines.append(| | .join(cells) |) return \n.join(md_lines)这段代码不复杂但它解决了一个真实痛点把表格从一个不可检索的文本块变成带行列结构的 Markdown 表格。转换之后嵌入时可选择把整张表格作为独立 chunk检索时命中率比原来高非常多。3.3 手写转换器 vs 现成工具做 txt 转 Markdown常见有三条路手写规则、用通用库、组合方案。手写规则适合格式相对固定、来源单一的文本。优点是可控性极强、不依赖外部依赖缺点是遇到没见过的格式就要改代码。通用库方面markdownify和html2text都很好用但它们主要面向 HTML 转 Markdown处理纯 txt 反而一般pandoc则是一个大而全的瑞士军刀txt 转 Markdown 只需要一个简单命令但它对“txt 里的标题和表格识别”也几乎不做智能判断更多是机械转换。我个人的建议是组合方案先把 txt 用自己写的规则清洗和结构化识别生成一个带简单标记的中间文本再用markdownify或pandoc做语法层面的规范化。这样既能保留规则转换的准确性又不需要自己手写 Markdown 语法的所有边界情况。实测下来组合方案比单纯手写规则省了大概 30% 的维护成本。3.4 针对 Markdown 的智能切分代码块和表格不能被拦腰切断这是全篇最值得记住的实操经验之一。很多人用 langchain 的RecursiveCharacterTextSplitter时发现默认按分隔符递归切分代码块和表格极容易在中间被切断。切断的代码块进入向量库后检索到它时你会看到一整段残缺的代码完全没有使用价值。解决办法有几种我推荐最简单的“保护块策略”在切分前先把 Markdown 里的代码块、表格、数学公式块$$...$$整体替换成占位符切分完成后把占位符还原并强制让这些块成为独立 chunk。这样能保证块内部的完整性也能让标题跟着正文走。核心思路就是先保护再切分最后合并。import re PROTECTED_BLOCKS [] def protect_md_blocks(text: str) - str: global PROTECTED_BLOCKS PROTECTED_BLOCKS [] def repl(match): PROTECTED_BLOCKS.append(match.group(0)) return f\n\n__PROTECTED_BLOCK_{len(PROTECTED_BLOCKS)-1}__\n\n text re.sub(r[\s\S]*?, repl, text) text re.sub(r\|[^\n]\|(?:\n\|[^\n]\|)*, repl, text) text re.sub(r\$\$[\s\S]*?\$\$, repl, text) return text def restore_md_blocks(text: str) - str: return re.sub(r__PROTECTED_BLOCK_(\d)__, lambda m: PROTECTED_BLOCKS[int(m.group(1))], text)这个保护策略同样适用于数学公式插件产出的大量$...$内容。RAG 知识库里如果存的是技术文档、论文笔记公式往往是检索的高频目标把公式块拆散会导致向量语义失真。另外GitHub 风格的 callout [!NOTE]和 admonition 也是同样的道理这类扩展语法块通常信息密度高必须整体保留。4. 结构化数据建模metadata 注入与半结构化文件处理4.1 结构化、半结构化、非结构化的边界聊到结构化解先说清楚概念因为很多人在项目里把这三类混在一起导致数据建模一团糟。非结构化数据就是前面说的纯文本、PDF、扫描件没有明显的行列结构机器只能看到字符流。半结构化数据典型代表是 JSON、YAML、XML有键值对和嵌套关系但同一层级下的内容长度、结构可以不统一。结构化数据则指 CSV、SQL 表、Excel 里那种规整的行列数据每一行都是同构记录。RAG 场景里三类数据要分开处理。非结构化文本走解析和切分半结构化数据优先设计成 metadata 和内容两级结构结构化数据则要考虑是直接向量化还是先转成自然语言描述再向量化。这三类数据混在一个知识库里时一定要在 chunk 的 metadata 里显式标注data_type否则检索时很难做过滤和排序。4.2 用 metadata 给知识库装上“索引标签”当前面做了 Markdown 转换后你已经有了“标题层级”这个结构信息这是做 metadata 的基础。我强烈建议在入库前为每个 chunk 生成四类 metadata来源信息source、file_path、page、结构信息doc_title、section_h1、section_h2、内容属性data_type、language、tags、时间信息created_at、updated_at。这些字段在后续做混合检索、过滤、精排时都是刚需。比如用户只搜“2024 年后的更新日志”你就可以用created_at做硬过滤把范围缩小后再做向量召回效果比单纯靠 embedding 找相似文本要好得多。metadata 的设计要遵循“宁少勿多”的原则。字段多了并不会提升检索效果只会增加向量库的内存占用和过滤时的查询复杂度。一般控制在 6 到 10 个字段以内除非你有特别明确的需求。4.3 CSV 和 JSON 怎么进知识库CSV 和 JSON 是 RAG 里最常被误处理的半结构化数据。有人直接把整个 JSON 文件当 txt 读有人把 JSON 转成字符串塞进 chunk结果检索时满屏的大括号。CSV 表格的处理建议是先按行转成自然语言描述再决定是否保留原表格。自然语言描述方案例如一行数据(协议, 端口, 加密方式)转成“该网络设备支持 TLS 协议端口 443使用 AES 加密方式”这种句子进 embedding 后非常容易被语义查询命中。如果你还想保留原始表格可以把原始行作为 metadata 存起来检索到描述时再展示原始行兼顾了语义检索和准确呈现。JSON 的处理则要区分用途。如果 JSON 是配置类数据建议原样保留并把所有 key 作为 metadata 字段检索“某个配置项是否开启”时用规则查询直接命中根本不需要走向量检索。如果 JSON 是业务文档那需要先做字段筛选只把有语义价值的字段转成自然语言段落其余的作为 metadata。把整个 JSON 一股脑塞进 chunk是典型的“看起来结构化了实际上检索效果更差”的做法。5. 一个完整的导入流水线从文件到可检索的知识库5.1 流水线总体设计把前面的内容串起来一个完整的 txt 到 Markdown 再到知识库的流水线大概是这样的文件加载 - 编码检测 - 文本清洗 - 结构化识别标题、列表、表格 - 转成 Markdown - 保护块切分 - 生成 metadata - embedding - 写入向量库。设计流水线时我强烈建议把每两个步骤之间做成“可中断、可持久化”的。也就是说每一步的输出单独存一份中间文件方便出问题时回溯。比如源文件清洗后存step1_clean.md转换后存step2_structured.md切分后存step3_chunks.json。这样做的好处是切分参数调优时不需要重新跑前面的流程能省下大量时间。尤其是批量导入几百个文件时你会发现这个设计非常值得。5.2 关键代码实现下面是一段简化版的完整流水线代码核心思路是前面几个模块的组合。实际项目里你可以把每个模块抽成函数再用pathlib批量遍历目录。import json from pathlib import Path def process_file(filepath: Path) - list[dict]: # 1. 读取并解码 encoding detect_encoding(str(filepath)) with open(filepath, r, encodingencoding, errorsignore) as f: raw_text f.read() if raw_text.startswith(\ufeff): raw_text raw_text[1:] # 2. 清洗 clean clean_text(raw_text) # 3. 结构化识别与 Markdown 转换示意 md_text convert_to_markdown(clean) # 4. 保护块 切分 protected protect_md_blocks(md_text) chunks split_into_chunks(protected, chunk_size600, overlap90) chunks [restore_md_blocks(c) for c in chunks] # 5. 生成 metadata docs [] for idx, chunk in enumerate(chunks): docs.append({ content: chunk, metadata: { source: filepath.name, chunk_id: f{filepath.stem}_{idx}, data_type: markdown, created_at: None, } }) return docs def build_embeddings_and_store(docs): for doc in docs: vec embedding_model.embed_query(doc[content]) vector_store.add_vectors([vec], documents[doc[content]], metadatas[doc[metadata]])这里我把convert_to_markdown和split_into_chunks具体实现省略了前面对应章节已经有思路。要提醒的是如果文件数量大建议用批量 embedding 接口单条逐条请求不仅慢还容易触发限流。我习惯把 docs 按批次累计到 64 条或 128 条再统一调用速度能快 5 到 10 倍。5.3 参数选型和效果对比流水线搭好后参数选型直接决定效果。我这里给出一组实测过的配置组合你可以按自己的语料微调。纯技术文档API 文档、使用手册chunk_size 用 512 字符overlap 用 64 字符切分器用段落优先策略效果很稳。长篇小说或叙事类文本chunk_size 可以调到 800 到 1000overlap 保持 100 左右因为这类文本上下文依赖强切太碎语义反而丢失。表格密集型数据建议以“整表”为最小切分单元不设固定 chunk_size而是让一个表占据一个 chunk表太大再按行二次切分。embedding 模型的选择上中文语料不要盲选英文模型实测下来中英文混合场景里m3e-base、bge-large-zh 这类模型的中文语义表现普遍优于通用英文模型。如果你追求的是全中文场景bge-large-zh 是性价比很高的选择如果语料中英文混排可以测试 m3e-large 或 OpenAI 的 text-embedding-3-small具体以你自己的评测集为准。注意embedding 模型换掉后历史向量全部需要重新生成所以前期确定好模型后期尽量别换。6. 常见问题与排查技巧实录6.1 乱码与内容丢失最容易遇到的是两类乱码一类是编码检测错误比如 GBK 文件被当成 UTF-8 读另一类是 BOM 导致的首字符异常。排查时先看文件的前 16 个字节用十六进制查看器确认是否存在 BOM 标志EF BB BF。如果文件是 GBK 而检测成了 UTF-8典型特征是中文表现为一字多符比如“你”变成“浣犲”。解决办法是打开一个文件目录下的一批文件抽样统计编码检测结果如果有一半被检测成gb18030那大概率是编码识别策略有问题。建议把检测失败的例外单独落地成日志人工复核后补规则。内容丢失则多数出在“清洗过度”上。我见过有人为了去掉广告重复文案用正则删除所有包含“点击购买”“免责声明”的行结果把正文里有同样词组的句子也删了。我的经验是清洗规则只做白名单式保留不要做高风险的全局删除。删除操作前置到“人工抽样 50 条确认无误”再做批量。6.2 chunk 截断导致检索质量下降检索结果经常出现“答案在 chunk 末尾戛然而止”的情况这基本是切分策略的问题不是模型的问题。排查时先检查相似 chunk 之间是否出现大量重复内容。如果 overlap 过大embedding 会出现严重的低效去重问题检索结果里前 5 条可能都是同一段内容的变体。过小则会出现语义断裂。另外还要检查是否切断了代码块和表格。如果 chunk 里包含残缺的def func(或表格只保留了一半表头那基本是保护块策略没做好重新跑一遍带保护块的切分即可。另一种隐蔽问题是切出来的 chunk 结构完整但语义不完整。比如标题“3.2 环境变量配置”落在前一个 chunk 尾部而配置内容全部在后一个 chunk。这种情况建议使用“标题前缀法”每个 chunk 生成时把它所属的完整标题链H1/H2/H3拼在 chunk 开头作为隐式的上下文提示。实测能明显提升包含对话式 QA 场景的召回率。6.3 表格和代码检索不到“检索不到表格数据”是我在 RAG 项目里收到的最多抱怨。原因通常不是 embedding 模型不行而是表格变成了无结构字符串语义信息被完全打散。我之前测试过把同一张设备参数表分别以“纯文本复制”和“Markdown 表格”两种形式进入知识库在“查询 2.4GHz 频段最大发射功率”这个问句下后者命中率高出接近 40 个百分点。原因很好理解Markdown 表格保留了“2.4GHz”和“最大发射功率”在同一行的行列关联embedding 时上下文信息更紧凑。如果你已经有大量表格型 txt建议批量转成 Markdown 表格再入库不要偷懒。代码检索不到的问题则多半是切分时把缩进空格当普通字符处理导致代码块内部被压缩。清洗时不要把行首缩进删除否则 Python 语义丢失。代码块的保护策略同样重要代码和注释必须一起保留在一个 chunk 里。6.4 文件解析速度太慢怎么办很多 txt 并不大但批量导入几千个文件时单文件处理开销会放大。速度瓶颈通常出现在编码检测上detect_encoding里如果每次都读 64KB几千个文件就要读几百 MB非常浪费时间。优化手段有三个第一对于目录内文件可以先做一次编码抽样检测把结果缓存起来不做重复检测第二用多进程代替多线程txt 解析和 embedding 都是 CPU 密集型任务多线程会因为 GIL 锁效率很低我用multiprocessing后批量处理速度快了 3 倍第三embedding 阶段务必用批量接口逐条调用是性能杀手。另外如果文件里包含大量超长段落比如几万字的日志型 txt段落优先的切分策略可能会退化成逐行切导致 chunk 过多。解决思路是先做摘要级预处理把超长段落按语义标记时间戳、日志级别、函数名拆成子段落再走常规流程。这一步虽然简单但对日志类知识库的检索效果提升非常明显。7. 后续系列与一些心里话原本这篇只想讲 txt 和 Markdown但写的过程中不断有朋友问我“PDF 里的表格怎么办”“Word 文档要不要先转 Markdown”“图片里的文字用 OCR 好不好”。这些都是很实际的问题我会在系列后续文章里逐步展开。第一篇刻意停在 txt 和 Markdown是因为这两类是基础中的基础你把它们的解析逻辑吃透了后面处理 PDF 和 Word 时只是换一个“前置提取器”的问题核心的清洗、切分、结构化思路完全通用。我个人在实际操作中最大的感受是数据导入与解析不是一个“一把梭”的工程而是一个需要持续迭代打磨的环节。你可能第一版跑通了觉得效果还行等到真实用户开始提问才发现某个类型的文档召回率特别差回头一查是表格没转好或者标题链没拼上。这些都是正常过程别焦虑。建议你在自己的项目里先把“保护块策略 metadata 设计 段落优先切分”这三板斧用上你会发现知识库的可用性比之前直接用默认 splitter 提升一大截。之后的每一篇系列文章我们都在这个基础上继续加料。