先月、私は個人開発で運用している自動化パイプラインに、設定ファイルを書き換えてから本番へ反映するアクションを任せていました。週明けにある反映の中身を見返したとき、「なぜこの値にしたのか」が自分でも再構成できなくなっていました。ログには「変更した」という事実だけが残り、「どの選択肢を比べて、何を理由に、何を却下したのか」がどこにも無かったのです。
エラーは出ていません。アクションは成功しています。それでも、後から自分が説明できない自動判断が積み上がっていく状態は、静かに危ういものだと感じました。今回は、その「なぜ」を後から追えるようにするための、判断根拠レコードという小さな仕組みの設計と実装を共有します。
規制業界が AI に求める「説明できること」を、個人開発に小さく持ち込む
ちょうど同じ頃、TCS と Anthropic が提携して銀行や航空といった規制業界へ Claude を導入する、というニュースが流れていました。DXC も既存システムへの統合を進めているとのことです。こうした領域で AI を本番に乗せるとき、必ず問われるのが「その判断を、後から人間が説明できるか」という点です。監査や事故調査の場面で、結果だけでなく根拠が残っていなければ、自動化はそもそも許可されません。
私自身が Dolice Labs で運用しているのは、規制とは無縁の小さなパイプラインです。それでも、この「説明できること」という要求は、個人開発の自律運用にこそ小さく持ち込む価値があると考えています。なぜなら、ひとりで運用していると、判断を後から検証してくれる第三者がいないからです。半年前の自分は完全な他人で、その他人が下した自動判断を引き継いで運用していくことになります。
ここで持ち込むのは大掛かりな監査基盤ではありません。「後戻りしづらいアクションを取る前に、根拠を構造化して1行残す」という、それだけの規律です。
判断根拠レコードのデータモデル — 採用案・却下案・前提・可逆性
最初に決めるべきは、何を残すかです。「変更した」という事実は既存のログに残っています。足りないのは、その手前にあった思考の構造でした。私は次の5つを最小単位と決めました。
chosen : 実際に取ったアクション(要約)
considered : 比較検討した選択肢の一覧
rejected : 却下した案と、その理由
assumptions : 判断の前提(後で崩れたら見直すべきもの)
reversibility : そのアクションが後戻りできるか(reversible / hard_to_reverse / irreversible)
最後の reversibility を入れたのは、根拠の濃さを後から評価するためです。後戻りできないアクションほど、却下案や前提が薄いと危険だと判断できます。
TypeScript で表すなら、レコードの形はこうなります。
interface DecisionRecord {
action_id : string ; // 紐づくアクションの識別子
chosen : string ; // 取った行動の要約
considered : string []; // 比較した選択肢
rejected : { option : string ; reason : string }[];
assumptions : string []; // 前提(崩れたら再検討)
reversibility : "reversible" | "hard_to_reverse" | "irreversible" ;
confidence : number ; // 0.0–1.0
}
structured output でモデル自身に根拠を書かせる実装
根拠を人間が後から書くのでは続きません。アクションを決めたモデル自身に、同じ呼び出しの中で構造化して書かせるのが現実的です。Anthropic SDK のツール定義を使い、根拠レコードのスキーマをそのまま入力スキーマにして「必ずこの形で返す」状態を作ります。
次のコードは、エージェントにアクションと根拠を同時に出させ、JSON として取り出すところまでを示します。
import json
from anthropic import Anthropic
client = Anthropic()
# 根拠レコードのスキーマをそのままツール入力にする
DECISION_TOOL = {
"name" : "record_decision" ,
"description" : "後戻りしづらいアクションを取る前に、判断根拠を構造化して記録する" ,
"input_schema" : {
"type" : "object" ,
"properties" : {
"chosen" : { "type" : "string" },
"considered" : { "type" : "array" , "items" : { "type" : "string" }},
"rejected" : {
"type" : "array" ,
"items" : {
"type" : "object" ,
"properties" : {
"option" : { "type" : "string" },
"reason" : { "type" : "string" },
},
"required" : [ "option" , "reason" ],
},
},
"assumptions" : { "type" : "array" , "items" : { "type" : "string" }},
"reversibility" : {
"type" : "string" ,
"enum" : [ "reversible" , "hard_to_reverse" , "irreversible" ],
},
"confidence" : { "type" : "number" },
},
"required" : [ "chosen" , "considered" , "rejected" ,
"assumptions" , "reversibility" , "confidence" ],
},
}
def decide_with_rationale (task: str ) -> dict :
"""アクション判断と根拠レコードを同時に取得する。"""
resp = client.messages.create(
model = "claude-opus-4-8" ,
max_tokens = 1024 ,
tools = [ DECISION_TOOL ],
tool_choice = { "type" : "tool" , "name" : "record_decision" },
messages = [{
"role" : "user" ,
"content" : (
"次のタスクについて、取るべきアクションと、その判断根拠を "
"record_decision の形式で出してください。"
"却下した選択肢には必ず理由を添えてください。 \n\n "
f "タスク: { task } "
),
}],
)
for block in resp.content:
if block.type == "tool_use" and block.name == "record_decision" :
return block.input
raise RuntimeError ( "根拠レコードが返りませんでした" )
record = decide_with_rationale( "本番の設定ファイルの retry 上限を 3 から 5 に上げる" )
print (json.dumps(record, ensure_ascii = False , indent = 2 ))
# => chosen / considered / rejected / assumptions / reversibility / confidence が揃った dict
tool_choice でツール使用を強制している点が要です。これを省くと、モデルは「根拠は明らかなので省略します」と判断して通常のテキストを返してくることがあります。強制しておけば、根拠レコードが返らないこと自体が異常検知になります。
根拠レコードを追記専用の台帳に残す — content hash と commit を添える
レコードが手に入ったら、後から改変されない形で残します。私は追記専用(append-only)の JSONL を使っています。1アクション1行で、行を上書きも削除もしません。ここに content hash と、その時点の git commit を添えるのが肝心です。
import hashlib
import json
import subprocess
import time
from pathlib import Path
LEDGER = Path( "decision_ledger.jsonl" )
def _git_commit () -> str :
try :
out = subprocess.run(
[ "git" , "rev-parse" , "--short" , "HEAD" ],
capture_output = True , text = True , check = True ,
)
return out.stdout.strip()
except Exception :
return "unknown"
def append_record (action_id: str , record: dict , served_model: str ) -> str :
"""根拠レコードを追記専用台帳に残し、行の content hash を返す。"""
entry = {
"ts" : time.strftime( "%Y-%m- %d T%H:%M:%S%z" ),
"action_id" : action_id,
"served_model" : served_model, # 実際に応答したモデル
"commit" : _git_commit(),
"record" : record,
}
line = json.dumps(entry, ensure_ascii = False , sort_keys = True )
digest = hashlib.sha256(line.encode( "utf-8" )).hexdigest()[: 16 ]
with LEDGER .open( "a" , encoding = "utf-8" ) as f:
f.write(line + " \n " )
return digest
served_model を入れているのは、要求したモデルと実際に応答したモデルがずれることがあるからです。フォールバックが効いた結果、別のモデルが下した判断を、後から「最新モデルが決めた」と誤解しないためです。応答モデルの記録については応答したモデルを記録する設計 で詳しく扱っています。台帳そのものを監査・再現の軸で残す話はClaude API のレスポンスをアーカイブする設計 が近い設計です。
どのアクションに根拠を要求するか — 影響度で線引きする
最初、私はすべてのアクションに根拠を求めていました。これは失敗でした。読み取りやログ出力のような無害なアクションにまで根拠を書かせると、出力トークンが体感で15%以上増え、しかも台帳が「特に理由なし」のノイズで埋まって、肝心の判断が埋もれてしまったのです。
そこで、アクションを影響度で3段階に分け、根拠を要求する範囲を絞りました。
read-only(根拠不要) : 取得・検索・閲覧。後戻りの概念がないもの
reversible-write(confidence のみ) : 後で戻せる書き込み。下書き保存、再生成できるキャッシュ更新など
hard-to-reverse 以上(フル根拠を強制) : 本番デプロイ、ファイルの一括削除、外部 API への書き込み、課金に関わる操作
判定はアクション名のマッピングで十分でした。モデルに自己申告させると、急いでいるときほど影響度を低く見積もる傾向があったため、影響度はコード側で決め打ちにしています。
ACTION_TIERS = {
"fetch" : "read_only" ,
"search" : "read_only" ,
"save_draft" : "reversible_write" ,
"deploy_production" : "hard_to_reverse" ,
"bulk_delete" : "irreversible" ,
"external_write" : "hard_to_reverse" ,
}
def requires_full_rationale (action_name: str ) -> bool :
tier = ACTION_TIERS .get(action_name, "hard_to_reverse" ) # 未知は厳しい側へ
return tier in ( "hard_to_reverse" , "irreversible" )
未知のアクション名を hard_to_reverse に倒している点だけは譲りませんでした。分類漏れがあったとき、根拠を取りすぎる方向に倒れるのは無駄で済みますが、逆だと後戻りできない操作が記録なしで通ってしまいます。
運用して気づいた3つの落とし穴
落とし穴1: 後付けの正当化(rationale theater)
最も警戒すべきはこれでした。モデルにアクションと根拠を同時に出させると、根拠が「すでに決めた結論を正当化するための文章」になりがちです。却下案の欄に、現実には検討していない当たり障りのない選択肢が並ぶ、という形で表れます。
対処として、却下案には「却下した具体的な理由」を必須にし、理由が30文字に満たないレコードは品質ゲートで弾くようにしました。「コストが高いため」のような一般論ではなく、「この値だと retry が直列で走り、tail latency が2倍近くになるため」のように、その状況に固有の理由が書かれているかを目安にしています。
落とし穴2: 根拠が判断を遅らせる
フル根拠を要求すると、当然レイテンシと出力トークンが増えます。これを read-only と reversible-write に広げてしまうと、運用全体が重くなります。前述の影響度による線引きは、品質のためというより、この重さを必要な場面だけに集中させるためのものです。
落とし穴3: 台帳を誰も読み返さない
残すだけで読み返さなければ意味がありません。私は週に一度、reversibility が hard_to_reverse 以上で、かつ confidence が 0.7 未満のレコードだけを抽出して目を通すようにしています。全件は読めませんが、「自信のない後戻りしづらい判断」だけなら数件で、ここに問題が潜んでいることがほとんどでした。
# 自信のない後戻りしづらい判断だけを週次で抽出
jq -c 'select(.record.reversibility != "reversible"
and .record.confidence < 0.7)' decision_ledger.jsonl
Fable 5 の突然の停止が教えてくれたこと
この設計をしている最中に、公開されたばかりの Claude Fable 5 と Mythos 5 が、米政府の輸出管理指令を受けて停止されました。スケジュールではなく、ある日突然です。最新モデルへの依存は、性能の問題ではなく可用性のリスクでもあると、改めて突きつけられました。
この出来事が判断根拠レコードと結びついたのは、「フォールバックという判断にも根拠が要る」と気づいたからです。あるモデルが使えなくなって別のモデルに切り替えるとき、単に切り替えるだけでなく、「どの能力(例えば長いコンテキスト)を諦めたのか」「その結果どのアクションを保留すべきか」を根拠として残しておけば、復旧後に何を戻すべきかが明確になります。可用性の判断こそ、後から説明できる形で残す価値が高いと考えています。エージェントの停止や切り替えそのものの設計は、承認ゲートを差し込むHuman-in-the-loop の本番運用設計 と組み合わせると、より堅牢になります。
次の一歩
まずは、いま自律的に動かしているパイプラインのアクションを書き出して、hard_to_reverse 以上に該当するものが何件あるかを数えてみてください。多くの場合、それは思ったより少数です。その数件にだけ、上記の record_decision ツールを差し込むところから始めれば、運用を重くせずに「後から説明できる自動判断」へ一歩進めます。
同じように、ひとりで自律運用を続けている方の参考になれば嬉しいです。