跳转到内容

模板方法模式:多模态文档处理流水线

模板方法模式:多模态文档处理流水线

学习目标

理解 Khoj 如何通过 TextToEntries 抽象基类和模板方法模式,统一处理 PDF、Markdown、Notion、DOCX、图片等 9 种内容类型。

前置知识

本章涉及分块策略的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 Khoj 的文档处理流水线实现。

项目实践

流水线架构

模板方法:统一的四步流水线

TextToEntries 抽象基类定义了所有处理器共享的流程:

1. 提取(Extract) → 从原始格式中提取文本(各处理器独立实现)
2. 分块(Split) → 按 token 上限分割(共享 RecursiveCharacterTextSplitter)
3. 嵌入(Embed) → 计算向量嵌入(共享,支持本地/远程后端)
4. 存储(Store) → 保存到 DB + 磁盘缓存(共享,含变更检测)

其中步骤 1 是 @abstractmethod,各处理器独立实现。步骤 2-4 在基类中实现,所有子类共享。

9 种内容处理器

处理器数据源提取方式特殊处理
PDFToEntriesPDF 文件PyMuPDFLoader逐页提取,null 字符清理
MarkdownToEntries.md 文件文件读取保留标题结构
DocxToEntries.docx 文件docx2txt表格处理
NotionToEntriesNotion APIOAuth token页面层级递归拉取
GithubToEntriesGitHub APIPAT token仓库/分支配置
ImageToEntries图片文件rapidocr-onnxruntime OCR多语言 OCR
OrgToEntriesEmacs org 文件文件解析org 语法解析
PlaintextToEntries纯文本文件文件读取无特殊处理

Token 级分块策略

所有处理器共享相同的分块逻辑:

RecursiveCharacterTextSplitter(
chunk_size=256, # 最大 token 数
separators=[ # 分割边界优先级
"\n\n", # 段落边界
"\n", # 换行
"!", # 感叹号
"?", # 问号
".", # 句号
" ", # 空格
"\t", # Tab
"" # 逐字符(最后的 fallback)
]
)

这个策略的核心思路:优先在语义边界分割。段落 > 句子 > 单词 > 字符,越高层级的边界越优先。256 token 的限制确保了嵌入模型处理短文本的精度。

嵌入缓存与变更检测

文档上传 → 计算 hashed_value (SHA)
是否已有相同 hash?
├── 是 → 跳过,使用已有嵌入
└── 否 → 计算嵌入 → 保存到 DB + 磁盘 .pt 文件
文档更新 → 比较 hashed_value
├── 未变 → 不重新索引
├── 已变 → 重新索引
└── 已删除 → 从索引中移除

磁盘 .pt 缓存(PyTorch 序列化格式)避免了重启后重复计算嵌入。对于大型文档库,这可以显著缩短启动时间。

内容上传 API

PUT /api/content 接收多部分文件上传:

请求参数:
- files: 文件列表
- type: 内容类型(pdf, markdown, docx, notion, github, image, org-mode, plaintext)
- 可选的格式特定配置(如 GitHub 的 owner/repo/branch)
响应:
- 索引状态(成功/失败/跳过)
- 新增/更新/删除的条目数量

问题与规避

问题影响规避策略
256 token 分块过小长上下文信息丢失检索时可合并相邻片段;或增大 chunk_size(但会降低精度)
OCR 质量依赖图片质量扫描件或低分辨率图片识别率低rapidocr-onnxruntime 对多语言支持好,但仍建议用户上传清晰文档
Notion API 速率限制大量页面同步可能超时分页拉取,每页之间间隔
嵌入缓存与模型更新不同步更换 embedding 模型后缓存过期需要手动清除 .pt 缓存文件,触发全量重新嵌入

设计取舍

为什么选择模板方法而非策略模式?

模板方法锁定了流程(提取 → 分块 → 嵌入 → 存储),新处理器只需要实现提取步骤。如果改为策略模式,每个处理器需要独立管理整个流程,容易导致各处理器行为不一致。模板方法的核心优势是一致性保证——所有处理器都经过相同的分块、嵌入和存储流程。

为什么 chunk_size 选择 256 而非更大的值?

小 chunk 的优势是嵌入更精确(短文本的语义表示更集中),检索结果更聚焦。缺点是可能丢失上下文——回答一个问题可能需要多个片段拼合。256 是在这两个极端之间的折中。如果应用场景需要更长上下文(如总结整篇文档),可以考虑增大到 512 或 1024。

参考来源