拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

RAG数据导入全攻略:从txt到Markdown的结构化解析

RAG数据导入全攻略:从txt到Markdown的结构化解析

1. 为什么数据导入与解析在我这永远是 RAG 项目的第一优先级

1.1 一句大白话:RAG 是给模型配了一位图书馆管理员

这几年我落地过的 RAG 项目不算少,有一个规律特别明显:每次迭代一开始大家喜欢调模型、改 prompt、换 vector store,折腾到最后才发现,真正决定问答质量的往往是“喂进去的文档到底被解析成什么样了”。RAG 的原理听起来很简单——把资料切碎、向量化、塞进向量库,检索时把相关片段捞出来拼给模型。但“把资料切碎”这四个字落地到真实数据上,就是一场没完没了的格式战争。

我喜欢把 RAG 比作图书馆的借阅流程:模型是读者,向量库是书架,解析器是图书管理员。管理员如果把书页撕得稀烂、目录标错,读者翻遍几层书架也找不到想要的信息,模型就只能一本正经地胡说八道。所以我现在越来越笃定一个观点:RAG 项目里的坑,八成不在推理侧,而在数据导入与解析侧。这个系列的标题叫“RAG 数据导入与解析全攻略”,今天先把第一块硬骨头解决掉:从 txt 到 Markdown 的通用文本与结构化解。

1.2 为什么第一篇文章就盯上 txt 和 Markdown

很多人一听“RAG 数据导入”,第一反应是解析 PDF、Word、扫描件,甚至直接上多模态模型做 OCR。我通常劝他们先把 txt 和 Markdown 这种最朴素格式处理明白。原因很实在:

  • txt 和 Markdown 足够轻,没有 PDF 那一堆坐标、字体、图层、扫描页干扰;
  • 几乎所有 RAG 框架自带的分块器,核心都是围绕纯文本和 Markdown 结构设计的;
  • 那些看起来高级的格式,最后基本也要先转成纯文本或类 Markdown 结构再进分块环节;
  • 编码清洗、换行归一化、标题识别、表格边界这些底层问题,在 txt 上最容易暴露,也最容易一次学透。

把这条通用文本链路打通,你已经能覆盖企业知识库里相当大一部分场景了。非要一上来啃 PDF 双栏排版,大概率是被“复杂”两个字带跑偏,先练基本功反而走得快。

1.3 这一篇解决什么、解决不了什么

这篇要解决三件事。第一,把一份编码不明、换行混乱、夹杂乱码的 txt,变成一份结构清晰的 Markdown。第二,从纯文本里识别出标题、列表、表格、代码块这些语义边界,而不是让它们糊成一片普通文本。第三,转换完之后,天然衔接后续向量化分块,让切出来的 chunk 不至于语义稀碎。

至于 PDF 版式还原、图片理解、OCR、多模态入库这些,属于系列后面几期的内容,这篇先不展开。先把通用文本这条线吃透,你会发现后续遇到“看起来复杂”的格式时,很多思路是共通的。

2. 通用文本与结构化到底在解什么

2.1 “解析”不是把文件读成字符串就完事

“解析”这个词在 RAG 圈子里被用得太泛了。很多同学觉得,用open()把文件读成一个字符串,就是解析完成。实际上一次合格的解析至少得经过四层处理:

  • 编码层:搞清楚文件是 UTF-8、GBK 还是 UTF-16,把字节流变成正确的字符;
  • 版式层:处理换行、空行、缩进,把“视觉上被拆成多行”的连续文字恢复成真实段落;
  • 结构层:识别标题、列表、表格、代码块、引用块的边界;
  • 语义层:为标题编号、章节层级、文档来源、术语注释等附加元信息。

如果只做前两层,你拿到的是“能读但不懂”的文本;只有做到第三层和第四层,文本才真正变成可以被分块、被检索、被模型利用的结构化数据。这一篇重点展开第三层,顺带把第四层的一些接口留出来,因为后续向量化分块正好需要用。

2.2 为什么马克标记成了“通用”中间格式

我见过不少团队折腾自研中间表示,什么 XML 树、JSON 嵌套结构,最后维护成本都高得离谱。Markdown 之所以是理想的通用中间格式,是因为它刚刚好卡在“人可读”和“机器可解析”之间:

  • 语法足够少,只有十几种模式,看到#知道是标题,看到连续用|连起来的行大概率是表格;
  • 表达力足够覆盖知识库的多数需求,标题层级、列表、表格、代码块、引用、链接、图片全有现成语法;
  • 主流工具对 Markdown 有原生偏好,LangChain 的MarkdownHeaderTextSplitter、LlamaIndex 的MarkdownNodeParser默认你喂给它的就是合法 Markdown;
  • 它可以随时降级回纯文本,也可以升级到 HTML、LaTeX 或 PDF,转换成本极低。

