世界從未停止變化。RSS
BIG CHANGE.
BIG CHANGE,輕觸即達
BIG CHANGE,輕觸即達

拉開視野。

將 BIG CHANGE 加入主畫面或桌面。它會在自己的視窗中開啟,並在您上線時顯示最新報導。

iPhone 與 iPad
在 Safari 中開啟 bigchange.ai。點一下「分享」,再點一下「加入主畫面」。如果顯示「以網頁 App 開啟」,請將其開啟,再點一下「加入」。
Android
在 Chrome 中開啟 bigchange.ai。開啟瀏覽器選單,選擇「安裝應用程式」或「加入主畫面」。
桌面裝置
在 Chrome 或 Edge 中,使用網址列中的安裝圖示或瀏覽器的應用程式選單。在 Mac 上的 Safari 中,選擇「檔案」→「加入 Dock」。

Markdown 版本

AI-translated from English; not yet reviewed by a fluent editor.

# 將代理輸出轉成經審核的行動登錄表

> 用自己的筆記建立具型別的 OpenAI Agents SDK 結果,審核來源證據與含義,再把接受的結果交給本機 Markdown 行動登錄表,並回讀核對雜湊。

By BIG CHANGE Editorial

Published: 2026-10-20T12:52:38.610Z
Updated: 2026-10-20T12:52:38.610Z
Canonical: https://bigchange.ai/blog/agents-sdk-reviewed-action-register-guide

