AI agent 擅长处理代码、配置文件和 Markdown,但在真实工作环境中,大量数据资产以 .docx 格式沉淀,大量协作流程以 Word 文档为流转载体。Agent 要真正融入组织,必须能可靠地读写这些遗留格式。
现有的 docx 处理工具普遍存在两个问题:
- 保真度不足:将 docx 转换为中间格式再重建时,编号格式、分页位置、样式继承关系等信息不可逆丢失。
- API 晦涩:以 OOXML 元素为操作粒度,Agent 难以理解文档结构、无法做可靠的语义编辑。
AtomDoc 采用另一种思路:分离内容与样式,通过逐参数比对精准识别样式类型,在原始 XML 上做最小侵入式文本覆盖。
docx ──split──→ content.yaml + style.yaml ──编辑──→ docx
│
├── resources/ 图片等二进制资源
└── passthrough/ 原始 XML 部件(主题、编号定义、页眉页脚等)
pip install atomdoc依赖 Python 3.10+。roundtrip 命令需要 LibreOffice(soffice 可用即可)。
# 拆分 docx 为可编辑的 YAML
atomdoc split 模板.docx -o workdir/
# 产物:
# workdir/content.yaml — 文档内容(段落、表格、图片、超链接)
# workdir/style.yaml — 样式定义(按层级组织:run / paragraph / cell / row / table)
# workdir/resources/ — 提取的图片
# workdir/passthrough/ — 透传的原始 XML(主题、设置、编号定义等)
# 识别重复格式模式——从 Normal 垃圾桶中捞出值得命名的格式
atomdoc patterns workdir/
# 编辑 content.yaml 和 style.yaml 后校验
atomdoc validate workdir/
# 组装回 docx
atomdoc assemble workdir/ -o 输出.docx
# 视觉保真度验证(拆分→组装→渲染→逐页像素对比)
atomdoc roundtrip 输入.docxOOXML 是深度嵌套的 XML 包,样式通过 basedOn 链继承,编号格式分布在 numbering.xml 的 abstractNum 中,段落属性同时来自样式引用和直接格式。一份看似简单的文档背后是几百个互相引用的 XML 元素。
直接构造或修改 OOXML 要求 Agent 同时理解这些间接关系。一旦某处引用断裂,Word 打开时就会报错或渲染异常。
AtomDoc 的做法是:
- 拆分时,将文档结构提取为扁平的 YAML 树,每一层显式声明自己的样式引用
- 编辑时,Agent 只需修改
text字段或增删 block,不需要理解 OOXML 包结构 - 组装时,修改后的文本覆盖回原始
document.xml,其余部件(样式定义、编号定义、主题、页眉页脚)原样穿透,保证页面数量和分页位置与原始文档一致
为量化不同工具 docx 往返编辑的保真度,我们对一份包含多级标题、7 个表格、页眉页脚、直接格式化的 9 页中文文档执行了逐像素视觉对比。
测试方法:每个工具对文档做一次完整往返(读取 → 写入),将原始文档与往返产物分别渲染为 PNG 页,逐像素对比相似度。1.000 表示像素级完全相同。测试文档不含任何 AtomDoc 处理痕迹,直接由 Word 生成。
| 工具 | 页数 | 整体相似度 | 最低单页 | 说明 |
|---|---|---|---|---|
| AtomDoc | 9→9 ✓ | 100.0% | 1.000 | text overlay 保留全部 OOXML 结构 |
| python-docx | 9→9 ✓ | 100.0% | 1.000 | 保真度高,但 API 是 OOXML 粒度的 |
| LibreOffice | 9→9 ✓ | 99.2% | 0.978 | 3/9 页出现轻微布局漂移 |
| pandoc | 9→6 ✗ | 0.0% | 0.000 | 格式转换导致结构性信息丢失 |
python-docx 的 1.000 分不代表它解决了问题——它在这个测试中保真是因为纯往返没有编辑操作。一旦 Agent 需要修改内容(替换文字、增删段落、填充表格),就必须在 w:rPr、w:pPr、basedOn 继承链等 OOXML 元素层面操作。这正是 Agent 难以驾驭的部分。
pandoc 丢失 3 页 是因为它将 docx 转为内部 AST 再写回,编号格式、分页、样式继承等 OOXML 特有信息在这个过程中不可逆丢失。这正是"中间格式转换"路线的系统性缺陷。
LibreOffice 的轻微漂移 表明即使是最成熟的 OOXML 实现,在重新解释和序列化时也会引入肉眼不可见但像素可检出的布局偏差。
AtomDoc 通过 text overlay 机制同时解决了保真度和可编辑性——Agent 在扁平 YAML 层编辑,组装时只覆盖文字,原始 XML 结构原样保留。
| 层 | 管什么 | 格式 |
|---|---|---|
| Content | 文本、结构、资源引用 | content.yaml |
| Style | 字符级(Run)和段落级(Paragraph)的视觉定义 | style.yaml |
每一层通过样式名引用样式定义。没有继承链,没有隐式默认值。
# content.yaml 示例
blocks:
- type: paragraph
style: Heading1 # 段落级样式
runs:
- style: H1 # 字符级样式
text: "第一章 引言"Run 管字符级(字体、字号、颜色、粗斜体),Paragraph 管布局级(对齐、缩进、间距、大纲级别),互不越界。
真实 Word 文档中,用户大量使用直接格式而非命名样式:选一段文字,手动调字体、改字号、设颜色。OOXML 用 Normal 或 DefaultParagraphFont 作为基底样式,把格式差异塞进 rPr/pPr 的直接属性里。
拆分时,AtomDoc 不只看样式 ID,而是对比每一个参数——字体、字号、颜色、粗斜体、对齐、缩进、行距等全部属性——为每种独特的参数组合生成一个指纹(hash),从而把"Normal 垃圾桶"里混杂的几十种实际格式区分开来。
这引出了 patterns 命令的核心价值——
atomdoc patterns workdir/输出会列出每个重复出现的合成样式(Normal+7d6e86ab、DefaultParagraphFont+6c494af4 等),包括:
- 属性描述(字体、字号、对齐、缩进等)
- 出现次数和位置
- 文本片段预览
同一个模式出现次数越多,越值得从 Normal+hash 重命名为语义名。例如:
DefaultParagraphFont+6c494af4出现 71 次,宋体 10pt 粗体 → 命名为BodyNormal+7d6e86ab出现 7 次,首行缩进 28pt → 命名为FormField
不要合并属性不完全相同的样式。 宋体 10pt bold color:auto 和 宋体 10pt bold color:#000000 虽然渲染可能相同,但 OOXML 语义不同(auto 继承段落色,显式 #000 不继承),合并会引入意外耦合。
拆分时将以下 OOXML 部件原样保存到 passthrough/:
| 文件 | 用途 |
|---|---|
document.xml |
原始文档体,作为 text overlay 模板 |
styles.xml |
原始样式 XML |
numbering.xml |
编号定义,保留列表格式 |
theme.xml |
主题配色 |
settings.xml |
文档设置 |
font_table.xml |
字体回落表 |
header*.xml / footer*.xml |
页眉/页脚 |
组装时,AtomDoc 不在抽象模型上重建 XML,而是直接在原始 XML 上覆盖文本节点——保留全部原始结构、属性和分页。
| 命令 | 说明 |
|---|---|
atomdoc split in.docx -o dir/ |
docx → content.yaml + style.yaml + resources/ + passthrough/ |
atomdoc assemble dir/ -o out.docx |
YAML → docx(text overlay 模式) |
atomdoc validate dir/ |
校验样式引用、图片存在性、字段合法性 |
atomdoc patterns dir/ [--min-freq 2] |
识别重复格式模式,辅助语义化清洗 |
atomdoc roundtrip in.docx [--threshold 0.99] |
视觉保真度验证(需 LibreOffice) |
atomdoc split 模板.docx -o workdir/
# 编辑 content.yaml — 替换占位文字、增删段落、填充表格
atomdoc validate workdir/
atomdoc assemble workdir/ -o 输出.docx- 段落样式独立于文本:改 Run 的
text即可,样式引用不动 - 新增段落:复制已有的
type: paragraph块,改text,插入到blocks的正确位置 - 每个表格单元格必须至少包含一个 Paragraph 和一个 Run,否则 Word 可能无法正确渲染
atomdoc split 输入.docx -o workdir/
atomdoc patterns workdir/ # 识别重复格式
# 在 style.yaml 中为高频模式创建语义名
# 在 content.yaml 中将 hash 引用替换为语义名
atomdoc validate workdir/
atomdoc assemble workdir/ -o 清洗后.docx用内置默认样式写 style.yaml → 写 content.yaml → validate → assemble。
内置默认样式包括:Body、Body_Bold、H1-H3、Link、Code(Run 级);Heading1-Heading3、BodyText、Normal(Paragraph 级);StandardTable、HeaderRow、DataRow、HeaderCell、DataCell(表格级)。
同填充模板——split → 改 content.yaml → assemble。不需改动的内容原样保留。
CLI 底层是纯 Python dataclass API,Agent 可直接操作:
from atomdoc.parser.docx_reader import parse_docx
from atomdoc.generator.docx_writer import generate_docx
from atomdoc.styles.defaults import default_styles
from atomdoc.models.content import Document, Block, Run
from atomdoc.validate import validate
# 读取
result = parse_docx("input.docx")
doc = result.document
styles = result.style_set
# 修改
doc.blocks[0].runs[0].text = "新标题"
# 从零构建
ss = default_styles()
doc = Document(blocks=[
Block(style="Heading1", type="paragraph",
runs=[Run(style="H1", text="报告")]),
Block(style="BodyText", type="paragraph",
runs=[Run(style="Body", text="正文内容。")]),
])
# 校验并生成
errors = validate(doc, ss)
if not errors:
generate_docx(doc, ss, "output.docx")- 显式优于隐式 — 每个元素通过名称声明样式,无继承链
- 正交分层 — Run 管字符级,Paragraph 管布局级,互不越界
- 逐参数比对 — 样式识别不以样式 ID 为准,而是对比全部属性,区分 Normal 垃圾桶中混杂的实际格式
- 默认穿透,最小重建 — 只有明确需要修改的部分才重建,原始 OOXML 原样保留
- 透明告警 — 不支持的特性输出 warning + 位置,不静默丢弃
详见 DESIGN.md 和 ROADMAP.md。