搞 AI Agent 的都懂一个心照不宣的秘密:一旦用户开始上传本地文档,整套流水线的品味立刻塌方。
一个二十年前的 .doc 合同、一份 2009 年导出的 .xls、一份路演 .pptx、一本 .epub,再夹一份扫描版 PDF。想让 LLM 认得,得先把它们统统翻译成 Markdown。可翻译这一步,从来没被谁真正搞定过。
老方案的四种痛法
先复盘一下过去这几年大家怎么凑合过日子。
MarkItDown:微软家的,Python 生态里最常见的一把小刀。docx 转得还行,pptx 勉强,xlsx 就开始飘,.doc 直接不认。速度中等偏慢。
Pandoc:万金油,写论文很香。缺点是格式覆盖偏窄——Excel 类完全不支持,PowerPoint 也不管,主要吃 docx / rtf / epub / odt。老技术栈,胜在稳。
Docling:IBM 出品,方向对着深度学习跑,能处理复杂版式的 PDF。但依赖重、启动慢、单文档五百毫秒起步,喂到批量流水线里马上变瓶颈。
Unstructured:野心大,什么格式都想吃一口,但吃得都不深。质量参差,输出经常带一堆冗余噪声。
LibreOffice CLI:无损,好像什么都支持。真跑起来一份文档要一秒多,纯写死的进程调用,别提做并发。
mammoth:docx 一枝独秀,其他都不管。
结果就是每个团队的文档管道,最后都长成同一副样子:
if pdf: 走 pdfplumber / pypdf → 图片页降级 OCR
elif docx: 走 mammoth 或 python-docx
elif xlsx: 走 openpyxl 硬解
elif pptx: 走 python-pptx 拼接
elif rtf: ???
elif epub: 走 ebooklib 再自己遍历
else: raise UnsupportedFormat
四五个库拼在一起,四五套依赖,四五种失败姿势,输出格式还各说各话——docx 出来的表格是一种样子,xlsx 出来的表格又是另一种。想给 LLM 一个统一的 Markdown 骨架?做梦。
AnyDoc 的登场姿势
AnyDoc 是 Firecrawl 在 2026 年 8 月开源的方案,GitHub 上已经拿了 17.6k stars。搭档是 pdf-inspector——同一个团队做的 PDF 专用引擎,13k stars,被内嵌进了 AnyDoc。
一句话:Rust 写的,一个二进制吃 14 种格式,中位转换 4.4 毫秒,输出统一 GitHub Flavored Markdown。
正式覆盖清单:
| 格式 | 扩展名 |
|---|---|
| Word | .doc .docx .docm |
| PowerPoint | .ppt .pps .pot .pptx .pptm .ppsx .ppsm |
| Excel | .xls .xlsx .xlsm .xlsb |
| OpenDocument | .odt .ods .odp |
| Rich Text | .rtf |
| EPUB | .epub |
| CSV | .csv |
.pdf(纯文本页) |
没有 API key,不依赖外部服务,不需要装 LibreOffice、不需要拉 Docker 镜像。npm install、pip install、cargo add 三选一,装完就能用。
一个调用,四种入口
同一个 API,四种语言绑定长得几乎一模一样。
CLI:
npx @firecrawl/anydoc report.docx > report.md
装成全局命令:
npm install -g @firecrawl/anydoc
anydoc report.docx -o report.md
Node.js:
import { toMarkdown } from '@firecrawl/anydoc';
const md = await toMarkdown('contract.docx');
// 或者从字节流,自动嗅探格式
const md2 = await toMarkdownBytes(bytes);
Python:
import anydoc
md = anydoc.to_markdown("contract.docx")
# 从字节流
md2 = anydoc.to_markdown_bytes(data)
Rust:
let md = anydoc::to_markdown("contract.docx")?;
浏览器(WebAssembly):
npm install @firecrawl/anydoc-wasm
这一条比较惊人。文档不出浏览器就能转成 Markdown,走 WASM 本地跑。合规敏感的场景直接省一整套后端。
Agent Skill:喂给 Claude Code 的最短路径
如果你在用 Claude Code、Codex、Cursor、OpenCode 这类 Agent 框架,AnyDoc 的最快接法是它自带的 Agent Skill:
npx skills add firecrawl/anydoc
一条命令下去,Agent 立刻学会"看到不认识的文档就去调 anydoc CLI 转 Markdown 再读"。不用你写 wrapper,不用你写 system prompt,不用你手工塞 tool schema。Skill 内部自带调用规约,AI 遇到 .docx、.pptx、.xlsx 会自己走这条路。
如果你熟悉 Claude Code Skills / MCP 的组合玩法,这个 Skill 就是那类"该到位的地方直接到位"的典范——不多一分抽象,也不少一分能力。(想深挖 Skill × MCP 联动可以顺手翻 Claude Code 多 Skill/MCP 实际工作流拆解。)
架构:为什么它能做到又快又统一
AnyDoc 的内部流水线很朴素:
document bytes
│
├─► 格式检测(读文件头,不看扩展名)
│
├─► 对应格式的 parser
│ │
│ └─► Document 共享模型(blocks / inlines / tables / footnotes / assets)
│ │
│ └─► GFM serializer → Markdown
│
└─► PDF → pdf-inspector → Markdown
三个设计点值得单独拎出来说。
格式检测走文件内容,不看扩展名。用户随手把 .pptx 存成 .pdf 这种事天天有。AnyDoc 直接读 ZIP 包的 mimetype、OLE stream 名字、PDF header、RTF open group。文件叫什么它不管,长什么样它才管。CSV 是唯一例外——没内容标记,必须靠扩展名或显式声明。
所有格式都走同一个 Document 模型。这一步是重头戏。docx 也好、rtf 也好、odt 也好,parser 输出的都是同一套抽象:block / inline / table / footnote / asset。最后由同一个 Markdown 序列化器落盘。
意思是:你不再需要"docx 表格的转义 bug"、“rtf 表格的转义 bug”、“odt 表格的转义 bug"分别写三次修复。修一次,全格式受益。
PDF 内嵌 pdf-inspector。这个组件本身值得单开一节讲。
pdf-inspector:给 PDF 分流的"路由器”
大多数 PDF 管道犯的是同一个错:默认每一页都可能是扫描件,所以都塞进 OCR。慢、贵、准确率还不如原文。
pdf-inspector 干的是路由这一层。它不渲染图像,只读 PDF 内部结构(字体编码、文本运算符、图像覆盖率),逐页判定:
- 纯文本页:直接原生抽取,阅读顺序保留
- 扫描或图像页:打标记,交给下游 OCR 处理
对于一份 200 页的报告,如果 150 页是纯文本,那 150 页压根不用碰 GPU。Firecrawl 自家的 Fire-PDF 靠这一层拿到了 3.5x 到 5x 的加速。
AnyDoc 里的 pdf 支持就是把 pdf-inspector 嵌了进来。文本 PDF 走它、无损、毫秒级;图像 PDF 会抛出 Unsupported——AnyDoc 明确不做 OCR。想要 OCR,要么自己接一层,要么走 Firecrawl 的托管 /parse API。
这是这套方案最诚实的边界。别指望它把扫描版合同变成结构化文本,那是另一个战场。
数字:4.4ms vs 1129ms
Firecrawl 团队跑了个基准测试,100 份真实文档,14 种格式,LLM 盲评质量。数字如下:
| 工具 | 覆盖格式 | 中位耗时 (ms) | 总分 |
|---|---|---|---|
| anydoc | 14/14 | 4.4 | 81 |
| libreoffice | 12/14 | 1129.5 | 40 |
| unstructured | 8/14 | 572.9 | 63 |
| markitdown | 6/14 | 134.8 | 65 |
| pandoc | 5/14 | 102.1 | 56 |
| docling | 4/14 | 513.6 | 57 |
| mammoth | 1/14 | 52.5 | 70 |
三件事得说清楚。
第一,总分口径。每个工具只算它支持的格式的平均分。mammoth 的 70 分是 docx 一个格式跑出来的,AnyDoc 的 81 分是 14 格式的平均。所以直接看总分不完全公平,得看单格式对比。
第二,单格式对比。就算按每个格式单独看,AnyDoc 在 doc、docm、docx、epub、odp、ods、odt、ppt、pptx、rtf、xls、xlsm、xlsx 上都是第一。没有第二。
第三,速度差是数量级级别。4.4ms 对 52ms(第二快的 mammoth),对 102ms(Pandoc),对 1129ms(LibreOffice)。批量场景差异非常吓人。
评测用的是 Ryzen 9 9950X3D 单核暖启动,corpus 是 Firecrawl 内部的(他们没开源,这一点保持诚实态度)。数字本身可以有争议,但架构给出的速度上限差得摆在明面上——纯 Rust 无 ML 模型对上一堆 Java/Python 进程 spawn,快十倍不奇怪。
错误处理:只在真的不行时才失败
AnyDoc 的错误类型设计得很干净:
| 错误 | 含义 |
|---|---|
Unsupported | 未知格式,或图像版 PDF |
Malformed | 结构损坏,抽不出任何东西 |
Encrypted | 加密或密码保护 |
ResourceLimit | 触发解压、嵌套、节点数上限 |
MissingPart | 关键部分缺失 |
Io | 读文件失败 |
这套设计意味着批量处理时你可以自信地 catch-and-continue。遇到 Encrypted 就记录跳过,遇到 Malformed 就报给用户重传,遇到 Unsupported 就走 OCR 分支。语义清晰,不用去猜异常里的 message 字符串。
适合谁,不适合谁
真值得换的场景:
- 用户上传的文档格式五花八门,你不想再维护四五个 parser
- 在做 RAG 或知识库,需要一致的 Markdown 结构做 chunk
- 做 AI Agent 需要读 attach 的 office 文件(Claude Code / Codex Skill 直接接)
- 批量转换任务,速度差异会直接变成成本差异
- 隐私合规不想让文档过任何第三方 API(本地跑,浏览器也能跑)
别硬上的场景:
- 主要处理扫描版 PDF:AnyDoc 不做 OCR,别为了图便宜勉强用,直接上 Mistral OCR、PaddleOCR、Fire-PDF、Tesseract 之类
- 只处理一种格式且已经稳定跑起来:比如只有 docx 而且 mammoth 已经跑通,收益边际
- 要求特定输出格式而非 Markdown:AnyDoc 出口就是 GFM,硬要 HTML/JSON 得自己再走一道
- 需要保留复杂版式(多列排版、精确定位):Markdown 本身丢版面,这不是 AnyDoc 的锅但结果就是丢
三分钟上手
如果你只想试一下手感,最短路径:
# 装
npm install -g @firecrawl/anydoc
# 转一份看看
anydoc ~/Downloads/合同.docx > 合同.md
# 或者从 stdin
cat report.pdf | anydoc > report.md
想直接接进 Claude Code / Codex:
npx skills add firecrawl/anydoc
然后重启 Agent,让它自己处理 attach 的文档。
想在 Python 项目里内嵌:
pip install firecrawl-anydoc
import anydoc
def load_doc_as_markdown(path: str) -> str:
try:
return anydoc.to_markdown(path)
except anydoc.ConvertError as e:
# Encrypted / Malformed / Unsupported 分类处理
return f"[无法解析: {type(e).__name__}]"
一点老实话
AnyDoc 不是革命,是收敛。
过去几年"把任何文档变成 LLM 能吃的东西"这个赛道,谁都想解决,谁都只解决了一半。MarkItDown 覆盖窄、Pandoc 老、Docling 重、LibreOffice 慢——它们背后的技术选型都有历史包袱。
Firecrawl 用纯 Rust 从零重写了这一层,没有历史包袱、没有 ML 模型依赖、没有 API key,一个二进制吃 14 格式,配套一个专门管 PDF 分流的 pdf-inspector。这不是魔法,就是在对的抽象层把力气花对了地方——共享 Document 模型让 bug 修一次通杀,格式检测走内容让扩展名骗不了它,把 OCR 明确划到边界之外让整个系统跑得又快又稳。
对于每天要处理各种奇怪文档格式的 AI 应用开发者来说,这是那种"用过就回不去了"的工具。
至于边界——AnyDoc 只做它能做好的部分。扫描版走 OCR,加密版走人工,复杂版式认命降级。诚实这一点,比"什么都能做"重要得多。