2014年に個人でアプリ開発を始めてから、私が最もよく口にするようになったフレーズがあります。「どうしてこうなった」です。
最初の数年間はコードが原因でした。自分で書いたロジックが予期しない状態に陥る。デバッガでステップを追えば必ず理由が見つかる。プログラムは正直で、実行した通りにしか動きません。バグは人間の思い込みから生まれますが、原因は必ずコードの中にあります。
AIエージェントを使うようになってから、その「正直さ」の性質が変わりました。エージェントが下した判断の理由は、コードのどこにも直接書かれていません。数百行の内部状態と、会話の文脈と、ツールの呼び出し履歴の中に、「なぜか」が溶け込んでいます。
去年、あるタスクを処理するエージェントが、想定外のファイルを上書きしました。バックアップがあったので被害はゼロでしたが、「なぜ上書きしたのか」を突き止めるのに2時間かかりました。最終的に判明した原因は、プロンプトの中の「最新のファイルで置き換えてください」という一文でした。私はそれを特定のファイルへの指示として書いたつもりでしたが、エージェントはより広いスコープに適用したのです。
この経験から、私はエージェントの「説明可能性」を設計することを開発プロセスに組み込むようになりました。
今回は、Antigravityのエージェントにおける意思決定ログの実装と、それを品質改善サイクルに活用するための設計パターンをお伝えします。2014年の個人開発スタートから累計5,000万ダウンロードを超えたアプリ事業を一人で運用してきた経験から言えば、「なぜ動いているか」を理解できないシステムは、必ずどこかで想定外の動作をします。規模が大きくなるほど、その代償も大きくなります。
可観測性と説明可能性の違い — 何を設計するのか
まず、似ているようで異なる2つの概念を整理しておきたいと思います。
**可観測性(Observability)**は、システムが今どのような状態にあるかを外部から観察する能力です。レスポンスタイム、エラーレート、リソース使用量 — これらはSREやインフラの領域でよく語られます。Langfuse、OpenTelemetryのようなツールがここをカバーします。
**説明可能性(Explainability)**は、「なぜその出力が生まれたか」を人間が理解できる形で記述する能力です。エージェントが特定のツールを選んだ理由、中間的な推論のステップ、判断に影響した文脈の要素 — これらは標準的な監視ツールだけでは捉えられません。
具体的に言うと、可観測性パイプラインが教えてくれるのは「エージェントがwrite_fileを呼んだ」という事実です。説明可能性が教えてくれるのは「エージェントが複数のファイルを検討した結果、foo.tsを変更対象として選んだ理由」です。
この2つを混同してしまうと、ログを集めているのに問題の原因が分からないという状況に陥ります。エラーが起きた後に「ログを見ても何も分からなかった」という経験は、おそらく多くの開発者が持っているのではないでしょうか。
私が設計を始めたきっかけも、まさにその経験でした。OpenTelemetryでエージェントのトレースを記録していたのに、問題の根本原因が分かりませんでした。ツール呼び出しの順序は記録されていましたが、「なぜその順序になったか」が追えなかったのです。
一般的な可観測性パイプラインはエージェントの動作を記録できますが、「なぜそう判断したか」の構造化されたログは自分で設計する必要があります。Antigravityでは、この2つの層を分けて実装するのが実用的です。
ハーネスエンジニアリング入門: 安定的なAIエージェントの設計手法では安定性の確保について詳しく解説されていますが、ここでは「なぜ安定していないか」を追跡するための記録設計にフォーカスします。
判断ログの基本設計 — DecisionLogger の実装
最もシンプルな判断ログは、エージェントの各思考ステップを構造化して記録するものです。設計の核心は「アクション記録」と「理由記録」を分離することにあります。
大切にしている設計方針が3つあります。
1つ目はステップ単位での記録です。タスク全体ではなく、判断の1ステップごとにエントリを作ります。問題が起きた場合に「何ステップ目で何が起きたか」を特定できるようにするためです。
2つ目は信頼度の自動推定です。エージェント自身に「この判断は確かだ/不確かだ」と評価させるのではなく、ツール入力の「理由フィールドの充実度」から外部で推定します。エージェントの自己評価は楽観的になりやすいため、客観的な指標を設ける点が肝心です。
3つ目はコンテキスト要因の記録です。その判断に影響したと思われる文脈を記録します。後の分析で「このコンテキストがある場合に判断が乱れる」というパターンを発見できます。
import json
import time
from datetime import datetime
from typing import Any
class DecisionLogger:
"""エージェントの判断ステップを記録するロガー"""
def __init__(self, session_id: str):
self.session_id = session_id
self.entries = []
self.start_time = time.time()
def log_decision(
self,
step: int,
reasoning: str,
action: str,
tool_name: str | None = None,
tool_input: dict | None = None,
confidence: str = "medium", # high / medium / low
context_factors: list[str] | None = None
) -> None:
entry = {
"session_id": self.session_id,
"step": step,
"timestamp": datetime.utcnow().isoformat(),
"elapsed_sec": round(time.time() - self.start_time, 2),
"reasoning": reasoning,
"action": action,
"tool": {
"name": tool_name,
"input": tool_input
} if tool_name else None,
"confidence": confidence,
"context_factors": context_factors or []
}
self.entries.append(entry)
print(f"[Step {step}] {action} → confidence: {confidence}")
def export(self) -> str:
return json.dumps(self.entries, ensure_ascii=False, indent=2)
def summary(self) -> dict:
"""ログのサマリーを返す"""
tool_calls = [e for e in self.entries if e.get("tool")]
confidence_dist = {}
for e in self.entries:
c = e["confidence"]
confidence_dist[c] = confidence_dist.get(c, 0) + 1
return {
"total_steps": len(self.entries),
"tool_calls": len(tool_calls),
"tools_used": list({e["tool"]["name"] for e in tool_calls}),
"confidence_distribution": confidence_dist,
"elapsed_total_sec": round(time.time() - self.start_time, 2)
}
このロガーを組み込んだエージェントループの実装例です。注目してほしいのは、システムプロンプトで「判断理由の言語化」を義務付けている点と、その理由の充実度から信頼度を自動推定している点です。
from anthropic import Anthropic
client = Anthropic()
def run_agent_with_logging(task: str, session_id: str) -> dict:
"""判断ログ付きエージェントの実行"""
logger = DecisionLogger(session_id)
tools = [
{
"name": "read_file",
"description": "ファイルの内容を読み込む",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "ファイルパス"}
},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "ファイルに内容を書き込む。副作用があるため reason を必ず記入する",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
"reason": {
"type": "string",
"description": "このファイルを書き換える理由(50文字以上で具体的に記述)"
}
},
"required": ["path", "content", "reason"]
}
}
]
# 判断理由の言語化をシステムプロンプトで義務付ける
system = """あなたはファイル操作エージェントです。
重要なルール:
- ツールを呼び出す前に「なぜこのツールを使うか」を内部で言語化してください
- write_file を使う場合は reason に具体的な理由を50文字以上で記入してください
- 曖昧な指示は、最も保守的な(影響範囲の小さい)解釈を採用してください
- 不確実な場合は、read_file で確認してから判断してください"""
messages = [{"role": "user", "content": task}]
step = 0
while True:
step += 1
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=4096,
system=system,
tools=tools,
messages=messages
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# reason の充実度から信頼度を推定
reason_text = block.input.get("reason", "")
if len(reason_text) < 20:
confidence = "low"
elif len(reason_text) < 50:
confidence = "medium"
else:
confidence = "high"
logger.log_decision(
step=step,
reasoning=reason_text or "(理由未記録)",
action=f"{block.name} を呼び出す",
tool_name=block.name,
tool_input=block.input,
confidence=confidence
)
tool_result = execute_tool(block.name, block.input)
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(tool_result)
}]
})
else:
logger.log_decision(
step=step,
reasoning="全ステップ完了",
action="タスク終了",
confidence="high"
)
break
if step > 20:
break
return {
"result": response.content[-1].text if response.content else "",
"decision_log": logger.export(),
"summary": logger.summary()
}
ツール定義で透明性を高める — Before/After パターン
判断ログ設計で最も効果を発揮するのが、副作用を持つツールへの対処です。ツール定義を変えるだけで、エージェントの思考が記録に残るようになります。
Before — ツールに理由フィールドがない設計
# ❌ 悪い例: ツールに理由フィールドがない
tools_bad = [
{
"name": "write_file",
"description": "ファイルに内容を書き込む",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"}
},
"required": ["path", "content"]
}
}
]
# → エージェントはなぜそのファイルを選んだか外部から判断できない
# → デバッグ時に「何をしたか」はわかるが「なぜしたか」が追跡不能
# → 問題が起きたとき、プロンプトを修正すべきかツール定義を修正すべきか判断できない
After — reason と scope を必須化した設計
# ✅ 良い例: reason と scope フィールドを必須化し、意図の言語化を促す
tools_good = [
{
"name": "write_file",
"description": "ファイルに内容を書き込む。副作用があるため reason と scope を必ず記入する",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "変更するファイルのパス"},
"content": {"type": "string", "description": "新しい内容"},
"reason": {
"type": "string",
"description": "このファイルを変更する理由を50文字以上で説明する"
},
"scope": {
"type": "string",
"enum": ["specific_file", "pattern_match"],
"description": "変更範囲(specific_file: 指定ファイルのみ、pattern_match: 条件に合致する全ファイル)"
}
},
"required": ["path", "content", "reason", "scope"]
}
}
]
# ✅ ログから後で確認できること:
# - どのファイルを変更したか(path)
# - なぜそのファイルを選んだか(reason フィールド)
# - 意図した変更範囲(scope フィールド)
# - 理由の充実度から推定した信頼度スコア
冒頭でお話しした「想定外のファイル上書き」は、このscopeフィールドがあれば防げていた可能性が高いです。エージェントにscope="specific_file"を選ばせることで、「特定のファイルだけ変更する意図」を明示的に確認できます。また、後のデバッグでも「エージェントが pattern_match を選んでいた」という事実が記録に残ります。
このBefore/Afterの違いで重要なのは、エージェントの動作を制限しているわけではないという点です。制約ではなく、「考えたことを言葉にする場所を提供する」設計です。エージェントは引き続き自由に判断できますが、その判断の根拠が記録に残るようになります。
ReAct ベースの構造化思考ログ
単純なアクション記録を超えて、「何を考慮したか」を構造化する設計です。ReActパターン(Reasoning + Acting)を応用して、判断に影響した要素を列挙させます。この設計の目的は、「検討した選択肢」と「最終的に選んだ理由」を分離して記録することです。どの選択肢が検討されたかを知ることで、プロンプト改善の方向性が見えてきます。
from dataclasses import dataclass, field
import re
from typing import Optional
@dataclass
class ReasoningRecord:
"""ReActパターンの思考ステップを記録する構造体"""
step: int
thought: str # エージェントが考えたこと
observation: str # 環境から得た情報
action_chosen: str # 選択したアクション
alternatives_considered: list[str] = field(default_factory=list) # 検討した他の選択肢
decision_factors: list[str] = field(default_factory=list) # 決定に影響した要因
uncertainty_flags: list[str] = field(default_factory=list) # 不確実だった点
def to_dict(self) -> dict:
return {
"step": self.step,
"thought": self.thought,
"observation": self.observation,
"action_chosen": self.action_chosen,
"alternatives": self.alternatives_considered,
"factors": self.decision_factors,
"uncertainties": self.uncertainty_flags
}
def build_structured_logging_prompt() -> str:
"""構造化ログを促すシステムプロンプトを構築する"""
return """各ステップで以下の形式で思考してください:
<思考>
現在の状況: [観察した内容]
検討した選択肢: [A案, B案, C案]
選択した理由: [なぜこのアクションを選んだか]
不確実な点: [確信が持てない要素があれば記録]
</思考>
<行動>
[実際のツール呼び出しまたは回答]
</行動>
副作用のあるアクション(書き込み・削除・外部API呼び出し)の前には
必ず「不確実な点」を明示してください。
不確実な点がある場合は、最も保守的な選択をしてください。"""
def parse_reasoning_from_response(response_text: str, step: int) -> Optional[ReasoningRecord]:
"""レスポンスから構造化された思考ステップを抽出する"""
thought_match = re.search(r'<思考>(.*?)</思考>', response_text, re.DOTALL)
action_match = re.search(r'<行動>(.*?)</行動>', response_text, re.DOTALL)
if not thought_match:
return None
thought_text = thought_match.group(1).strip()
action_text = action_match.group(1).strip() if action_match else "(行動未記録)"
# 各要素を抽出
uncertainties = re.findall(r'不確実な点[::]\s*(.+)', thought_text)
alternatives = re.findall(r'検討した選択肢[::]\s*(.+)', thought_text)
factors = re.findall(r'選択した理由[::]\s*(.+)', thought_text)
observation = re.search(r'現在の状況[::]\s*(.+)', thought_text)
return ReasoningRecord(
step=step,
thought=thought_text,
observation=observation.group(1) if observation else "",
action_chosen=action_text[:100],
alternatives_considered=alternatives,
decision_factors=factors,
uncertainty_flags=uncertainties
)
このプロンプト設計で特に重要なのが「不確実な点」の明示です。エージェントに自分の判断の不確実性を言語化させることで、後から「このステップが問題だった」と特定しやすくなります。
実際にこの設計を導入した後、問題の切り分け速度が大きく変わりました。以前は「エージェントが間違えた」という事実からプロンプト全体を見直していましたが、今は「ステップ3の不確実フラグが高く、alternatives_consideredが1件しかなかった」という具体的な情報から、プロンプトのどの部分を修正すべきかを特定できます。
品質スコアの設計と改善サイクル
ログを蓄積した後の分析を自動化するためのスコアリング実装です。3つのメトリクスを組み合わせてquality_scoreを算出します。
① 低信頼度判断の割合(Low Confidence Rate)
confidenceが"low"になったステップの割合を追跡します。この数値が高いタスクは、プロンプトが曖昧か、ツールの説明が不十分な可能性があります。私は20%を超えたタスク定義を見直す対象としています。
② 不確実フラグの密度(Uncertainty Density)
不確実フラグが記録されたステップの密度です。特定のドメイン(ファイル操作、外部API)で高くなる傾向があれば、そのドメインの知識補完が必要です。
③ ツール選択の一貫性(Tool Selection Consistency)
同じタスクタイプで呼ばれるツールの順序がどれだけ一定かを測ります。バリエーションが多すぎるエージェントは、プロンプトの曖昧さを反映していることが多いです。
def analyze_decision_logs(log_entries: list[dict]) -> dict:
"""判断ログを分析してダッシュボード用メトリクスを算出する"""
total = len(log_entries)
if total == 0:
return {"error": "ログエントリが空です"}
# 信頼度分布
confidence_counts = {"high": 0, "medium": 0, "low": 0}
for entry in log_entries:
c = entry.get("confidence", "medium")
confidence_counts[c] = confidence_counts.get(c, 0) + 1
low_confidence_rate = confidence_counts["low"] / total
# ツール使用パターン
tool_sequence = [
e["tool"]["name"]
for e in log_entries
if e.get("tool")
]
# 不確実フラグ密度
uncertainty_steps = sum(
1 for e in log_entries
if e.get("uncertainty_flags") and len(e["uncertainty_flags"]) > 0
)
uncertainty_density = uncertainty_steps / total
# 品質スコア(0〜100)
quality_score = max(0, min(100, int(
100
- (low_confidence_rate * 40) # 低信頼度が多いと減点
- (uncertainty_density * 20) # 不確実フラグが多いと減点
)))
return {
"quality_score": quality_score,
"total_steps": total,
"confidence_distribution": confidence_counts,
"low_confidence_rate": f"{low_confidence_rate:.1%}",
"uncertainty_density": f"{uncertainty_density:.1%}",
"tool_sequence": tool_sequence,
"recommendations": generate_recommendations(
low_confidence_rate,
uncertainty_density
)
}
def generate_recommendations(
low_confidence_rate: float,
uncertainty_density: float
) -> list[str]:
"""メトリクスから改善推奨事項を生成する"""
recs = []
if low_confidence_rate > 0.20:
recs.append(
"低信頼度判断が20%を超えています。"
"タスク指示を具体化するか、ツールの説明を詳細化することを推奨します"
)
if uncertainty_density > 0.30:
recs.append(
"不確実フラグの密度が高いです。"
"エージェントに渡すコンテキスト情報を増やすか、"
"不確実なケースの判断基準をシステムプロンプトに追記することを推奨します"
)
if not recs:
recs.append("品質指標は良好です。このプロンプト設計を参考に他のタスクを構築してください")
return recs
この改善サイクルを始めてから6ヶ月後、私が運用しているエージェントのquality_scoreの平均値は62から81に上がりました。数値が全てではありませんが、「なんとなく動いている」から「設計に基づいて動いている」へ、感覚的な変化もありました。
マルチエージェント・オーケストレーション実装ガイドで複数エージェントを連携させている場合は、各エージェント固有のsession_idを付与したうえでログを統合すると、どのエージェントで問題が発生したかを素早く特定できます。
判断ログをどこに保存するか — JSONL と SQLite の二段構え
DecisionLogger を実装した後、最初に直面したのは「このログをどこに置くか」という地味な問題でした。
設計記事ではあまり語られない部分ですが、保存層の選択を誤ると、せっかく記録したログが分析されないまま溜まっていきます。私自身、最初の1ヶ月はタスクごとに JSON ファイルを書き出すだけの運用にしてしまい、「ファイルが400個を超えた時点で誰も開かなくなる」という当たり前の事実に直面しました。
現在は二段構えに落ち着いています。書き込みは日付単位の JSONL(1行1エントリ)、分析は SQLite です。
書き込み側を JSONL にしている理由は、エージェントの実行中にスキーマの都合で書き込みが失敗する事故を避けたいからです。ログ機構そのものがエージェントを止めてしまっては本末転倒です。追記オープンで1行書くだけなら、ほぼ失敗しません。
import json
import sqlite3
from datetime import datetime
from pathlib import Path
LOG_DIR = Path("logs/decisions")
def append_entry(entry: dict) -> None:
"""実行中はJSONLに追記するだけ。スキーマ検証は読み込み側で行う"""
LOG_DIR.mkdir(parents=True, exist_ok=True)
path = LOG_DIR / f"{datetime.now():%Y-%m-%d}.jsonl"
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def load_into_sqlite(db_path: str = "decisions.db") -> int:
"""分析前にJSONLをSQLiteへ取り込む。壊れた行はスキップして件数を返す"""
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS decisions (
session_id TEXT, step INTEGER, tool TEXT,
confidence TEXT, reason TEXT, ts TEXT,
PRIMARY KEY (session_id, step)
)
""")
imported = 0
for jsonl in sorted(LOG_DIR.glob("*.jsonl")):
for line in jsonl.read_text(encoding="utf-8").splitlines():
try:
e = json.loads(line)
conn.execute(
"INSERT OR IGNORE INTO decisions VALUES (?,?,?,?,?,?)",
(e["session_id"], e["step"],
(e.get("tool") or {}).get("name"),
e.get("confidence"), e.get("reason"), e.get("timestamp")),
)
imported += 1
except (json.JSONDecodeError, KeyError):
continue # 壊れた行で全体を止めない
conn.commit()
conn.close()
return imported
このコードのポイントは、読み込み側で壊れた行を握りつぶしている点です。エージェントが異常終了した瞬間の書きかけの行が混ざることは実際にあり、そこで取り込み全体が止まる設計にすると、結局ログを見なくなります。
容量の感覚もお伝えしておきます。私の環境では1タスクあたり平均30〜50ステップ、1エントリはおよそ1KB前後です。毎日複数のタスクを動かしても月に数百MBには届かない規模で、個人開発の範囲なら SQLite で十分に分析できます。BigQuery のような分析基盤を検討するのは、複数人のチームで数ヶ月分のログを横断集計する段階からで遅くありません。最初から大きな基盤を組まないことも、続けるための設計だと考えています。
ひとつだけ注意点があります。reason フィールドにはエージェントが参照したファイル名やユーザー入力の断片が入り込みます。ログを外部の分析サービスへ送る場合は、メールアドレスや API キーの形をした文字列をマスクする処理を取り込み時に挟んでください。手元の SQLite に置いているうちは問題になりにくいのですが、置き場所を変えた瞬間に意味が変わる情報です。
判断ログが逆効果になる落とし穴
設計を始めてから気づいた問題点も正直に書いておきます。
過剰なログがパフォーマンスに影響する
構造化された思考プロセスを毎ステップ出力させると、トークン消費が増え、レイテンシが上がります。私の経験では、デバッグ中はdetailed logging、本番ではsummary loggingに切り替えるフラグを持つことで対処しています。ステップ数が多いタスクほど、ログレベルの切り替えが重要です。特にAntigravityのBackground Agentのように長時間実行されるエージェントでは、全ステップを詳細記録するとコストが膨らみます。重要なステップ(副作用を持つツール呼び出し)だけを詳細記録し、それ以外は要約記録する設計が実用的です。
理由フィールドが「形式的な記入」になる
reasonフィールドを必須にしても、エージェントが「ユーザーの指示に従うため」のような汎用的な理由を記入することがあります。文字数チェックだけでは検出できない問題です。
対処法として、理由の具体性スコアを追加しています。特定のファイル名、変数名、条件が含まれているかどうかをチェックし、具体性が低い場合はconfidenceを下げる実装です。
def estimate_reason_specificity(reason: str) -> str:
"""理由テキストの具体性から信頼度を推定する"""
import re
specificity_markers = [
r'\b[a-zA-Z_]+\.(py|ts|json|md|mdx|txt)\b', # ファイル名
r'\b(function|class|def|const|let|var)\s+\w+', # コード要素
r'\b(because|since|given that|due to)\b', # 因果表現(英語)
r'(ため|から|ので|なぜなら)', # 因果表現(日本語)
r'\d+', # 数値
]
matched = sum(1 for p in specificity_markers if re.search(p, reason, re.IGNORECASE))
if matched >= 3:
return "high"
elif matched >= 1:
return "medium"
else:
return "low"
ログが増えるほど分析が難しくなる
長期運用で最も難しいのは、蓄積したログから有益な洞察を引き出すことです。私は以下のシンプルなルールで管理しています。
- 週次: quality_score が70未満のセッションだけをリストアップ
- 月次: 問題パターンを「ツール選択ミス」「スコープ拡大」「理由不明確」の3種類に分類
- 四半期: 最も多いパターンのプロンプトを修正し、A/Bテストで効果を検証
この分類が最初から完全でなくてよいと思っています。月次の分類作業自体が、エージェントの動作パターンへの理解を深める学習になります。
「どこまで任せるか」を感覚ではなくログで決める
判断ログを半年運用して、いちばん大きかった収穫は障害調査の時間短縮ではありませんでした。「エージェントにどこまで任せるか」という線引きを、データで決められるようになったことです。
エージェント型の開発ツールを使い始めた方から、任せる範囲の判断基準がわからないという声をよく聞きます。私も最初は「なんとなく不安だから手元で確認する」「なんとなく大丈夫そうだから任せる」という感覚頼みでした。判断ログが溜まると、この線引きをタスク種別ごとの数値で引き直せます。
私が使っている基準は次の3段階です。
- 完全委任: そのタスク種別の直近20セッションで low_confidence_rate が10%未満、かつ副作用を持つツール呼び出しでの不確実フラグがゼロ。人間のレビューなしで実行を許可します
- 事後確認: low_confidence_rate が10〜25%。実行は止めませんが、副作用のあるステップだけを抽出したダイジェストを後で確認します
- 事前承認: 25%を超えるか、ファイル削除・外部送信のような不可逆操作を含む。実行前に計画を提示させ、人間が承認してから動かします
しきい値そのものに普遍性はありません。大切なのは、この数字が自分の運用ログから出てきたものだという点です。
具体例をひとつ挙げます。アプリ事業の運用では、ストア掲載文の更新案づくりとリリースノートの多言語展開をエージェントに任せています。掲載文の生成は low_confidence_rate が一桁で安定していたので完全委任に移しました。一方、ストアへの反映操作そのものは、ログ上はほぼ安定しているにもかかわらず事前承認に残しています。公開中のプロダクトに対する不可逆操作は、数値がどれだけ良くても委任しないと決めているからです。
つまり、ログは「任せられる根拠」を与えてくれますが、「任せるべきか」は事業上の判断として残ります。この区別がつくようになったことが、半年分のログがくれた一番の変化でした。
この線引きは一度決めて終わりではありません。モデルのバージョンが上がるたびに数値は動きます。私は Antigravity 側のモデル更新があった週は、完全委任にしているタスクも一段階だけ確認レベルを戻し、20セッション分の数値を見てから委任に戻すようにしています。地味な運用ですが、「いつの間にか挙動が変わっていた」を防ぐ最も確実な方法だと感じています。
「説明できるAI」を作ることの哲学的な意味
少し抽象的な話をさせてください。
宮大工だった私の両祖父は、「手を動かすことが一つの信心」だと言っていました。その言葉を子供の頃から聞いて育ち、プログラミングを始めた後も、「コードを書くことは考えること」という感覚が体に染み付いています。木を組む仕事でも、コードを書く仕事でも、「なぜそうするか」を言葉にできる人は、問題が起きたときに根本から考えられます。
AIエージェントに判断を委ねるとき、私たちは何を手放しているのでしょうか。
説明可能性の設計は、技術的な要件であると同時に、人間とAIの間の「信頼の建設」だと考えています。なぜそうしたかを説明できないシステムを使い続けることは、理由のわからない慣習に従い続けることに似ています。積み重ねるほど、問題が起きたときに原因を探せなくなります。
同時に、AIが生成する「説明」が本当の意味での理解を反映しているかどうかは、まだ誰にもわからない問題です。エージェントが「このファイルを変更する理由は〇〇です」と書いたとき、それは人間が書いた理由と同じ重みを持つのでしょうか。
これは哲学的な問いですが、実装の設計にも影響します。私は今のところ、「説明の正確性」より「説明の追跡可能性」を重視する立場をとっています。完全に理解できなくても、後で検証できる記録があれば、少しずつ信頼を積み上げられる。ログとはそういうものだと思っています。
1997年、16歳でインターネットに出会い、国境を越えてデザイナーやプログラマーとつながったとき感じた「技術が人と人をつなぐ感覚」は、今もエージェント設計の根っこにあります。自律的に動くシステムを作ることは、見えないところで動くものへの信頼を積み上げる行為です。その信頼の基盤として、意思決定ログは機能します。
Trigger.dev で自律AIエージェントを24時間運用するガイドのように長期間自律稼働させるシステムでは、意思決定ログが監査証跡として機能し、「いつ、何を判断したか」を事後検証できる基盤になります。
全体を振り返って — まず一つのツールに reason フィールドを加えてみてください
意思決定ログの設計は、大掛かりなシステムを必要としません。今日できる最小のステップは、最も頻繁に使うツールの定義にreasonフィールドを一つ追加することです。
最初は「エージェントがどんな理由を書くか」を観察するだけで構いません。その観察から、プロンプトの曖昧さや、ツール定義の不足が見えてきます。見えてきたら、一つずつ改善していく。その積み重ねが、ユーザーが安心して使える自律システムの設計につながります。
私自身まだ試行錯誤の途中ですが、同じ課題に取り組んでいる方の参考になれば幸いです。