跳到正文
DEV Community· Avery Lin·· 2 小時前AI 評分61

模型在技術文件中可起草甚麼:句子角色契約與主張隔離閘門

What a Model May Draft in Docs: A Sentence-Role Contract and a Claim Quarantine Gate

AI 導讀

文章提出一套模型協助起草技術文件的流程,以句子角色界定模型可撰寫、可改寫及必須由人員負責的內容,並用事實登記表和檢查閘門隔離未登記負責人的主張。閘門以正則表達式偵測數值、絕對措辭、相容性及安全聲明,只有能對應登記項目的主張才會通過。作者提醒,閘門只檢查責任歸屬,不代表主張正確,也可能漏掉改寫後的主張或誤報一般文字。

正文

What a Model May Draft in Docs: A Sentence-Role Contract and a Claim Quarantine Gate

The question that scales is not whether a model may draft documentation, but which sentence roles it may fill. Answer that at the sentence level and the drafting pass becomes reviewable line by line instead of argued page by page. This article defines three sentence roles, gives you a contract file that assigns ownership, and ships a small gate that quarantines every claim without a registered owner. Everything below is a reference implementation; calibrate the patterns and thresholds against your own corpus before you trust them.

Why page-level permissions keep failing

Risk is not distributed evenly across a page. The same file contains a heading, a transitional sentence, and a compatibility statement, and only one of those can mislead a reader in production. When you grant permission at the page or section level, you must accept the risky sentences to get the cheap ones.

Teams then compensate with review, and review becomes the bottleneck the model was supposed to remove. The fix is to make the writable region smaller than the review region, so a reviewer can skip whole classes of lines with confidence. That requires a machine-checkable definition of which sentence carries a claim.

The three sentence roles

Classify every sentence before generation, not after. The table below is the contract this workflow enforces mechanically.

Sentence role Example Who writes it Merge condition
Registry mirror "The retry limit defaults to the value in facts.toml." Model, constrained Inline marker resolves to an existing registry key
Connective tissue "The next two steps cover failure handling." Model, free No claim pattern matches the line
Substantive claim "Uploads finish in under 40 ms at p99." Named human owner Registry row plus named reviewer
Normative or security claim "Tokens are never written to disk." Human plus security review Never model-owned

The important asymmetry sits in the bottom two rows. A model may reorder and preview owned material, but it may not originate a number, an absolute, a compatibility statement, or a security guarantee. Those lines are quarantined until a human registers them.

Step 1: Write the fact registry first

No gate works without a registry, because the gate has nothing to check markers against. Keep one file per repository, give every entry a human owner and a source pointer, and treat the file as the only legal origin of volatile values.

# facts.toml — every value here has a named human owner.
[facts.retry_limit]
value = "3"
owner = "@docs-owner"
source = "internal/runtime/config.go#L214"

[facts.upload_timeout]
value = "30s"
owner = "@platform-owner"
source = "internal/gateway/limits.go#L88"

A registry with stale rows is worse than no registry, so add a review date field if your release cadence is slow. The gate only proves that a claim has an owner, never that the owner is still right.

Step 2: Run the connective-tissue pass

The drafting job is deliberately narrow: ordering, transitions, and previews of steps that already exist in the source material. This is high-volume and low-difficulty generation, so a free model tier is a reasonable fit for it, and the batch nature means a small server is enough.

I run this pass against MonkeyCode's free model access, with the free server option handling the batch and the CI-side checks. Those two availability claims are the operator's, not measurements of mine: I am not asserting model names, quotas, hardware, or throughput here, because those change and I have not benchmarked them. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Keep the prompt short and forbid claim shapes explicitly, because vague instructions like "stay accurate" produce exactly the sentences you must then reject.

You may write only two sentence roles.
1. Registry mirrors: restate one value from facts.toml per sentence.
2. Connective tissue: ordering, transitions, previews of already-owned steps.

Do not introduce numbers, absolutes, compatibility statements, or security claims.
If a sentence would need one, emit <!-- TODO(claim): what is missing --> instead.

The TODO(claim) output is a feature, not a failure. It converts an invisible gap into a countable number you can put in a report and assign to a person.

Step 3: Quarantine unowned claims

This script is the load-bearing part of the workflow. It walks the drafted Markdown, matches a small set of claim patterns, and fails when a matching line lacks an inline marker that resolves to a registry key.

#!/usr/bin/env python3
"""quarantine.py — fail CI on documentation claims with no registered owner."""
from __future__ import annotations

import re
import sys
import tomllib
from pathlib import Path

CLAIM_PATTERNS = {
    "numeric": re.compile(r"\b\d+(?:\.\d+)?\s*(?:ms|s|m|h|MB|GB|%|rps|req/s)\b", re.I),
    "absolute": re.compile(r"\b(always|never|guaranteed|all|none|must not|only)\b", re.I),
    "compat": re.compile(r"\b(supports|compatible with|requires|deprecated|removed in)\b", re.I),
    "security": re.compile(r"\b(encrypted at rest|sanitized|never logged|safe to)\b", re.I),
}
OWNED = re.compile(r"<!--\s*owned:\s*(?P<id>[A-Za-z0-9_.\-]+)\s*-->")
TODO = re.compile(r"<!--\s*TODO\(claim\):")

def load_facts(root: Path) -> dict:
    path = root / "facts.toml"
    if not path.exists():
        return {}
    with path.open("rb") as fh:
        return tomllib.load(fh).get("facts", {})

