💡 一句话总结:RAG 表现差,多半不是模型问题,而是预处理把 Word 表格、PPT 版面、PDF 标题层级毁了。markitdown 把这些异构文档统一转成 LLM 友好的 Markdown,单条命令上手,是 2026 年 RAG 预处理流水线最值得装的小工具之一。
一、为什么 RAG 的瓶颈在预处理
很多团队第一版 RAG 跑出来都有这种感觉:演示问题答得挺好,真业务文档喂进去就开始胡说。常见的具体表现:
- Word 表格答非所问——表头和数据被切断到不同 chunk
- PPT 的图说不见了——只剩下 bullet 文字
- PDF 扫描件直接进不来或乱码
- 文档里的章节层级关系丢失——「第 3 章的小节」和「第 5 章的小节」在向量空间里混成一团
这些都是预处理问题,不是模型问题。
微软在 2025 年底开源的 markitdown 解决的是「文档 → LLM 友好格式」这件事的工程标准化。它不是世界上最先进的文档解析器(VLM 路线的 docling 在某些场景比它好),但它做到了简单、快、覆盖广、Markdown 输出对 LLM 极友好,在 RAG 预处理这个层级是最优解之一。
二、5 分钟上手
2.1 安装
pip install 'markitdown[all]' # 装全部可选依赖
# 仅核心
pip install markitdown
# 按需安装
pip install 'markitdown[docx,pptx,pdf,audio-transcription]'
2.2 单文件转换
最简单的 API:
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("report.docx")
print(result.text_content) # Markdown 字符串
print(result.title) # 文档标题(如有)
命令行:
markitdown report.docx -o report.md
markitdown slides.pptx -o slides.md
markitdown data.xlsx -o data.md
2.3 开启 LLM 增强(OCR + 图片 caption)
如果文档里有图片需要描述:
from markitdown import MarkItDown
from openai import OpenAI
client = OpenAI() # 或指向 Ollama/兼容端点
md = MarkItDown(llm_client=client, llm_model="gpt-4o-mini")
result = md.convert("slides_with_diagrams.pptx")
这样每张图片会被 GPT-4o-mini 描述成一段 caption 嵌进 Markdown。如果不想付费,把 client 指向本地 LLaVA 或者 Ollama 同样可以。
2.4 音频转写
md = MarkItDown(llm_client=client)
result = md.convert("meeting.mp3") # 内部调用 Whisper API
三、批量处理:1000 份 PPT 的实战
下面这段代码是生产管道常用的批处理模板:
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor, as_completed
from markitdown import MarkItDown
import json
INPUT_DIR = Path("./raw_docs")
OUTPUT_DIR = Path("./processed_md")
OUTPUT_DIR.mkdir(exist_ok=True)
def convert_one(path: Path):
md = MarkItDown() # 每个 worker 独立实例
try:
result = md.convert(str(path))
out = OUTPUT_DIR / (path.stem + ".md")
out.write_text(result.text_content, encoding="utf-8")
return {
"input": str(path),
"output": str(out),
"title": result.title,
"length": len(result.text_content),
"status": "ok",
}
except Exception as e:
return {"input": str(path), "status": "fail", "err": str(e)}
def main():
files = list(INPUT_DIR.rglob("*"))
files = [f for f in files if f.suffix.lower() in
{".docx", ".pptx", ".xlsx", ".pdf", ".html"}]
print(f"Found {len(files)} files")
results = []
with ProcessPoolExecutor(max_workers=8) as pool:
futures = {pool.submit(convert_one, f): f for f in files}
for fut in as_completed(futures):
r = fut.result()
results.append(r)
print(r["status"], r.get("input"))
Path("manifest.json").write_text(json.dumps(results, indent=2))
if __name__ == "__main__":
main()
在 16 核机器上跑 1000 份混合格式(无 OCR)大概 30-60 秒结束。开 OCR 时间会拉到 30 分钟以上,建议把 OCR 单独走异步队列处理。
四、与 docling、unstructured 横向对比
实测 50 份样本(文档报告 + PPT 演示 + 表格 + 部分扫描页):
| 维度 | markitdown | docling | unstructured |
|---|---|---|---|
| 路线 | 规则解析 + LLM 辅助 | VLM 视觉理解版面 | 元素级解析 |
| 文本文档质量 | 优 | 优 | 优 |
| 复杂表格 | 中 | 优 | 中 |
| 扫描件 PDF | 需 OCR 外挂 | 优 | 需 OCR |
| 输出格式 | Markdown | Markdown / HTML | JSON 元素流 |
| 速度(无 OCR) | 极快 | 慢(GPU) | 中 |
| LLM 调用 | 仅图片 | 全文 | 否 |
| 学习曲线 | 低 | 中 | 高 |
| 适合场景 | 通用 RAG 预处理 | 扫描件/版面敏感 | 元素粒度控制 |
取舍建议:
- 普通 RAG 索引(90% 场景):markitdown
- 扫描件、复杂科学表格、版面位置很重要:docling
- 要做按元素分类(标题/段落/图/表)下游处理:unstructured
很多团队的现实做法是同时部署 markitdown 处理常规文档、docling 处理扫描件,分流路由。
五、集成进 RAG 管道:保留层级是关键
朴素地把 markitdown 输出整段切给 chunker 会浪费它的层级信息。我推荐的最小集成模板:
import re
from markitdown import MarkItDown
from markdown_it import MarkdownIt
from typing import List
def convert_and_chunk(path: str, max_chunk_chars: int = 800):
md = MarkItDown()
result = md.convert(path)
markdown_text = result.text_content
parser = MarkdownIt()
tokens = parser.parse(markdown_text)
chunks: List[dict] = []
stack = [] # 当前祖先标题路径
current_text = [] # 当前 chunk 累积文本
current_chars = 0
def flush():
nonlocal current_text, current_chars
if not current_text:
return
chunks.append({
"text": "".join(current_text).strip(),
"ancestors": list(stack),
"source": path,
})
current_text = []
current_chars = 0
i = 0
while i < len(tokens):
tok = tokens[i]
if tok.type == "heading_open":
flush()
level = int(tok.tag[1])
title = tokens[i + 1].content
while stack and stack[-1]["level"] >= level:
stack.pop()
stack.append({"level": level, "title": title})
i += 3
continue
if tok.type == "inline":
text = tok.content + "\n"
if current_chars + len(text) > max_chunk_chars:
flush()
current_text.append(text)
current_chars += len(text)
i += 1
flush()
return chunks
输出的 chunk 不只是文本,还带着「祖先标题路径」。在 embed 时把祖先路径拼到文本前面:
def embed_input(chunk):
path = " > ".join(a["title"] for a in chunk["ancestors"])
return f"[{path}]\n{chunk['text']}"
实测在三个内部知识库上这一步能让 top-5 召回准确率提升 5-15%。
六、五个生产环境最易踩的坑
6.1 PDF 扫描件默认不处理
markitdown 的 PDF 路径只处理文本层。扫描件 PDF 会得到空字符串或乱码。应对:先用 PyMuPDF 检测有没有文本层,没有的话路由到 docling 或 OCR pipeline。
import fitz
def has_text_layer(pdf_path):
doc = fitz.open(pdf_path)
for page in doc:
if page.get_text().strip():
return True
return False
6.2 Excel 多 sheet 容易丢
默认行为只处理第一个 sheet。多 sheet 文档需要遍历:
import pandas as pd
xls = pd.ExcelFile(path)
all_md = []
for sheet in xls.sheet_names:
df = xls.parse(sheet)
all_md.append(f"## Sheet: {sheet}\n\n")
all_md.append(df.to_markdown(index=False))
6.3 LLM OCR 成本失控
开了 LLM 增强后,1000 份富图 PPT 转换可能消耗几十美元 token。应对:先用 quick path 转一遍,再针对 result 中含图片占位符的文件单独走 LLM。
6.4 文档内嵌图片不进检索
markitdown 把图片转成  占位,alt 文字进 Markdown,但图片本身不会进 embedding。如果场景里图片很重要(例如电路图、流程图),需要在 chunking 时识别图片占位、把图片原始字节单独索引到多模态 embedding。
6.5 表格在 Markdown 里被「分段切」
Markdown 表格本质是按行拼出来的。如果 chunker 按字符数硬切,可能把表头切走、留下没头的行。应对:chunking 前先用正则识别 GFM 表格段落,把整张表作为不可拆分的单位。
TABLE_PATTERN = re.compile(r"((?:^\|.*\|\n)+)", re.MULTILINE)
七、结语
markitdown 不是炫技工具,但它解决了 LLM 应用里最不性感、最容易被低估的环节——把异构文档稳定地转成 LLM 友好的输入。把它装进你的 RAG 流水线,下游模型效果通常会立刻有可感知提升。
如果你的 RAG 系统还在用「unstructured + 各种胶水」凑出预处理,给 markitdown 一个机会。一份 docx 几十毫秒、一份 PPT 几百毫秒、CLI 一句话搞定——这种简单和速度对工程团队的吸引力,比任何 benchmark 都直接。