langchain-md-chunker
把 markdown / MDX 文档切成 chunk,并为每个 chunk 记录源文件路径 + 字符偏移,方便回溯原文。
核心是可复用的 Python 包(纯标准库、零运行时依赖);demo/ 里的网页只是演示,不参与打包。
本项目是社区/第三方项目,与 LangChain 官方组织没有隶属关系;包名中的
langchain仅表示适用场景。
API 参考见 docs/api/index.md。
特点
- 偏移精确:chunk 是原文的精确切片,
source[start:end] == chunk.text恒成立(切分只挑断点,不改写、不拼接、不重复内容)。 - MDX 感知:跳过 frontmatter /
export/import/ MDX 注释;把<Tabs>、<CodeGroup>、<Accordion>、:::name容器、$$公式与脚注定义当作整体;支持列表内缩进围栏,代码围栏不会被腰斩。 - 标题感知:按标题层级分节,chunk 携带该节的标题路径(如
LangChain > Agents > Middleware)作为上下文。 - 标题可写入正文(可选):
--include-headings会把标题路径按原层级拼进content,更适合 embedding;text仍保持精确切片。 - 分片语义完整:超长代码 / 表格 / MDX 组件被切开后,
content会自动补回围栏、重复表头、标注组件名,单个片段可以直接拿去 embedding。 - 可调参数:目标长度、小节省合并、id 策略、要丢弃的块类型/样板前缀均可配置;目录读取支持指定编码与跳过坏文件。
快速开始
作为依赖安装(PyPI):
uv add langchain-md-chunker
# 需要 to_langchain_documents() 时
uv add "langchain-md-chunker[langchain]"
仓库内开发:
uv sync # 安装(含 dev 依赖)
uv run python data/download_docs.py # 下载语料,约 180 篇 / 5 MB
uv run chunk data/langchain-docs --preset langchain-docs --verify # 切分并校验
uv run python demo/server.py # 打开 http://127.0.0.1:8000 浏览
切分命令
uv run chunk <INPUT> [-o OUTPUT] [--max-chars 1500] [--preset generic|langchain-docs] \
[--include-headings] [--min-chars 0] [--no-merge-small-sections] \
[--id-strategy location|content] [--encoding utf-8] [--on-error raise|skip] \
[--verify] [--quiet]
| 参数 | 默认值 | 说明 |
|---|---|---|
INPUT |
必填 | 源文档目录或单个文件(不内置任何默认语料) |
-o, --output |
data/chunks |
chunk 输出目录 |
--preset |
generic |
语料预设:generic 不丢样板,langchain-docs 丢弃 LangChain 文档每页样板 |
--max-chars |
1500 |
单 chunk 目标上限(字符,针对原文) |
--version |
— | 打印版本号并退出 |
-p, --pattern |
*.md, *.mdx, *.MD, *.MDX |
文件匹配模式,可重复 |
--include-headings |
关闭 | 把标题路径写进 content(见下节) |
--min-chars |
0 |
同一父节下的小节合并阈值;0 关闭 |
--no-merge-small-sections |
关闭 | 即使给了 --min-chars 也保留小节边界 |
--id-strategy |
location |
location(路径+偏移)或 content(内容哈希) |
--encoding |
utf-8 |
源文件读取编码 |
--on-error |
raise |
skip 时跳过不可读 / 解码失败的文件 |
--verify |
关闭 | 写盘前回读源文件,逐条校验偏移 |
--quiet |
关闭 | 只输出错误与警告 |
输出:
data/chunks/
├── chunks.jsonl # 每行一个 chunk,保持源文件顺序
└── manifest.json # 源目录、配置、统计信息
两个文件都是“临时文件 + 原子替换”写入:切分时刷新演示页面,不会读到写了一半的索引。
data/langchain-docs/ 与 data/chunks/ 都在 .gitignore 中:语料和派生产物可随时重建,不入库。
语料预设
核心包本身不知道任何具体语料,默认配置不做语料相关的丢弃,CLI 默认也是 --preset generic。处理 LangChain 文档时显式启用 langchain-docs 预设(丢弃每页重复的索引横幅、页脚、空锚点与 *** 分隔线);在 Python 里同样显式选择:
from langchain_md_chunker import Chunker, langchain_docs
chunker = Chunker(langchain_docs(max_chars=1200)) # 或 Chunker() 走通用默认
自有语料可以自己组合配置:ChunkConfig(skip_kinds=(...), skip_prefixes=(...));预设只是返回 ChunkConfig 的工厂函数,见 presets.py。分层与扩展点见 ARCHITECTURE.md。
数据格式
chunks.jsonl 单行示例:
{
"id": "c32352242f276a1b",
"doc": "oss/python/concepts/context.md",
"start": 181,
"end": 1541,
"start_line": 5,
"end_line": 27,
"chars": 1360,
"headings": ["Context overview", "Context engineering"],
"context": "Context overview > Context engineering",
"text": "## Context engineering\n\n...",
"content": "# Context overview\n\n## Context engineering\n\n...",
"kind": "mixed",
"lang": ""
}
| 字段 | 说明 |
|---|---|
id |
c + 16 位十六进制。默认由 doc + start + end + content 派生(内容变化会得到新 id);id_strategy="content" 时只由 content 派生 |
doc |
相对 manifest.source_root 的源文件路径 |
start / end |
半开区间 [start, end),单位是 Unicode 码点 |
start_line / end_line |
1-based 行号,闭区间 |
chars |
end - start |
headings |
标题路径(由外到内) |
context |
headings 用 > 拼接,可直接拼到 embedding 输入前 |
text |
原文切片,逐字符等于 source[start:end] |
content |
用于 embedding / 展示的文本:text 加上标题路径(可选)与分片包装(必要时) |
kind |
主导块类型:para / code / table / list / quote / mdx / math / container / footnote;跨多种块时为 mixed |
lang |
当 chunk 全部来自同一个代码块时为其围栏语言(如 python),否则为空 |
content中的标题、补出的围栏、重复的表头、MDX 组件注释都是合成的,不要拿它回原文定位; 回溯原文请始终使用text与start/end。id_strategy="content"下,内容相同的 chunk (例如多页共用的示例)会得到同一个 id,便于跨文档去重。
偏移以码点计(Python
str下标)。JavaScript 字符串是 UTF-16,跨码点时需Array.from(text)后再切片——demo/static/app.js已这样做。
标题前缀(可选)
默认关闭。开启后,每个 chunk 的 content 会按原文层级带上它所在的标题路径:
输入:
# 1
## 2
### 3
xxxxx
输出(该 chunk 只有 xxxxx 一个正文块):
text = "### 3\n\nxxxxx"
content = "# 1\n## 2\n\n### 3\n\nxxxxx"
规则:
- chunk 已经包含自身标题行时不会重复,只补祖先标题;
- 层级按原文保留(
# A下直接### C就输出# A+### C,不臆造层级); - 空标题跳过;无标题且未发生分片包装的文档
content == text; start / end / chars / 行号始终描述text(原文切片),--max-chars也只约束原文长度。
uv run chunk data/langchain-docs --preset langchain-docs --include-headings --verify
# 或
Chunker(ChunkConfig(include_headings=True))
分片包装
超长代码块 / 表格 / MDX 组件按行切开后,每个片段是独立的 chunk:它们不共享 chunk,
因此 content 可以安全地补齐上下文(text 不变,仍是精确切片):
| 块类型 | 中间片段的 content |
|---|---|
| 代码 | 补上与原文一致的开围栏与闭围栏(如 ```python … ```) |
| 表格 | 重复表头行与对齐行(首行 + --- 分隔行) |
| MDX | 首尾加组件名注释:{/* <Tabs> */} … {/* </Tabs> */} |
text = "print(42)"
content = "```python\nprint(42)\n```"
片段之间不会互相吞并,所以每个 content 都是自洽的 Markdown 片段;代价是长代码/长组件会产生
更多、更小的 chunk(可用 min_chars 合并普通小节,但不会合并同一块的两个片段)。
切分策略
- 分块:逐行扫描,识别标题(ATX 与 Setext)、代码围栏(``` / ~~~)、表格、引用、MDX 组件、列表、段落、分隔线、frontmatter、
export块。 - 分节:按标题层级构造标题栈,每个 chunk 归属唯一一节,并带上该节标题路径。
- 打包:同节内按块贪心累积到
max_chars,超出才断点;标题行始终与其正文首块同行。设置min_chars后,同一父节下、同一层级的相邻小节会先合并(不跨父节,且合并后仍不超max_chars);合并后的 chunk 采用第一个小节的标题路径,正文仍包含后续小节标题。 - 超大块:单个块超过上限时按行切分,优先落在子元素起始行(如
<Tab>、<Step>),其次空行,最后才选普通行;切出的片段在content中补围栏 / 表头 / 组件名(见上节)。 - 丢弃:
skip_kinds(frontmatter / export / import / MDX 注释)与skip_prefixes(LangChain 文档每页重复的索引横幅与页脚)不产出 chunk;它们会强制断开,避免混进相邻 chunk。export块按大括号配平整段跳过(文档里的PatternEmbed组件有 45 KB);import无分号且没有{/(时只取一行,不会吞掉下一段正文。
已知限制与设计取舍:
- 标题行会与正文首块绑定;超长块按行切分并在
content中补齐语义。只有三种情况会超过max_chars:单行本身超长(无法在不破坏内容的前提下再切);标题必须跟随正文首块(超出部分就是标题长度);为避免尾部分隔符单独成块而并入上一片段(只多出分隔符的几个字符)。超长单行正文会按句末标点 → 逗号 → 硬切回退。 max_chars按字符而非 token 计量:中英文密度不同,需要 token 预算时请按语料换算后调整。- 相邻 chunk 不重叠:这是“偏移精确、可回溯”的刻意取舍;需要 overlap 的检索策略请在消费端处理。
- 不支持 4 空格缩进、无围栏的代码块(它更像列表续行或组件内容);列表项内带围栏的代码(任意缩进)已支持。
- 列表项内的围栏代码会与列表分成相邻块(内容不丢失,通常仍落在同一个 chunk);若需要片段自带的围栏/表头,请使用
content(text始终不含合成内容)。 split_directory是单线程同步 IO;5 MB 语料约 0.5 s,更大规模建议按目录分片。- 偏移按 Unicode 码点,且
split_file用newline=""读取,保留原始\r\n;浏览器/JS 定位需按 UTF-16 换算(demo/static/app.js有示例)。 - 解析器是面向 Docusaurus / Nextra 的启发式实现,不是 CommonMark/MDX 规范解析器;复杂嵌套、字符串内大括号等边界可能与规范解析器不同。
Python API
from pathlib import Path
from langchain_md_chunker import Chunker, ChunkConfig
chunker = Chunker(ChunkConfig(max_chars=1200, include_headings=True))
chunks = chunker.split_directory("data/langchain-docs") # 或 split_file / split_text
source = Path("data/langchain-docs", chunks[0].doc).read_text(encoding="utf-8")
assert source[chunks[0].start : chunks[0].end] == chunks[0].text
print(chunks[0].context, len(chunks))
其他常用选项:
# 合并同一父节下的小节(不跨父节)
Chunker(ChunkConfig(min_chars=200))
# 内容寻址 id(相同内容跨文件共享 id)
Chunker(ChunkConfig(id_strategy="content"))
# 流式处理大语料;跳过无法解码的文件
for chunk in Chunker().iter_directory("data/langchain-docs", encoding="utf-8", on_error="skip"):
index.add(chunk)
持久化与读取:
from langchain_md_chunker import build_manifest, read_chunks, read_manifest, write_chunks
manifest = build_manifest(chunks, source_root="data/langchain-docs", config=chunker.config)
write_chunks(chunks, "data/chunks", manifest=manifest)
chunks = read_chunks("data/chunks") # 目录或 .jsonl 均可
manifest = read_manifest("data/chunks")
与 LangChain 集成(可选,自行安装 langchain-core):
from langchain_md_chunker import to_langchain_documents
documents = to_langchain_documents(chunks) # page_content 默认取 content
documents = to_langchain_documents(chunks, page_content="text") # 精确原文
公开 API 即 langchain_md_chunker.__all__:Chunker、ChunkConfig、Chunk、langchain_docs、
to_langchain_documents、verify_chunks、write_chunks、read_chunks、build_manifest、read_manifest 及文件常量。
解析层(blocks.py)与切分层(splitter.py)属于内部实现,不作为兼容性承诺;
完整的公开接口清单见 docs/api/index.md。
演示前端
uv run python demo/server.py --port 8000
- 左侧按源文件顺序列出
[序号] 预览,支持按路径 / 标题 / 预览片段过滤;预览取自content,开启--include-headings后可直接看到标题前缀; - 点击任意
[序号],右侧直接渲染源文件并高亮该 chunk,自动滚动到高亮位置; - 顶部显示文档路径、字符偏移、行号、标题路径,可另开原始文件;
- URL 形如
http://127.0.0.1:8000/#c32352242f276a1b,可直接分享某个 chunk。
前端没有构建步骤(原生 ES modules + HTML/CSS),服务端只用标准库,不导入 chunker 包,因此不会污染核心。修改语料后重新 uv run chunk data/langchain-docs,服务会自动重载。
目录结构
src/langchain_md_chunker/
├── __init__.py # 公开 API(__all__)
├── blocks.py # 内部:行 → Block(围栏/表格/MDX/引用…)
├── splitter.py # 内部:Block → 分节打包 → RawChunk(偏移)
├── chunker.py # 编排:文件 / 目录 + 标题前缀 → Chunk
├── models.py # 输出模型 Chunk
├── config.py # ChunkConfig(与语料无关的默认值)
├── presets.py # 语料预设(langchain-docs)
├── storage.py # JSONL + manifest 读写
├── verify.py # 偏移一致性校验
└── cli.py # `uv run chunk <INPUT>`
demo/
├── server.py # 演示服务(stdlib)
└── static/ # index.html / app.js / styles.css
tests/
├── conftest.py # 语料 fixture(全量切分只跑一次)
├── test_parser.py # 围栏/表格/MDX/引用等解析边界
├── test_splitter.py # 打包、标题上下文、超长块、样板剔除
├── test_chunker.py # 文件/目录 API、编码(Unicode、CRLF)、校验、幂等
├── test_storage.py # JSONL / manifest 往返
├── test_cli.py # 命令行端到端与退出码
├── test_package.py # 包入口、公开 API 与语料预设
├── test_fuzz.py # 固定种子的随机文档不变量测试
├── test_docs.py # 公开 API 文档覆盖与链接检查
└── test_demo.py # 演示服务的数据装载、重载容错与路径安全
docs/api/ # API 参考(纯 Markdown,无需构建)
CHANGELOG.md # 版本变更记录
RELEASING.md # PyPI 发布流程与检查清单
data/download_docs.py
测试
uv run pytest # 229 项,秒级
uv run pytest --cov # 覆盖率(已配置分支覆盖)
uv run ruff check src demo tests # 静态检查(ruff 已固定为 dev 依赖)
当前覆盖率:核心包 100% 行 / 100% 分支(uv run pytest --cov 可复现)。
测试关注的是不变量而不是实现细节:
- chunk 切片等于源文件
[start, end),且行号、标题路径、上下文自洽; - 代码围栏、表格、MDX 组件不会被“半个标签”切断,围栏内的
#不当标题; - Unicode(非 BMP 字符)、CRLF 文件的偏移按原始文件(保留
\r\n)的码点计; - 文件/目录 API 的
doc路径、排序稳定性与verify_chunks的负例; - CLI 退出码、
--verify失败、--max-chars/-p生效; - 语料存在时对全量 4700+ chunk 校验:不重叠、不越界、ID 唯一、超长块占比 < 1%。
发布
PyPI 包名 langchain-md-chunker。版本号、构建、Trusted Publishing 配置与回滚流程见
RELEASING.md,
变更记录见 CHANGELOG.md。
uv build # 产物:dist/*.whl、dist/*.tar.gz
uvx --from twine twine check dist/*
版权
代码以 MIT License 发布。
data/langchain-docs/ 中的文档版权归 LangChain 所有,仅用于本地学习与测试。
Metadata
Release files for langchain-md-chunker 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| langchain_md_chunker-0.4.2.tar.gz | 31.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langchain_md_chunker-0.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.2 kB
Release files / langchain_md_chunker-0.4.2.tar.gz
| Download URL | langchain_md_chunker-0.4.2.tar.gz |
|---|---|
| Size | 31.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4b50aedf2f15fc9d10293ef8bc079308aa76e8eac1bce714baf6553169b833ed
|
|
BLAKE2b-256 checksum How to use checksums |
5dcbe39b397862dfb34e487b70bf7a6ea5f80cc447d42a7bb9ed50fc3f34e770
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / langchain_md_chunker-0.4.2-py3-none-any.whl
| Download URL | langchain_md_chunker-0.4.2-py3-none-any.whl |
|---|---|
| Size | 36.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
93acc9fd5ae92b3c68b1de74571ee049e13a7f7bdfe7608efd71278072999512
|
|
BLAKE2b-256 checksum How to use checksums |
e7e735dd55096181fb425d2c2d86ad7340aa668d480fe99ce01136ec93656d48
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for langchain-md-chunker 0.4.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.