Vibe Coding 的模式已经深入人心:你用自然语言描述意图,AI 生成代码,你跑一遍、改几句、再跑一遍,几轮下来一个能用的项目就出来了。整个过程你几乎不需要逐行手写——"氛围"对了,代码就对了。
但换到办公场景——写一份带格式、带图表、带合并单元格的季度汇报——同样的"描述意图、AI 输出、微调迭代"流程却走不通。市面上的 AI 文档工具要么输出 Markdown,要么输出 HTML,最终产物和一份真正的 .docx / .xlsx / .pptx 之间总有肉眼可见的落差。问题不在 AI 的生成能力,而在文档格式的表达力。
AI + 文档工具的现状与瓶颈
当前主流的 AI 文档工具大致分三类:
- Markdown 驱动:Typora + AI 插件、Notion AI、Obsidian + Copilot 等。输出是 Markdown,渲染靠各平台自己的引擎。
- HTML 驱动:部分 SaaS 产品(如 Gamma 做 PPT)用 HTML/CSS 渲染后导出 PDF 或截图。
- 模板填充型:用预设的 .docx / .pptx 模板,AI 只填充文本占位符,格式完全锁死。
前两类的问题很明显——Markdown 和 HTML 无法表达办公文档的核心结构。第三类看似绕过了格式问题,但模板一旦固定,"氛围"就没了:你没法让 AI 自由调整布局、增删图表、合并单元格,因为模板里没有这些槽位。
Vibe Officing 的核心诉求是:我描述意图,AI 生成一份结构完整、格式到位的办公文档,我微调后直接交付。这要求格式层必须能承载"任意办公文档"的表达。
Markdown 和 HTML 为什么撑不住
Markdown 的硬伤
Markdown 的设计目标是"易读易写的纯文本标记",它天然缺少以下办公文档常见能力:
| 能力 | Markdown | .docx (OOXML) |
|---|---|---|
| 合并单元格 | ❌ 无语法 | ✅ <w:gridSpan> / <w:vMerge> |
| 分页与页眉页脚 | ❌ | ✅ <w:sectPr> |
| 嵌套样式(字体+颜色+间距组合) | ❌ 只能粗体/斜体 | ✅ <w:rPr> 完整样式链 |
| 图表(柱状图/折线图) | ❌ 只能贴图片 | ✅ <c:chart> 原生嵌入 |
| 修订与批注 | ❌ | ✅ <w:comment> / <w:ins> |
| 页面布局(边距/纸张方向) | ❌ | ✅ <w:pgSz> / <w:pgMar> |
你可以在 Markdown 里写 | A | B | 做表格,但一旦需要合并单元格、设置列宽、给表头加底色,Markdown 就彻底失效。而这些恰恰是季度汇报、合同、提案里最基础的需求。
HTML 的局限
HTML + CSS 的表达力远强于 Markdown,理论上可以渲染出非常复杂的视觉布局。但问题在于:
- HTML 不是办公文档的交换格式。客户要的是 .docx,不是一个网页链接。从 HTML 转换到 .docx 的工具(如 Pandoc、html-to-docx)在样式保真度上损失严重——CSS Flexbox/Grid 无法映射到 OOXML 的表格模型,
position: absolute无法映射到<wp:anchor>。 - HTML 缺少办公语义。HTML 的
<table>没有"合并单元格后导出 Excel 仍可编辑"的语义;<h1>没有"对应 Word 标题样式 Heading 1"的映射保证。 - 分页不可控。HTML 是连续流式布局,没有分页概念。打印时浏览器自行分页,结果往往和预期相差甚远。
一句话总结:HTML 能"看",但不能"交"。Vibe Officing 需要的是可直接交付的办公文档,不是好看的网页截图。
OOXML:被忽视的正确答案
OOXML(Office Open XML)是 .docx、.xlsx、.pptx 的底层格式,本质是一组 ZIP 压缩的 XML 文件。一个最简的 .docx 解压后结构如下:
my-doc.docx (ZIP)
├── [Content_Types].xml # 全局类型声明
├── _rels/
│ └── .rels # 包级关系
├── word/
│ ├── document.xml # 主文档内容(段落、表格、样式引用)
│ ├── styles.xml # 样式定义
│ ├── numbering.xml # 列表编号定义
│ ├── settings.xml # 文档设置(页边距、默认字体等)
│ ├── fontTable.xml # 字体表
│ ├── _rels/
│ │ └── document.xml.rels # 图片、图表等资源关系
│ ├── media/
│ │ └── image1.png # 嵌入图片
│ └── charts/
│ └── chart1.xml # 原生图表定义
OOXML 能完整表达办公文档的全部语义:合并单元格、分页、页眉页脚、修订批注、原生图表、样式继承链……这些正是 Markdown 和 HTML 缺失的部分。
过去没人用 OOXML 做 Vibe Officing,原因很直接:OOXML 的 XML 太冗长了。一个只有"Hello World"的 .docx,document.xml 就有几十行命名空间声明。让 AI 直接生成这些 XML,token 消耗惊人,且极易出错。
但这个问题现在有了新的解法。
用 python-docx 做 OOXML 的"高层协议"
不需要让 AI 直接写 OOXML 的原始 XML。正确的做法是:让 AI 生成结构化的意图描述(JSON 或轻量 DSL),再用程序层将其编译为 OOXML。python-docx 就是这个编译器。
下面是一个可运行的示例:你给出一份季度汇报的意图描述(JSON),脚本自动生成一份带合并单元格表头、样式化标题、原生图表的 .docx。
"""
vibe_officing.py — 从意图 JSON 生成 .docx 季度汇报
依赖:pip install python-docx matplotlib
运行:python vibe_officing.py
输出:quarterly_report.docx
"""
import json
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
import matplotlib.pyplot as plt
# ── 1. 意图描述(AI 生成这一层即可)───────────────────────
INTENT_JSON = """
{
"title": "2025 Q1 业务汇报",
"subtitle": "智能硬件事业部",
"sections": [
{
"heading": "营收概览",
"chart": {
"type": "bar",
"title": "Q1 月度营收(万元)",
"categories": ["1月", "2月", "3月"],
"values": [320, 410, 580]
}
},
{
"heading": "区域明细",
"table": {
"headers": ["区域", "1月", "2月", "3月", "合计"],
"merge_header_cols": [1, 3],
"merge_label": "营收(万元)",
"rows": [
["华东", "120", "150", "210", "480"],
["华南", "100", "130", "180", "410"],
["华北", "100", "130", "190", "420"]
]
}
}
]
}
"""
intent = json.loads(INTENT_JSON)
# ── 2. 编译为 OOXML (.docx) ──────────────────────────────
doc = Document()
# 设置默认字体
style = doc.styles["Normal"]
font = style.font
font.name = "微软雅黑"
font.size = Pt(11)
# 设置中文字体(East Asian font)
style.element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
# 标题
title_para = doc.add_paragraph(intent["title"], style="Title")
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 副标题
sub_para = doc.add_paragraph(intent["subtitle"])
sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = sub_para.runs[0]
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
doc.add_paragraph() # 空行
for section in intent["sections"]:
doc.add_heading(section["heading"], level=1)
# ── 图表段落 ──
if "chart" in section:
chart_cfg = section["chart"]
fig, ax = plt.subplots()
ax.bar(chart_cfg["categories"], chart_cfg["values"], color="#4472C4")
ax.set_title(chart_cfg["title"])
ax.set_ylabel("万元")
for i, v in enumerate(chart_cfg["values"]):
ax.text(i, v + 15, str(v), ha="center", fontsize=10)
plt.tight_layout()
img_path = "_chart_tmp.png"
fig.savefig(img_path, dpi=150)
plt.close(fig)
doc.add_picture(img_path, width=Inches(5.5))
last_para = doc.paragraphs[-1]
last_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# ── 表格段落(含合并单元格)──
if "table" in section:
tbl_cfg = section["table"]
headers = tbl_cfg["headers"]
rows = tbl_cfg["rows"]
num_cols = len(headers)
num_rows = len(rows) + 2 # 合并表头行 + 列名行 + 数据行
table = doc.add_table(rows=num_rows, cols=num_cols)
table.style = "Light Grid Accent 1"
# 第一行:合并表头(如 "营收(万元)" 跨 1-3 列)
merge_start = tbl_cfg["merge_header_cols"][0]
merge_end = tbl_cfg["merge_header_cols"][1]
cell_0_0 = table.rows[0].cells[0]
cell_0_0.text = headers[0]
cell_0_merge = table.rows[0].cells[merge_start]
cell_0_merge.text = tbl_cfg["merge_label"]
# 合并单元格:OOXML 层操作
cell_0_merge.merge(table.rows[0].cells[merge_end])
# 居中
for cell in table.rows[0].cells:
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
for paragraph in cell.paragraphs:
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 第二行:列名
for j, h in enumerate(headers):
table.rows[1].cells[j].text = h
for paragraph in table.rows[1].cells[j].paragraphs:
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 数据行
for i, row_data in enumerate(rows):
for j, val in enumerate(row_data):
table.rows[2 + i].cells[j].text = val
# 合计列加粗
for i in range(len(rows)):
last_cell = table.rows[2 + i].cells[-1]
for run in last_cell.paragraphs[0].runs:
run.bold = True
doc.add_paragraph() # 表后空行
# 页脚
section_obj = doc.sections[0]
section_obj.top_margin = Cm(2.54)
section_obj.bottom_margin = Cm(2.54)
section_obj.left_margin = Cm(3.17)
section_obj.right_margin = Cm(3.17)
output_path = "quarterly_report.docx"
doc.save(output_path)
print(f"✅ 已生成: {output_path}")
# 清理临时图片
import os
os.remove("_chart_tmp.png")
运行后你会得到一份 quarterly_report.docx,打开后能看到:
- 居中标题 + 灰色副标题
- 柱状图(matplotlib 生成后嵌入)
- 合并单元格表头("营收(万元)"跨三列)
- 数据行 + 加粗合计列
- 标准页面边距
关键洞察:AI 不需要生成 OOXML 的原始 XML。AI 只需要生成上面那段 INTENT_JSON——一个结构化的意图描述。编译层(python-docx)负责把意图变成合法的 OOXML。这和 Vibe Coding 里 AI 生成高层代码、编译器/运行时负责底层机器码是同一个模式。
通往 Vibe Officing 的架构
把上面的单脚本思路扩展为完整工作流:
用户意图(自然语言)
│
▼
┌─────────┐
│ LLM │ 生成意图 JSON / DSL
└─────────┘
│
▼
┌─────────────┐
│ 编译层 │ python-docx / openpyxl / python-pptx
│ (OOXML SDK)│ JSON → .docx / .xlsx / .pptx
└─────────────┘
│
▼
┌─────────────┐
│ 微调层 │ 用户在 Word / Excel / PPT 中手动微调
└─────────────┘
│
▼
交付文档
这个架构的每个环节都有现成工具:
| 环节 | 工具 |
|---|---|
| 意图生成 | GPT-4o / Claude / DeepSeek,输出 JSON |
| .docx 编译 | python-docx |
| .xlsx 编译 | openpyxl |
| .pptx 编译 | python-pptx |
| 微调 | Microsoft Office / WPS / LibreOffice |
落地前需要想清楚的几件事
-
意图 JSON 的 Schema 要先定。AI 生成自由格式 JSON 很容易跑偏。建议为每类文档(汇报、合同、提案)定义一份 JSON Schema,约束字段名和结构,让 AI 在边界内生成。
-
复杂布局仍需模板辅助。OOXML 的样式继承链(
<w:rPr>→<w:pPr>→<w:style>)非常复杂,纯 JSON 描述难以覆盖所有细节。务实做法:准备几份样式模板 .docx,编译层用docx.Document(template_path)加载,AI 只填充内容结构,样式由模板兜底。 -
图表是最大难点。python-docx 不支持原生 OOXML 图表(
<c:chart>),只能嵌入图片。如果需要可编辑的 Excel 图表,要直接操作 OOXML XML 或用 openpyxl 生成 .xlsx 后嵌入。这是目前生态最薄弱的环节。 -
迭代闭环还没打通。Vibe Coding 的关键体验是"改一句提示 → 重新生成 → 立刻看到结果"。Vibe Officing 需要同样的闭环:用户在 Word 里改了两行 → 系统识别修改意图 → 重新生成整份文档。这需要 diff 语义解析,目前没有成熟方案。
-
Token 成本。一份 20 页的 .docx,其
document.xml可能有 5000+ 行 XML。如果让 AI 直接编辑原始 XML,单次修改可能消耗上万 token。意图 JSON 层把这个数字压到几百——这是架构选择的核心收益。
Vibe Officing 不是把 Markdown 渲染得更漂亮,也不是往模板里填更多占位符。它需要一种能完整表达办公文档语义的中间格式,以及一个把高层意图编译为该格式的程序层。OOXML 是唯一具备完整表达力的格式,python-docx / openpyxl / python-pptx 是现成的编译器。缺的只是一份意图 JSON 的 Schema 约定,和一个让用户在 Office 里微调后自动回传意图的迭代闭环。
先把意图 JSON 的 Schema 定下来,用 python-docx 跑通生成流程,Vibe Officing 就有了起点。