「OpenAI、初のハードウェア『Codex Micro』を230ドルで発売」
7月15日のこのニュースを見て、複数の作業を並行で見張る道具が前に出てきたなと思いました。これが意味するのは、AIに仕事を一件ずつ頼む時間が終わり、同時に走る仕事の「完了」をどう扱うかが現場の詰まりどころになる、ということです。
僕は、完了報告に「できました」とだけ書かせる運用には反対です。成果物の場所も検証結果もない一行は、件数が増えるほど確認作業を後ろへ送るだけです。今日、下のスクリプトをサンプルJSONとCSVで動かしたところ、ファイルの有無だけでなく、JSONの必須キーとCSVの列・行数まで分けて落とせました。ここ地味に効きます。
完了の判断を、報告文から成果物の検収へ移します。
先に「何があれば終わりか」をJSONにする
たとえば営業資料の要約なら、要約ファイルがあるだけでは足りません。次のアクションが入っているか、担当者の一覧がCSVなら列名が揃っているかまで決めます。AIへの依頼文とは別に、検収用の tasks.json を置きます。
[
{
"id": "sales-01",
"status": "done",
"artifact": "deliverables/summary.json",
"format": "json",
"required_keys": ["summary", "next_actions"]
},
{
"id": "sales-02",
"status": "done",
"artifact": "deliverables/list.csv",
"format": "csv",
"required_columns": ["company", "owner"],
"min_rows": 1
}
]
artifact を deliverables/ 配下に限定しているのも大事です。報告側が勝手に別の場所のファイルを指しても、検収対象にはしません。
Pythonで成果物を検収する
次を verify_deliverables.py として保存し、python3 verify_deliverables.py tasks.json で実行します。外部ライブラリは使いません。AIが書いた任意のコマンドは実行せず、ファイルの形だけを確認します。
import csv
import json
import sys
from pathlib import Path
REQUIRED = ("id", "status", "artifact", "format")
def under_deliverables(value, root):
path = Path(value).resolve()
try:
path.relative_to(root)
except ValueError:
raise ValueError("artifact は deliverables/ 配下に置く")
return path
def verify(task, root):
errors = [f"{key} がない" for key in REQUIRED if not task.get(key)]
if errors:
return errors
if task["status"] != "done":
return ["status が done ではない"]
try:
artifact = under_deliverables(task["artifact"], root)
except ValueError as err:
return [str(err)]
if not artifact.is_file():
return [f"成果物がない: {task['artifact']}"]
if artifact.stat().st_size < task.get("min_bytes", 20):
return [f"成果物が小さすぎる: {artifact.stat().st_size} bytes"]
if task["format"] == "json":
try:
data = json.loads(artifact.read_text(encoding="utf-8"))
except json.JSONDecodeError as err:
return [f"JSONとして読めない: {err.msg}"]
missing = [key for key in task.get("required_keys", []) if key not in data]
return [f"JSONの必須キーがない: {', '.join(missing)}"] if missing else []
if task["format"] == "csv":
with artifact.open(encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
rows = list(reader)
columns = set(reader.fieldnames or [])
missing = set(task.get("required_columns", [])) - columns
if missing:
return [f"CSVの必須列がない: {', '.join(sorted(missing))}"]
if len(rows) < task.get("min_rows", 1):
return [f"CSVの行数が足りない: {len(rows)} 行"]
return []
return [f"未対応の format: {task['format']}"]
def main():
tasks = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
root = Path("deliverables").resolve()
failed = False
for task in tasks:
reasons = verify(task, root)
label = task.get("id", "idなし")
if reasons:
failed = True
print(f"{label}: NG {' / '.join(reasons)}")
else:
print(f"{label}: OK")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
手元のサンプルでは、sales-01: OK、sales-02: OK。必須キーをわざと外した sales-03 は、NG JSONの必須キーがない: approved_by で止まりました。報告の文面を読むより、どこを直せばよいかが早く分かります。
完了率より、差し戻し理由を残す
この仕組みの狙いは、完了率をきれいに見せることではありません。成果物なし、形式不正、必要項目不足を別々に残すと、依頼の書き方が悪いのか、出力形式が曖昧なのかを次回に直せます。全部を人が読む運用より、確認する場所がずっと小さくなります。
最初はJSONとCSVだけで十分です。資料ならページ数、メール下書きなら宛先リストなど、業務に合わせて検収条件を一つ足します。AIを並行で使うほど、速さより先に「終わった」の定義を固める。僕はここから始めるのがいちばん実務的だと思っています。