Markdown Object:为 PHP 开发者打造的智能文档分块方案

如果你正在用 PHP 构建一个可以与文档对话的应用,那你一定会遇到这个问题:如何将文档拆分成适合 Embedding 模型处理的语义片段?

这个问题在 Python 生态中早已有成熟答案——LangChain、LlamaIndex 提供了丰富的分块策略。但在 PHP 领域,开发者要么选择搭建一个 Python 微服务来处理分块,要么自己从零实现一套分块逻辑。

Markdown Object 填补了这个空白。这是一个专为 RAG(检索增强生成)场景设计的 PHP 包,它不是简单地按字符数切割文档,而是真正理解 Markdown 的层次结构,在保持语义完整性的前提下生成适配 Embedding 模型上下文窗口的分块。

为什么分块策略如此重要

如果你用过向量数据库和 Embedding,应该深有体会:分块策略直接决定了搜索质量。

把整篇文档塞进一个 Embedding?你会丢失细节的检索能力。按固定 500 字符暴力切割?你会破坏语义的完整性。一个关于"如何配置数据库连接"的段落可能被从中间切断,前半部分在一个向量里,后半部分在另一个向量里,用户搜索时两边都匹配不上。

Markdown Object 的解决思路是:先将 Markdown 转换为层次化的对象表示,理解文档的标题结构,然后基于这个结构智能地生成分块。每个分块都会带上一个「面包屑」路径,记录它在文档中的位置:

guide.md › Getting Started › Installation

这意味着当用户的查询命中某个分块时,系统不仅能返回匹配的内容,还能精确定位它在原文档中的上下文。

层次化贪婪打包算法

Markdown Object 的核心算法是 "Hierarchical Greedy Packing"(层次化贪婪打包)。

算法的工作流程很直观:首先检查整个文档是否在限制范围内,如果是,直接作为单个分块返回。如果文档太大,就从最高级别的标题(H1)开始拆分。拆分后,算法会尝试将相邻的小节"贪婪地"合并——只要它们加起来不超过限制。如果某个小节仍然太大,就递归地用同样的逻辑处理,同时更新面包屑路径。

算法使用两个阈值来控制分块行为:

  • target:软限制,当遇到超长的段落、代码块或表格时,算法会在这个边界附近寻找合适的断点
  • hardCap:硬限制,决定了何时必须进行层次拆分,确保不会产生超出 Embedding 模型上下文窗口的分块

这种设计让相关的内容尽可能待在一起,同时又不会超出模型的处理能力。

实际使用

Markdown Object 构建在 League CommonMark 之上进行解析,用 Yethee\Tiktoken 来做精确的 Token 计数。使用起来非常直接:

php
use League\CommonMark\Environment\Environment;
use League\CommonMark\Parser\MarkdownParser;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use BenBjurstrom\MarkdownObject\Build\MarkdownObjectBuilder;
use BenBjurstrom\MarkdownObject\Tokenizer\TikTokenizer;

// 用 CommonMark 解析 Markdown
$env = new Environment();
$env->addExtension(new CommonMarkCoreExtension());
$parser = new MarkdownParser($env);
$doc = $parser->parse($markdown);

// 构建结构化模型
$builder = new MarkdownObjectBuilder();
$tokenizer = TikTokenizer::forModel('gpt-3.5-turbo');
$mdObj = $builder->build($doc, 'guide.md', $markdown, $tokenizer);

// 生成分块
$chunks = $mdObj->toMarkdownChunks(target: 512, hardCap: 1024);

每个分块对象都包含了你需要的信息:Markdown 内容、Token 计数、面包屑路径,甚至还有在原文档中的行号位置。这让你在向量搜索命中后可以精确地定位到原文。

php
foreach ($chunks as $chunk) {
    echo implode(' › ', $chunk->breadcrumb) . "\n";
    // 输出: guide.md › Getting Started › Installation
    
    echo $chunk->markdown;
    // 分块的 Markdown 内容,可以直接送去向量化
}

另一个值得注意的设计是,整个结构化模型可以序列化为 JSON。这意味着你可以在首次处理后缓存结果,后续只需要反序列化就能直接使用,不必重复解析:

php
$json = $mdObj->toJson(JSON_PRETTY_PRINT);
$mdObj = MarkdownObject::fromJson($json);

找到合适的分块参数

有效的分块不仅仅是算法问题,还需要针对具体内容找到合适的参数平衡。Markdown Object 提供了一个交互式演示应用,可以实时预览分块效果。

粘贴你的 Markdown,调整 target 和 hardCap 参数,立即看到内容如何被分块。你可以清楚地看到每个分块的起止位置、携带的面包屑,以及包含多少 Token。这对于调试分块策略非常有帮助。

完整的 Laravel RAG 方案

虽然 Markdown Object 本身是独立的,但如果你在用 Laravel 构建 RAG 应用,可以配合 pgvector Laravel Scout 驱动 使用。两者配合构成完整的原生 PHP 方案:Markdown Object 负责智能分块,pgvector 驱动处理向量存储和相似度搜索,Scout 的 Model Observer 自动保持数据同步。

不需要 Python 微服务,不需要外部依赖,纯 PHP 实现。

写在最后

PHP 生态在 AI 相关工具上一直相对滞后,Markdown Object 的出现是一个可喜的信号。它的设计思路清晰——不是简单地移植 Python 方案,而是基于 Markdown 的特点从零思考如何做分块。层次化贪婪打包算法在保持语义完整性与控制分块大小之间找到了不错的平衡点。

对于正在探索用 PHP 构建 RAG 应用的开发者来说,这是一个值得尝试的工具。


相关链接

本作品采用《CC 协议》,转载必须注明作者和本文链接