MarkItDown 深度拆解:微软如何用 Python 重新定义文档转换——从 89K Star 到 RAG 时代的「通用语」
当你的 LLM 对着一堆 PDF 束手无策时,这个工具已经默默完成了 20+ 格式的 Markdown 化。微软 AutoGen 团队的开源神作,正在成为 AI 时代的「文档翻译官」。
一、背景:为什么我们需要「文档转 Markdown」?
1.1 LLM 的「格式盲区」
你有没有遇到过这样的场景:
- 把一份 50 页的产品文档丢给 GPT,结果它只读了前几页
- 想让 Claude 帮你分析一份 Excel 表格,却发现它对表格结构一无所知
- 试图用 RAG 构建企业知识库,却在 PDF 解析这一步卡了三天
这不是 LLM 的能力问题,而是格式鸿沟。
大语言模型的核心能力是处理纯文本,而真实世界的知识沉淀在各种格式的文档里:PDF、Word、PPT、Excel、图片、音频……这些格式对 LLM 来说,就像人类面对甲骨文——能看,但看不懂。
1.2 Markdown:AI 时代的「通用语」
为什么是 Markdown?
# Markdown 的本质:结构化纯文本
# 标题 → <h1>标题</h1>
- 列表 → <ul><li>列表</li></ul>
**加粗** → <strong>加粗</strong>
| 表格 | → <table>...</table>
Markdown 用最少的符号表达了完整的文档结构。对 LLM 来说:
- Token 效率高:同样的内容,Markdown 比 HTML 节省 60% 的 token
- 结构完整:标题层级、列表嵌套、表格对齐,一个不落
- 语义清晰:没有 CSS 干扰,模型专注于内容本身
- 易于分块:RAG 向量化时的天然切分边界
这就是为什么微软 AutoGen 团队开发了 MarkItDown——让所有文档都能说 LLM 听得懂的语言。
1.3 市面方案的问题
在 MarkItDown 之前,文档转换已经有很多方案:
| 方案 | 问题 |
|---|---|
| pdf2text | 只能提取纯文本,结构全丢 |
| Apache Tika | Java 生态,部署复杂,配置地狱 |
| 商业 API | 按页收费,文档量大时成本失控 |
| 自研脚本 | 每种格式都要写一套,维护成本高 |
MarkItDown 的定位很清晰:一个 Python 包,统一处理 20+ 格式,专为 LLM/RAG 场景优化。
二、MarkItDown 是什么?
2.1 项目概览
项目名: markitdown
开发团队: 微软 AutoGen 团队
开源协议: MIT
语言: Python 3.10+
GitHub Star: 99,000+(2026年4月)
定位: 多格式文档转 Markdown 工具
核心场景: LLM 输入预处理、RAG 知识库构建、文档分析
2.2 支持的格式(20+)
办公文档:PDF、DOCX、PPTX、XLSX、CSV
图片格式:JPG、PNG、GIF、BMP(支持 OCR 文字提取)
音频格式:MP3、WAV、FLAC(支持语音转文字)
网页相关:HTML、EPUB、YouTube 字幕
数据格式:JSON、XML、ZIP(自动解压遍历)
其他格式:RST、ORG、IPYNB(Jupyter Notebook)
2.3 核心特性
- 结构保留:标题层级、列表嵌套、表格对齐、超链接、图片描述
- 智能识别:自动检测文件格式,无需手动指定
- 插件机制:支持自定义转换器扩展新格式
- 双模式:命令行工具 + Python API
- LLM 优化:输出格式专为 RAG 分块设计
三、架构设计:从文件到 Markdown 的旅程
3.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ MarkItDown │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ CLI │ │ Python API │ │ Stream Processing │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ File Type Detection │ │
│ │ (magic number + extension + content sniffing) │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Converter Registry │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │PDFConv │ │DOCXConv │ │PPTXConv │ │XLSXConv │... │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────┼────────────┼────────────┼────────────┼────────┘ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ DocumentConverter Base Class │ │
│ │ - convert(file_stream, stream_info) │ │
│ │ - accept(file_stream) -> bool │ │
│ │ - convert_to_md(file_stream) -> str │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Markdown Output │ │
│ │ - text_content: str │ │
│ │ - title: str (extracted) │ │
│ │ - metadata: dict (EXIF, creation time, etc.) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.2 核心类设计
# src/markitdown/_base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import BinaryIO, Optional
import mimeparse
@dataclass
class StreamInfo:
"""文件流元信息"""
mimetype: Optional[str] = None
extension: Optional[str] = None
filename: Optional[str] = None
@dataclass
class DocumentConverterResult:
"""转换结果"""
text_content: str
title: Optional[str] = None
metadata: dict = None
class DocumentConverter(ABC):
"""转换器基类"""
@abstractmethod
def accept(self, file_stream: BinaryIO, stream_info: StreamInfo) -> bool:
"""判断是否能处理该文件"""
pass
@abstractmethod
def convert(
self,
file_stream: BinaryIO,
stream_info: StreamInfo,
**kwargs
) -> DocumentConverterResult:
"""执行转换"""
pass
3.3 转换器注册机制
# src/markitdown/_converter_registry.py
class ConverterRegistry:
"""转换器注册表"""
def __init__(self):
self._converters: list[DocumentConverter] = []
self._mime_map: dict[str, DocumentConverter] = {}
def register(self, converter: DocumentConverter):
"""注册转换器"""
self._converters.append(converter)
# 按 mimetype 建立索引
for mime in converter.supported_mimetypes:
self._mime_map[mime] = converter
def find_converter(
self,
file_stream: BinaryIO,
stream_info: StreamInfo
) -> Optional[DocumentConverter]:
"""查找合适的转换器"""
# 优先按 mimetype 精确匹配
if stream_info.mimetype:
if converter := self._mime_map.get(stream_info.mimetype):
return converter
# 回退到逐个检查
for converter in self._converters:
file_stream.seek(0)
if converter.accept(file_stream, stream_info):
return converter
return None
# 内置转换器注册
registry = ConverterRegistry()
registry.register(PdfConverter())
registry.register(DocxConverter())
registry.register(PptxConverter())
registry.register(XlsxConverter())
registry.register(ImageConverter()) # OCR
registry.register(AudioConverter()) # Whisper
registry.register(HtmlConverter())
# ... 更多转换器
四、核心转换器深度解析
4.1 PDF 转换器:从扫图到结构
PDF 是最复杂的格式,因为它的设计初衷是「打印保真」,而不是「结构化」。
# src/markitdown/_pdf_converter.py
import pdfplumber
from pdf2image import convert_from_path
import pytesseract
class PdfConverter(DocumentConverter):
"""PDF 转换器"""
supported_mimetypes = ["application/pdf"]
def accept(self, file_stream, stream_info) -> bool:
# PDF 文件头:'%PDF-'
header = file_stream.read(5)
file_stream.seek(0)
return header == b'%PDF-'
def convert(self, file_stream, stream_info, **kwargs):
enable_ocr = kwargs.get('enable_ocr', False)
# 方案一:pdfplumber 提取文本层
if not enable_ocr:
return self._extract_text_layer(file_stream)
# 方案二:OCR 识别扫描件
return self._ocr_extract(file_stream)
def _extract_text_layer(self, file_stream):
"""提取 PDF 内嵌文本"""
with pdfplumber.open(file_stream) as pdf:
markdown_parts = []
for page_num, page in enumerate(pdf.pages, 1):
# 提取文本,保留布局
text = page.extract_text(
layout=True, # 保留原始布局
x_tolerance=3,
y_tolerance=3
)
# 提取表格
tables = page.extract_tables()
for table in tables:
text += self._table_to_markdown(table)
markdown_parts.append(f"## 第 {page_num} 页\n\n{text}")
return DocumentConverterResult(
text_content="\n\n".join(markdown_parts),
title=self._extract_title(pdf)
)
def _table_to_markdown(self, table_data):
"""二维数组转 Markdown 表格"""
if not table_data:
return ""
# 表头
header = table_data[0]
separator = ["---"] * len(header)
rows = table_data[1:]
lines = []
lines.append("| " + " | ".join(str(cell or "") for cell in header) + " |")
lines.append("| " + " | ".join(separator) + " |")
for row in rows:
lines.append("| " + " | ".join(str(cell or "") for cell in row) + " |")
return "\n".join(lines)
def _ocr_extract(self, file_stream):
"""OCR 识别扫描版 PDF"""
# 转为图片
images = convert_from_path(file_stream, dpi=300)
markdown_parts = []
for i, image in enumerate(images, 1):
# OCR 识别
text = pytesseract.image_to_string(image, lang='chi_sim+eng')
markdown_parts.append(f"## 第 {i} 页 (OCR)\n\n{text}")
return DocumentConverterResult(
text_content="\n\n".join(markdown_parts)
)
关键设计点:
- 双模式切换:文本层优先,OCR 保底
- 表格智能识别:
pdfplumber.extract_tables()自动检测表格边框 - 布局保留:
layout=True保持多栏排版的阅读顺序 - DPI 可调:OCR 模式默认 300 DPI,平衡识别率和速度
4.2 DOCX 转换器:XML 解析的艺术
Word 文档本质是 ZIP 包 + XML 结构:
# src/markitdown/_docx_converter.py
from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
class DocxConverter(DocumentConverter):
"""Word 文档转换器"""
supported_mimetypes = [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
]
def convert(self, file_stream, stream_info, **kwargs):
doc = Document(file_stream)
parts = []
for para in doc.paragraphs:
parts.append(self._parse_paragraph(para))
# 处理表格
for table in doc.tables:
parts.append(self._parse_table(table))
return DocumentConverterResult(
text_content="\n\n".join(parts),
title=self._extract_title(doc)
)
def _parse_paragraph(self, para):
"""解析段落"""
text = para.text.strip()
if not text:
return ""
# 标题检测
if para.style.name.startswith('Heading'):
level = int(para.style.name.replace('Heading ', ''))
return f"{'#' * level} {text}"
# 列表检测
if para.style.name.startswith('List'):
return f"- {text}"
# 格式处理(加粗、斜体)
formatted_text = self._apply_formatting(para)
return formatted_text
def _apply_formatting(self, para):
"""处理行内格式"""
result = []
for run in para.runs:
text = run.text
if run.bold:
text = f"**{text}**"
if run.italic:
text = f"*{text}*"
if run.underline:
text = f"<u>{text}</u>"
result.append(text)
return "".join(result)
def _parse_table(self, table):
"""解析表格"""
rows = [[cell.text for cell in row.cells] for row in table.rows]
return self._table_to_markdown(rows)
4.3 PPTX 转换器:幻灯片的智能降维
PPT 的核心挑战是视觉布局 → 文本序列:
# src/markitdown/_pptx_converter.py
from pptx import Presentation
from pptx.util import Inches
class PptxConverter(DocumentConverter):
"""PowerPoint 转换器"""
supported_mimetypes = [
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
]
def convert(self, file_stream, stream_info, **kwargs):
prs = Presentation(file_stream)
slides_content = []
for slide_num, slide in enumerate(prs.slides, 1):
slides_content.append(f"## 幻灯片 {slide_num}\n")
slides_content.append(self._parse_slide(slide))
return DocumentConverterResult(
text_content="\n\n".join(slides_content),
title=self._extract_title(prs)
)
def _parse_slide(self, slide):
"""解析单页幻灯片"""
# 按 Y 坐标排序,保持阅读顺序
shapes = sorted(
[s for s in slide.shapes if hasattr(s, 'text')],
key=lambda s: (s.top if s.top else 0, s.left if s.left else 0)
)
parts = []
for shape in shapes:
text = shape.text.strip()
if not text:
continue
# 根据形状类型判断是否为标题
if hasattr(shape, 'text_frame'):
# 标题形状
if shape.has_text_frame:
first_para = shape.text_frame.paragraphs[0]
if first_para.font.size and first_para.font.size > Inches(0.5):
parts.append(f"### {text}")
continue
parts.append(text)
return "\n\n".join(parts)
关键设计:
- 坐标排序:
sorted(key=lambda s: (s.top, s.left))确保从上到下、从左到右的阅读顺序 - 字体大小检测:大号字体自动识别为标题
- 备注提取:可选提取演讲者备注作为补充信息
4.4 XLSX 转换器:表格到表格的哲学
Excel 文件可能是最「结构化」的输入:
# src/markitdown/_xlsx_converter.py
import openpyxl
from openpyxl.utils import get_column_letter
class XlsxConverter(DocumentConverter):
"""Excel 转换器"""
supported_mimetypes = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel"
]
def convert(self, file_stream, stream_info, **kwargs):
wb = openpyxl.load_workbook(file_stream, read_only=True, data_only=True)
sheets_content = []
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
sheets_content.append(f"## 工作表:{sheet_name}\n")
sheets_content.append(self._parse_sheet(sheet))
return DocumentConverterResult(
text_content="\n\n".join(sheets_content)
)
def _parse_sheet(self, sheet):
"""解析工作表"""
# 找到有效区域
max_row = sheet.max_row
max_col = sheet.max_column
if max_row == 0 or max_col == 0:
return ""
rows = []
for row in sheet.iter_rows(min_row=1, max_row=max_row, max_col=max_col):
cells = [str(cell.value) if cell.value is not None else "" for cell in row]
rows.append(cells)
return self._table_to_markdown(rows)
4.5 图片转换器:OCR 的工程化封装
# src/markitdown/_image_converter.py
from PIL import Image
import pytesseract
class ImageConverter(DocumentConverter):
"""图片转换器(OCR)"""
supported_mimetypes = [
"image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp"
]
def convert(self, file_stream, stream_info, **kwargs):
image = Image.open(file_stream)
# 提取 EXIF 元数据
exif_data = self._extract_exif(image)
# OCR 识别
ocr_lang = kwargs.get('ocr_lang', 'chi_sim+eng')
text = pytesseract.image_to_string(image, lang=ocr_lang)
# 构建输出
parts = []
if exif_data:
parts.append("### 图片元信息")
parts.append(self._format_exif(exif_data))
if text.strip():
parts.append("### 识别文本")
parts.append(text)
return DocumentConverterResult(
text_content="\n\n".join(parts),
metadata=exif_data
)
五、插件机制:如何扩展新格式
MarkItDown 的插件机制设计得非常优雅:
5.1 自定义转换器模板
# my_converter.py
from markitdown import DocumentConverter, DocumentConverterResult, StreamInfo
class MyCustomConverter(DocumentConverter):
"""自定义格式转换器示例"""
supported_mimetypes = ["application/x-myformat"]
supported_extensions = [".myf"]
def accept(self, file_stream, stream_info) -> bool:
# 检查文件头魔数
header = file_stream.read(8)
file_stream.seek(0)
return header == b'MYFORMAT'
def convert(
self,
file_stream,
stream_info,
**kwargs
) -> DocumentConverterResult:
# 解析文件内容
content = self._parse_my_format(file_stream)
# 转换为 Markdown
markdown = self._to_markdown(content)
return DocumentConverterResult(
text_content=markdown,
title=content.get('title'),
metadata=content.get('metadata', {})
)
def _parse_my_format(self, file_stream):
"""解析特定格式"""
# 实现你的解析逻辑
pass
def _to_markdown(self, parsed_content):
"""转换为 Markdown"""
# 实现转换逻辑
pass
5.2 注册插件
# 方式一:代码注册
from markitdown import MarkItDown
md = MarkItDown()
md.register_converter(MyCustomConverter())
# 方式二:配置文件
# ~/.markitdown/plugins.yaml
plugins:
- module: my_converter
class: MyCustomConverter
priority: 10 # 数字越大优先级越高
5.3 插件钩子
class DocumentConverter(ABC):
"""扩展的钩子方法"""
def pre_convert(self, file_stream, stream_info):
"""转换前处理,可用于预处理"""
pass
def post_convert(self, result: DocumentConverterResult):
"""转换后处理,可用于后处理"""
return result
def on_error(self, exception: Exception):
"""错误处理"""
pass
六、实战:构建 RAG 知识库
6.1 场景描述
假设你要为企业构建一个智能问答系统,需要处理以下文档:
- 产品手册(PDF)
- 技术规范(DOCX)
- 会议记录(PPTX)
- 数据报告(XLSX)
- 产品截图(PNG)
6.2 完整代码
# rag_document_processor.py
import os
from pathlib import Path
from typing import Generator
from markitdown import MarkItDown
import chromadb
from chromadb.utils import embedding_functions
class RAGDocumentProcessor:
"""RAG 文档处理器"""
def __init__(
self,
input_dir: str,
chunk_size: int = 1000,
chunk_overlap: int = 200,
enable_ocr: bool = True
):
self.input_dir = Path(input_dir)
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
# 初始化 MarkItDown
self.md_converter = MarkItDown(enable_ocr=enable_ocr)
# 初始化向量数据库
self.chroma_client = chromadb.PersistentClient(path="./chroma_db")
self.embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="BAAI/bge-large-zh-v1.5"
)
self.collection = self.chroma_client.get_or_create_collection(
name="knowledge_base",
embedding_function=self.embedding_fn
)
def process_all(self):
"""处理所有文档"""
supported_extensions = {
'.pdf', '.docx', '.pptx', '.xlsx', '.csv',
'.jpg', '.png', '.gif',
'.html', '.htm', '.json', '.xml'
}
for file_path in self.input_dir.rglob('*'):
if file_path.suffix.lower() in supported_extensions:
print(f"处理: {file_path}")
try:
self._process_single_file(file_path)
except Exception as e:
print(f"失败: {file_path}, 错误: {e}")
def _process_single_file(self, file_path: Path):
"""处理单个文件"""
# 转换为 Markdown
result = self.md_converter.convert(str(file_path))
markdown_content = result.text_content
# 智能分块
chunks = self._smart_chunk(markdown_content, file_path)
# 存入向量库
for i, chunk in enumerate(chunks):
self.collection.add(
documents=[chunk['content']],
metadatas=[{
'source': str(file_path),
'chunk_id': i,
'title': result.title or file_path.stem,
**chunk['metadata']
}],
ids=[f"{file_path.stem}_{i}"]
)
def _smart_chunk(self, markdown: str, file_path: Path) -> list[dict]:
"""智能分块:按标题层级切分"""
chunks = []
current_chunk = []
current_headers = []
for line in markdown.split('\n'):
# 检测标题
if line.startswith('#'):
# 保存当前块
if current_chunk:
chunks.append({
'content': '\n'.join(current_headers + current_chunk),
'metadata': {'section': current_headers[0] if current_headers else ''}
})
current_chunk = []
# 更新标题层级
level = len(line) - len(line.lstrip('#'))
if level <= len(current_headers):
current_headers = current_headers[:level]
current_headers.append(line.strip())
else:
current_chunk.append(line)
# 保存最后一块
if current_chunk:
chunks.append({
'content': '\n'.join(current_headers + current_chunk),
'metadata': {'section': current_headers[0] if current_headers else ''}
})
return chunks
def query(self, question: str, n_results: int = 5) -> list[dict]:
"""查询知识库"""
results = self.collection.query(
query_texts=[question],
n_results=n_results
)
return [
{
'content': doc,
'source': meta['source'],
'section': meta.get('section', '')
}
for doc, meta in zip(results['documents'][0], results['metadatas'][0])
]
# 使用示例
if __name__ == "__main__":
processor = RAGDocumentProcessor(
input_dir="./documents",
enable_ocr=True
)
# 处理所有文档
processor.process_all()
# 查询
answer = processor.query("产品的核心功能有哪些?")
for item in answer:
print(f"来源: {item['source']}")
print(f"内容: {item['content'][:200]}...")
七、性能优化指南
7.1 批量处理优化
import concurrent.futures
from functools import partial
class BatchProcessor:
"""批量处理优化"""
def __init__(self, max_workers: int = 4):
self.md_converter = MarkItDown()
self.max_workers = max_workers
def process_batch(self, file_paths: list[str]) -> list[str]:
"""并行处理多个文件"""
convert_func = partial(
self._safe_convert,
converter=self.md_converter
)
with concurrent.futures.ThreadPoolExecutor(
max_workers=self.max_workers
) as executor:
results = list(executor.map(convert_func, file_paths))
return results
def _safe_convert(self, file_path: str, converter: MarkItDown) -> str:
"""安全转换,带错误处理"""
try:
result = converter.convert(file_path)
return result.text_content
except Exception as e:
return f"ERROR: {file_path} - {str(e)}"
7.2 内存优化
# 流式处理大文件
class StreamingConverter:
"""流式转换,降低内存占用"""
def convert_large_pdf(self, file_path: str, output_path: str):
"""流式处理大 PDF"""
import pdfplumber
with pdfplumber.open(file_path) as pdf:
with open(output_path, 'w') as out:
for page in pdf.pages:
text = page.extract_text()
if text:
out.write(f"## 第 {page.page_number} 页\n\n{text}\n\n")
# 及时释放内存
page.close()
7.3 缓存策略
import hashlib
import pickle
from pathlib import Path
class CachedConverter:
"""带缓存的转换器"""
def __init__(self, cache_dir: str = "./cache"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.md_converter = MarkItDown()
def convert_with_cache(self, file_path: str) -> str:
"""带缓存的转换"""
# 计算文件哈希作为缓存键
file_hash = self._compute_hash(file_path)
cache_file = self.cache_dir / f"{file_hash}.pkl"
# 检查缓存
if cache_file.exists():
with open(cache_file, 'rb') as f:
return pickle.load(f)
# 执行转换
result = self.md_converter.convert(file_path)
# 写入缓存
with open(cache_file, 'wb') as f:
pickle.dump(result.text_content, f)
return result.text_content
def _compute_hash(self, file_path: str) -> str:
"""计算文件哈希"""
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
sha256.update(chunk)
return sha256.hexdigest()
八、与 LLM 集成最佳实践
8.1 作为 Function Calling 工具
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "convert_document",
"description": "将文档转换为 Markdown 格式,便于 LLM 处理",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "文档文件路径"
},
"enable_ocr": {
"type": "boolean",
"description": "是否启用 OCR(适用于扫描件)",
"default": False
}
},
"required": ["file_path"]
}
}
}
]
def convert_document(file_path: str, enable_ocr: bool = False) -> str:
"""文档转换函数"""
md = MarkItDown(enable_ocr=enable_ocr)
result = md.convert(file_path)
return result.text_content
def chat_with_document(user_message: str, file_path: str = None):
"""带文档理解的对话"""
messages = [{"role": "user", "content": user_message}]
if file_path:
# 先转换文档
doc_content = convert_document(file_path)
messages[0]["content"] = f"""
参考以下文档内容回答问题:
{doc_content[:5000]} # 截取前 5000 字符避免超长
问题:{user_message}
"""
response = client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=tools,
tool_choice="auto"
)
return response.choices[0].message.content
8.2 与 LangChain 集成
from langchain.tools import BaseTool
from langchain.agents import initialize_agent
from langchain.llms import OpenAI
class MarkItDownTool(BaseTool):
"""LangChain 工具封装"""
name = "document_converter"
description = "将各种格式的文档转换为 Markdown"
def _run(self, file_path: str) -> str:
md = MarkItDown()
result = md.convert(file_path)
return result.text_content
async def _arun(self, file_path: str) -> str:
return self._run(file_path)
# 创建 Agent
llm = OpenAI(temperature=0)
tools = [MarkItDownTool()]
agent = initialize_agent(
tools,
llm,
agent="zero-shot-react-description",
verbose=True
)
# 使用
agent.run("请分析 ./report.pdf 的内容,并总结关键发现")
九、与其他方案对比
| 维度 | MarkItDown | Apache Tika | Unstructured | pdfplumber |
|---|---|---|---|---|
| 语言 | Python | Java | Python | Python |
| 格式支持 | 20+ | 1000+ | 30+ | 仅 PDF |
| 结构保留 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 部署复杂度 | pip install | JVM + 配置 | 多依赖 | 轻量 |
| LLM 优化 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| OCR 支持 | 内置 | 需插件 | 内置 | 需集成 |
| 性能 | 快 | 中 | 中 | 快 |
| 开源协议 | MIT | Apache 2.0 | MIT | MIT |
十、总结与展望
10.1 MarkItDown 的价值
- 统一接口:一个 API 处理 20+ 格式,告别碎片化方案
- LLM 优先:输出格式专为 RAG 分块设计
- 工程友好:CLI + API 双模式,适配各种场景
- 可扩展:插件机制支持自定义格式
- 微软背书:AutoGen 团队维护,质量和更新有保障
10.2 适用场景
- ✅ 企业知识库构建
- ✅ LLM 应用文档预处理
- ✅ 文档批量格式转换
- ✅ 自动化测试报告解析
- ✅ 多模态内容提取(图片 OCR、音频转写)
10.3 不适用场景
- ❌ 精确排版还原(Markdown 不承载样式)
- ❌ 加密文档解密
- ❌ 实时协作编辑
- ❌ 复杂图表重绘
10.4 未来展望
随着 LLM 应用的普及,文档预处理将成为基础设施。MarkItDown 的定位类似「AI 时代的 imagemagick」——一个默默工作在流水线前端的格式转换器。
可能的演进方向:
- 更多格式:CAD 图纸、3D 模型、视频帧提取
- 云端版本:Serverless API,按调用计费
- 与 LLM 深度集成:自动摘要、关键信息抽取、问答生成一体化
- 多语言 SDK:Go、Rust、JavaScript 绑定
附录:快速上手命令
# 安装(完整版)
pip install "markitdown[all]"
# 基础转换
markitdown document.pdf > output.md
# 启用 OCR
markitdown scan.pdf --enable-ocr -o result.md
# 批量转换
for f in *.docx; do
markitdown "$f" > "${f%.docx}.md"
done
# Python API
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("report.xlsx")
print(result.text_content)
参考链接:
- GitHub: https://github.com/microsoft/markitdown
- PyPI: https://pypi.org/project/markitdown/
- 文档: https://microsoft.github.io/markitdown/
一万行代码,换来一句
pip install markitdown。这就是开源的魅力——把复杂留给工具,把简单还给用户。