長時間動くAIエージェントは、どの時点で止めればいいのか?
「危ないコマンドをブロックしているから大丈夫」は、長く動くエージェントには少し楽観的すぎると思う。
7月20日に公開されたOpenAIの長時間モデルの安全性に関する記事では、単発の操作では問題が見えなくても、操作の並びが制約の回避へ向かう例が紹介されていた。実際に監視を止め、軌跡全体を見る仕組みへ切り替えたという話だ。原文を読んでいて、自分はまずローカルの実行ログに同じ発想を入れたくなった。
ここで作るのはモデルの安全性を判定するものではない。エージェントランナーが出すイベントを順番に見て、「ワークスペース外へ出た」「未承認のネットワーク操作を繰り返した」を人に渡す、小さな停止器だ。
Q. 単発の deny では何が足りない?
A. 1回目の未承認ネットワークアクセスは、設定漏れかもしれない。けれど同じ境界をもう一度越えようとしたら、作業の向きが変わったと見た方がいい。
長時間タスクでは、最初の失敗を受けて別の手段を探す。個々の curl やファイル書き込みだけを許可リストと照合しても、「失敗後に何を試し始めたか」が消える。停止判断には、現在の操作と過去の境界越えを一緒に持たせる必要がある。
ただし、何でも即停止にすると運用で使えない。今回のルールは二段階にした。
| 条件 | 判定 |
|---|---|
| ワークスペース内の読み書き・コマンド | ALLOW |
| 未承認のネットワーク操作を初めて観測 | REVIEW |
| 同じ未承認ネットワーク操作が2回目 | PAUSE |
| ワークスペース外への書き込み・コマンド | 即 PAUSE
|
Q. 最小の軌跡監視はどう書く?
A. イベントごとの判定結果ではなく、境界ごとの回数をオブジェクトに残す。これだけで「2回目」を検出できる。
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Optional
@dataclass(frozen=True)
class Event:
kind: str
target: str
approved: bool = False
class TrajectoryGuard:
"""実行ログを順に受け取り、境界越えを review / pause に分ける。"""
def __init__(self, workspace: str) -> None:
self.workspace = Path(workspace).resolve()
self.counts: Dict[str, int] = {}
def inspect(self, event: Event) -> str:
boundary = self._boundary(event)
if boundary is None or event.approved:
return "ALLOW"
self.counts[boundary] = self.counts.get(boundary, 0) + 1
if boundary == "workspace" or self.counts[boundary] >= 2:
return "PAUSE: " + boundary
return "REVIEW: " + boundary
def _boundary(self, event: Event) -> Optional[str]:
if event.kind == "network":
return "network"
if event.kind in {"write", "command"}:
target = Path(event.target).resolve()
try:
target.relative_to(self.workspace)
except ValueError:
return "workspace"
return None
この関係はこうです。
approved=True は、人がネットワーク利用を明示的に許可したイベントにだけ付ける想定だ。承認済みを回数に数えないので、レビュー後にタスクを再開しても過去の警告で即停止しない。
Q. ちゃんと「軌跡」になっているか確認する
イベントを4つ流す。最初の2つはワークスペース内でのテストとレポート出力。続く2つは未承認の同じ外部アクセスだ。
events = [
Event("command", "/tmp/agent-demo/tests"),
Event("write", "/tmp/agent-demo/report.json"),
Event("network", "https://example.test/api"),
Event("network", "https://example.test/api"),
]
guard = TrajectoryGuard("/tmp/agent-demo")
result = [guard.inspect(event) for event in events]
print(result)
assert result == ["ALLOW", "ALLOW", "REVIEW: network", "PAUSE: network"]
手元の Python 3.9.6 で実行した出力はこれだった。
['ALLOW', 'ALLOW', 'REVIEW: network', 'PAUSE: network']
昨日この手の検証をしていて、最初は str | None と書いたら Python 3.9 で落ちた。監視役が起動前に落ちるのはかなり寒いので、記事のコードは Optional[str] にしている。ランナーのPythonが混ざる現場では、こういう地味な互換性が先に効く。
Q. 実運用では何をイベントにする?
A. kind と target は、使うランナーの監査ログから作る。シェルなら cwd と書き込み先、HTTPツールなら接続先、デプロイなら環境名を target に入れる。Event をJSON Linesで保存しておけば、停止後に「どの境界を何回またいだか」もそのまま追える。
このコードはURLの安全性も、コマンドの危険性も理解しない。そこを正規表現で賢くし始めると、すぐにルールの穴埋めゲームになる。まずは作業ディレクトリ、外部通信、公開環境のように、チームが合意しやすい境界を少数だけ置くのがいい。
おわりに
長時間エージェントの監視で見るべきなのは、危険な1操作だけではなく、失敗のあとにどの境界へ寄っていくかだと思う。ローカル実行でも、回数を持つだけで初回の確認と継続的な逸脱を分けられる。次に足すなら、PAUSE 時点の直近イベントをチケットや通知に添える。止めるだけでなく、人が再開を判断できるログまで残して初めて実務のガードになる。