Markdown 表格统一识别与切块实施计划
面向 zleap-sag 的 Markdown、CSV、XLSX 统一语料处理方案:文件表格先由 MarkItDown 转为 Markdown,再通过 TABLE Block 完成表头感知、按行切分、证据引用和持久化。
01. 方案结论
表格的标准中间形态确定为 Markdown,不再引入独立的单元格领域模型作为 LLM 输入。Markdown 原文与 CSV/XLSX 转换结果进入同一条结构识别和切块链路。
LLM 读取 Markdown
Chunk 的 content 与 raw_content 都保留完整 Markdown 表格。
一套 TABLE 策略
Markdown、CSV、XLSX 不再分别维护三套表格切块算法。
表头和行可追踪
表头只持久化一次,各表格 Chunk 共享表头 Section 引用。
02. 实施基线与已补齐能力
BlockType.TABLE已定义。ParsedBlock、SectionItem、ChunkItem支持类型和 metadata。ArticleSection.type可存储TABLE。- 向量文档已经支持写入
chunk_type。 - Parse、Chunk、Index 阶段边界清晰,可局部扩展。
- 增加 MarkItDown 可选依赖和默认 CSV/XLSX converter。
- MarkdownBlockParser 可生成 TEXT/TABLE Block。
- Block Router 将 TABLE 交给 MarkdownTableChunker。
- Assembler 按表头、完整数据行和表格分组组装。
- Index 复用跨 Chunk 重复的表头 Section。
heading_strict 使用独立 heading-only Block 视图,不直接消费 TABLE 拆分后的
parsed.blocks,因此仍保持一个标题块对应一个 Chunk 的 benchmark 语义。
03. 目标架构
CSV/XLSX 由 MarkItDown 转换
TEXT Block / TABLE Block
Embedding / 检索 / 提取
.md ───────────────────┐
.csv → MarkItDown ──────┼→ Markdown → TABLE Block → Section → Chunk
.xlsx → MarkItDown ──────┘
这里统一的是“转换后的 Markdown 结构”,不是 Excel 原始单元格结构。样式、合并单元格、公式对象等 不进入本次合同;LLM 只消费 MarkItDown 输出的可读 Markdown。
04. 必须满足的切块不变量
TABLE Block。table_header 和零到多个 table_row Section。standard 和 overlap 支持表格感知;heading_strict 保持旧语义。max_tokens。05. 参考项目逻辑的采用与修正
| 参考项目行为 | 本次决定 | 原因 |
|---|---|---|
| MarkItDown 转 Markdown | 采用 | 满足统一 LLM 语料方向。 |
| Markdown 表格识别为 TABLE Block | 采用 | 与现有 BlockType 和 pipeline 契约匹配。 |
| 表头、数据行分别生成 Section | 采用 | 支持按行切分和精细引用。 |
| 大表拆分时重复表头 | 采用 | 保证每个 Chunk 可被 LLM 独立理解。 |
| 单个正则识别整个表格 | 替换 | 使用逐行状态机,避免吞掉表后空白行并改善偏移计算。 |
| 小表与前后正文跨 Block 合并 | 不采用 | 混合后 chunk_type 取第一个 Section,表格类型可能丢失。 |
| 数据行去掉管道符后作为语义文本 | 不采用 | 用户已确认最终语料应保持 Markdown。 |
| 超长单行隐式超过上限 | 显式化 | 保持行完整,但增加 oversized metadata 和测试。 |
| HTML table 作为 raw Section | 暂不纳入 | 本次范围是 Markdown、CSV、XLSX。 |
06. 一次性完整实施方案
单一实施单元:打通 CSV/XLSX/Markdown 到 LLM 表格语料的完整链路
下表是同一次代码变更必须覆盖的修改面,不代表多个实施阶段。所有修改在同一工作分支中完成, 统一运行全量测试,达到文末 Definition of Done 后整体交付。
| 修改面 | 必须同时完成的内容 | 涉及文件 |
|---|---|---|
| 依赖与转换 |
增加 tables 可选依赖;CSV/XLSX 通过 MarkItDown 转 Markdown;
使用 asyncio.to_thread();支持未 start 的 engine.parse();
保留显式 converter 优先级,并提供稳定错误码与安装提示。
|
pyproject.tomlpipeline/converters/markitdown.pypipeline/parse.py
|
| Block 识别 | 使用逐行状态机识别 Markdown 表格;保护代码围栏;校验分隔行和列数; 保留准确 start/end 偏移;表格以外内容继续生成 TEXT Block;非法表格安全降级为 TEXT。 | modules/load/chunking/parser/markdown.py |
| Section 构建 |
新增 Block Router 和 MarkdownTableChunker;生成一个 table_header 与多个
table_row;同表共享 render group;空白行不生成 Section;
whitespace merge 仅处理 TEXT。
|
chunker/table.pychunker/router.pychunker/markdown.pychunker/__init__.py
|
| Chunk 组装 |
增加 table_with_header 策略;按完整行贪婪切分;每个 Chunk 重复表头;
表格与正文、其他表格隔离;content/raw_content 保持合法 Markdown;
超长单行保留完整并标记 oversized_row。
|
modules/load/chunking/assembler/generic.py |
| 模式与版本 | standard 启用完整表格链路;overlap 禁止跨 TABLE 边界拼接; heading_strict 使用独立 heading-only 解析路径以保持 benchmark 语义; 表格算法使用独立 implementation version,确保已有表格来源正确重切块。 | pipeline/chunk.py |
| 引用与持久化 | Index 按全局 section order 复用表头 ArticleSection;校验重复 Section 内容一致; 每个 SourceChunk.references 保持“表头 → 数据行”顺序; SourceReference 使用表头/行级稳定键。 | _pipeline_adapters.py |
| 回归、文档与交付 | 同时补齐 Parser、Chunker、Assembler、CSV/XLSX E2E、模式兼容、Index provenance 测试; 固定普通 Markdown 黄金输出;更新 README、中英文 API、CHANGELOG 和安装示例; 全量单测、lint、format、mypy、wheel 构建全部通过后一次性交付。 |
tests/unit/test_*table*.pyREADME.mdapi.md / api.zh-CN.mdCHANGELOG.md
|
统一依赖合同
[project.optional-dependencies]
tables = [
"markitdown[xlsx]>=0.1.5,<0.2",
"pandas>=2.2,<3",
]
完整变更的集成判定
- 任何中间状态都不作为可交付版本;例如“能转换但不能识别 TABLE”视为未完成。
- Markdown、CSV、XLSX 必须在同一测试运行中证明进入同一表格切块策略。
- Parse → Chunk → Index 的 Section/Chunk/reference 必须端到端验证,不能只测局部函数。
- 普通 Markdown 和 heading_strict 回归必须与新表格用例一起通过。
- 代码、依赖锁、测试、API 文档和 CHANGELOG 在同一变更中提交。
唯一完成条件: 本文第 12 节 Definition of Done 全部满足;否则整个实施单元保持未完成状态。
07. 数据与 metadata 合同
ParsedDocument metadata
{
"original_format": "xlsx",
"normalized_format": "markdown",
"converter": "markitdown",
"converter_version": "0.1.5"
}
TABLE Block metadata
{
"table_format": "markdown",
"header_line_count": 2,
"row_count": 17,
"column_count": 5,
"source_format": "xlsx"
}
Section metadata
{
"block_type": "TABLE",
"block_id": "table-3",
"render_format": "markdown_table",
"assemble_policy": "table_with_header",
"role": "table_row",
"row_index": 7,
"column_count": 5,
"start_index": 1234,
"end_index": 1290
}
Chunk metadata
{
"chunk_type": "TABLE",
"assemble_policy": "table_with_header",
"table_block_id": "table-3",
"row_start": 1,
"row_end": 12,
"row_count": 12,
"header_repeated": true,
"oversized_row": false
}
08. 表格切块算法
flush 当前正文 Chunk
header = table_header
current_rows = []
for row in table_rows:
candidate = render_markdown(header, current_rows + [row])
if tokens(candidate) <= max_tokens:
current_rows.append(row)
continue
if current_rows:
emit(render_markdown(header, current_rows))
current_rows = [row]
continue
# header + 单行本身已经超限
emit(
render_markdown(header, [row]),
metadata={"oversized_row": True},
)
if current_rows:
emit(render_markdown(header, current_rows))
完整性优先
不会为了满足 token 上限而把一条 Markdown 数据行切成多个无效片段。
单行超限
允许 header + 单行超过 max_tokens,但必须可观测、可测试、可统计。
09. Chunk 模式兼容策略
| 模式 | 表格行为 | 兼容要求 |
|---|---|---|
standard |
完整 TABLE Block、按行切分、每块重复表头。 | 新能力主路径 |
overlap |
复用 standard 表格切块,但禁止跨 TABLE 边界追加 overlap。 | 保证 Markdown 有效 |
heading_strict |
整个标题块保持一个 Chunk,表格只作为标题块内 Markdown 内容。 | 保持 benchmark 语义 |
10. 测试矩阵
| 层级 | 代表用例 | 核心断言 |
|---|---|---|
| Block Parser | 标题 + 表格 + 正文 | Block 顺序为 TEXT/TABLE/TEXT,heading 和偏移正确。 |
| Block Parser | 代码围栏内伪表格 | 不生成 TABLE。 |
| Block Parser | 表后空白行 | 不生成空 table_row。 |
| Block Parser | 非法分隔行 | 完整降级为 TEXT。 |
| Section Builder | 表头 + 3 行 | 1 header + 3 row,同一 render group。 |
| Assembler | 小表格 | 独立 TABLE Chunk,不与正文合并。 |
| Assembler | 长表格 | 多 Chunk;每块重复表头;行精确覆盖原表。 |
| Assembler | 超长单行 | 行不拆,oversized_row=true。 |
| CSV E2E | 中文、引号、逗号 | MarkItDown 后生成 TABLE Block/Chunk。 |
| XLSX E2E | 两个 Sheet | 两个独立 TABLE Block,heading 为 Sheet 名。 |
| Pipeline | overlap + table | 不向表格前后插入跨块文本。 |
| Pipeline | heading_strict + table | 原始标题块仍为一个 Chunk。 |
| Index | 表格拆成多个 Chunk | 表头 Section 只保存一次,多个 Chunk 共享 ID。 |
| 确定性 | 同一输入执行两次 | 版本、Chunk 内容和引用顺序一致。 |
| 回归 | 普通 Markdown | 既有输出无非预期变化。 |
建议新增测试文件
tests/unit/test_markdown_table_parser.py
tests/unit/test_markdown_table_chunker.py
tests/unit/test_table_source_chunk_assembler.py
tests/unit/test_markitdown_table_conversion.py
tests/unit/test_table_pipeline_e2e.py
tests/unit/test_table_index_provenance.py
11. 风险、限制与控制措施
| 风险 | 影响 | 控制措施 |
|---|---|---|
| MarkItDown 输出随版本变化 | 同一文件产生不同 Markdown 和 Chunk。 | 固定 >=0.1.5,<0.2,记录 converter_version,增加黄金测试。 |
CSV 单元格包含裸 | 或换行 |
转换后的 Markdown 列语义可能变化。 | 加入边界测试;无法可靠识别时保留 TEXT 并产生 warning,不静默丢数据。 |
| 正则误识别代码块中的表格 | 代码内容被错误按表格切分。 | 逐行状态机先保护 fenced code ranges。 |
| 表格与正文合并 | chunk_type 丢失,Markdown 上下文混杂。 | 表格默认独立,进入和离开表格时 flush。 |
| 表头跨 Chunk 重复落库 | ArticleSection 冗余、引用不一致。 | Index 阶段按全局 order_index 去重并校验内容一致。 |
| heading_strict 行为被 TABLE Block 改变 | benchmark 语料不再对齐。 | heading_strict 使用独立 heading-only 解析路径。 |
| 单行超过 max_tokens | 无法同时满足完整行和 token 硬上限。 | 保留完整行,设置 oversized_row;后续可增加 strict/error 策略。 |
12. 一次性交付与验收标准
单一交付单元
feat: add markitdown-backed table ingestion and chunking。
不拆分 converter、chunker、index provenance 等中间提交作为独立交付物。
验证命令
uv lock
uv sync --package zleap-sag --extra dev --extra tables
uv run --project packages/sag pytest \
tests/unit/test_markdown_table_parser.py \
tests/unit/test_markdown_table_chunker.py \
tests/unit/test_table_source_chunk_assembler.py \
tests/unit/test_markitdown_table_conversion.py \
tests/unit/test_table_pipeline_e2e.py \
tests/unit/test_table_index_provenance.py \
-q
uv run --project packages/sag pytest -m "not integration"
uv run --project packages/sag ruff check .
uv run --project packages/sag ruff format --check .
uv run --project packages/sag mypy
uv build --package zleap-sag
Definition of Done
- Markdown 管道表格能识别为 TABLE Block。
- CSV/XLSX 能通过 MarkItDown 自动转成 Markdown。
- XLSX 多 Sheet 能识别为多个独立表格。
- 每个表格 Chunk 都是完整合法 Markdown。
- 长表拆分后每个 Chunk 都重复表头。
- 数据行不丢失、不重复、不乱序。
- 表格不与正文或其他表格混合。
- 超长单行有明确可观察策略。
- 表头只持久化一次并被多个 Chunk 共享。
- heading_strict 原有语义保持不变。
- 普通 Markdown 无非预期回归。
- 缺少可选依赖时给出安装提示。
- 单测、lint、格式、类型和 wheel 构建全部通过。
- README、中文/英文 API 和 CHANGELOG 已更新。