AI 起草文件如何建立段落責任清單
Who Owns This Paragraph? An Ownership Manifest for AI-Drafted Docs
文章提出一套管理 AI 起草文件的責任流程,按內容區塊指定撰寫類別、負責人和證據,並以 CI 檢查清單是否完整。區塊分為 draftable、attestable 和 prohibited,文中提供 YAML manifest、Python 檢查腳本、pytest 及 CI 範例;作者指出檢查器只能核對結構與參照是否存在,不能確認參照內容仍支持段落主張。
Here is a scenario that repeats in most repositories that let a model write documentation. A pull request adds a runbook section: rotate the API credential, then restart the worker. The diff is clean, the headings match the style guide, and the reviewer approves it in ninety seconds.
Six weeks later the steps are wrong. Rotation moved behind a queue, and the text now describes a procedure that no longer exists. Nobody can name the person who was supposed to catch that. The paragraph has an author. It has no owner.
That gap is the interesting part. Style is cheap to automate; accountability is not. What follows is a workflow that keeps generated prose draftable while keeping ownership human, plus the check that enforces the boundary in CI.
Generated docs rarely fail on prose
They fail on four smaller things:
- Unverified claims. A number, endpoint, or step that was plausible in the draft and never checked against code.
- Missing ownership. No named person or team is on the hook when the claim goes stale.
- Silent drift. The source of truth moved; the document did not.
- Unbounded scope. The model was allowed to write about customer impact, pricing, or compliance because nobody drew the line.
Better prompts do not fix any of these. A contract next to the document does.
Classify blocks, not whole files
Treat a document as a set of addressable blocks. Each block gets one of three classes:
-
draftable — the model writes the first pass. Required evidence: a
source_refpointing into code, config, or schema. -
attestable — the model drafts a skeleton, a human confirms each step actually ran. Required evidence: a
human_attestationreference. - prohibited — the model emits nothing here. Legal, pricing, customer impact, incident postmortems, and anything with an external commitment live in this class.
A three-state enum is boring on purpose. It is easy to review and easy to test, and both of those matter more than expressiveness.
Mark blocks in the source
Blocks are delimited with HTML comments so they stay invisible when rendered:
<!-- block:rotation-steps -->
1. Drain the worker pool.
2. Rotate the credential in the secret store.
3. Restart the worker and confirm the health endpoint.
<!-- /block -->
The manifest then lists every block, its class, its owner, and its evidence:
# docs/ownership.yaml
version: 1
documents:
- path: docs/ops/rotate-api-credentials.md
blocks:
- id: overview
class: draftable
owner: '@ops-docs'
evidence: source_ref
ref: 'services/auth/src/rotate.ts#L40-L96'
- id: rotation-steps
class: attestable
owner: '@ops-oncall'
evidence: human_attestation
ref: 'ops-review-log#R-2214'
- id: customer-impact
class: prohibited
owner: '@support-lead'
evidence: human_attestation
ref: 'reachability-review#R-88'
Paths and review-log identifiers above are illustrative placeholders; substitute your own.
The gate
One script is enough to turn the manifest into a failing check:
#!/usr/bin/env python3
'''check_ownership.py — fail when a docs block has no owner or no evidence.'''
from __future__ import annotations
import json
import sys
from pathlib import Path
import yaml # pip install pyyaml
REQUIRED = {'id', 'class', 'owner', 'evidence'}
CLASSES = {'draftable', 'attestable', 'prohibited'}
EVIDENCE = {'source_ref', 'command_output', 'human_attestation'}
NEEDS_REF = {'draftable', 'attestable'}
def block_ids(markdown: str) -> set[str]:
'''Collect block markers: <!-- block:id --> ... <!-- /block -->'''
found = set()
for raw in markdown.splitlines():
line = raw.strip()
if line.startswith('<!-- block:') and line.endswith('-->'):
found.add(line[len('<!-- block:'):-3].strip())
return found
def check(manifest_path: Path) -> list[str]:
errors: list[str] = []
data = yaml.safe_load(manifest_path.read_text(encoding='utf-8'))
if data.get('version') != 1:
errors.append('manifest: unsupported version')
for doc in data.get('documents', []):
path = Path(doc['path'])
if not path.exists():
errors.append(f'{path}: declared but missing')
continue
declared = {b['id']: b for b in doc.get('blocks', [])}
present = block_ids(path.read_text(encoding='utf-8'))
for orphan in sorted(present - declared.keys()):
errors.append(f'{path}: block {orphan!r} missing from manifest')
for ghost in sorted(declared.keys() - present):
errors.append(f'{path}: manifest declares {ghost!r}, no such block')
for bid, block in declared.items():
missing = REQUIRED - block.keys()
if missing:
errors.append(f'{path}#{bid}: missing {sorted(missing)}')
continue
cls = block['class']
ev = block['evidence']
if cls not in CLASSES:
errors.append(f'{path}#{bid}: unknown class {cls!r}')
if ev not in EVIDENCE:
errors.append(f'{path}#{bid}: unknown evidence {ev!r}')
if cls in NEEDS_REF and not block.get('ref'):
errors.append(f'{path}#{bid}: class {cls} needs a ref')
return errors
if __name__ == '__main__':
target = Path(sys.argv[1] if len(sys.argv) > 1 else 'docs/ownership.yaml')
failures = check(target)
print(json.dumps({'failures': failures}, indent=2))
sys.exit(1 if failures else 0)
A pytest wrapper makes it part of the normal test run:
# tests/test_docs_ownership.py
import subprocess
import sys
def test_ownership_manifest_is_satisfied():
result = subprocess.run(
[sys.executable, 'tools/check_ownership.py', 'docs/ownership.yaml'],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stdout + result.stderr
And the CI job is small enough to read in one screen:
name: docs-ownership
on:
pull_request:
paths:
- 'docs/**'
- 'tools/check_ownership.py'
- 'tests/test_docs_ownership.py'
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install pyyaml pytest
- run: python tools/check_ownership.py docs/ownership.yaml
- run: pytest -q tests/test_docs_ownership.py
The decision table
| Block class | Model may | Human must | Evidence key | CI behaviour |
|---|---|---|---|---|
| draftable | write the first pass | verify every factual claim against ref
|
source_ref |
fails without ref
|
| attestable | draft the skeleton only | run each step and record the attestation | human_attestation |
fails without ref
|
| prohibited | nothing | write it or approve it explicitly | human_attestation |
review only |
One honest limit: detecting that a model touched a prohibited block requires your generator to record provenance. If it cannot, drop that claim and let review carry it rather than pretending the check proves it.
Where a free tier actually helps
The manifest makes the draft step cheap and the gate non-negotiable. That split is where free infrastructure helps instead of getting in the way.
- Drafting runs are high-volume and low-risk: one call per block, output only ever landing in
draftableandattestableblocks. MonkeyCode's free model access fits that shape, because the artifact is still a first pass a human signs. - The gate is the opposite: short, deterministic, and it has to run on every contributor's machine or in CI. A free server option lets you run the checker from a hosted environment instead of asking each contributor to install Python and PyYAML locally.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Two caveats that matter more than the convenience. Quotas, limits, and availability on any free tier change, so check the current plan before routing a pipeline through it, and never treat a free tier as a production SLA. A cheap draft does not reduce what the human still owns.
Limitations and who should not use this
- If your docs come from a schema, generate them instead. OpenAPI, protobuf, and Terraform can emit reference pages with no ownership manifest at all. Manifests are for prose.
- If a document has exactly one maintainer, the manifest is overhead. A single owner field in front matter is enough.
-
If you cannot name an owner for a block, that is the finding. Do not invent a name to make CI green; mark the block
prohibitedand escalate. - Regulated or contractual text — SLOs, security commitments, pricing — should not sit in a generated pipeline no matter how good the check is.
- The checker is structural, not semantic. It proves a block has an owner and that a reference exists. It cannot prove the reference still says what the paragraph claims; that needs a link check and a human read.
Start with three blocks
You do not need to retrofit an entire docs tree. Pick one runbook, mark three blocks, write three manifest entries, and run the script once. If it passes on the first attempt, the manifest is usually too vague to be useful.
The rule worth keeping is short: the model may draft anything a human will read, and it may never own the sentence someone will act on. Keep the gate yours, and a free first pass is a reasonable place to begin.
來源:DEV Community · dev.to