AI 輔助文件流程如何預算人工審核時間並建立契約驅動的草稿佇列
Budget Human Minutes Before You Generate Docs: A Contract-Driven Draft Queue
文章提出一套契約驅動的 AI 輔助文件草稿佇列方法,在生成前為各分區設定模型或人工處理路徑、負責人、預計審核時間及需追溯證據的主張數量。Python 腳本按審核時間排序,並阻止無負責人或超過 45 分鐘上限的分區進入模型流程;範例中只有 quickstart/draft 入列,兩項被阻止、一項交由人工處理,總預算為 20 分鐘。
The scarce resource in an AI-assisted documentation pipeline is not model output; it is the reviewer minutes you can actually schedule. Free model access makes drafts nearly costless to produce, so pipelines that optimize generation quality often just create a larger unreviewed backlog. A better control point is a per-section contract that decides, before any generation, whether a section is draftable at all and how many minutes its review will consume. The artifact below turns that contract into a sorted queue and exits non-zero whenever an unowned or over-budget section tries to enter the model lane.
1. What the model may draft, and what stays human
The split is not about difficulty; it is about who can be held responsible for the sentence. A model may draft text whose correctness is checkable against an existing artifact: a command that already runs in CI, a config key that exists in the schema, a return type declared in code. A human must own anything that encodes intent, tradeoffs, deprecation promises, security posture, or a claim that no file in the repository can confirm.
That rule is only useful if it is executable, so encode it as a column. Each documentation section gets a lane, a named owner, an estimated review cost in minutes, and a count of claims that need traceable evidence. The model lane is allowed to fill the text; the human lane produces text that a model may only format.
2. Write the section contract as data
Keep the contract as a flat CSV next to the docs, because it diffs cleanly in review and any script can read it. Start with a fixture that intentionally contains one valid draftable section and two that must be refused.
section,lane,owner,review_minutes,claims
quickstart/draft,model,alex,20,2
concepts/auth-model,human,,0,5
reference/api-errors,model,,15,3
guides/migration,model,priya,90,4
The fixture encodes three different failure modes: a human-lane section, a model-lane section with no owner, and a model-lane section whose review cost exceeds a single sitting. Only quickstart/draft should reach the queue.
3. Turn the contract into a queue with a hard stop
The script below uses only the standard library, so it runs identically on a laptop and on any server. It sorts draftable sections by rising review cost, which lets you fill short review windows first instead of opening the most expensive draft last.
#!/usr/bin/env python3
"""Order model-draftable doc sections by the human minutes they will cost."""
import csv
import sys
REQUIRED = {"section", "lane", "owner", "review_minutes", "claims"}
MAX_MINUTES = 45
def load(path):
with open(path, newline="", encoding="utf-8") as fh:
rows = list(csv.DictReader(fh))
if not rows:
raise SystemExit("empty contract file")
missing = REQUIRED - set(rows[0].keys())
if missing:
raise SystemExit(f"contract missing columns: {sorted(missing)}")
return rows
def classify(rows):
draftable, blocked, human = [], [], []
for row in rows:
minutes = int(row["review_minutes"])
if row["lane"] == "human":
human.append(row)
continue
if row["lane"] != "model":
blocked.append((row["section"], f"unknown lane={row['lane']}"))
continue
if not row["owner"].strip():
blocked.append((row["section"], "no owner"))
continue
if minutes > MAX_MINUTES:
blocked.append((row["section"], f"review_minutes={minutes} > {MAX_MINUTES}"))
continue
draftable.append((minutes, row["section"], row["claims"]))
return draftable, blocked, human
def main(argv):
if len(argv) != 2:
raise SystemExit("usage: draft_queue.py <contract.csv>")
draftable, blocked, human = classify(load(argv[1]))
for minutes, section, claims in sorted(draftable):
print(f"DRAFT {section} minutes={minutes} claims={claims}")
for section, reason in blocked:
print(f"BLOCK {section} reason={reason}")
for row in human:
print(f"HUMAN {row['section']} review_minutes={row['review_minutes']}")
budget = sum(minutes for minutes, _, _ in draftable)
print(f"summary draft={len(draftable)} blocked={len(blocked)} "
f"human={len(human)} budget_minutes={budget}")
return 1 if blocked else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
Run it against the fixture and compare with the expected output shown below. The non-zero exit code is the important part: the same check can gate a CI job, so a section with no owner cannot silently enter the model lane even if the docs build is green.
python3 draft_queue.py fixture_contract.csv; echo "exit=$?"
DRAFT quickstart/draft minutes=20 claims=2
BLOCK reference/api-errors reason=no owner
BLOCK guides/migration reason=review_minutes=90 > 45
HUMAN concepts/auth-model review_minutes=0
summary draft=1 blocked=2 human=1 budget_minutes=20
exit=1
4. Run batches from the queue head, never from the file tree
A scheduled job should read the queue rather than glob a directory, because the queue already encodes the owner and cost decisions. This keeps capacity honest: if the queue is empty, the job does nothing and you have not manufactured review work.
#!/usr/bin/env bash
set -euo pipefail
cd /srv/docs-pipeline
mapfile -t SECTIONS < <(
python3 draft_queue.py contract.csv | awk '/^DRAFT/{print $2}' | head -n "${BATCH:-2}"
)
for section in "${SECTIONS[@]}"; do
./draft_one.sh "$section" >> "drafts/$(date +%F).log" 2>&1 \
|| echo "FAILED $section" >&2
done
This is where a free drafting tier changes the economics: the batch can run on a free server option with free model access, so the cost of a wasted draft is bounded by scheduling noise rather than by tokens. I use MonkeyCode for that layer because the operator supplies free model access and a free server option, and I keep every release-blocking document out of that path regardless. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The draft_one.sh script stays deliberately thin: it reads the section contract row, sends the section's evidence files as context, and writes the output to drafts/<section>.md with the contract row appended as a header comment. Nothing in that script is allowed to overwrite a file outside drafts/, which keeps the human lane physically separate from generated text.
5. Decide in advance what happens when the draft tier is unavailable
Free capacity is best-effort, and a pipeline that blocks on it will eventually stall a release. Write the fallback before you need it, and treat each row as a default action rather than a discussion.
| Condition observed | Default action | Human lane impact |
|---|---|---|
| Draft request fails twice | Skip section, log to drafts/failed.log
|
None; humans continue on their lane |
| Draft tier slow beyond your batch window | Lower BATCH to 1 and keep the queue intact |
None |
| Draft tier unavailable for the whole run | Publish human-lane sections only, queue stays frozen | Unchanged scope |
| Draft rejected by review twice | Move section to human lane in the contract | Owner writes the section directly |
The last row matters most: repeated rejection is evidence that the section needs intent, not more prompting. Moving it to the human lane records that decision in the same file the pipeline reads.
6. Record the minutes you actually spent
Estimated review cost drifts, so keep a ledger and recalibrate the contract monthly. A five-column CSV is enough, and it converts opinions about drafting speed into a number you can compare across sections.
section,drafted_at,reviewer,minutes,verdict
quickstart/draft,2026-10-05,alex,26,accepted
reference/api-errors,2026-10-05,priya,44,rewritten
Compute the median minutes per lane and compare it with review_minutes in the contract. When the median exceeds the estimate by a wide margin for a section type, raise the estimate and let the queue push that type behind cheaper work. This keeps the budget column honest without any scoring model.
7. Verify the pipeline with three assertions
A short test plan is enough to prove the queue does its job, and it runs in under a second. Feed the fixture to the script and assert on exit code, on sort order, and on the absence of blocked sections in the draft list.
-
python3 draft_queue.py fixture_contract.csvexits with status 1 because two sections are blocked. - The
DRAFTlines appear in ascendingminutesorder; add a second draftable row at 10 minutes to confirm the sort. - No section with an empty
ownerappears on aDRAFTline, even after you add more rows.
If all three pass, the model lane cannot absorb unowned work through a contract edit alone, which is the property the pipeline exists to guarantee.
Limitations and who should skip this
The script cannot tell whether a claim is true; it only enforces that someone is accountable for checking it. review_minutes is an estimate that decays as the codebase changes, and the contract itself can drift out of sync with the docs tree unless the CI check runs on every docs change. Free-tier availability is best-effort, so a nightly cron with no fallback will occasionally produce nothing at all. Teams without an owner-per-section habit should not adopt this, because the queue will block everything on day one. Solo maintainers of a handful of pages will find the contract heavier than the docs. Projects with formal audit requirements should keep drafting inside their approved tooling rather than adding a free external tier to the path.
If you want to try it, copy the fixture and the script into the docs repository first, run the three assertions, and only then wire the batch job into a schedule.
來源:DEV Community · dev.to