AI 应用后端工程化:从原型到可交付系统 · 第 12 篇 · 第三章 · 数据管道
沿数据进入索引前的路径,解释文件解析、中文分词、文本切片、嵌入和片段管理各自承担什么责任。
检索结果不理想时,人们最先调整向量模型,实际上问题经常更早发生:PDF 正文没读全,标题与内容被拆开,中文分词破坏专有名词,片段没有保留来源。文档进入索引之前的处理,决定了之后还能检索到什么。
把处理链拆成可替换阶段
flowchart TD
Upload[上传文件] --> Detect[检测真实类型]
Detect --> Extract[内容提取]
Extract --> Normalize[清洗与规范化]
Normalize --> Segment[语义切片]
Segment --> Tokenize[分词/关键词]
Segment --> Embed[向量嵌入]
Tokenize --> Save[(片段记录)]
Embed --> Save
Save --> Index[(全文与向量索引)]
每个阶段都应有明确输入输出,并保存处理版本。解析器升级或切片策略变化后,可以知道哪些文档需要重建,而不是把整个知识库当成无法解释的黑箱。
文件提取器需要统一契约
不同格式返回的信息不同:Markdown 有标题层级,PDF 有页码,网页有链接,表格有行列。统一契约不应把这些差异全部抹掉,而是提供正文加可扩展元数据。
from dataclasses import dataclass, field
@dataclass(frozen=True)
class ExtractedBlock:
text: str
kind: str
position: int
metadata: dict[str, str] = field(default_factory=dict)
class Extractor:
def supports(self, media_type: str) -> bool:
raise NotImplementedError
def extract(self, content: bytes) -> list[ExtractedBlock]:
raise NotImplementedError
调用方只依赖 Extractor 接口,注册表根据真实 MIME 选择实现。解析失败要指出是加密、损坏还是格式不支持,便于决定是否重试或提示用户。
清洗要克制
删除重复页眉页脚可以提升质量,但过度清洗会改掉代码缩进、表格分隔和编号。清洗步骤应该可测试并保留原文引用。对于法规和合同,甚至要保存原始字符偏移,确保引用可以回到证据位置。
字符编码是入口的一部分。文本文件不能默认跟随操作系统编码;优先明确 UTF-8,必要时检测 BOM 或让用户指定。用“忽略错误字符”打开文件虽然不报错,却可能悄悄损坏名字和数字。
中文分词与向量切片不是一回事
全文检索需要将句子拆成可匹配词项,中文没有天然空格,Jieba 一类分词器可以提供基础能力。向量嵌入通常直接处理文本,不一定需要先分词。把全文检索的分词结果当向量输入,可能破坏模型训练时的自然文本分布。
领域词典能改善产品名和缩写,但也需要版本。词典更新后,旧索引不会自动获得新分词结果。应记录 tokenizer 版本并安排重建,而不是只改一份配置。
切片要保留结构
固定长度适合兜底,优先按标题、段落和列表边界切分。一个片段可以拥有 heading_path、page、ordinal 和 parent_id。命中时既能展示位置,也能按父级扩展上下文。
重叠并非越多越好。大量重叠会让相邻片段同时占据 Top K,看似召回五条,实际只有一个信息源。检索后可以按父文档去重或使用最大边际相关性增加多样性。
片段编辑是有价值的人工纠错能力。用户修正错误段落后,应生成新版本嵌入并原子替换索引;保留是谁、何时、基于哪个原文修改,避免人工内容失去出处。
Embedding 服务也需要边界
嵌入模型有批大小、速率限制、向量维度和最大 Token。服务层负责批处理、重试、指标和维度校验。更换模型会改变向量空间,新旧向量不能直接比较,必须使用新集合或完整重建。
缓存可按规范化文本、模型和版本生成键。只按文本缓存,更换模型后会取回错误维度。空文本和超长文本应在调用前拒绝或切分,减少昂贵的无效请求。
一次典型的隐形失败
某个 PDF 解析器把双栏文章按横向顺序读取,句子被交叉拼接。嵌入接口仍返回成功,向量库也正常写入,只有检索质量下降。若管道没有保存提取预览和阶段指标,团队很可能误以为嵌入模型不适合中文。
因此每次处理应保留少量可查看样本:首段、片段数量、平均长度、空片段比例、语言和异常字符比例。数据质量指标比“任务成功”更能提前发现问题。
迁移到搜索与 ETL
商品目录、日志搜索和邮件归档同样需要提取、规范化、分词、索引和版本管理。练习可以准备一个含标题、表格和中文专有名词的 Markdown,分别用固定长度与结构切片,比较关键词查询和语义查询的命中。保存切片预览,解释差异来自哪一步。
七个紧密相邻的提交
945b704:加入中文分词与嵌入服务。1444435:建立多格式文件提取器。3d893dc:补齐文档处理服务与实体。a17f0b0:调整处理和嵌入配置,阶段边界开始稳定。bdc209f:增加文档路由,让处理能力进入 API。294afae:建立片段管理,切片成为可操作资源。a61e69c:继续整理结构,降低服务间耦合。
这一组变化说明,文档处理不是一个 split_text 函数,而是从格式边界到可管理片段的一整套数据工程。