跳到正文
DEV Community· Avery Lin·· 5 小時前AI 評分53

AI 輔助文件流程如何預算人工審核時間並建立契約驅動的草稿佇列

Budget Human Minutes Before You Generate Docs: A Contract-Driven Draft Queue

AI 導讀

文章提出一套契約驅動的 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.

  1. python3 draft_queue.py fixture_contract.csv exits with status 1 because two sections are blocked.
  2. The DRAFT lines appear in ascending minutes order; add a second draftable row at 10 minutes to confirm the sort.
  3. No section with an empty owner appears on a DRAFT line, 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