「AI エージェントって、本番に置いて大丈夫なんですか?」
Antigravity で作ったエージェントを会社の業務フローに組み込もうとした初日、SRE チームのリードからこう言われました。同じ入力でも違う出力が返ってくる確率的な挙動を、99.9% の可用性で動いている既存サービスと並べて語ろうとすると、どうしても噛み合わないのです。
この記事は、その日の会議で私が答えられなかった問いに対する、半年分の運用経験をまとめた答えです。結論から言うと、「AI は気まぐれ」を前提にしたまま、SRE の古典的な道具である SLI / SLO / エラーバジェット を適用すれば、エージェントは本番で十分に扱えます。ただし従来の Web サービスとは違う形で SLI を定義しなければならず、ここで多くのチームがつまずきます。
ここではAntigravity の エージェント評価フレームワーク と組み合わせながら、実運用で使えるコード付きで解説します。LangFuse を使ったエージェント可観測性ガイド の内容と重ねて読むと、計装から意思決定までの流れが見えてきます。
従来の SLI がなぜ AI エージェントで通じないのか
Google の SRE Book に出てくる SLI の典型例は「HTTP リクエストの成功率」「レイテンシ 200ms 以下の割合」です。しかし、Antigravity エージェントで「成功」を定義しようとすると、途端に壁にぶつかります。
例えば「このコードベースをリファクタリングしてください」というリクエストに対して、200 OK で返ってきたレスポンスが本当に成功なのかは、コードを実際に読むまで分かりません。HTTP ステータスだけを見ていると、意味不明な差分を 200 で返しているエージェントも「成功」にカウントされてしまいます。
私が最初に失敗したのは、まさにここでした。is_success = response.status == 200 という雑な SLI を定義して 2 週間運用した結果、メトリクスは完璧なのに、開発者からの苦情は毎日届くという乖離が起きました。
AI エージェントで SLI を定義するときの鉄則は次のとおりです。
- 決定論的な成功条件と確率的な品質評価を分ける(どちらか片方では足りない)
- ユーザーが実際に感じる価値の観点で測る(内部 API のステータスではなく、タスクが完了したか)
- サンプリング評価を前提にする(全件を LLM-as-a-Judge に通すとコストが爆発する)
以降では、これを具体的にどう実装するかを見ていきます。
SLI を 3 層で定義する
私が現在運用している Antigravity エージェントの SLI は 3 層構造です。それぞれが違う信頼性の側面を測っています。
第 1 層: インフラ SLI(従来型)
ここは通常の Web サービスと同じです。
- リクエスト成功率(HTTP 5xx を除いたレスポンス割合)
- レイテンシ P95 / P99
- タイムアウト率
これだけでは足りませんが、ここが壊れているとそもそも次のレイヤーの議論に進めないので、まず押さえます。
第 2 層: タスク完了 SLI(決定論的)
エージェントが「タスクを完了した」と主張していることを、コード側で確認できる範囲で検証します。
- ツール呼び出しが成功したか(例:
git commit が exit code 0 だったか)
- 出力が指定した JSON スキーマに一致するか
- 生成されたコードが TypeScript の型チェックを通るか
- テストが実行可能で、パスしているか
これは LLM ではなく普通のプログラムで判定できるので、全件に対してリアルタイムで計装します。
第 3 層: 品質 SLI(確率的)
ここで初めて LLM-as-a-Judge が登場します。
- 出力が要求仕様を満たしているか
- コードのスタイルが規約に沿っているか
- 回答がハルシネーションを含んでいないか
これは高価なので、全トラフィックの 5〜10% にサンプリングして評価するのが現実的です。
この 3 層の切り分けを飛ばして、いきなり LLM-as-a-Judge でぜんぶ測ろうとすると、コストが月 10 万円単位で膨らむうえに、評価の揺らぎで SLO が安定しません。私はこの罠に最初の 3 ヶ月でハマりました。
SLI 計装の実装 — 第 1・第 2 層
まずは決定論的に測れる部分の計装コードです。Antigravity エージェントから呼び出される TypeScript のサーバーサイドを想定しています。OpenTelemetry でメトリクスを吐き、Prometheus で集計する構成です。
// agent-sli.ts
// 目的: Antigravity エージェントの SLI を計装する
// ポイント: 第1層(インフラ)と第2層(タスク完了)を 1 箇所で扱う
import { metrics, trace } from "@opentelemetry/api";
const meter = metrics.getMeter("antigravity-agent", "1.0.0");
// 第1層: インフラ SLI
const requestCounter = meter.createCounter("agent_requests_total", {
description: "エージェントへのリクエスト総数",
});
const requestDuration = meter.createHistogram("agent_request_duration_ms", {
description: "エージェントリクエストの処理時間",
unit: "ms",
});
// 第2層: タスク完了 SLI
const taskCompletionCounter = meter.createCounter("agent_task_completions_total", {
description: "タスク完了判定の結果",
});
export interface SliContext {
agentId: string;
taskType: string;
userId: string;
}
export async function instrumentAgentRun<T>(
ctx: SliContext,
run: () => Promise<T>,
verify: (result: T) => Promise<TaskVerification>,
): Promise<T> {
const span = trace.getTracer("agent").startSpan(`agent.${ctx.taskType}`);
const start = Date.now();
try {
const result = await run();
// 第1層: インフラ的には成功
requestCounter.add(1, { outcome: "ok", task_type: ctx.taskType });
requestDuration.record(Date.now() - start, { task_type: ctx.taskType });
// 第2層: タスクとして成立しているか検証
const verification = await verify(result);
taskCompletionCounter.add(1, {
outcome: verification.completed ? "completed" : "incomplete",
reason: verification.reason ?? "none",
task_type: ctx.taskType,
});
span.setAttributes({
"agent.task_completed": verification.completed,
"agent.verification_reason": verification.reason ?? "",
});
return result;
} catch (err) {
requestCounter.add(1, { outcome: "error", task_type: ctx.taskType });
taskCompletionCounter.add(1, {
outcome: "error",
reason: err instanceof Error ? err.name : "unknown",
task_type: ctx.taskType,
});
span.recordException(err as Error);
throw err;
} finally {
span.end();
}
}
interface TaskVerification {
completed: boolean;
reason?: string;
}
// 使用例: コード生成タスクの検証
export async function verifyCodeGenerationResult(result: {
files: Array<{ path: string; content: string }>;
}): Promise<TaskVerification> {
// JSON スキーマに一致しているか
if (!Array.isArray(result.files) || result.files.length === 0) {
return { completed: false, reason: "empty_output" };
}
// 最低限の構文チェック(TypeScript ファイルなら tsc --noEmit を走らせる)
for (const file of result.files) {
if (file.path.endsWith(".ts") && !file.content.includes("export")) {
return { completed: false, reason: "no_export_statement" };
}
}
return { completed: true };
}
期待する出力は Prometheus 側で次のようにクエリできます。
# タスク完了 SLI(直近 28 日)
sum(rate(agent_task_completions_total{outcome="completed"}[28d]))
/
sum(rate(agent_task_completions_total[28d]))
ここで重要なのは、verify 関数をタスクタイプごとに用意することです。「コード生成」「ドキュメント生成」「調査レポート」でそれぞれ「完了」の意味が違うので、横展開できるよう verify 関数をプラグイン化しておくと後で楽になります。
SLI 計装の実装 — 第 3 層(LLM-as-a-Judge)
品質 SLI は別ジョブで処理します。リアルタイム処理と分けるのは、コスト・レイテンシ・信頼性の 3 つすべての理由からです。
// quality-judge.ts
// 目的: サンプリングした出力を LLM-as-a-Judge で品質評価する
// ポイント: 失敗は graceful に扱う — Judge が落ちても本番は落ちない
import { GoogleGenAI } from "@google/genai";
const judge = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const JUDGE_PROMPT = `
あなたは AI エージェントの出力品質を評価する評価者です。
次の評価対象について、以下の 3 軸を 1〜5 で採点し JSON で返してください。
- correctness: タスク要求を正しく満たしているか
- safety: セキュリティ上の問題はないか (ハードコードされた秘密情報・SQL インジェクション等)
- style: プロジェクトのコーディング規約に沿っているか
評価対象のタスク: {task}
ユーザー入力: {input}
エージェント出力: {output}
返却形式: {"correctness": 1-5, "safety": 1-5, "style": 1-5, "reason": "..."}
`;
interface QualityScore {
correctness: number;
safety: number;
style: number;
reason: string;
}
export async function evaluateQuality(
task: string,
input: string,
output: string,
): Promise<QualityScore | null> {
const prompt = JUDGE_PROMPT
.replace("{task}", task)
.replace("{input}", input.slice(0, 4000))
.replace("{output}", output.slice(0, 8000));
try {
const response = await judge.models.generateContent({
model: "gemini-2.5-flash",
contents: prompt,
config: {
responseMimeType: "application/json",
temperature: 0.1, // 評価は低温度で安定化
},
});
const text = response.text;
if (!text) {
console.warn("judge returned empty response");
return null;
}
const parsed = JSON.parse(text) as QualityScore;
// 妥当性チェック(Judge が壊れた JSON を返すことがある)
if (![parsed.correctness, parsed.safety, parsed.style].every(
(v) => typeof v === "number" && v >= 1 && v <= 5,
)) {
console.warn("judge returned invalid scores", parsed);
return null;
}
return parsed;
} catch (err) {
// Judge の失敗は本番エージェントの失敗ではない — ログだけ残して続行
console.error("judge failed:", err);
return null;
}
}
// サンプリング: 全件の 5% だけ評価に回す
export function shouldSample(): boolean {
return Math.random() < 0.05;
}
shouldSample を呼ぶ場所を工夫するのがコツです。ランダムサンプリングだけだと、レアなタスクタイプの評価がいつまでも溜まらないので、実際には「タスクタイプごとに最低 N 件は評価する」という層化サンプリングを組み合わせます。
なぜ LLM-as-a-Judge の結果を信じていいのか、と聞かれることがあります。答えは「信じない」です。これはあくまで大量の出力を粗く篩い分けるためのもので、最終的な品質判断は定期的な人的レビューで補正します。私のチームでは週に 1 時間、Judge が低スコアをつけた出力をサンプルして、Judge の採点と人の採点の一致度を測っています。この一致度自体も、メタ SLI として追跡しておくと安心です。
SLO をどう決めるか — 数値に感情を入れない
SLI の準備ができたら、次は SLO(目標値)を決めます。ここで「とりあえず 99.9%」と書いてはいけません。ビジネスが許容する失敗の上限から逆算するのが原則です。
私が運用しているコード生成エージェントの SLO は次のとおりです。
- 第 1 層(インフラ): 月間 99.5% のリクエスト成功率、P95 レイテンシ 5 秒以下
- 第 2 層(タスク完了): 月間 92% のタスク完了率
- 第 3 層(品質): サンプル評価で correctness 平均 4.0 以上、safety 平均 4.5 以上
最初に「99.9%」と書かなかった理由ははっきりしています。エージェントが扱うタスクの 5% は、そもそも人間が書いても間違える曖昧な要件だからです。ここに 99.9% を要求しても達成できないし、達成できない目標は運用チームを疲弊させるだけです。
SLO の決め方についてもう少し深く
エラーバジェットを自動判定するワークフロー
SLO が決まると、その逆数である エラーバジェット が決まります。月間 92% の SLO なら、許容される失敗は月間 8% です。
エラーバジェットの真価は、「残量に応じて意思決定を変える」運用にあります。残量が潤沢なら新機能をどんどん出す、残量が尽きかけたらリスクのある変更を止める、という切り替えを自動化するコードが次です。
// error-budget-gate.ts
// 目的: エラーバジェットの残量をチェックし、デプロイや実験の可否を決める
// ポイント: 現場の判断を数値に委ねる仕組み — 感情論でデプロイ可否を議論しない
interface ErrorBudgetStatus {
consumedPercentage: number; // 使ってしまった割合 (0-100)
remainingPercentage: number;
recommendation: "proceed" | "proceed_with_caution" | "freeze";
reason: string;
}
export async function checkErrorBudget(
promUrl: string,
sloTarget: number, // 例: 0.92
): Promise<ErrorBudgetStatus> {
// 直近 28 日のタスク完了率を Prometheus から取得
const query = `
sum(rate(agent_task_completions_total{outcome="completed"}[28d]))
/
sum(rate(agent_task_completions_total[28d]))
`;
const res = await fetch(
`${promUrl}/api/v1/query?query=${encodeURIComponent(query)}`,
{ signal: AbortSignal.timeout(5000) },
);
if (!res.ok) {
// Prometheus が落ちているときは保守的に proceed_with_caution を返す
// これも運用方針の設計判断 — チームによっては freeze を返すべき
return {
consumedPercentage: 0,
remainingPercentage: 100,
recommendation: "proceed_with_caution",
reason: `prometheus query failed: ${res.status}`,
};
}
const data = await res.json() as {
data: { result: Array<{ value: [number, string] }> };
};
const completionRate = parseFloat(data.data.result[0]?.value[1] ?? "0");
const allowedFailure = 1 - sloTarget; // 例: 0.08
const actualFailure = 1 - completionRate;
const consumed = (actualFailure / allowedFailure) * 100;
if (consumed < 50) {
return {
consumedPercentage: consumed,
remainingPercentage: 100 - consumed,
recommendation: "proceed",
reason: "budget has plenty of headroom",
};
}
if (consumed < 90) {
return {
consumedPercentage: consumed,
remainingPercentage: 100 - consumed,
recommendation: "proceed_with_caution",
reason: "budget is being consumed — risky changes require review",
};
}
return {
consumedPercentage: consumed,
remainingPercentage: 100 - consumed,
recommendation: "freeze",
reason: "error budget nearly exhausted — freeze non-critical deploys",
};
}
// CI から呼び出す例
if (import.meta.main) {
const status = await checkErrorBudget(
process.env.PROM_URL ?? "http://localhost:9090",
0.92,
);
console.log(JSON.stringify(status, null, 2));
if (status.recommendation === "freeze") {
console.error("❌ Error budget exhausted. Deploy blocked.");
process.exit(1);
}
}
GitHub Actions からこのスクリプトを呼び出して、freeze が返ったらデプロイを止める仕組みにすると、運用の温度感が一気に変わります。深夜の障害から目を覚ました運用担当が、泣きながら freeze を訴えなくても、自動で止まるのです。
なぜ Prometheus が落ちているときに proceed_with_caution を返すか、迷うところです。チームの方針によってはここを freeze にすべきで、これは典型的な「セキュリティ vs 可用性」のトレードオフです。私は自分のプロジェクトでは proceed_with_caution を選びました。Prometheus の短時間障害を理由に全エージェントが止まるより、人間の判断で一時的にリスクを取るほうがビジネス上の損失が小さかったからです。この判断基準はプロジェクトごとに明文化して、ADR として残しておくべきです。
バーンレートアラートの実装 — 鳴らすべきときにだけ鳴らす
アラート設計はエージェント SRE の真の肝です。失敗が起きた瞬間に鳴らすアラートは、オンコール担当を疲弊させて離職につながります。Google SRE Workbook が提唱する マルチウィンドウ・マルチバーンレート の考え方を Antigravity エージェントに適用してみます。
基本のアイデアはシンプルです。エラーバジェットを「どれくらいの速さで」使っているかを複数の時間窓で見て、短時間と長時間の両方で閾値を超えたときだけ鳴らします。これにより、一瞬のスパイクや、逆にゆるやかな劣化を見逃すことなく、本当に介入が必要なタイミングを捉えられます。
# Fast burn alert: 1 時間で月間予算の 2% を消費(1 時間で 6 時間ぶん消費)
(
1 - (
sum(rate(agent_task_completions_total{outcome="completed"}[1h]))
/
sum(rate(agent_task_completions_total[1h]))
)
) > (1 - 0.92) * 14.4
# Slow burn alert: 6 時間で月間予算の 5% を消費
(
1 - (
sum(rate(agent_task_completions_total{outcome="completed"}[6h]))
/
sum(rate(agent_task_completions_total[6h]))
)
) > (1 - 0.92) * 6
この 2 つのアラートを AND で組み合わせると、誤報の少ない実用的なアラートになります。「今のペースが続いたら月内に予算を使い切る」という閾値設計なので、夜中に起こされる価値のある通知だけが届きます。
私のチームでは、最初の 1 ヶ月は閾値を甘めに(5 や 2 ではなく 10 や 4 に)設定して、実際の発火頻度を見ながらチューニングしました。いきなり厳しい閾値で運用すると、最初の 1 週間で何度も誤報が起きてチーム全体がアラート疲れを起こします。閾値チューニングも運用の一部として予算を確保しておきましょう。
インシデント対応 — エージェントが詰まったときに何をするか
SLO とエラーバジェットの設計ができても、実際のインシデントが起きたときに何をするかをチームで合意していないと、結局は属人的な対応になります。Antigravity エージェントに固有のインシデントパターンをいくつか挙げ、それぞれのランブックを整理しておきます。
パターン 1: 突然の失敗率急増(例: 1 時間で 10% → 50%)
まず疑うべきは、上流の LLM プロバイダー(Gemini API 等)の障害です。LLM プロバイダーのステータスページを確認し、同時に自分のメトリクスでレイテンシの異常(急な増加か、逆に 1ms のような異常な減少)を確認します。次に疑うのが、直前にデプロイしたプロンプトテンプレートやツール定義の変更です。エージェントのプロンプトは脆弱で、1 行の変更で挙動が崩れることがあります。直前の変更を一時的にロールバックする判断を即断できるように、プロンプトも通常のコードと同じく Git で管理し、ロールバック手順をランブックに書いておきます。
パターン 2: 徐々に品質が低下する(エラーバジェットがじわじわ減る)
これは最も対応が難しいパターンです。インフラも第 2 層 SLI も正常なのに、LLM-as-a-Judge のスコアだけが 4.2 → 4.0 → 3.8 と下がっていく、というケース。原因の多くは、本番のデータ分布が学習時から乖離していく データドリフト です。入力プロンプトのトークン数分布や、扱うライブラリのバージョンを時系列で追跡しておき、「いつから」分布が変わったかを特定できるようにしておきます。これが分かると、プロンプトの改訂や few-shot の追加で対応できることが多いです。
パターン 3: 特定ユーザー・タスクタイプでだけ失敗する
全体 SLI は健全なのに、特定の顧客セグメントから苦情が来るパターン。SLI をタスクタイプやテナントでラベル付けしておけば、Prometheus 上でサブセットの SLO を切り出せます。ラベルをケチらずに計装するのが、後で効いてきます。
SLO を社内で合意する — 説得の技術
SRE ツールをどんなに丁寧に作っても、経営層と機能開発チームの合意が取れていなければ意味がありません。私が SLO の導入で最も時間を使ったのは、コードを書く部分ではなく、「なぜ 92% なのか」を納得してもらう会議でした。
経営層に対しては、SLO を サービス品質のコミットメントライン として提示するのが有効でした。「92% のタスク完了率を下回ったら、新機能開発を一時停止してエージェントの改善に集中する」と事前に合意しておくことで、品質劣化時の意思決定が感情論にならずに済みます。ここを曖昧にしたまま運用すると、「売上が大事」と「品質が大事」の押し付け合いが起きて、結局エージェント改善の優先度が下がっていきます。
機能開発チームに対しては、エラーバジェットを「予算」として可視化することが効きました。「今月はエラーバジェットを 60% 使っているので、リスクの高いプロンプト変更は来月以降にしましょう」と言えば、納得してくれます。逆に「残量が潤沢なので、実験的な機能を試す好機です」と言えば、挑戦を推奨できます。SRE の文脈でよく引用されるフレーズに「エラーバジェットは使うためにある」というものがあり、これは機能開発チームとの協調に欠かせない考え方です。
ダッシュボードの作り方にもコツがあります。経営層向けには 1 枚で現状が分かる俯瞰ビューを、エンジニア向けには時系列で深掘りできる詳細ビューを、それぞれ分けて用意します。1 つのダッシュボードで両方の需要を満たそうとすると、どちらのユーザーにとっても使いづらいものになります。
タスクタイプごとのラベリング戦略
第 2 層 SLI で触れたラベリングは、運用が長くなるほど効いてきます。私が実際に付けているラベルと、その理由を共有します。
task_type: コード生成 / テスト生成 / ドキュメント生成 / 調査レポート — タスク種別ごとに SLO を切り分けるため
tenant_id: 顧客または内部チーム識別子 — 特定テナントで品質劣化を検知するため
model: gemini-2.5-flash / gemini-3-pro / claude-sonnet-4.6 など — モデル別の性能比較と切替判断のため
prompt_version: プロンプトテンプレートのハッシュ — プロンプト変更が SLI に与える影響を追跡するため
client_version: Antigravity クライアントのバージョン — クライアント側の変更が SLI に与える影響を切り分けるため
ラベルが多すぎるとカーディナリティ爆発で Prometheus が重くなるので、user_id のような高カーディナリティのラベルは付けません。代わりに、ユーザー単位の追跡が必要な場合はログ(構造化 JSON ログ)と Trace を使います。メトリクスは「集計用」、ログと Trace は「個別追跡用」と役割を分けるのが長く運用する上での鉄則です。
私のチームでは、新しいラベルを追加するときに「このラベルで最低どんなクエリが書けるか」を先に 2〜3 個想定して、それを満たせるラベル設計になっているかを確認します。目的なくラベルを増やすと、後で「なんとなく付けた」ラベルが散らかって手がつけられなくなります。
よくある落とし穴と、そこから学んだこと
私自身が踏み抜いた落とし穴と、それぞれの回避策をまとめておきます。
落とし穴 1: SLO を社内で公表しないまま運用する
運用チームだけが SLO を知っていて、機能開発チームが知らないと、「ちょっとだけ速度を優先したいんだけど」という要望がエラーバジェットを食い潰します。SLO は社内ダッシュボードに公開し、誰でも「今月のエラーバジェット残量」を確認できるようにします。
落とし穴 2: 評価用データセットをバージョン管理しない
LLM-as-a-Judge のスコアが悪化したとき、「評価データセットが変わったせい」か「エージェントの性能が落ちたせい」かが区別できなくなります。評価データセットは必ず Git で管理し、日付とコミット ID をメトリクスに付与します。
落とし穴 3: アラートを SLO ではなく SLI で鳴らす
SLI(例: 失敗率 5%)で即アラートを鳴らすと、オンコールが疲弊します。アラートは エラーバジェットの燃焼速度 で鳴らすのが正解です。例えば「このペースで失敗が続くと、月内に予算を使い切る」という条件で鳴らします。これは Burn Rate Alert と呼ばれ、Google の SRE Workbook にも詳細があります。
落とし穴 4: 人的レビューを後回しにする
LLM-as-a-Judge の採点は LLM の好みに偏ります。週 1 時間でも、人間が Judge のスコアと実際の出力を見比べないと、Judge が壊れていることに気づけません。この人的レビューの工数は SRE のコストとして明示的に計上します。
落とし穴 5: エージェントごとに SLO を切らない
複数のエージェントを 1 つの SLO で管理すると、特定エージェントの劣化が他のエージェントの余剰でマスクされます。エージェントごとに独立した SLO を持たせ、ダッシュボードも別々にするのが運用上の鉄則です。
運用に落とし込む最初の一歩
ここまで読んで「うちでも始めたい」と感じた方は、いきなり全部は作らないでください。SRE は成熟度のレベルで段階的に導入するのが定石です。
私がお勧めする最初の一歩は、第 2 層の SLI(タスク完了率)だけを 1 週間計装する ことです。第 1 層は既存の Web モニタリングでカバーされていることが多く、第 3 層はコストと労力がかかります。第 2 層だけでも計装すると、「エージェントが意味のある完了を返した割合」が見えて、それを眺めながらチームで SLO を議論する材料になります。
この一週間で、あなたのエージェントが実はどのくらいの完了率で動いているかが初めて数値化されます。その数字を見て「思ったより高い」「思ったより低い」と感じたところから、SLO の議論を始めればよいのです。