
1. RAG 数据导入的底层逻辑与方案选型1.1 为什么数据导入是 RAG 系统的隐形瓶颈做过 RAG 项目的人都有一个共同体会模型选型、向量库调优、检索策略这些环节固然重要但真正让项目翻车的往往是数据导入这一步。我见过太多团队在 POC 阶段用几十个干净的 PDF 跑得风生水起一上生产环境面对几万个格式各异的文件就彻底崩盘。问题出在哪出在大家把数据导入当成了一个“读文件”的简单动作而实际上它是一个完整的数据工程管线。RAG 的核心链路是“检索-增强-生成”检索质量直接决定了生成质量的上限。而检索质量又取决于什么取决于你导入的文本块是否语义完整、结构是否清晰、元数据是否丰富。如果导入阶段把一份结构良好的技术文档切成了语义断裂的碎片后面用再好的 Embedding 模型也救不回来。这就是所谓的“垃圾进垃圾出”。从工程角度看数据导入与解析要解决的核心问题有三个格式兼容性、结构保留度、语义完整性。格式兼容性决定了你能吃进多少种数据源结构保留度决定了你能否利用标题、列表、表格等结构信息做增强检索语义完整性决定了切块后的文本是否还能被模型正确理解。这三个问题层层递进任何一个环节处理不好都会成为整个 RAG 系统的短板。1.2 从 txt 到 Markdown 的选型考量在众多文档格式中为什么我们要专门讨论 txt 和 Markdown 这两种因为它们代表了两个极端txt 是最简单的纯文本格式没有任何结构信息Markdown 则是轻量级标记语言用极低的成本表达了丰富的结构语义。把 txt 转成 Markdown本质上是一个从无结构到有结构的升维过程。这个升维过程的价值在哪里举个例子。一份产品需求文档如果用 txt 存储你看到的是一堆连续的段落标题和正文混在一起列表项和普通句子没有区别。切块的时候你只能按固定字数硬切切出来的块可能前半段在讲功能 A后半段突然跳到功能 B。但如果转成 Markdown你可以用#标记标题层级用-标记列表项用**标记重点。切块时就可以按标题层级做语义切分每个块都自带“我是哪个章节的”这个上下文信息。Markdown 还有一个被低估的优势它是 LLM 的原生友好格式。大语言模型在预训练阶段见过海量的 Markdown 文本对#、##、-、这些符号有天然的语义理解能力。你把 Markdown 格式的文本喂给模型它比喂纯文本能更好地把握文档结构。这一点在 RAG 的生成阶段尤其重要因为模型需要根据检索到的上下文来组织答案如果上下文本身结构清晰生成质量会明显提升。至于为什么不是 HTML 或 JSONHTML 太冗余标签噪音大清洗成本高JSON 太结构化适合程序处理但不适合直接喂给模型。Markdown 恰好卡在中间结构足够表达语义又足够简洁不干扰阅读。这就是我们选择 Markdown 作为中间格式的核心原因。1.3 通用文本解析的整体架构设计一个健壮的文本导入管线应该长什么样我的经验是分成四层接入层、识别层、转换层、输出层。接入层负责对接各种数据源可能是本地文件系统、对象存储、数据库导出甚至是网盘同步目录。这一层的关键是做好文件类型识别和编码检测。我踩过最大的坑就是编码问题一份 GBK 编码的中文 txt用 UTF-8 去读直接乱码后面所有处理都白费。所以接入层必须做编码嗅探常用的方案是用chardet或charset-normalizer做检测然后统一转成 UTF-8。识别层负责判断文件的实际格式。这里有个常见误区不能只看扩展名。我遇到过.txt文件里装的是 HTML 内容也遇到过.md文件其实是纯文本。更可靠的做法是内容嗅探读取文件头部若干字节用魔数或特征模式来判断真实格式。对于文本类文件还可以用启发式规则比如检测是否包含 Markdown 语法特征#开头、[]()链接、|表格等。转换层是核心负责把各种格式统一转成 Markdown。txt 转 Markdown 需要做结构推断PDF 转 Markdown 需要做版面分析HTML 转 Markdown 需要做标签映射。这一层的设计原则是插件化每种格式一个转换器统一接口方便扩展。输出层负责把 Markdown 文本和元数据一起写入下游存储。元数据包括来源文件路径、转换时间、原始格式、字符数、预估 token 数等。这些元数据在后续检索和溯源时非常有用。2. 纯文本 txt 的结构化解析实战2.1 编码检测与文本清洗的完整流程处理 txt 文件的第一步永远是编码检测。我见过太多人直接open(file, r)然后被UnicodeDecodeError教做人。正确的做法是先用二进制模式读取然后做编码嗅探。import chardet def detect_encoding(file_path, sample_size100000): with open(file_path, rb) as f: raw f.read(sample_size) result chardet.detect(raw) return result[encoding], result[confidence]这里有个细节chardet对短文本的检测准确率不高所以采样量要足够大。我的经验是至少读 100KB如果文件本身小于 100KB 就全读。另外chardet返回的编码名可能和 Python 的编解码器名称不完全一致比如它可能返回GB2312而实际内容是GBK需要做一个映射表来兼容。检测到编码后读取内容并统一转成 UTF-8。这里要注意 BOM 的处理UTF-8 with BOM 的文件开头会有\ufeff字符如果不处理会污染第一个文本块。用utf-8-sig编码读取可以自动去掉 BOM。文本清洗是下一步。原始 txt 里常见的噪音包括连续空行、行尾空格、制表符和空格的混用、不可见控制字符。清洗策略要克制不要过度清洗导致有意义的内容被删掉。我的原则是只清理确定无意义的字符保留所有可能携带语义的格式信息。比如连续三个以上空行可以压缩成两个但单个空行要保留因为它可能代表段落分隔。2.2 基于规则的标题与段落识别txt 文件没有显式的标题标记但人类写的文档通常有隐式的结构线索。我们需要用规则来推断这些结构。最常见的标题模式有几种数字编号标题如“1. 引言”、“1.1 背景”、中文编号标题如“第一章”、“第一节”、全大写或全中文加粗标题在纯文本中通常表现为单独一行且前后有空行、以及用特殊符号装饰的标题如“ 概述 ”。我通常用一组正则表达式来匹配这些模式import re HEADING_PATTERNS [ (r^#{1,6}\s(.)$, markdown), # 已经是 Markdown 标题 (r^(\d\.)\s(.)$, numbered), # 1.1 这种编号 (r^第[一二三四五六七八九十百][章节部分]\s*(.*)$, chinese), # 第X章 (r^[A-Z][A-Z\s]{3,}$, uppercase), # 全大写行 ]匹配到标题后还要推断标题层级。数字编号的层级可以从编号的点分深度来判断“1”是一级“1.1”是二级“1.1.1”是三级。中文编号则按“章 节 部分”的顺序映射。这里有个坑有些文档的编号不连续比如从“1”直接跳到“3”这时候不能假设层级只能按编号深度来。段落识别相对简单连续的非空行组成一个段落空行分隔段落。但要注意一种特殊情况有些 txt 是硬换行的即每行末尾都有换行符但语义上属于同一段。这种需要做行合并如果一行末尾没有句号、问号、感叹号等结束标点且下一行开头不是标题模式就把两行合并。2.3 列表、表格与代码块的启发式转换列表的识别主要靠前缀符号-、*、、•、·以及数字加点的形式。但这里有个歧义一个以-开头的行可能是列表项也可能是分隔线还可能是普通文本中的破折号。我的判断逻辑是如果连续多行都以相同符号开头且符号后有空格就判定为列表。单行出现的-开头行需要结合上下文判断。表格的识别是 txt 转 Markdown 中最难的部分。纯文本表格通常用空格或制表符对齐或者用|分隔。对于|分隔的表格直接按|切分再补上 Markdown 的表头和分隔行即可。对于空格对齐的表格需要检测列对齐模式找出多行中空格出现的位置是否一致如果一致就按这些位置切分列。def detect_space_aligned_table(lines): # 找出所有行中空格的位置 space_positions [] for line in lines: positions [i for i, c in enumerate(line) if c ] space_positions.append(set(positions)) # 取交集交集位置就是列分隔点 common set.intersection(*space_positions) if space_positions else set() return sorted(common)代码块的识别靠缩进或围栏标记。如果连续多行都有相同的缩进通常是 4 个空格或 1 个制表符且这些行看起来像代码包含{}、()、、;等符号就判定为代码块。如果原文有围栏直接保留即可。注意启发式规则永远会有误判。我的做法是给每个转换结果打一个置信度分数低置信度的转换结果标记出来后续可以人工抽检。不要追求 100% 自动化的完美转换那是不现实的。3. Markdown 结构化解析与元数据提取3.1 Markdown 语法树解析的核心要点Markdown 虽然语法简单但解析起来并不简单因为它的语法有大量边界情况和方言差异。比如#后面有没有空格、*和_的嵌套规则、列表的缩进规则等不同解析器行为可能不一致。我的建议是使用成熟的解析库Python 生态里markdown-it-py和mistune都是不错的选择。markdown-it-py遵循 CommonMark 规范解析结果稳定mistune性能更好适合大批量处理。选哪个取决于你的场景如果对规范一致性要求高选markdown-it-py如果追求吞吐量选mistune。解析的目标是得到一棵语法树每个节点代表一个结构元素标题、段落、列表、代码块、表格、引用等。有了语法树后续的切块和元数据提取就有了依据。from markdown_it import MarkdownIt md MarkdownIt() tokens md.parse(markdown_text) def walk_tokens(tokens, depth0): for token in tokens: if token.type heading_open: print( * depth fHeading level {token.tag}) elif token.type inline: print( * depth fText: {token.content[:50]}) # 递归处理子 token这里的关键是理解 token 的嵌套结构。Markdown 的 token 流是扁平的但通过_open和_close配对可以还原出树形结构。标题是heading_openinlineheading_close三个 token 组成一组列表是bullet_list_open包裹多个list_item_open。3.2 标题层级与文档大纲的自动构建从语法树中提取标题层级就能构建出文档的大纲树。这棵大纲树是后续语义切块的基础。构建大纲树的逻辑是维护一个栈遇到标题时如果当前标题层级比栈顶高就压栈如果比栈顶低就弹栈直到找到合适的父节点。最终每个标题节点都挂载了它下属的内容块。class OutlineNode: def __init__(self, level, title): self.level level self.title title self.children [] self.content [] def build_outline(tokens): root OutlineNode(0, root) stack [root] for token in tokens: if token.type heading_open: level int(token.tag[1]) # 弹栈直到找到层级更小的父节点 while stack[-1].level level: stack.pop() node OutlineNode(level, ) stack[-1].children.append(node) stack.append(node) elif token.type inline and stack[-1].level 0: if not stack[-1].title: stack[-1].title token.content else: stack[-1].content.append(token.content) return root这棵大纲树的价值在于切块时可以按标题边界切保证每个块都在同一个标题下不会跨章节。同时每个块都可以带上它的标题路径作为元数据比如“第3章 3.2节 3.2.1小节”这个路径在检索时可以作为强力的过滤条件。3.3 元数据提取与增强检索的关联Markdown 解析不仅能得到结构还能提取丰富的元数据。这些元数据在 RAG 检索阶段能发挥巨大作用。Front Matter是 Markdown 文件头部的 YAML 元数据块通常包含标题、作者、日期、标签等信息。解析 Front Matter 可以直接得到结构化的元数据这些信息应该附加到该文档的所有文本块上。链接和图片也是重要的元数据。文档中引用的外部链接可以提取出来作为该块的“相关资源”图片的 alt 文本可以作为该块的补充描述。我试过在检索时把图片 alt 文本也纳入向量化范围对于图文混排的文档召回率有明显提升。代码块的语言标记同样有价值。如果用户问的是编程问题检索时优先召回带对应语言标记的代码块准确率会高很多。表格的结构化数据可以单独提取出来转成 JSON 或 CSV 存储。有些问题用表格数据直接回答比用文本生成更准确比如“某产品的参数是多少”这类问题。元数据类型提取方式检索增强用途Front MatterYAML 解析文档级过滤、来源溯源标题路径大纲树遍历层级过滤、上下文补充链接正则/AST相关资源推荐图片 altAST 提取多模态检索补充代码语言围栏标记按语言过滤表格数据AST 提取结构化问答提示元数据不是越多越好。我见过有人把文件大小、修改时间、inode 号都塞进元数据结果向量库的 payload 膨胀到影响性能。只保留对检索有实际帮助的元数据其他的放到外部数据库按需关联。4. 从解析结果到 RAG 就绪数据的完整链路4.1 语义切块策略与参数计算切块是数据导入的最后一公里也是最容易出问题的地方。切块太大检索精度下降因为一个块里混了太多主题切块太小上下文丢失模型无法理解。找到平衡点是关键。我的切块策略是结构优先语义兜底。具体来说第一步按 Markdown 的标题层级做粗切。每个最小标题单元比如三级标题下的内容作为一个候选块。这样切出来的块天然有语义边界。第二步对超长的候选块做细切。如果一个块超过max_chunk_size就按段落边界继续切。段落边界比句子边界好因为段落是完整的语义单元。第三步对过短的候选块做合并。如果相邻两个块都属于同一个父标题且合并后不超过max_chunk_size就合并。参数怎么定max_chunk_size取决于你的 Embedding 模型的最大输入长度和检索粒度需求。以常见的 512 token 模型为例我通常设max_chunk_size400留出余量给元数据和特殊 token。min_chunk_size设为 100低于这个值的块要么合并要么丢弃。def semantic_chunk(outline_node, max_size400, min_size100): chunks [] for child in outline_node.children: text \n.join(child.content) if len(text) max_size: if len(text) min_size: chunks.append({ text: text, heading_path: get_heading_path(child), level: child.level }) else: # 太短尝试与兄弟节点合并 pass else: # 太长按段落切分 paragraphs text.split(\n\n) current for p in paragraphs: if len(current) len(p) max_size: current p \n\n else: if current: chunks.append({...}) current p \n\n if current: chunks.append({...}) return chunks这里有个容易被忽略的点重叠窗口。相邻块之间保留一定的重叠通常 10%-20%可以避免关键信息恰好落在切分边界上导致丢失。但重叠也不能太多否则检索时会召回大量重复内容浪费上下文窗口。4.2 批量导入的性能优化与错误处理生产环境的数据导入往往是几万到几十万个文件性能是必须考虑的问题。我的优化经验有这么几条并行处理。文件解析是 IO 密集型和 CPU 密集型混合的任务用多进程池可以显著提速。但要注意如果下游是向量化 API并发太高会触发限流。我的做法是解析阶段用多进程向量化阶段用异步加信号量控制并发。增量导入。不要每次都全量重跑。记录每个文件的哈希值和修改时间只处理新增和变更的文件。这能把日常导入的耗时从小时级降到分钟级。断点续传。批量导入过程中难免有文件解析失败不能让一个坏文件中断整个任务。每个文件独立处理失败记录到错误日志继续处理下一个。最后统一重试失败的文件。import hashlib from concurrent.futures import ProcessPoolExecutor def file_hash(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def batch_import(file_paths, state_db): to_process [] for path in file_paths: h file_hash(path) if state_db.get(path) ! h: to_process.append(path) with ProcessPoolExecutor(max_workers8) as executor: results executor.map(process_file, to_process) for path, result in zip(to_process, results): if result.success: state_db[path] file_hash(path) else: log_error(path, result.error)错误处理要分级别编码错误、解析错误、切块错误分别记录方便定位问题。对于编码错误可以尝试用errorsreplace强制读取虽然会有乱码但至少不会丢文件对于解析错误可以降级到纯文本模式放弃结构信息但保留内容。4.3 导入质量校验与常见陷阱导入完成后必须做质量校验否则你可能在错误的道路上跑很久才发现问题。我通常检查这几个指标块长度分布。如果大量块的长度集中在max_chunk_size附近说明切块策略太粗暴可能切断了语义。如果大量块低于min_chunk_size说明结构识别有问题把不该切的地方切了。标题覆盖率。统计有多少块带有标题路径元数据。如果覆盖率很低说明标题识别规则没生效需要调整正则。空块和重复块比例。空块通常是清洗不彻底导致的重复块可能是重叠窗口设置过大。抽样人工检查。随机抽 20-30 个块人工看一遍。这是最有效但也最容易被跳过的一步。我每次导入新数据源都会做抽样几乎每次都能发现自动化指标看不出来的问题。常见陷阱我列几个印象最深的第一个是表格跨页。PDF 转 Markdown 时跨页的表格会被切成两个表头丢失。需要在转换后做表格合并检测。第二个是代码块误判。有些文档的正文缩进和代码块缩进一样导致正文被误判为代码。解决办法是结合上下文判断如果缩进行前后都是普通段落就不判定为代码。第三个是列表嵌套丢失。txt 转 Markdown 时嵌套列表的缩进层级容易丢失导致所有列表项都变成同级。需要在转换时保留原始缩进信息。第四个是特殊字符转义。Markdown 中的*、_、[、]等字符有特殊含义如果原文包含这些字符需要转义否则会破坏 Markdown 结构。但转义过度又会影响可读性需要权衡。注意质量校验不是一次性的应该做成持续监控。每次导入后自动跑一遍校验脚本指标异常时告警。我吃过亏有一次上游数据源格式变了导入的块全是乱的过了两周才发现不得不全量重跑。5. 常见问题排查与实操避坑指南5.1 编码与乱码问题的系统排查乱码是文本导入的头号杀手而且表现形式多样排查起来需要系统方法。症状一全部乱码。通常是编码检测错误。排查步骤用十六进制编辑器看文件头几个字节判断是否有 BOM用chardet检测并打印置信度如果置信度低于 0.7 就要警惕尝试用常见编码UTF-8、GBK、GB18030、Big5分别解码看哪个能解出可读文本。症状二部分乱码。通常是混合编码即文件里既有 UTF-8 又有 GBK 的内容。这种情况最难处理我的做法是逐行检测编码按行解码后再拼接。虽然慢但能最大程度保留内容。症状三特殊符号乱码。比如引号变成“这是 UTF-8 被误读为 Latin-1 的典型表现。解决办法是先用 Latin-1 编码回去再用 UTF-8 解码。def fix_mojibake(text): try: return text.encode(latin-1).decode(utf-8) except (UnicodeEncodeError, UnicodeDecodeError): return text症状四零宽字符和不可见字符。这些字符肉眼看不见但会干扰后续处理。用正则[\u200b-\u200f\ufeff]可以匹配并清除。5.2 结构识别失败的典型场景与修复结构识别失败的表现是标题没被识别、列表变成了普通段落、表格散架了。每种情况都有对应的修复策略。标题识别失败的常见原因是标题格式不在预设规则内。比如有些文档用【标题】这种中文方括号有些用 标题这种箭头。解决办法是收集足够多的样本不断补充正则规则。我维护了一个规则库每遇到一种新格式就加一条现在已经有二十多条规则了。列表识别失败通常是因为列表符号不标准。比如用→或·作为列表符号。这种情况需要扩展列表符号的匹配范围。另一个原因是列表项跨行即一个列表项的内容分成了多行第二行没有列表符号。这需要做行合并判断。表格识别失败最常见于空格对齐的表格。如果列之间的空格数量不一致对齐检测就会失败。我的改进方案是用聚类代替精确匹配把所有行的空格位置做聚类取聚类中心作为列分隔点允许一定误差。from sklearn.cluster import KMeans import numpy as np def cluster_columns(lines, n_cols): all_positions [] for line in lines: positions [i for i, c in enumerate(line) if c ] all_positions.extend(positions) if not all_positions: return [] X np.array(all_positions).reshape(-1, 1) kmeans KMeans(n_clustersn_cols-1, n_init10).fit(X) return sorted(kmeans.cluster_centers_.flatten().astype(int))5.3 大批量导入的性能瓶颈定位当导入速度慢到无法接受时需要定位瓶颈在哪。我用分段计时的方法在接入、识别、转换、切块、向量化每个阶段打时间戳统计各阶段耗时占比。常见的瓶颈和优化手段瓶颈阶段典型症状优化手段文件读取IO 等待高用 SSD、批量读取、异步 IO编码检测CPU 占用高采样检测、缓存检测结果Markdown 解析单核跑满多进程并行、换更快的解析器向量化网络等待高批量请求、异步并发、本地模型向量库写入写入慢批量 upsert、调整索引参数我遇到过一次典型的性能问题导入 10 万个文件耗时 8 小时分段计时后发现 70% 时间花在向量化 API 调用上。优化方案是把单条请求改成批量请求每批 100 条耗时直接降到 1.5 小时。后来又发现向量库的索引构建是瓶颈调整了 HNSW 的参数后进一步降到 40 分钟。提示性能优化要先测量再优化不要凭感觉。我见过有人一上来就上多进程结果发现瓶颈在数据库写入多进程反而因为锁竞争更慢了。5.4 导入后检索效果不佳的归因方法导入完成后检索效果不好问题可能出在导入阶段也可能出在检索阶段。需要系统归因。第一步检查召回内容。把检索到的原始块打印出来看内容是否相关。如果不相关问题在检索阶段Embedding 模型或索引参数如果相关但生成的答案不好问题在生成阶段Prompt 或模型。第二步检查块质量。如果召回的块内容相关但语义不完整比如一句话被切断了问题在切块策略。调整max_chunk_size和重叠窗口。第三步检查元数据。如果检索时无法按来源过滤或者无法按标题层级过滤问题在元数据提取。补充缺失的元数据字段。第四步检查覆盖率。如果某些文档的内容完全检索不到可能是这些文档在导入时被跳过了或者切块后块太小被过滤了。检查导入日志和块长度分布。我总结了一个归因速查表现象可能原因排查方向召回内容不相关Embedding 质量差换模型、微调召回内容相关但答案差块语义不完整调整切块策略部分文档检索不到导入遗漏或块太小检查导入日志无法按来源过滤元数据缺失补充元数据重复召回同一内容重叠窗口过大减小重叠比例长文档检索效果差块太大主题混杂减小 max_chunk_size这套归因方法我用了很多次基本能在半小时内定位到问题所在。关键是要有完整的日志和可观测性否则就是盲人摸象。6. 工程化落地的经验沉淀6.1 配置化与可扩展的管线设计数据导入管线最忌讳写死。不同数据源、不同文档类型、不同业务场景需求差异很大。我的做法是把所有可变部分做成配置。配置分三层全局配置定义默认参数比如max_chunk_size、overlap_ratio、encoding_fallback数据源配置针对特定来源覆盖参数比如某个目录下的文件都是 GBK 编码就单独配置文件级配置针对特殊文件做定制比如某个 PDF 需要特殊的版面分析参数。global: max_chunk_size: 400 min_chunk_size: 100 overlap_ratio: 0.15 encoding_fallback: [utf-8, gbk, gb18030] sources: - path: /data/tech_docs encoding: utf-8 chunk_size: 500 - path: /data/legacy_txt encoding: gbk chunk_size: 300可扩展性体现在转换器的插件化。每个转换器实现统一的接口can_handle(file) - bool和convert(file) - Markdown。新增一种格式只需要加一个转换器不用改主流程。6.2 导入日志与可观测性建设没有日志的导入管线就是黑盒。我要求日志至少记录这些信息每个文件的处理状态成功/失败/跳过、耗时、字符数、块数、编码、格式、错误信息。这些日志汇总后可以生成报表一眼看出导入健康度。import logging import json logger logging.getLogger(rag_import) def log_import(file_path, status, duration, char_count, chunk_count, errorNone): record { file: file_path, status: status, duration_ms: duration, chars: char_count, chunks: chunk_count, error: str(error) if error else None } logger.info(json.dumps(record, ensure_asciiFalse))日志用 JSON 格式方便后续用 ELK 或类似工具做聚合分析。关键指标包括成功率、平均耗时、平均块大小、编码分布、格式分布。这些指标做成仪表盘导入异常时能第一时间发现。6.3 增量更新与版本管理策略生产环境的文档是不断更新的全量重跑不现实。增量更新需要解决两个问题识别变更和处理删除。识别变更用文件哈希加修改时间双重判断。哈希变了说明内容变了修改时间变了但哈希没变说明只是 touch 了一下不需要重新处理。处理删除稍微复杂。如果源文件被删了对应的向量也应该删掉。我的做法是维护一个文件到块 ID 的映射表文件删除时根据映射表删除对应的向量。但要注意如果多个文件的内容有重叠删除一个文件不应该影响另一个文件的检索结果。所以映射表要精确到块级别。版本管理方面我建议保留最近 N 个版本的块检索时默认只搜最新版本但支持按版本过滤。这样既能保证检索到最新内容又能在需要时回溯历史。6.4 从单机脚本到生产服务的演进路径很多 RAG 项目都是从单机脚本开始的一个 Python 文件跑完全流程。但随着数据量增长和需求复杂化必须演进到生产服务。第一阶段单机脚本。适合 POC 和小数据量特点是简单直接缺点是没法并行、没法监控、没法增量。第二阶段模块化管线。把接入、解析、切块、向量化拆成独立模块用消息队列串联。每个模块可以独立扩展和部署。这个阶段解决了并行和增量问题。第三阶段服务化。把管线封装成 API 服务支持按需触发和定时调度。加上任务队列、重试机制、监控告警。这个阶段解决了运维和可观测性问题。第四阶段平台化。提供 Web 界面配置数据源和参数支持多租户支持 A/B 测试不同的切块策略。这个阶段适合有多团队协作的大型组织。我个人的建议是不要过度设计。大部分项目到第二阶段就够了第三阶段按需演进。我见过有人一上来就搞平台化结果三个月没跑通一个数据源得不偿失。6.5 我踩过的那些坑与最终建议最后分享几个我实际踩过的坑都是血泪教训。坑一忽略文件权限。批量导入时遇到没有读权限的文件整个任务崩溃。后来加了权限检查无权限的文件跳过并记录。坑二符号链接循环。目录里有指向父目录的符号链接递归遍历时无限循环。后来加了 inode 去重和最大深度限制。坑三超大文件。一个 2GB 的日志文件读进内存直接 OOM。后来加了文件大小限制超过阈值的文件流式处理或跳过。坑四并发写入冲突。多进程同时写向量库导致部分数据丢失。后来改成单进程写入或者用支持并发写的向量库。坑五忽略时区。文件修改时间没带时区增量更新时判断错误。后来统一用 UTC 时间戳。这些坑看起来都是小问题但每一个都可能导致导入失败或数据错误。我的最终建议是把数据导入当成一个正式的数据工程项目来做而不是一个临时脚本。投入在导入阶段的每一分精力都会在检索和生成阶段得到回报。数据质量是 RAG 系统的地基地基不牢上面盖什么都是危房。