![Conceptual charcoal illustration of one person holding a larger blank ruled source page and a smaller blank candidate page, with an orange reading strip across the source.](https://bigchange.ai/api/media/file/source-candidate-human-review-hero-v2.png)
AI-generated conceptual illustration by BIG CHANGE.

代理結果即使 JSON 結構正確,也可能誤讀來源。本指南為 Python 開發者提供從一份獲准筆記檔到本機行動登錄表的完整途徑:具型別的產生端結果、來源與語意檢查、綁定精確雜湊的人工核准,以及寫入並回讀分組 Markdown 報告的消費端。請使用自己含有明確行動承諾的筆記。準備本指南時未使用真實筆記或模型結果。

## 大變化

- **改變了什麼:**OpenAI Agents SDK 接受 Pydantic 的 `output_type`,因此應用程式可要求代理提供定義好的結果,並在使用前再次檢查。本例替結果加入來源引用及有版本的契約。
- **為何重要:**將代理接到下一個工作步驟的開發者,可以在輸出成為行動清單前阻止格式錯誤、不完整或沒有依據的內容。審核者仍須判斷引用筆記是否真的支持每項任務、負責人和日期,以及模型是否漏掉內容。
- **留意事項:**決策點在消費端邊界。本做法僅在核准精確執行、來源及候選後產生本機分流登錄表。日後匯入任務系統,仍需自身的權限、重試政策及目的端回讀。

## 準備一份獲准來源

你需要 Python 3.12、獲准處理這些筆記的 OpenAI API 帳號、能閱讀原始檔的審核者,以及已確認目前在帳號內支援 JSON Schema 結構化輸出的模型。執行產生端前先檢查資料條款及目前價格。不要將憑證放在筆記或核准檔。以下唯一模型呼叫是 `produce`;其餘均在本機處理。

將原始筆記以 UTF-8 儲存至 `notes.txt`。筆記必須包含自己工作中的真實明確行動承諾。指定穩定的 `source_id`,例如自己的紀錄識別碼,並在審核與消費前保持檔案每個位元組不變。產生前建立 `candidate/`;候選檔必須是新檔。為之後的本機報告備妥獨立輸出目錄。

執行前記錄來源位元組雜湊:

```sh
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"
```

在隔離的 Python 3.12 環境中建立 `recipe-requirements.txt`,放入以下精確內容並安裝:

```text
openai-agents==0.24.0
pydantic==2.14.1
```

```sh
python -m pip install -r recipe-requirements.txt
mkdir candidate
```

這些版本於 2026 年 10 月 20 日在本機 Python 3.12.7 環境一起安裝成功。實際執行時確認相容性與模型可用性。將下方完整指令碼存成 `action_register_recipe.py`,放在同一目錄。它有兩個命令:`produce`產生候選及執行收據;`consume`只在驗證和核准後建立報告。

## 執行具型別的產生端

這個 `Extraction` 契約要求版本 `action-register/v1`,狀態為 `ready`、`partial`或`blocked`,以及來源 ID、精確檔案位元組的 SHA-256、行動與 `gap_note`。每項行動需要文字、精確引用及從 1 起算的來源行範圍、允許為 null 的負責人和日期,以及 `complete`或`needs_review`證據狀態。負責人或日期為 null 表示筆記未說明;模型被要求不得推斷。所有欄位都必填,額外欄位會遭拒。

明確選擇結構化輸出模型。提供自己獲准的路徑與識別碼:

```sh
python action_register_recipe.py produce --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --model YOUR-STRUCTURED-OUTPUT-MODEL
```

閱讀 `candidate/actions.json`及旁邊的 `candidate/actions.run.json`。後者記錄 UUID 執行 ID、來源與候選雜湊、最後完成的代理及請求模型。失敗時改為建立附執行 ID 後綴的 `.failure.json`診斷紀錄。中斷、`None`或純字串最終輸出,或由非預期代理完成,都不算成功。此命令不保留 SDK 可恢復的 `RunState`;其失敗收據不能恢復執行。決定是否重新執行前,檢查請求、候選及任何下游狀態。

## 審核含義並核准精確候選

應用程式檢查嚴格型別與必填欄位、來源身分、就緒狀態、至少一項完整行動、有效行範圍、引用出現在指定範圍、非空任務或負責人,以及標準 `YYYY-MM-DD`日期。這證明的是結構與引用位置,無法確立句子真的是承諾、姓名或日期符合候選的解讀,或所有承諾都已找到。

閱讀原始筆記及每項建議行動,比對任務含義、負責人、日期、引用與遺漏。透過新的經審核執行釐清來源或候選的歧義;不要核准無依據、部分完成或受阻的候選。確認未變動的檔案和候選符合 `actions.run.json`後,撰寫 `approval.json`,只含以下七個鍵。複製已查核收據的執行 ID 和雜湊。使用審核者的真實識別碼,充分說明實際檢查過的內容:

```json
{
  "approved": true,
  "run_id": "COPY-RUN-ID",
  "source_id": "OWNED-ID",
  "source_sha256": "COPY-SOURCE-SHA256",
  "payload_sha256": "COPY-PAYLOAD-SHA256",
  "reviewer": "YOUR-REVIEWER-ID",
  "review_note": "Describe the source, task, owner, date, quotation and omission checks actually performed."
}
```

`approved`必須是 JSON 布林值 `true`,不能是字串或數字。其他值都須為字串;審核者與說明不得空白。多餘欄位會失敗。核准綁定一個候選和來源修訂,不涵蓋未來改寫。

## 消費並回讀報告

```sh
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --approval approval.json --out private-output
```

消費端將接受的行動分組為 **Assigned with dates**、**Needs an owner**和**Needs a date**。每筆含任務、負責人與日期狀態、引用及來源行。它寫入 `action-register-<payload-hash-prefix>.md`以及旁邊的 `.receipt.json`,記錄執行、來源、候選與報告雜湊。打開兩個檔案,將報告引用的行動與筆記比對,並將 `report_sha256`對照 Markdown 檔案位元組。有效收據只說明本機產物已建立並回讀,不批准寫入外部任務系統。

消費命令會印出實際報告與收據路徑。用這些路徑進行位元組檢查:

```sh
python -c "from pathlib import Path; import hashlib,json,sys; report=Path(sys.argv[1]); receipt=json.loads(Path(sys.argv[2]).read_text(encoding='utf-8')); assert hashlib.sha256(report.read_bytes()).hexdigest()==receipt['report_sha256']; print('report hash matches receipt')" PATH-PRINTED-FOR-REPORT PATH-PRINTED-FOR-RECEIPT
```

取得候選後,以清楚標明為合成的結構樣本檢查拒絕路徑。把 JSON 複製到另一檔案,移除第一項行動的一個必填欄位,再呼叫 `consume`,使用這個格式錯誤的檔案及新的空輸出目錄。必須在建立產物前失敗。不要視樣本為事實擷取或核准它。例如以下本機檢查從副本移除 `evidence_quote`:

```sh
python -c "import json; from pathlib import Path; p=json.loads(Path('candidate/actions.json').read_text(encoding='utf-8')); p['actions'][0].pop('evidence_quote'); Path('candidate/SYNTHETIC-malformed.json').write_text(json.dumps(p), encoding='utf-8')"
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/SYNTHETIC-malformed.json --approval approval.json --out synthetic-rejection-output
```

第二個命令應失敗,輸出目錄仍不存在。如果真實候選沒有第一項行動,不要使用樣本;它對此消費端已是不完整的。消費寫入不確定時,先看確定性報告與收據再重試。現有相同位元組可重用;不同位元組會停止操作等待核對。來源或綱要版本變更需要重新審核。不要只因本機消費端回應不明就重播模型呼叫。

## 完整 Python 做法

將這份指令碼原樣複製到 `action_register_recipe.py`:

```python
"""Private, bounded recipe. No model call occurs unless `produce` is explicitly run."""

import argparse
import hashlib
import json
import uuid
from datetime import date
from pathlib import Path
from typing import Literal

from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr, ValidationError


class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)


class Action(StrictModel):
    action_text: StrictStr
    evidence_quote: StrictStr
    line_start: StrictInt
    line_end: StrictInt
    owner: StrictStr | None
    due_date: StrictStr | None
    evidence_status: Literal["complete", "needs_review"]


class Extraction(StrictModel):
    schema_version: Literal["action-register/v1"]
    status: Literal["ready", "partial", "blocked"]
    source_id: StrictStr
    source_sha256: StrictStr
    actions: list[Action]
    gap_note: StrictStr | None


def digest(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def canonical(payload: Extraction) -> bytes:
    return json.dumps(payload.model_dump(mode="json"), sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")


def check(payload: Extraction, source_id: str, source: bytes) -> None:
    if payload.source_id != source_id or payload.source_sha256 != digest(source):
        raise ValueError("source identity or revision mismatch")
    lines = source.decode("utf-8").splitlines()
    if payload.status == "ready" and (not payload.actions or payload.gap_note is not None):
        raise ValueError("ready requires actions and no gap")
    if payload.status != "ready":
        raise ValueError("partial or blocked result cannot reach consumer")
    for index, item in enumerate(payload.actions, 1):
        if item.evidence_status != "complete":
            raise ValueError(f"action {index}: evidence needs review")
        if not item.action_text.strip() or not item.evidence_quote.strip():
            raise ValueError(f"action {index}: empty task or evidence")
        if not 1 <= item.line_start <= item.line_end <= len(lines):
            raise ValueError(f"action {index}: invalid source line range")
        segment = "\n".join(lines[item.line_start - 1:item.line_end])
        if item.evidence_quote not in segment:
            raise ValueError(f"action {index}: quotation absent from source span")
        if item.owner is not None and not item.owner.strip():
            raise ValueError(f"action {index}: blank owner")
        if item.due_date is not None:
            try:
                if date.fromisoformat(item.due_date).isoformat() != item.due_date:
                    raise ValueError()
            except ValueError as exc:
                raise ValueError(f"action {index}: due date must be YYYY-MM-DD") from exc


def report(payload: Extraction) -> str:
    """Application consumer: a triage register grouped by missing ownership/date."""
    groups = [
        ("Assigned with dates", lambda x: x.owner is not None and x.due_date is not None),
        ("Needs an owner", lambda x: x.owner is None),
        ("Needs a date", lambda x: x.owner is not None and x.due_date is None),
    ]
    rows = ["# Action register for " + payload.source_id, "", "Review artifact; source meaning was approved separately.", ""]
    for heading, predicate in groups:
        rows += ["## " + heading, ""]
        matches = [(n, a) for n, a in enumerate(payload.actions, 1) if predicate(a)]
        for number, action in matches:
            rows += [f"{number}. {action.action_text}", f"   Owner: {action.owner or 'Unspecified'}; due: {action.due_date or 'Unspecified'}", f"   Source lines {action.line_start}-{action.line_end}: {action.evidence_quote}", ""]
        if not matches:
            rows += ["None.", ""]
    return "\n".join(rows)


def load_payload(path: Path) -> Extraction:
    return Extraction.model_validate_json(path.read_bytes(), strict=True)


def consume(source_id: str, source_path: Path, payload_path: Path, approval_path: Path, out_dir: Path) -> tuple[Path, Path]:
    source = source_path.read_bytes()
    payload = load_payload(payload_path)
    check(payload, source_id, source)
    run = json.loads(payload_path.with_suffix(".run.json").read_text(encoding="utf-8"))
    payload_hash = digest(canonical(payload))
    if run.get("source_id") != source_id or run.get("source_sha256") != digest(source) or run.get("payload_sha256") != payload_hash or run.get("last_agent") != "Owned-source action extractor" or not str(run.get("run_id", "")).strip():
        raise ValueError("producer receipt does not match accepted candidate")
    approval = json.loads(approval_path.read_text(encoding="utf-8"))
    if (type(approval) is not dict
            or set(approval) != {"approved", "run_id", "source_id", "source_sha256", "payload_sha256", "reviewer", "review_note"}
            or approval["approved"] is not True
            or any(type(approval[key]) is not str for key in ("run_id", "source_id", "source_sha256", "payload_sha256", "reviewer", "review_note"))
            or not approval["reviewer"].strip() or not approval["review_note"].strip()
            or approval["run_id"] != run["run_id"]
            or approval["source_id"] != source_id
            or approval["source_sha256"] != digest(source)
            or approval["payload_sha256"] != payload_hash):
        raise ValueError("human source/meaning approval must bind exact source and payload hashes")
    body = report(payload).encode("utf-8")
    stem = "action-register-" + payload_hash[:16]
    out_dir.mkdir(parents=True, exist_ok=True)
    target = out_dir / (stem + ".md")
    receipt = out_dir / (stem + ".receipt.json")
    record = {"run_id": run["run_id"], "source_id": source_id, "source_sha256": digest(source), "payload_sha256": payload_hash, "report_sha256": digest(body), "reviewer": approval["reviewer"], "operation": "local action-triage register"}
    encoded = (json.dumps(record, indent=2) + "\n").encode("utf-8")
    for path, data in [(target, body), (receipt, encoded)]:
        if path.exists():
            if path.read_bytes() != data:
                raise ValueError(f"uncertain prior output differs: {path}; inspect before retry")
        else:
            with path.open("xb") as handle:
                handle.write(data)
        if path.read_bytes() != data:
            raise IOError(f"readback failed: {path}")
    return target, receipt


def produce(source_id: str, source_path: Path, candidate_path: Path, model: str) -> None:
    from agents import Agent, Runner

    source = source_path.read_bytes()
    source_text = source.decode("utf-8")
    if not source_text.strip() or candidate_path.exists() or not candidate_path.parent.is_dir():
        raise ValueError("source must contain text; candidate must be new and its parent directory must already exist")
    run_id = str(uuid.uuid4())
    agent = Agent(
        name="Owned-source action extractor",
        model=model,
        instructions=("Extract only explicit action commitments from the supplied numbered source. "
                      "Return action-register/v1. Copy an exact quote with matching 1-based line span. "
                      "Use null for an unstated owner or due date; never infer them. "
                      "Use partial and needs_review for ambiguous evidence; blocked for unusable input. "
                      "Do not use tools or hand off."),
        output_type=Extraction,
    )
    numbered = "\n".join(f"{n}: {line}" for n, line in enumerate(source_text.splitlines(), 1))
    try:
        result = Runner.run_sync(agent, f"Source ID: {source_id}\nSHA-256: {digest(source)}\nNumbered source:\n{numbered}")
        if result.interruptions or result.last_agent is not agent or not isinstance(result.final_output, Extraction):
            raise ValueError("run interrupted, unfinished, or finished by unexpected agent; this CLI does not persist resumable RunState, so do not resume from its failure receipt")
        output = result.final_output
        if output.source_id != source_id or output.source_sha256 != digest(source):
            raise ValueError("producer returned wrong source identity")
        candidate = canonical(output)
        with candidate_path.open("xb") as handle:
            handle.write(candidate)  # Candidate only; consumer still requires review.
        receipt = {"run_id": run_id, "source_id": source_id, "source_sha256": digest(source), "payload_sha256": digest(candidate), "last_agent": result.last_agent.name, "model": model}
        with candidate_path.with_suffix(".run.json").open("x", encoding="utf-8") as handle:
            handle.write(json.dumps(receipt, indent=2) + "\n")
    except Exception as exc:
        failure = {"run_id": run_id, "source_id": source_id, "source_sha256": digest(source), "error_type": type(exc).__name__, "error": str(exc), "state": "unknown_or_blocked; inspect run and destination before retry"}
        with candidate_path.with_name(candidate_path.stem + "." + run_id + ".failure.json").open("x", encoding="utf-8") as handle:
            handle.write(json.dumps(failure, indent=2) + "\n")
        raise


def main() -> None:
    parser = argparse.ArgumentParser()
    sub = parser.add_subparsers(dest="command", required=True)
    for name in ("produce", "consume"):
        p = sub.add_parser(name)
        p.add_argument("--source-id", required=True)
        p.add_argument("--source", type=Path, required=True)
        if name == "produce":
            p.add_argument("--candidate", type=Path, required=True)
            p.add_argument("--model", required=True)
        else:
            p.add_argument("--candidate", type=Path, required=True)
            p.add_argument("--approval", type=Path, required=True)
            p.add_argument("--out", type=Path, required=True)
    args = parser.parse_args()
    if args.command == "produce":
        produce(args.source_id, args.source, args.candidate, args.model)
    else:
        print(*consume(args.source_id, args.source, args.candidate, args.approval, args.out), sep="\n")


if __name__ == "__main__":
    main()
```

## 限制與來源

指令碼契約和本機消費端經過五項離線測試及七個模擬產生端或重試的合成案例檢查。涵蓋嚴格綱要產生、建立前拒絕、分組輸出、雜湊及失敗處理。沒有向供應商提交綱要、呼叫模型、擷取真實筆記、取得事實性的人工核准、建立真實來源報告或測量帳單與品質。API 費用依帳號而異;傳送來源前確認目前模型相容性、價格及資料許可。

相關背景可看 BIG CHANGE 的[從自己工作中選擇 AI 任務的指南](https://bigchange.ai/blog/map-work-choose-first-ai-task-guide),討論更早的選擇工作測試決策。本指南從已選且獲准的擷取任務開始,將一個具型別結果交給經查核的本機消費端。

### 來源與延伸閱讀

- [OpenAI Agents SDK: Agents](https://openai.github.io/openai-agents-python/agents/)說明 Pydantic 的 `output_type`,不提供本做法的人工核准或本機消費端。
- [OpenAI Agents SDK: Results](https://openai.github.io/openai-agents-python/results/)說明 `final_output`、`last_agent`、中斷與 `to_state()`。此命令列程式不儲存可恢復狀態。
- [OpenAI Agents SDK: Quickstart](https://openai.github.io/openai-agents-python/quickstart/)介紹安裝與 API 金鑰設定。金鑰應放在來源與收據以外。
- [OpenAI Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs)解釋嚴格綱要規則與模型支援。請確認所選模型的支援。
- [Pydantic ConfigDict](https://docs.pydantic.dev/latest/api/config/)說明嚴格驗證與禁止額外欄位。
- [OpenAI Agents SDK 0.24.0](https://pypi.org/project/openai-agents/0.24.0/)與[Pydantic 2.14.1](https://pypi.org/project/pydantic/2.14.1/)標示本機安裝的固定版本。未測試供應商端是否接受。

## Sources

- [OpenAI Agents SDK: Agents](https://openai.github.io/openai-agents-python/agents/) — Pydantic output_type 官方手冊。不實作本文的人工核准或本機消費端。
- [OpenAI Agents SDK: Results](https://openai.github.io/openai-agents-python/results/) — final_output、last_agent、中斷及 to_state 的官方行為。本做法不持久化 RunState,也不提供恢復。
- [OpenAI Agents SDK: Quickstart](https://openai.github.io/openai-agents-python/quickstart/) — 官方安裝與 API 金鑰設定背景;準備時未用金鑰或真實供應商呼叫。
- [OpenAI Agents SDK:輸出綱要參考](https://openai.github.io/openai-agents-python/ref/agent_output/) — 官方嚴格輸出綱要產生參考;本機檢查不代表供應商接受。
- [OpenAI Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) — 嚴格必填欄位、additionalProperties 規則與模型相容性;讀者須於使用時檢查所選模型。
- [Pydantic ConfigDict](https://docs.pydantic.dev/latest/api/config/) — 說明 strict 與 extra-forbid 設定;語意正確性仍需人工來源審核。
- [openai-agents 0.24.0 版本](https://pypi.org/project/openai-agents/0.24.0/) — 在隔離 Python 3.12.7 離線檢查中安裝的精確 SDK 固定版本;無模型呼叫或帳單。
- [Pydantic 2.14.1 版本](https://pypi.org/project/pydantic/2.14.1/) — 與 SDK 0.24.0 一起在本機安裝的精確驗證程式庫版本。
- [BIG CHANGE:盤點自己的工作,選擇首個待測試的 AI 任務](https://bigchange.ai/blog/map-work-choose-first-ai-task-guide) — 本整合做法之前選擇任務的相關指南;它不證明本做法正確。