换句话说,Markdown 就是 RAG 数据管道里的普通话。不管原始文件说的什么方言,先翻译成普通话,后面所有环节都好对接。

2.3 结构化不是形式主义,是给模型划重点

很多人看到“结构化”就联想到 JSON、CSV、数据库表,觉得必须把文档拆成一堆字段才算完。我个人的理解是,RAG 场景下的结构化,核心目的是让检索器和模型知道“信息的边界在哪里”。对文本来说,边界就是这些问题:

  • 这一段是普通正文,还是一段代码?
  • 这个表格是三行五列,还是被错误切成了零散文本?
  • 这个“1.1”是二级标题,还是正文里随手写的序号?

如果这些边界识别对了,检索器召回时就能精准命中“包含完整上下文”的片段,而不是把代码片段、表格碎片、标题行混在一起扔给模型。打个比方:文本里的结构就是地图上的地标和分界线,没地标的连续文本,模型检索的时候只能靠猜,猜输的概率非常高。

3. 实操第一步:先解决编码、换行和隐藏字符

3.1 编码检测与统一,别信“看起来正常”

处理企业 txt 的真实情况,第一个坎就是字符编码。同一个“产品FAQ.txt”,可能是 ANSI(GBK)、UTF-8、UTF-8 with BOM,偶尔还有 UTF-16。如果你直接用utf-8硬读,报错其实算运气好的,更常见的是读出满屏乱码,然后你把乱码又做了清洗和分块,数据早就被污染了。

我推荐的做法是,在进入任何解析逻辑之前,先做一次多层尝试。核心思路是准备一条 fallback 链:先试 UTF-8,失败后试 GB18030,再试 UTF-16,最后用 Latin-1 兜底保证不会中断流水线。

from pathlib import Path def read_text_file(path: Path) -> str: raw = path.read_bytes() candidates = [] if raw.startswith(b'\xef\xbb\xbf'): candidates = ['utf-8-sig'] elif raw.startswith(b'\xff\xfe') or raw.startswith(b'\xfe\xff'): candidates = ['utf-16'] else: candidates = ['utf-8', 'gb18030', 'utf-16', 'latin-1'] for enc in candidates: try: return raw.decode(enc) except UnicodeDecodeError: continue return raw.decode('utf-8', errors='replace')

这里有个细节:utf-8-sig和utf-8是有区别的。前者会吃掉文件开头的 BOM,后者会把不可见的\ufeff一并读进来。BOM 留在文本开头,进了向量库以后就是个奇怪的干扰项,某些模型还可能把它当特殊 token。所以我倾向于用utf-8-sig专门处理带 BOM 的文件。

补充一句,chardet这类工具我偶尔也用,但它对短文本和中文编码的误判率不低,我更多把它当作“候选来源”而不是最终结论。完全依赖它,等于是把命运交给出题人。

3.2 换行符归一化,以及最头疼的“假换行”

Windows 导出的文本默认\r\n,Linux 和 macOS 是\n,老式 Mac 还有单独\r。不统一换行符,后面按行解析时你会看到一堆\r残留在字符串里,正则匹配行首行尾全部要踩坑。先做一次机械归一化:

def normalize_newlines(text: str) -> str: return text.replace('\r\n', '\n').replace('\r', '\n')

机械归一化简单,真正麻烦的是“假换行”。很多 txt 是从 PDF、网页或 OCR 工具里复制出来的,为了排版每行末尾都加了换行,但语义上它是一段连续的话。这种文件如果不处理,直接按行分块,每一行只有十几个字,检索时不仅召回割裂,embedding 质量也差。

我的经验是先用空行划分大段落,没有空行时启用启发式规则:如果当前行以中文句号、问号、感叹号结尾,允许断行;如果当前行以逗号、分号、冒号结尾,大概率合并到下一行;如果下一行以大写字母或中文序号开头,保留断行。实际操作里,我特别强调一个原则:原始文档如果已经有空行,就先尊重它的段落边界;只有在通篇没有空行的地方,才动用行尾标点规则合并。这样能最大限度避免误伤原本正常的文档结构。

3.3 清掉看不见的隐藏字符

