生成式文档应引用OpenAPI/测试快照等有据可查的源头;对兼容性承诺、弃用时间线等无法从artifact还原的内容必须人工决策留档,否则审阅变成猜谜。
API 文档在起草前应哈希溯源
生成的 API 文档要保持可审查,关键在于每个起草的章节都要引用带哈希的源材料,且人类要确保每个对客户的承诺都经过确认。模型可以有效地填充 OpenAPI 文件、测试快照或变更日志中已有的表格,但不应凭空编造任何后续无法佐证的废弃时间线、支持边界或故障叙述。本文其余部分描述的是一套源绑定流水线,它在持续集成中强制执行上述边界划分。
未绑定的草稿制造的是文档债,而非额外覆盖率
生成的门槛很低,但这不会让文档的运营成本降低,因为审查者仍需决定哪些句子是事实。从 OpenAPI 文档复制过来的参数表在糟糕的草稿之后仍可恢复,因为规范本身仍然具有权威性。而一个承诺多年兼容性窗口的句子,除了人类决策记录之外,无法从任何文件中恢复。当这两类文本在同一个 markdown 文件中不带绑定关系混在一起时,审查就变成了猜谜而非哈希比对。
因此,文档债的表现形式是缺少来源追踪,而非缺页或缺截图。团队如果从一个无约束的 prompt 生成整篇指南,往往无法说出"当规范变更时哪一段需要改动"——这个缺口后来表现为相互矛盾的支持回复,而不是文档 Pull Request 上的 linter 失败。把生成视为对提取事实的渲染步骤,可以让可恢复的部分保持低成本,而把承诺类内容有意地保持高成本。
决策表:派生事实与人类所有的解读
真正有用的划分不是"用模型"vs"禁用模型"。真正有用的划分是:某章节是否可以从仓库中已存在的带哈希的工件派生而来。用这张表作为 CI 策略,而非写作风格偏好;当来源单元格为空时拒绝生成。
模型只能在人类发布了带有稳定哈希的架构决策记录后,才能对其进行摘要。而且摘要仍然只是草稿,直到记录所有者在已发布的树状结构中接受了措辞。任何向客户承诺产品将如何保证的内容都是解读,即使周围的表格是纯粹派生的。
创建一份清单,将已发布的 markdown 路径映射到源工件、负责人和草稿策略。将这份清单放在文档仓库中,这样绑定任务就可以在每个 Pull Request 中读取它。下面的 YAML 是结构参考草图,并非声称来自实际部署的生产配置。
# docs/bindings.yaml
version: 1
published_root: docs/published
draft_root: docs/_drafts
sections:
- id: payments-params
output: docs/published/payments/parameters.md
source_kind: openapi
source_path: specs/payments.yaml
source_hash: sha256:pending
draft_policy: derived_only
owner: api-platform
- id: payments-examples
output: docs/published/payments/examples.md
source_kind: fixture
source_path: tests/fixtures/payments/
source_hash: sha256:pending
draft_policy: derived_only
owner: api-platform
- id: payments-support-window
output: docs/published/payments/support.md
source_kind: policy
source_path: policy/support-windows.md
source_hash: sha256:pending
draft_policy: human_owns
owner: support-lead
- id: payments-migration
output: docs/published/payments/migration.md
source_kind: adr
source_path: docs/adrs/2026-08-payments-v3.md
source_hash: sha256:pending
draft_policy: human_owns
owner: payments-api
draft_policy 字段是此工作流中唯一面向 prompt 的控制项,而且应该保持这么小。derived_only 意味着模型收到的是提取的事实加上禁止话题列表,而不是整个文档站。human_owns 意味着跳过生成,已发布文件必须与上次负责人批准的 blob 匹配。更新 source_hash: pending 发生在提取器写入事实文件之后,而不是在人敲入 prompt 之后。
先运行提取器,这样模型就不会把原始规范当作改写政策的由头。先对提取器输出做哈希,然后在任何草稿任务启动前将该摘要存储到清单的对应条目中。下面的 Python 草图读取一个简单的 OpenAPI 子集,写入一个排序后的 JSON 事实文件,后续头部可以引用它。
# scripts/extract_openapi_facts.py
# Reference sketch: adapt field names to the local specification convention.
import hashlib, json, sys, pathlib
try:
import yaml
except ImportError:
sys.exit("install pyyaml before running this extractor")
def load_spec(path):
with open(path, encoding="utf-8") as handle:
return yaml.safe_load(handle)
def extract(spec):
facts = {"paths": []}
for path, methods in (spec.get("paths") or {}).items():
for method, op in (methods or {}).items():
if method.startswith("x-") or not isinstance(op, dict):
continue
params = []
for param in op.get("parameters") or []:
schema = param.get("schema") or {}
params.append({
"name": param.get("name"),
"in": param.get("in"),
"required": bool(param.get("required")),
"type": schema.get("type"),
})
facts["paths"].append({
"path": path,
"method": method.upper(),
"operationId": op.get("operationId"),
"status_codes": sorted((op.get("responses") or {}).keys()),
"parameters": params,
})
return facts
def write_facts(spec_path, out_path):
facts = extract(load_spec(spec_path))
payload = json.dumps(facts, sort_keys=True, indent=2)
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
pathlib.Path(out_path).parent.mkdir(parents=True, exist_ok=True)
pathlib.Path(out_path).write_text(payload + "\n", encoding="utf-8")
print(f"{out_path} sha256:{digest}")
if __name__ == "__main__":
write_facts(sys.argv[1], sys.argv[2])
本地提取检查只需两条命令,而且两者都应该足够轻量,可以在每个文档 Pull Request 上运行。如果事实文件的摘要与上次接受派生草稿后记录的摘要不匹配,则从新事实重新生成该草稿。如果摘要匹配且仅人类所有文件中的文案有变化,则拒绝该变更——除非该负责人列在 Pull Request 上。
python scripts/extract_openapi_facts.py specs/payments.yaml docs/_facts/payments.json
sha256sum docs/_facts/payments.json
向模型提供事实 JSON、章节标识符,以及一份禁止话题清单。不要喂给整个文档站,因为相邻的策略页面会把承诺泄露到派生表格中。Prompt 契约可以保持如此精简,但仍然足够具体,以便后续进行头部检查。
You are drafting section payments-params from docs/_facts/payments.json.
Write markdown tables only. Do not mention support windows, uptime, or migration advice.
Do not invent parameters, status codes, or examples that are absent from the fact file.
If a field is missing in the fact file, write TODO-MISSING-FACT and stop that row.
Output must start with: SOURCE_HASH sha256:<hash-from-facts>
将模型输出写入 docs/_drafts/ 下,绝不写入 docs/published/。晋升是独立的复制步骤,由持续集成在绑定检查通过且章节负责人被列出后才执行。需要隔离环境来运行起草步骤的团队可以使用 MonkeyCode 的免费模型访问和免费服务器选项。披露:本文是作为 MonkeyCode 产品推广的一部分准备的,文中提及该产品仅用于那个隔离的起草步骤。隔离服务器之所以有用,是因为草稿文件不应与已发布的客户页面共享工作树,而且此工作流不依赖特定的模型名称、配额或硬件配置。
晋升可以是一个简短的 shell 门控,而非另一个框架。下面的命令拒绝复制头部与当前事实文件摘要不匹配的草稿。
DRAFT=docs/_drafts/payments-params.md
FACTS=docs/_facts/payments.json
OUT=docs/published/payments/parameters.md
HASH=$(sha256sum "$FACTS" | awk '{print $1}')
HEAD=$(sed -n '1p' "$DRAFT")
test "$HEAD" = "SOURCE_HASH sha256:$HASH"
mkdir -p "$(dirname "$OUT")"
cp "$DRAFT" "$OUT"
门控应该保持简单:解析已发布的 markdown,在派生文件上要求来源头部,并比对摘要。当派生文件没有头部时Fail closed,当人类所有的文件突然出现了模型头部时也 Fail closed。下面的草图是这对规则的参考检查器。
# scripts/check_doc_bindings.py
# Reference sketch for CI. Extend it with a real manifest loader.
import hashlib, pathlib, re, sys
HEADER = re.compile(r"^SOURCE_HASH sha256:([0-9a-f]{64})\s*$", re.M)
FORBIDDEN = ("uptime credit", "we will support", "guaranteed", "you should migrate")
def file_hash(path):
data = pathlib.Path(path).read_bytes()
return hashlib.sha256(data).hexdigest()
def check_pair(markdown_path, facts_path, policy):
text = pathlib.Path(markdown_path).read_text(encoding="utf-8")
match = HEADER.search(text)
if policy == "human_owns":
if match:
raise SystemExit(
f"{markdown_path}: human-owned file must not carry SOURCE_HASH"
)
return
if not match:
raise SystemExit(f"{markdown_path}: missing SOURCE_HASH header")
expected = file_hash(facts_path)
if match.group(1) != expected:
raise SystemExit(
f"{markdown_path}: header {match.group(1)} != facts {expected}"
)
lowered = text.lower()
for token in FORBIDDEN:
if token in lowered:
raise SystemExit(
f"{markdown_path}: derived draft contains human-owned language"
)
if __name__ == "__main__":
check_pair(sys.argv[1], sys.argv[2], sys.argv[3])
将提取器和检查器接线为路径上的必需状态检查,这些路径可以变更事实或已发布的正文。在草稿任务后,检查 git diff -- docs/published;任何意外的小块意味着晋升步骤泄露到了客户可见的文件中。
# .github/workflows/doc-bindings.yml
name: doc-bindings
on:
pull_request:
paths:
- "docs/**"
- "specs/**"
- "tests/fixtures/**"
- "scripts/**"
jobs:
bind:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install pyyaml
- run: python scripts/extract_openapi_facts.py specs/payments.yaml docs/_facts/payments.json
- run: python scripts/check_doc_bindings.py docs/published/payments/parameters.md docs/_facts/payments.json derived_only
- run: python scripts/check_doc_bindings.py docs/published/payments/support.md docs/_facts/payments.json human_owns
哈希绑定证明的是来源,而非提取器或周围产品行为的正确性。OpenAPI 文件可能遗漏了仅在生产环境出现的头部,模型会忠实地在表格中遗漏该头部。Token 禁止列表很脆弱,草稿可以用检查器禁止短语以外的方式改写支持承诺。
此方法不适用于叙事博客、设计合作伙伴预告,以及任何没有规范、fixture 或决策记录可供哈希的仓库。对于法务文档也不适用——即使政策文件已存在于 git 中,律师仍须起草。仅有单个 README 且无客户支持窗口的小型库,不应为实现这个划分而采用多任务流水线。
审查者仍需以与生成出现前相同的审慎态度来阅读 human_owns 文件。只有在提取器可信且负责人已命名时,流水线才能将派生表格从阅读列表中移除。如果团队无法为支持语言指定负责人,应该停止生成那些页面,而不是将句子路由到隔离的起草服务器。
如果隔离的起草工作空间有助于将哈希源与已发布页面保持分离,请在添加另一条生成路径之前,根据绑定清单评估这种划分。
披露:本文是作为 MonkeyCode 产品推广的一部分准备的。