1. 为什么文档流水线里 Markdown 转 Word 总翻车
Markdown 到 Word 文档的工程化转换,本质是把一套「内容与样式分离」的纯文本语法,映射到 OOXML 这套「样式继承 + 资源关联」的复杂容器里。Pandoc 是目前最稳的开源转换器,它把 Markdown 解析成抽象语法树(AST),再生成符合 ISO/IEC 29500 的 DOCX 包。适合谁?技术团队里负责文档流水线、需要批量产出规范 Word 的工程师,尤其是那些被「本地预览好好的,CI 里样式全丢」折磨过的人。
我见过太多团队的做法是:本地 Typora 导出、手动调格式、再上传到知识库。单篇没问题,一旦文档量上到几十上百篇,样式漂移、图片丢路径、标题层级错乱就会集中爆发。核心矛盾在于——Markdown 只有语义(这是 H1、这是代码块),而 Word 的样式是「命名样式 + 直接格式」双层结构。你不显式告诉 Pandoc「H1 对应哪个 Word 样式」,它就套默认模板,产出和你企业规范对不上。
工程化转换要解决三件事:样式映射(Markdown 元素 → Word 命名样式)、模板注入(用 reference.docx 固定字体/页边距/页眉)、资源处理(图片相对路径在转换后仍能定位)。这三件事做扎实,一次配置就能稳定产出。而调用通道这块,如果你在流水线里还要接模型做文档润色、术语校验,用 TaoToken 统一 Key 管理会比每个脚本各配一套密钥清爽得多,后面会讲怎么接。
先明确一个认知:Pandoc 不是「格式美化器」,它是「结构翻译器」。你给它什么 AST,它就生成什么 OOXML。所以样式问题的根因往往不在 Pandoc,而在你的 reference.docx 没配对,或者 Markdown 里混了 HTML 导致 AST 里出现 RawBlock,Pandoc 只能原样塞进 document.xml,Word 打开就是一堆没样式的裸内容。
2. TaoToken 统一 Key 的前置准备与调用通道管理
在动手配 Pandoc 之前,先把「调用通道」这件事理清楚。文档流水线里常见的模型调用场景:批量把 Markdown 里的口语化描述润色成规范技术表述、自动生成术语表、校验标题层级是否符合规范。这些如果每个脚本各写一套 base_url 和 key,维护成本会随脚本数量线性上涨。TaoToken 的做法是给你一个统一的 API 入口,所有模型调用走同一个 Key,切换模型只改 model 字段。
前置准备分两步。第一步,拿到 Key。访问 https://taotoken.net/api-keys 创建你的 API Key,注意这个 Key 只在创建时完整显示一次,复制后存到 CI 的 secret 里,别硬编码进仓库。第二步,确认你的调用基址。TaoToken 的 API 端点是 https://taotoken.net/api,兼容 OpenAI 风格的/v1/chat/completions,也支持 Anthropic 风格调用,具体看你用哪个 SDK。
这里要强调一个工程习惯:把 base_url 和 key 都走环境变量,脚本里只读os.environ。这样本地调试和 CI 跑的是同一套代码,不会出现「本地能跑线上 401」。下面是一个最小验证片段,用 curl 确认 Key 有效:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" curl -s "$TAOTOKEN_BASE_URL/v1/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "回复 OK 两个字母"}], "max_tokens": 16 }'返回里能看到choices[0].message.content就说明通道通了。如果你更习惯用 Anthropic 原生 SDK,把 base_url 指向 TaoToken 的兼容端点即可,Key 还是同一个。这一步做完,你的文档流水线就有了「内容加工」能力,接下来才是 Pandoc 的主场。
关于模型选择,文档润色类任务用 claude 系列对长文本的连贯性处理比较稳,术语抽取类任务用轻量模型就够,成本可控。具体模型 ID 以 https://taotoken.net/doc 的文档为准,别照抄我这里的示例 ID,模型会迭代。如果你打算把文档转换 + 模型润色做成长期跑的 Agent 流水线,可以看下 Coding Plan 的额度方案,比按次调用更适合高频场景。
3. 可复制的 Pandoc 配置与 reference.docx 模板注入
这一节是全文的核心,给你能直接抄的配置。先装 Pandoc,版本建议 3.x 以上,2.x 对 GFM 表格和tex_math_dollars的支持有差异。
# macOS brew install pandoc # Ubuntu/Debian sudo apt-get install -y pandoc # 验证 pandoc --version第一步,生成基础模板。Pandoc 可以导出一份默认的 reference.docx,你在它基础上改样式,比从零建 Word 模板靠谱:
pandoc -o reference.docx --print-default-template=docx生成的 reference.docx 里已经预置了 Heading1 到 Heading6、Body Text、Source Code、Block Text 等命名样式。用 Word 打开它,改这几处:字体(正文用你企业规范的字体,比如思源黑体)、标题颜色和字号、代码块底纹、页边距、页眉页脚。改完保存,这份文件就是你所有转换的样式基准。
第二步,写转换命令。关键参数逐个说:
pandoc input.md \ -f markdown+tex_math_dollars+pipe_tables+footnotes \ -t docx \ --reference-doc=reference.docx \ --resource-path=.:./assets:./images \ --extract-media=./media \ -o output.docx-f markdown+...是启用扩展语法,tex_math_dollars让$...$被识别为公式,pipe_tables支持 GFM 表格。--reference-doc注入你的模板。--resource-path告诉 Pandoc 去哪找图片,多个路径用冒号分隔。--extract-media把图片抽到指定目录,避免路径混乱。
第三步,如果你要批量转换,写个 shell 循环或者用 Makefile:
#!/usr/bin/env bash set -euo pipefail mkdir -p dist for md in docs/*.md; do name=$(basename "$md" .md) pandoc "$md" \ -f markdown+tex_math_dollars+pipe_tables \ -t docx \ --reference-doc=reference.docx \ --resource-path=.:./assets \ -o "dist/${name}.docx" echo "converted: $name" done这里有个坑要提前说:--resource-path里的相对路径是相对于「当前工作目录」,不是相对于 md 文件所在目录。所以如果你在仓库根目录跑脚本,图片路径要按根目录写;如果在 docs/ 里跑,路径基准就变了。统一在根目录跑,路径规则最清晰。
关于样式映射的细节,Pandoc 的映射规则是固定的:Markdown 的#映射到 Word 的Heading 1,##到Heading 2,以此类推;代码块映射到Source Code;引用块映射到Block Text。你要改样式,就改 reference.docx 里这些命名样式,别去改 document.xml。这是 OOXML 的样式继承体系决定的——命名样式是「类」,直接格式是「实例覆盖」,改类才能全局生效。
如果你在流水线里还要用模型做术语统一,可以在转换前插一步:读 Markdown,调 TaoToken 的接口做术语替换,写回临时文件,再喂给 Pandoc。这样内容和样式解耦,各管各的。
4. 验证转换结果与 OOXML 结构校验
转换完不能只看「文件生成了」,要验证三件事:样式是否套上、图片是否嵌入、公式是否可编辑。最直接的办法是解压 DOCX 看内部结构,因为 DOCX 本质是个 zip 包。
# 解压看结构 unzip -o output.docx -d output_unzipped ls output_unzipped/word/ # 应该看到 styles.xml document.xml _rels/ media/word/styles.xml里是你的命名样式定义,word/document.xml是内容主体,word/_rels/document.xml.rels是资源关联表,word/media/是嵌入的图片。如果图片没进去,media/会是空的,document.xml.rels里也不会有图片关系项。
验证样式是否生效,可以 grep document.xml 里的段落样式引用:
# 看标题有没有引用 Heading 样式 grep -o 'w:pStyle w:val="[^"]*"' output_unzipped/word/document.xml | sort | uniq -c正常输出里应该能看到Heading1、Heading2、SourceCode这些值。如果全是Normal或者没有 pStyle,说明样式没映射上,回去检查 reference.docx 里的样式名是否和 Pandoc 期望的一致。
验证公式,看 document.xml 里有没有m:oMath节点:
grep -c 'm:oMath' output_unzipped/word/document.xml返回大于 0 说明公式转成了 OMML(Office Math Markup Language),在 Word 里是可编辑的原生公式,不是图片。这是 Pandoc 相比「截图贴公式」方案的核心优势。
再给一个用 Python 做批量校验的脚本,检查每个产出的 DOCX 是否包含预期样式:
import zipfile import re from pathlib import Path def check_docx(path): with zipfile.ZipFile(path) as z: doc = z.read("word/document.xml").decode("utf-8") styles = set(re.findall(r'w:pStyle w:val="([^"]+)"', doc)) has_math = "m:oMath" in doc media = [n for n in z.namelist() if n.startswith("word/media/")] return { "file": path.name, "styles": sorted(styles), "has_math": has_math, "media_count": len(media), } for f in Path("dist").glob("*.docx"): print(check_docx(f))跑完你能一眼看出哪篇文档样式缺失、哪篇图片没嵌进去。这套校验放进 CI,转换质量就有兜底了。
5. 常见报错排查:401、local proxy failed、reading choices
这一节按真实报错来,都是我在流水线里踩过的。
401 Unauthorized。如果你在文档润色步骤调 TaoToken 报 401,先确认三件事:Key 有没有带Bearer前缀、环境变量有没有在 CI 里注入、Key 是不是被复制时带了空格。用echo $TAOTOKEN_API_KEY | wc -c看长度对不对。还有一种情况是 base_url 写成了https://taotoken.net/api/带尾斜杠,某些 SDK 会拼成双斜杠导致鉴权失败,统一去掉尾斜杠。
local proxy failed。这个报错通常出现在你本地配了某些网络工具,SDK 走了系统代理但代理没起来。排查方式:env | grep -i proxy看有没有HTTP_PROXY/HTTPS_PROXY残留,有就 unset 掉再跑。CI 环境一般没这问题,本地调试容易中招。
reading choices 相关报错。典型的是KeyError: 'choices'或者list index out of range,说明返回体里没有 choices 字段。原因通常是:请求体 JSON 格式错了(比如 messages 不是数组)、model 字段填了不存在的模型 ID、或者 max_tokens 设成了 0。把返回体完整打印出来看error字段,比猜快得多。下面这段能帮你定位:
import os, json, urllib.request req = urllib.request.Request( f"{os.environ['TAOTOKEN_BASE_URL']}/v1/chat/completions", data=json.dumps({ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16, }).encode(), headers={ "Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}", "Content-Type": "application/json", }, ) try: with urllib.request.urlopen(req) as resp: body = json.loads(resp.read()) print("choices:", body.get("choices")) except urllib.error.HTTPError as e: print("HTTP", e.code, e.read().decode())OAuth 相关报错。如果你用的是 Claude Code 这类工具接 TaoToken,报 OAuth 失败,检查是不是把 API Key 模式配成了 OAuth 模式。API Key 走的是Authorization: Bearer,OAuth 走的是另一套 token 流程,两者别混。Claude Code 的配置里,Base URL 填https://taotoken.net/api,Key 填你的 API Key,Model ID 填文档里给的模型标识,三件套对齐就不会报 OAuth。
样式丢失但没报错。这种最隐蔽。表现是转换成功、文件能打开,但标题全是正文样式。根因是 reference.docx 里的样式名被 Word 改过(比如你手动重命名了 Heading1),Pandoc 找不到就 fallback 到 Normal。解决:别改样式名,只改样式属性。
图片路径失效。报错可能是Could not fetch resource。检查--resource-path是否覆盖了图片实际所在目录,以及 Markdown 里的图片路径是相对路径还是绝对路径。绝对路径在 CI 里必挂,统一用相对路径 + resource-path。
6. 把转换接进流水线与长期调用方案
单次转换跑通后,下一步是接进 CI。以 GitLab CI 为例,一个最小 job:
convert-docs: image: pandoc/core:latest script: - apk add --no-cache bash - bash scripts/convert.sh artifacts: paths: - dist/注意pandoc/core镜像里没有 bash,要装。转换脚本和 reference.docx 都进仓库,保证每次构建用的是同一套样式基准。如果流水线里要调模型做润色,把TAOTOKEN_API_KEY配成 CI 的 masked variable,脚本里读环境变量。
长期跑的话,调用通道的稳定性比单次成本更重要。TaoToken 统一 Key 的好处是:你所有脚本、所有模型调用走一个入口,换模型只改 model 字段,不用动鉴权逻辑。文档流水线里常见的模型任务——术语统一、摘要生成、标题规范化——都可以挂在这套通道下。如果你要做的是长期编码或 Agent 类流水线,Coding Plan 的额度模式比按次调用更适合,具体在 https://taotoken.net/coding-plan 看。
最后给一个实用技巧:把 reference.docx 也纳入版本控制,每次改样式都提交,这样样式变更可追溯。转换脚本里加一步校验,样式缺失就 fail 掉 CI,别让不合格的 Word 流到下游。文档流水线的质量,靠的是「配置固定 + 校验兜底」,不是靠人肉检查。