还有一类问题容易被忽略:全角空格、不间断空格\xa0、零宽空格\u200b、软连字符\u00ad,以及各种控制字符。这些字符屏幕上几乎看不见,但进入字符串后会干扰正则、影响 token 统计,甚至让向量化产生无用噪声。

我习惯在规范化阶段统一处理:

def clean_invisible_chars(text: str) -> str: text = text.replace('\xa0', ' ').replace('\u200b', '').replace('\ufeff', '') return ''.join(ch for ch in text if ch.isprintable() or ch in '\n\t')

注意,isprintable()会把换行和制表符也过滤掉,所以必须显式保留\n和\t。隐藏字符清掉之后,后面写正则匹配段落、表格和代码块的时候会省心很多。

4. 从 txt 到 Markdown 的关键转换环节

4.1 先定清楚要识别哪些结构

不管你是用正则、手写状态机,还是用语言模型做解析,第一步都得明确目标结构清单。我给通用文本定的清单是这六类:

  • 标题:以#、数字编号(1.1、第一章、一、)开头的行,按层级转成#到######;
  • 列表:以-、*、+、1.开头的行,保留嵌套缩进;
  • 表格:连续多行用|或 Tab 分隔、字段数量固定的内容,转成 Markdown 表格语法;
  • 代码块:围栏(```)包裹或连续缩进的内容,转成围栏代码块;
  • 引用:以>开头的连续行,转成引用块;
  • 链接和图片:裸 URL、[文字](地址)、图片说明,尽量保留下标信息。

很多 txt 是纯手写笔记,一点 Markdown 痕迹都没有,那么前四类全靠启发式猜测。我的经验是:宁可少转,不要错转。一个错误识别的表格或代码块,对后续分块和检索的破坏性,远大于“继续当普通段落”处理。

4.2 标题识别与层级还原的几种套路

标题识别是纯文本转 Markdown 里最重要的环节,因为标题层级直接决定了后续分块边界。常见情况分三种:

  • 原文本身就是 Markdown,只被存成了 .txt 后缀,这种用^#{1,6}\s直接匹配就能拿层级;
  • 原文是中文排版常见的编号标题,比如“一、二、三”“1.1”“第一章”,需要启发式规则判断层级;
  • 原文完全没有标题标记,只能靠字体大小、加粗、编号推断,最费劲,我一般建议配合人工抽查。

具体实现上,我通常先跑 Markdown 标题正则,再跑编号标题启发式:

import re MD_HEADING = re.compile(r'^(#{1,6})\s+(.*)$') NUM_HEADING = re.compile(r'^(\d{1,2}(?:\.\d{1,2}){0,2})\s+(.{4,})$') CN_HEADING = re.compile(r'^(第[一二三四五六七八九十百千]+[章篇节]|[一二三四五六七八九十]+[、.)])\s*(.{2,})$') def detect_heading(line: str): m = MD_HEADING.match(line) if m: return len(m.group(1)), m.group(2).strip() m = NUM_HEADING.match(line) if m: num = m.group(1) level = num.count('.') + 1 if num.replace('.', '').isdigit() else 1 return level, f"{num} {m.group(2).strip()}" m = CN_HEADING.match(line) if m: return 1, line.strip() return None, None

这里有一个取舍我明确说一下:编号类标题,我会把“3.2 环境配置”中的编号也保留在标题文本里。好处是用户搜索“环境配置”能命中,搜索“3.2”也能命中;坏处是某些模型喜欢反复引用编号,让答案看起来啰嗦。对大多数知识库场景,保留编号利大于弊,尤其适合需要引用出处的系统。

4.3 列表与表格:看着简单,坑不少

列表识别的坑主要在嵌套层级和误伤编号段落。比如这种:

1. 打开设置 2. 选择网络 请确保网络畅通后再继续

“请确保网络畅通后再继续”和上面两行之间没有空行,按列表规则它会被当成正文,倒是没问题;但如果规则写得太宽,把“1.”后面跟的普通段落也误判成列表,分块时就麻烦了。我的建议是:列表识别要求连续两行以上才认定为列表块,单行孤立的-或1.不要急着转,先看上下文。

表格识别比列表更麻烦。很多“txt 表格”其实是从 Excel、HTML 或 PDF 里复制出来的,分隔符五花八门:有竖线|、有空格对齐、有 Tab、有制表符网格。我给出一个实用的优先级策略:先处理“竖线分隔 + 第二行是---分隔线”的标准 Markdown 表格,然后处理 Excel 复制出来的 Tab 分隔文本,最后才用空格对齐去猜。

Tab 分隔文本转表格相对规则化:

def tab_to_markdown_table(block: str): rows = [line.split('\t') for line in block.strip().splitlines()] if not rows or len(rows) < 2: return None col_count = len(rows[0]) if any(len(row) != col_count for row in rows): return None header = rows[0] sep = ['---'] * col_count body = rows[1:] return '\n'.join([ '| ' + ' | '.join(header) + ' |', '| ' + ' | '.join(sep) + ' |', *['| ' + ' | '.join(row) + ' |' for row in body], ])

空格对齐的表格,我反而建议人工处理掉。原因很简单:对齐信息一旦丢失,无法判断哪些连续空格是“列分隔”、哪些只是“排版留白”。靠正则强行猜,十次里能有四次把相邻列内容拼到一起,得不偿失。

4.4 代码块与转义,两个最容易翻车的地方

先说代码块。很多 txt 里粘贴的代码是整体缩进了 4 个空格,符合老式 Markdown 的缩进代码块规范,但缩进内容一旦混入普通段落,容易被列表识别误伤。我的做法分三层递进:

  1. 优先识别围栏代码块,也就是以```开头和结尾的内容,原样保留;
  2. 其次识别“连续缩进且包含明显编程符号({}、;、def、import)”的行组,转成围栏代码块;
  3. 最后才考虑纯缩进转代码块。

这样做能显著减少误判:正常中文正文里极少出现def、import这种前缀,所以误伤概率很低。

再说转义。这是新手最容易“过度处理”的地方。我见过一个同学把整篇文档里所有*、_、[都加上反斜杠,结果标题和链接全被破坏了,整个 Markdown 看起来像被反斜杠弹幕刷屏。

我的转义原则很简单:只在“普通段落”里对必要的字符做转义,标题、链接地址、表格单元格、代码块内部一律不动。具体判断依据有三个:

  • 如果字符前后都是空格或标点,大概率是普通符号,转义;
  • 如果它和前后文字组成词,比如c++、C#,不要转,转了反而影响阅读;
  • 如果它在 URL、邮箱、文件路径里,跳过。

这说明一个现实:纯正则是处理不了所有歧义的。所以我的最终建议是,第一版转换脚本先做保守处理,不转义,只做结构识别;跑完一轮人工抽查,再针对出现的问题追加转义规则。不要试图一步到位。

4.5 保命设计:识别失败就当普通段落

无论你的启发式规则写得多么周密,总有文本会出人意料。所以转换器必须留一个兜底逻辑:识别不出结构的时候,老老实实把它当成普通段落输出,而不是硬塞进某个结构里。我会在每一行判定完结构之后,记录一个分类结果,方便后续统计和定位问题:

def classify_line(line: str) -> str: if detect_heading(line): return 'heading' if is_fenced_code_start(line): return 'code_start' if is_table_row(line): return 'table' if is_list_item(line): return 'list' return 'paragraph'

不要小看这个分类输出。真到线上跑脏数据时,你能快速看到每类结构的数量,比如“表格识别了 300 次”,然后抽查这些位置是不是真的表格。这比写完脚本一跑了之强得多。

5. 实操过程:一个轻量级 txt 到 Markdown 转换脚本

5.1 为什么没直接甩给你一段 LangChain 代码

看到这里你可能会问:LangChain、LlamaIndex 都有现成的 loader,为什么还要自己写解析器?我的回答是:框架的 loader 解决的是标准场景,恰恰解决不了企业脏数据场景。你让TextLoader去读一个 GBK 编码的 txt,默认就是报错;你让MarkdownHeaderTextSplitter去处理一个根本没有 Markdown 标题的 txt,它也无从切起。

所以我倾向于把“格式清洗 + 结构识别”这个环节握在自己手里,后面再对接框架的分块器和向量库。这样两边职责清晰:脏活累活自己干,标准化的分块和检索交给框架,问题定位起来最顺。

听起来工程量不小,其实核心代码很少。我把一个能跑通的生产级脚本拆成两步:文本规范化、结构分类与输出。下面的代码已经能应对绝大多数纯文本知识库场景。

5.2 脚本主流程与结构识别的完整骨架

from pathlib import Path import re # ---------- 第一步:读取与规范化 ---------- def read_text_file(path: Path) -> str: raw = path.read_bytes() candidates = [] if raw.startswith(b'\xef\xbb\xbf'): candidates = ['utf-8-sig'] elif raw.startswith(b'\xff\xfe') or raw.startswith(b'\xfe\xff'): candidates = ['utf-16'] else: candidates = ['utf-8', 'gb18030', 'utf-16', 'latin-1'] for enc in candidates: try: return raw.decode(enc) except UnicodeDecodeError: continue return raw.decode('utf-8', errors='replace') def normalize_text(text: str) -> str: text = text.replace('\r\n', '\n').replace('\r', '\n') text = text.replace('\ufeff', '').replace('\xa0', ' ') text = re.sub(r'\n{3,}', '\n\n', text) return text # ---------- 第二步:结构识别 ---------- MD_HEADING = re.compile(r'^(#{1,6})\s+(.*)$') NUM_HEADING = re.compile(r'^(\d{1,2}(?:\.\d{1,2}){0,2})\s+(.{4,})$') TABLE_ROW = re.compile(r'^\s*\|.*\|\s*$') LIST_ITEM = re.compile(r'^\s*([-*+]|\d{1,3}[.)])\s+') FENCE_START = re.compile(r'^\s*(```+|~~~+)') FENCE_END = re.compile(r'^\s*(```+|~~~+)\s*$') def detect_line(line: str): m = MD_HEADING.match(line) if m: return 'heading', len(m.group(1)), m.group(2).strip() m = NUM_HEADING.match(line) if m: num = m.group(1) level = num.count('.') + 1 if num.replace('.', '').isdigit() else 1 return 'heading', level, f"{num} {m.group(2).strip()}" if FENCE_START.match(line): return 'code_start', 0, line if TABLE_ROW.match(line): return 'table_row', 0, line.strip() if LIST_ITEM.match(line): return 'list_item', 0, line.rstrip() return 'paragraph', 0, line.rstrip() # ---------- 第三步:把行序列组织成 Markdown ---------- def build_markdown(text: str) -> str: lines = text.split('\n') out = [] in_code = False for line in lines: if FENCE_START.match(line) and not in_code: in_code = True out.append(line) continue if in_code: out.append(line) if FENCE_END.match(line): in_code = False continue kind, level, content = detect_line(line) if kind == 'heading': out.append(f"{'#' * level} {content}") elif kind == 'code_start': out.append(content) else: out.append(content) return '\n'.join(out) def convert_txt_to_markdown(src: Path, dst: Path): text = read_text_file(src) text = normalize_text(text) markdown = build_markdown(text) dst.write_text(markdown, encoding='utf-8') print(f"转换完成: {src.name} -> {dst.name}, 源字符数 {len(text)}")

这个版本是“可读大于完整”的。真正上生产,我还会加表格 block 聚合、列表嵌套缩进、目录导出这些功能。但核心思路已经能说明白:先用行级分类器把 Markdown 骨架搭好,再针对特殊情况做修正,最后出来的文本就能进分块环节了。

5.3 转换后必须做一次“三轮检查”

我吃过不少“以为转换完就能向量化”的亏,后来总结成三轮检查法:

第一轮,结构统计。数一数最终 Markdown 里有多少标题、表格、代码块、列表项,和源文件里目测的数量对比。如果文档里明显有 30 个表格,只识别出 5 个,说明表格识别规则漏了。

第二轮,抽样目检。不要全文逐行读,太累了;从开头、中间、结尾各抽 50 行,快速扫一眼标题层级是否连续、表格是否对齐、有没有满屏反斜杠。这一步十分钟内搞定,能拦下 80% 的明显问题。

第三轮,端到端检索验证。把一个最小 RAG 管道跑起来,用几个你心里有标准答案的问题去检索,看命中的 chunk 有没有包含正确上下文。这一步最真实,因为很多解析问题只有检索时才暴露。比如你问“库存周转率怎么算”,召回结果里连表头都不完整,说明表格切块策略有问题。

5.4 转换完接入 RAG 分块时的三个关键经验

转换完的 Markdown 可以直接交给分块器。我目前的常用组合是MarkdownHeaderTextSplitter先按标题分区间,再用RecursiveCharacterTextSplitter把大标题下的长段落切成固定长度块。这里有三个经验:

  • 分块器要能感知表格,不要把一张三行五列表格拦腰截断。实在要切,也要给表格单独一个“整体保留”标志;
  • chunk 的 overlap 不要机械地复制上一段末尾,最好在标题或段落边界处补上下文。我在代码里会给每个 chunk 附加section元数据,比如section=h2:实施细节;
  • 标题层级本身是极好的检索上下文。把h1 > h2 > h3拼到 chunk 头部,召回准确率会有肉眼可见的提升。

这套流程下来,不管是几十页的手写笔记,还是从网页扒下来的长文,都能稳定转成结构清晰的 Markdown,为后续向量化省掉大量返工。

6. 高频问题与避坑实录

6.1 通篇硬换行,转完段落碎成一地

现象:转换后的 Markdown 每行都是孤零零的半句话,检索时一个完整知识点被拆成七八个碎片。

原因:源头是 PDF 或网页复制产生的“视觉换行”,每个物理行末尾都有\n,但语义上是一段连续文字。

解法:进 Markdown 转换前先做段落恢复。我是先按空行分大段,然后把段内所有物理行用空格拼起来,再根据句末标点和下一行类型重新断行。特别提醒:中文合并时不要加空格,否则 embedding 计算时会多出很多无意义字符。

6.2 表格识别失败,行数据变成普通文本

现象:从 Excel 复制的数据,在 Markdown 转换后变成一堆 Tab 连接的文本,分块时切得乱七八糟。

原因:Tab 分隔文本和普通正文之间没什么明显标记,正则分不清。

解法:把“表格识别”放到段落兜底之前,并加一个强条件:连续三行以上字段数量一致,才认定是表格。宁可漏一点,也不要误判。

6.3 过度转义,全文出现一堆反斜杠

现象:转换后的文本里\*、\[、\(到处都是,连标题和链接也被转义了,视觉效果像被弹幕刷屏。

原因:转义逻辑写得太粗暴,没有区分“正文普通文本”和“Markdown 结构语法”。

解法:转义只作用于paragraph类型的行,标题、表格、代码块内部全部跳过。遇到c++、C#中跟字母黏在一起的符号,直接保留原样。

6.4 代码块被列表识别误伤

现象:一段缩进 4 个空格、内部有- item样式的代码,被转成了列表。

原因:列表识别的优先级高于代码块识别,缩进规则先拦截了。

解法:调整识别顺序,先处理围栏代码块,再处理连续缩进代码,最后才做列表识别。这个顺序非常重要,一定要写对。

6.5 中文编码识别失败,变成乱码

现象:源文件是 GBK,但某些工具把它识别成 ISO-8859-1,直接乱码。

原因:中文编码识别对短样本和混合文本经常判断不准。

解法:不要只信单一检测结果。准备 fallback 链,优先尝试 GB18030,它是 GB2312、GBK 的超集,中文 Windows 平台导出的文本基本都能吃下。再失败才用 Latin-1 兜底。

6.6 特殊字符在分块后被截断

现象:token 被截断后,一个代码块或表格只保留了一半,另一半落到下一个 chunk。

原因:固定长度分块时没有感知结构边界。

解法:在分块前做一次结构保护,把代码块和表格整体包装成一个单元,分块时优先切段落边界。实在要跨结构切,就给两个 chunk 都补上“该内容属于 XX 表格”的元信息。

6.7 文件名和路径信息被丢掉

现象:源文件是“2025年产品发布会QA.txt”,但向量库里的 chunk 完全没带文件信息。

原因:导入时没有把文件路径作为元数据写入。

解法:转换脚本里把source_path、file_name、converted_at这类字段一并写入元数据。RAG 检索返回时,用户至少能知道答案来自哪份文档,这对知识库类产品尤其重要。

7. 先聊到这儿,把接口留给下一期

写到这里,“通用文本与结构化解”的核心链路算是讲完了。从 txt 的编码清洗、换行归一化、隐藏字符清理,到 Markdown 的标题识别、列表表格转换、代码块处理,再到分块前的三轮检查,每步都有实打实的坑,最后沉淀下来的方案相对稳定。

我个人做这类数据管道有个习惯:每一步都留检查点,不管是统计输出、抽样文件还是日志文件。解析脚本跑完不算完,人工抽查两三处才算完。这个习惯看着慢,但能帮你把脏数据带来的风险降到最低,尤其在知识库数据量越来越大的时候,很值得。

下一篇我会接着讲 PDF 和 Word 的版式还原,重点拆解扫描件、双栏排版、页眉页脚这些“看似不复杂实际上很要命”的场景。如果你在 txt 或 Markdown 转换上遇到过别的怪异现象,也可以试着往这个四层模型里套一套。大多数问题都能在这里找到位置。

现在这个骨架已经足够稳了,下一期见。

返回列表