def scan(text: str, facts: dict) -> list[tuple[int, str, str, bool]]:
    rows = []
    for lineno, raw in enumerate(text.splitlines(), start=1):
        marker = OWNED.search(raw)
        owned_id = marker.group("id") if marker else None
        line = OWNED.sub("", raw).strip()
        for kind, pattern in CLAIM_PATTERNS.items():
            if pattern.search(line):
                rows.append((lineno, kind, line, bool(owned_id and owned_id in facts)))
                break
    return rows

def main(argv: list[str]) -> int:
    docs = Path(argv[1] if len(argv) > 1 else "docs")
    root = Path(argv[2] if len(argv) > 2 else ".")
    facts = load_facts(root)
    claims = owned = todos = 0
    unowned: list[str] = []
    for path in sorted(docs.rglob("*.md")):
        text = path.read_text(encoding="utf-8")
        todos += len(TODO.findall(text))
        for lineno, kind, line, is_owned in scan(text, facts):
            claims += 1
            if is_owned:
                owned += 1
                continue
            unowned.append(f"{path}:{lineno}: [{kind}] {line[:110]}")
    coverage = owned / claims if claims else 1.0
    print(f"claims={claims} owned={owned} coverage={coverage:.0%} open_todos={todos}")
    for row in unowned:
        print(row)
    if unowned:
        print(f"QUARANTINE: {len(unowned)} claim line(s) need a human owner.", file=sys.stderr)
        return 1
    return 0

if __name__ == "__main__":
    raise SystemExit(main(sys.argv))

Run it locally with python tools/quarantine.py docs .. The script requires Python 3.11 or newer for tomllib, and it reports both the quarantine list and two numbers worth tracking over time.

Step 4: Make the gate mandatory but narrow

Wire the check into pull requests, and scope it to documentation paths so it never blocks unrelated work. A narrow gate that runs on every relevant change beats a broad gate people learn to bypass.

name: docs-claim-gate
on:
  pull_request:
    paths:
      - "docs/**"
      - "facts.toml"
jobs:
  quarantine:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: python tools/quarantine.py docs .

Start with the gate as a required check only after a week of clean runs. Early false positives come mostly from legitimate prose that happens to contain the word "only" or "requires", and you fix those by rewording, not by widening the exceptions.

Step 5: Prove the gate can fail

A gate you have never seen fail is decoration. Keep a mutation pair that asserts both directions, and run it whenever you edit the patterns.

import subprocess
import sys
from pathlib import Path

SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "quarantine.py"

def run_gate(root: Path) -> subprocess.CompletedProcess:
    return subprocess.run(
        [sys.executable, str(SCRIPT), str(root / "docs"), str(root)],
        capture_output=True, text=True,
    )

def test_registered_claim_passes(tmp_path):
    (tmp_path / "docs").mkdir()
    (tmp_path / "facts.toml").write_text(
        '[facts.retry_limit]\nvalue = "3"\nowner = "@docs-owner"\n', encoding="utf-8")
    (tmp_path / "docs" / "config.md").write_text(
        "The retry limit is 3. <!-- owned: retry_limit -->\n", encoding="utf-8")
    assert run_gate(tmp_path).returncode == 0

def test_injected_latency_claim_is_quarantined(tmp_path):
    (tmp_path / "docs").mkdir()
    (tmp_path / "facts.toml").write_text("", encoding="utf-8")
    (tmp_path / "docs" / "index.md").write_text(
        "Requests finish in 40 ms at p99.\n", encoding="utf-8")
    result = run_gate(tmp_path)
    assert result.returncode == 1
    assert "[numeric]" in result.stdout

The second test is the one that matters. If someone loosens a regex to unblock a release, the mutation test should start failing in the same pull request.

Step 6: Track coverage and open gaps, not sentiment

The script prints coverage and open_todos, which are the two numbers to put in a weekly report. Coverage is the share of claim-bearing lines that resolve to a registry entry, and open todos are the explicit holes the model refused to fill. Improving coverage by adding registry rows is progress; improving it by rewording claims into vagueness is not, and you should watch the two numbers together for that reason.

A reasonable sequence is to publish the numbers for two weeks, then gate coverage with a floor below the current value. Never set the floor at one hundred percent, because some pages legitimately contain human-owned claims that no registry should own.

Limitations and who should not use this

The detectors are recall-biased regular expressions, so they will miss paraphrased claims and flag harmless prose; they do not understand negation, hedging, or scope. The coverage ratio is a proxy for ownership, not a measurement of correctness, and it says nothing about whether an owned claim is still true. The patterns here are English-only and will misfire on localized documentation without rewriting.

Skip this workflow if you have no maintained fact registry, because the markers would have nothing to resolve against and the gate becomes noise. Skip it for specifications, contracts, and legal text where nearly every sentence is a claim, since the model's writable surface would shrink to nothing. Skip it on solo projects where no second person exists to be the named owner, and use a plain review checklist instead.

Closing

The durable part of this workflow is the sentence-role contract, not the script; the script only makes the contract enforceable. Decide what a model may order, what it may mirror, and what a human must originate, then let a gate hold that line on every pull request. If you want to try the drafting pass without a separate inference budget, MonkeyCode's free model access and free server option are the route this account uses — but keep the quarantine gate in your own repository either way, because the gate is what makes the drafting pass safe to repeat.

來源:DEV Community · dev.to