AI Native 文档工作流:让文档和代码一起演进
2024 年底,我负责一个内部组件库的文档治理。那个库有 60 多个组件,文档在另一个仓库里用 Storybook 维护。每次发版前后,文档负责人会拉一个群,催各模块 owner 更新对应文档。大多数人会在发版日前两天集中补一段,复制一段代码片段,写几句用法说明,然后合进去。发版完事,群里安静三个月,等下一个版本再来一遍。
这样的循环跑了快两年,组件文档和实际 API 之间的偏差越来越大。新人按文档写代码,经常踩到已经废弃的 props 或者没写明的行为变化。我开始尝试用 AI 帮忙:让模型读 PR 的 diff 和 changelog,生成文档更新草稿,再让人 review。效果确实快,一周的文档积压一天就能清完。但两个月后回看,新文档里又有三成和代码对不上——AI 把 changelog 里「新增多选支持」理解成了一个新组件,而不是对已有组件 onSelect 签名的修改。
这件事让我意识到:问题不在 AI 生成得对不对,而在整个文档工作流从源头开始就没有稳定的事实锚点。AI 是一个放大器,输入的事实清楚,它能把产出放大几倍;输入模糊,它能把错误也放大几倍。这个判断改变了我后续所有文档建设的思路。
Docs as Code 的下一跳
Docs as Code 不是新概念。2010 年代中期,Write the Docs 社区和 Google 的工程实践博客把这套方法论推到了主流:文档放代码仓库、Markdown 写作、PR 审查、CI/CD 发布。它解决了版本控制、协作和发布自动化的问题,但没有解决文档的准确性问题——文档仍然是手写文本,和代码之间没有可验证的绑定关系。
DrExplain 在 2026 年 5 月提出 Docs-as-Code 2.0 框架时,把这种变化描述为从「人类便利性」到「机器便利性」的维度扩展。文档的消费者不再只是人,还有 AI agent。对应地,文档需要两层结构:给人看的可视化站点,给机器读的结构化数据层。llms.txt 放在站点根目录作为导航地图;MCP(Model Context Protocol)提供统一接口,让 LLM 按需获取文档片段;JSON-LD 在页面里嵌入结构化元数据,让 agent 一进来就知道这是哪个版本、什么类型的信息。
这个框架的启示是:文档不是静态产物,而是从代码、测试、配置、changelog 这些可验证事实派生出来的结构化数据。一旦把文档看成「派生数据」,它的准确性就不再取决于写文档的人有多认真,而取决于事实源头的质量和派生流程的自动化程度。
这和我写测试的直觉是一致的。测试的质量不取决于测试作者的文笔,而取决于被测代码的结构和断言的颗粒度。AI 在文档工作流里的角色,更接近一个「结构化信息处理管道」:
- 事实源:代码、类型定义、OpenAPI、测试、changelog、配置文件
- 提取与转换:从事实源抽取结构化中间表示(JSON Schema、AST、配置 schema)
- AI 生成:基于中间表示生成自然语言文档、代码示例、多语言版本
- 人类审查:确认准确性、上下文完整性、读者路径
- CI 验证:跑通示例代码、检查链接和版本一致性
- 发布与反馈:发布站点,收集搜索日志和错误报告,回流到事实源
这个管道里每一环都不能省。arXiv 2024 年的一篇论文用 LLM 给开源仓库自动生成 README,发现生成内容和人工文档的 BERT 语义相似度平均 85% 左右,但 BLEU 分数只有 20/100。也就是说 AI 能抓住大意,但具体细节需要人工校准。Kapa.ai 的工程博客里有一句话很准确:「当我们说一个 agent 在幻觉时,我们其实在说一个 agent 饿 context。」解决办法不是换更强的模型,而是把 API reference 从 OpenAPI spec 或类型定义派生出来,让「文档漂移」在结构上变得不可能。
哪些环节适合 AI,哪些不适合
在动手搭流程之前,我把文档工作拆成了两类,交给不同角色。
| 维度 | AI 擅长 | 必须人来把关 |
|---|---|---|
| 初稿与改写 | README、changelog、API 描述、示例代码改写 | 架构决策、权衡说明、业务上下文 |
| 结构化 | 从类型定义生成 API 参考、生成 JSON Schema | 概念层次划分、读者路径设计 |
| 验证 | 跑通代码示例、检查链接、术语一致性 | API 兼容性判断、破坏性变更评估 |
| 翻译与本地化 | 多语言翻译、术语对齐 | 文化差异、产品定位、敏感表述 |
| 维护 | 检测过期文档、生成更新 PR | 决定是否删除、归档策略 |
这个分类决定了流程里哪些环节可以自动放行、哪些必须人审。AI 生成的初稿可以自动开 PR,但 PR 的合并必须由了解业务的人 review;AI 跑过的示例代码可以作为「可运行」的证据,但示例是否真的教了读者想学的东西,还是要人判断。
案例一:API 文档为什么总对不上代码
2025 年初,我在一个中大型 SaaS 项目里遇到一个典型的文档事故。组件库的一位维护者修改了 Select 组件的 onSelect 签名——从原来的单对象改成了数组,支持多选。这个改动在 PR 里写了,changelog 里也提了,但文档没人动。三个月后,一位新同事按文档里的旧签名写业务代码,上线后直接报错。
事故复盘时我意识到,根因不是谁疏忽了,而是文档和代码之间没有可验证的绑定。改代码不需要改文档也能合 PR。这种「双源真相」靠人盯不住。
修复方案是把 API 文档绑定到代码本身,让文档从类型定义派生。我写了一个脚本,从 TypeScript 类型定义生成 API 文档的骨架:
// ❌ 坏做法:文档手写,和类型定义分离
// docs/select.md
// > onSelect: (item: Item) => void
// 当用户选择一个选项时触发。
// (三个月后签名改了,这行没人记得同步)
// ✅ 好做法:文档从类型定义派生
// scripts/generate-api-docs.ts
import ts from 'typescript'
import fs from 'node:fs'
// 从源码读取类型定义,而不是人复述
const program = ts.createProgram(['src/components/Select.tsx'], {})
const checker = program.getTypeChecker()
const sourceFile = program.getSourceFile('src/components/Select.tsx')
function extractProps(node: ts.Node): PropMetadata[] {
if (ts.isInterfaceDeclaration(node) && node.name.text === 'SelectProps') {
return node.members.map((member) => {
const symbol = checker.getSymbolAtLocation(member.name)
const type = checker.getTypeOfSymbolAtLocation(symbol, member)
return {
name: member.name.getText(),
type: checker.typeToString(type),
// 取 JSDoc 注释作为描述,改类型时作者必须顺手更新注释
description: ts.displayPartsToString(
symbol?.getDocumentationComment(checker) ?? []
),
required: !member.questionToken,
}
})
}
return []
}
// 生成 Markdown,CI 里跑,类型变了文档会自动 diff
const props = sourceFile.flatMap(extractProps)
const markdown = props
.map((p) => `### \`${p.name}\`\n\n类型:\`${p.type}\`${p.required ? '(必填)' : ''}\n\n${p.description}`)
.join('\n\n')
fs.writeFileSync('docs/select-api.md', markdown)差异一目了然。坏做法里,文档是独立的 Markdown 文件,和 SelectProps 之间没有机器可读的关系,改一边不用动另一边。好做法里,文档是类型定义的派生数据:onSelect 签名变了,脚本重新跑一遍,新文档会出现在 PR diff 里。如果有人在 PR 里只改了类型不重新生成文档,CI 会失败——「文档漂移」在结构上变得不可能。
这对应了 Kapa.ai 工程博客里的建议:「API 文档应该从 OpenAPI spec 或类型定义派生,让文档漂移在结构上不可能发生。」arXiv 论文的实验也表明,从代码派生的文档在语义上能覆盖 85% 左右的关键信息,但具体的约束、边界条件和业务上下文仍然需要人工补充。把类型定义作为事实源,AI 作为改写器,人作为审查者,是一个更稳的组合。
案例二:文档里的代码示例为什么跑不通
2025 年中,我做了一次内部文档质量审查,发现一个尴尬的数字:120 篇教程里,34 篇的代码示例不能直接跑。问题各异——依赖版本过时、API 签名已变、缺环境变量、README 里的 npm install 命令和 package.json 对不上。这些文档的共同特征是:作者写的时候能跑,后来代码改了,文档没同步,作者离职了,没人敢删也没人敢改。
「跑不通的代码示例」比没有示例更糟糕——它给人一种「应该能跑」的错觉,让读者在自我怀疑和怀疑文档之间浪费一个小时。
修复思路是:把文档里的代码块当作一等公民对待,在 CI 里跑一遍。我用 Vitest 写了一个文档示例校验器:
// ❌ 坏做法:文档里的代码块当纯文本,没人验证
// docs/getting-started.md
// ```ts
// import { createClient } from './sdk'
// const client = createClient({ apiKey: 'xxx' })
// ```
// (sdk 的 createClient 签名早改了,但这篇文档三年没人动)
// ✅ 好做法:文档代码块当作可执行规范,CI 里跑一遍
// tests/docs-examples.test.ts
import { test, expect } from 'vitest'
import fs from 'node:fs'
import { extractCodeBlocks, compileAndRun } from './doc-test-utils'
const docFiles = fs
.readdirSync('docs')
.filter((f) => f.endsWith('.md'))
.map((f) => `docs/${f}`)
for (const file of docFiles) {
test(`${file} 中的代码示例可运行`, async () => {
const content = fs.readFileSync(file, 'utf-8')
const blocks = extractCodeBlocks(content, { lang: 'ts' })
for (const [index, block] of blocks.entries()) {
// 编译并执行代码块,捕获任何错误
const result = await compileAndRun(block, {
// 用测试环境的 mock,不依赖真实服务
env: { API_KEY: 'test-key', BASE_URL: 'http://localhost:3000' },
// 捕获 console.log 用于断言
captureStdout: true,
})
expect(result.exitCode).toBe(0)
// 可选:对输出做快照,发现文档行为变化立即报错
expect(result.stdout).toMatchSnapshot(
`${file}-block-${index}.stdout`
)
}
})
}差异的关键在于:坏做法把文档示例当文本处理,写的时候对、后面就再也无人问津;好做法把文档示例当可执行规范,每次代码变更都会重新验证。成本是每个文档 PR 多跑 30 秒到 2 分钟,收益是「合并进 main 的文档示例都是能跑的」。代价很低,但带来的信心差异非常大——读者打开文档,第一行 npm install 跑下去,就能进到正题,而不是先去排查为什么 createClient 报类型错。
这个方案还有一个副作用:一旦代码变更导致文档示例失效,PR 检查会立刻标红。维护者不需要等用户来报「文档跑不通」,问题在合并前就被拦住。
案例三:文档更新触发器怎么做
前面两个案例讲的都是「如何从代码派生文档」和「如何验证文档示例」。但还有一个更上游的问题:什么时候触发文档检查?
多数团队的文档更新触发器是被动的——用户报错了、新人踩坑了、发版前被催了,才去改文档。这种触发器的覆盖率很低,大量小改动悄悄累积,半年后文档就没人敢信了。
更合理的触发器应该是按风险分类:
| 代码变更类型 | 是否需要触发文档检查 | 理由 |
|---|---|---|
| API 签名变化 | 必须 | 对外契约,文档必须同步 |
| 配置项变化 | 必须 | 影响部署和运行 |
| CLI 命令变化 | 必须 | 用户直接操作 |
| 公开行为变化(错误码、返回值) | 必须 | 影响集成方 |
| 内部重构 | 通常不需要 | 不改变对外契约 |
| 性能优化 | 视情况 | 如有可观测的指标变化,需要更新基准文档 |
| 测试改动 | 不需要 | 测试实现细节 |
| CI 配置 | 视情况 | 如果影响贡献者流程则需要 |
分类之后,落地可以很简单。我在一套项目里用文件模式检测 + PR 评论提醒:
// ❌ 坏做法:文档检查靠人记得,靠发版前集中补
// 发版日,项目经理在群里 @ 所有人
// 「大家检查一下自己模块的文档是不是最新的」
// 结果:每次都漏几个,用户两周后来报错
// ✅ 好做法:CI 检测特定文件模式,自动触发文档检查
// scripts/check-docs-triggers.ts
import { execSync } from 'node:child_process'
// 拿到当前 PR 的变更文件列表
const changedFiles = execSync('git diff --name-only origin/main...HEAD')
.toString()
.trim()
.split('\n')
// 触发文档检查的模式:API / 配置 / CLI / 错误码
const triggers = [
{ pattern: /src\/api\/.*\.ts$/, reason: 'API 签名可能变化' },
{ pattern: /src\/config\/.*\.ts$/, reason: '配置项可能变化' },
{ pattern: /src\/cli\/.*\.ts$/, reason: 'CLI 命令可能变化' },
{ pattern: /src\/.*errors\.ts$/, reason: '错误码可能变化' },
]
const triggered = triggers.flatMap(({ pattern, reason }) => {
const matched = changedFiles.filter((f) => pattern.test(f))
return matched.length ? [{ reason, files: matched }] : []
})
if (triggered.length > 0) {
console.log('以下变更需要文档 owner 确认:')
for (const t of triggered) {
console.log(`- ${t.reason}: ${t.files.join(', ')}`)
}
// CI 里直接挂一个「docs-check」job,要求 docs owner review
process.exit(1)
}结合 PR 机器人提醒,效果比被动触发好得多:
// .github/workflows/docs-nudge.ts
// 当 PR 包含 API / 配置 / CLI 变更时,自动评论提醒
if (containsApiOrConfigChanges(pr.files)) {
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: [
'⚠️ 此 PR 包含可能影响文档的变更:',
...changedApiFiles.map((f) => `- \`${f}\``),
'',
'请确认:',
'1. 相关文档已更新',
'2. 文档代码示例已验证可运行',
'3. 如无需文档更新,请在此评论说明原因',
].join('\n'),
})
}| 触发方式 | 覆盖率 | 维护成本 | 对开发流程的干扰 |
|---|---|---|---|
| 靠人记得 | 低 | 零 | 低(但累积债务) |
| 发版前集中补 | 中 | 低 | 高(阻塞发版) |
| 文件模式检测 + 提醒 | 高 | 中 | 低(PR 内解决) |
| CI 强制 + docs owner 审查 | 最高 | 中 | 中(可能拖慢 PR) |
AI Native 文档工作流全景
把前面几个案例拼在一起,整个流程可以画成这样:
这个图里几个关键节点对应前面的案例:「提取脚本生成 JSON Schema」是案例一,「CI 触发文档检查」包含案例二和案例三,「回流」是整个闭环的维护机制。任何一个节点缺失,流程都会退化——没有事实源提取,AI 就凭印象写;没有 CI 验证,文档示例就慢慢过期;没有回流,半年后事实源本身也会过时。
在选型上,不同团队的起点不一样,可以按这个表判断该优先补哪一环:
| 方案 | 适用场景 | 局限性 |
|---|---|---|
| 纯手工 + AI 润色 | 小项目、探索期 | 依赖作者自觉,漂移不可避免 |
| 类型定义 → API 文档自动生成 | TypeScript / OpenAPI 项目 | 概念层次和业务上下文仍需人写 |
| 文档示例 CI 验证 | 任何有教程的项目 | 增加 CI 时间,需要维护测试环境 |
| 文件模式检测 + PR 提醒 | 多人协作、变更频繁 | 会有误报,需要调阈值 |
| Mintlify / Fern 这类 AI-native 平台 | 有预算、希望端到端 | 厂商绑定,定制受限 |
| Docusaurus + 自研脚本 | 想要完全控制 | 维护成本高 |
几条可以带走的原则
写到这里,我把这一年的经验压缩成五条:
- 文档从代码派生,而不是凭记忆写。事实源永远应该是代码、类型、配置、测试,而不是「某个同事记得」。
- 文档里的代码示例必须能跑。跑不通的示例不如没有。
- 文档更新触发器要嵌在代码变更流程里。改代码时同步检查文档,而不是发版前集中补。
- AI 生成的文档要更严格地 review,而不是更宽松。AI 是加速器,不是质量关卡。
- 文档质量上限由流程决定,不由 AI 模型决定。事实源清楚、更新流程自动化、验证机制到位、审查标准合理——这些比用哪个 LLM 更重要。
检查清单
按阶段分,可以直接拿去对团队现状打分:
阶段一:事实源建立
- API 文档从类型定义或 OpenAPI spec 派生,构建时自动生成
- 配置文档从配置 schema 派生
- 文档里的代码示例可被测试框架直接运行
- 文档引用的文件路径和版本号有明确标注
阶段二:AI 辅助生成
- 生成文档时提供完整上下文(文件路径、版本、变更历史)
- 明确区分事实陈述(来自类型、测试)和建议性表述(来自 AI 推断)
- AI 生成内容打标,方便后续审查聚焦
阶段三:验证与 CI 集成
- 文档代码示例通过 vitest / 类似工具的可执行检查
- 配置变化时触发相关文档检查
- PR 包含 API / 配置变更时强制 docs owner review
- 发布前做全量文档扫描,标记过期内容
阶段四:审查与维护
- 文档审查清单覆盖准确性、结构、术语一致性、示例可运行性
- AI 生成内容额外做事实核查
- 定期清理过期文档,明确归档策略
参考资料
- arXiv, Free and Customizable Code Documentation with LLMs, 2024. https://arxiv.org/html/2412.00726v1
- Kapa.ai, Optimizing Technical Documentations for LLMs, 2024. https://www.kapa.ai/blog/optimizing-technical-documentation-for-llms
- Fern, Write LLM-Friendly Docs, 2026. https://buildwithfern.com/post/how-to-write-llm-friendly-documentation
- DrExplain, Docs-as-Code 2.0: a new standard for AI-ready user documentation, 2026. https://www.drexplain.com/press/articles/docs_as_code_2_0_a_new_standard_for_ai_ready_user_documentation/
- State of Docs, The State of Docs Report 2026 – AI and documentation creation, 2026. https://www.stateofdocs.com/2026/ai-and-documentation-creation
- Mintlify, Best Code Documentation Tools 2026, 2026. https://www.mintlify.com/library/best-code-documentation-tools
- Google, Google Developer Documentation Style Guide. https://developers.google.com/style