LiteLLMとLangGraphで構築したAIエージェントをMicrosoft製agent-sreでSRE運用してみる

LiteLLMとLangGraphで構築したAIエージェントをMicrosoft製agent-sreでSRE運用してみる

2026.05.28

はじめに

データ事業本部のkobayashiです。

LiteLLMやLangGraphを使ってAIエージェントを開発し、いざ本番運用に乗せようとすると、従来のWebサービスとは異なる障害モードへの対処が必要になると感じることがあります。従来のSRE(Site Reliability Engineering)はHTTP 500エラーやタイムアウトといった明確な障害を監視対象としていますが、AIエージェントは**「動いているが正しくない」**という形で静かに劣化するのが特徴です。

  • 精度の低下(ハルシネーションの増加)
  • ツール呼び出しの膨張(1タスクに大量のAPI呼び出し)
  • コスト爆発(単一タスクで数百ドルのAPI費用)
  • 暴走ループ(同じツールを延々と呼び続ける)
  • カスケード障害(1つのエージェント障害が連鎖する)

これらはHTTPステータスコードだけでは検出できません。MicrosoftはAIエージェント向けのSREライブラリとしてagent-sreというPythonパッケージを公開しており、SLO・エラーバジェット・サーキットブレーカーといったSREの概念をAIエージェント向けに再定義しています。「エージェントが正しく動作しているか」を定量的に管理できるようになります。

今回はagent-sreパッケージをLangGraph + LiteLLMで構築したエージェントに統合してみたのでその内容をまとめます。

agent-sreとは

agent-sreはMicrosoftが公開するagent-governance-toolkitの中に含まれるAIエージェント向けのSREライブラリです。OWASP Agentic Top 10に対応する信頼性エンジニアリングの構成要素をPythonパッケージとして提供しています。

なお、ここで言うOWASP Agentic Top 10は、OWASP GenAI Security Projectが公開している、自律的に計画・行動するAIエージェント特有のセキュリティリスクを整理したフレームワーク(識別子ASI01〜ASI10、Agentic Security Initiative)です。目的の乗っ取り(ASI01: Agent Goal Hijack)、ツール悪用(ASI02: Tool Misuse)、メモリ・コンテキスト汚染(ASI06: Memory & Context Poisoning)、暴走エージェント(ASI10: Rogue Agents)など、従来のWebアプリ向けTop 10やLLM Top 10では扱いきれない脅威がまとめられています。agent-sreagent-governance-toolkitのうち信頼性レイヤを担当するパッケージで、特にASI10(Rogue Agents)の検出などを実装しています。Top 10全般の緩和策はagent-governance-toolkit全体で対応する設計です。

主な特徴としては以下になります。

  • サーキットブレーカー: 障害が連続するエージェントを自動的に遮断し、復旧を自動確認
  • SLO / エラーバジェット: タスク成功率やコストなどの信頼性指標を定義し、バジェット消費速度(バーンレート)をリアルタイム追跡
  • 不正エージェント検出: 許可外ツール使用・呼び出し頻度・行動エントロピーの3シグナルで暴走を検出
  • コストガード: LLM API費用のタスク単位・エージェント単位・組織単位の予算管理
  • カオステスト: 9種類のフォールトテンプレートで本番前に耐障害性を検証
  • フレームワーク連携: LangChain, LangGraph, CrewAI, AutoGenなどのアダプタが用意されている

従来のSREとAgent SREの違い

従来のWebサービスSREとAgent SREの違いを整理します。

観点 従来の SRE Agent SRE
障害の現れ方 500エラー、タイムアウト 精度低下、ハルシネーション、コスト異常
主な監視指標 可用性、レイテンシ タスク成功率、ツール呼び出し数、ハルシネーション率
SLO の対象 リクエスト成功率 99.9% タスク成功率 95%、ツール呼び出し上限 10回/タスク
サーキットブレーカーのトリガー HTTPエラー率 精度低下、コスト超過、暴走ループ
問いかけ 「サービスは動いているか?」 「エージェントは正しく動いているか?」

Agent SREが真に効果を発揮するのは、ツール呼び出しをループ的に繰り返すエージェントです。単発のLLM呼び出しにはオーバーエンジニアリングになりますが、LangGraphで構築するReActエージェントは以下のループを持ちます。

このループの中で暴走・劣化・コスト爆発が起きるため、Agent SREによる監視が有効になります。

agent-sreの主要コンポーネント

agent-sreが提供する主なコンポーネントは以下になります。

CircuitBreaker

障害が続くエージェントを自動的に遮断し、下流サービスへの影響を防ぎます。用語は電気回路の「ブレーカー」が由来で、CLOSED(回路が閉じている=通電している=通常運転)OPEN(回路が開いている=遮断されている)とソフトウェア文脈の直感とは逆になっています。

SLO / SLI / ErrorBudget

エージェント固有のSLI(Service Level Indicators)を定義し、SLO(Service Level Objectives)として目標値を設定します。95%のSLOなら残り5%がエラーバジェットとなり、バジェットの消費速度(バーンレート)を監視できます。agent-sre 3.5.0 ではバジェット枯渇時のアクション(exhaustion_action)としてTHROTTLE / CIRCUIT_BREAK等を設定できますが、これらは自動発動するものではなくSLO.evaluate()の結果やexhaustion_actionの値を見て呼び出し側でCostGuardCircuitBreakerを発動させる際のヒント値として機能します。

SLI 説明 SLO 例
タスク成功率 タスクが正しく完了した割合 >= 95%
レイテンシ 応答にかかった時間 <= 30秒
ツール呼び出し数 1タスクあたりの呼び出し回数 <= 10回
ハルシネーション率 事実と異なる出力の割合 <= 2%
コスト/タスク 1タスクあたりのAPI費用 <= $0.10

RogueAgentDetector

OWASP Agentic Top 10 のASI10 (Rogue Agents) 対策として、3つのシグナルを組み合わせてリスクを評価します。

  • Capability: 許可されていないツール(allowed_tools外)の使用
  • Frequency: Zスコアによるツール呼び出し頻度の異常(バースト検出)
  • Entropy: 行動エントロピー(低すぎる=ループ、高すぎる=カオス的挙動)

CostGuard

LLM API呼び出しの課金が暴走しないよう、タスク単位・エージェント単位・組織単位で予算を管理します。閾値を超えると自動スロットル、さらに超過でキルスイッチが発動します。

全体のアーキテクチャ

今回構築する全体のアーキテクチャは以下になります。LangGraph + LiteLLMで構築したReActエージェントを、agent-sreの各コンポーネントで保護する構成です。

LiteLLMはモデル抽象化レイヤーで、モデル名を変えるだけでOpenAI / Anthropic / Googleなどのプロバイダーを切り替えられます。agent-sreのサーキットブレーカーとLiteLLMのフォールバック機能(前回記事で紹介)を組み合わせることで多層的な耐障害性を実現できます。

では早速試してみます。

環境構築

環境

今回使用した環境は以下の通りです。

Python 3.13.9
agent-sre 3.5.0
langgraph 1.1.8
langchain-litellm 0.6.4
litellm 1.83.9

インストール

uv pip でインストールします。agent-sreはPublic Preview段階でAPI変更が入る可能性があるため、再現性確保のためバージョンを固定しています。

$ uv pip install \
    "agent-sre==3.5.0" \
    "langgraph==1.1.8" \
    "langchain-litellm==0.6.4" \
    "litellm==1.83.9"

APIキーの設定

LLMプロバイダーはLiteLLM経由で切り替え可能ですが、今回はOpenAIを使用します。

$ export OPENAI_API_KEY="sk-..."

主要クラス

実装で使用するagent-sreの主要クラスは以下になります。

クラス 説明
CircuitBreaker サーキットブレーカー本体。call()で関数をラップするだけで成功/失敗を自動記録
CircuitBreakerConfig サーキットブレーカーの設定(失敗閾値、復旧タイムアウトなど)
SLO SLI群とErrorBudgetをまとめたSLO定義
SLI 個別のSLI。collect()メソッドを実装したサブクラスを作成する
ErrorBudget エラーバジェット。record_event()で成功/失敗を記録し、burn_rate()でバーンレートを取得
RogueAgentDetector 不正エージェント検出。ツール使用を記録してassess()でリスク評価を返す
RogueDetectorConfig 不正検出の閾値設定(Zスコア閾値、エントロピー閾値など)
CostGuard コストガード。check_task()でプリフライト、record_cost()で事後記録

LangGraph + LiteLLMでReActエージェントを構築する

はじめにAgent SREで保護する対象となるエージェントを作成します。

ツールの定義

エージェントが使用するツールを@toolデコレータで定義します。デモ用にモックレスポンスを返すようにしています。

demo.py
from langchain_core.tools import tool

@tool
def web_search(query: str) -> str:
    """Web で情報を検索する."""
    data = {
        "東京タワー": "東京タワーの高さは333メートルです。1958年に完成しました。",
        "富士山": "富士山の標高は3,776メートルで、日本最高峰です。",
    }
    for key, val in data.items():
        if key in query:
            return val
    return f"'{query}' の検索結果: 関連情報が見つかりました。"

@tool
def calculator(expression: str) -> str:
    """数式を計算する(四則演算のみ対応)."""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "エラー: サポートされない式です"
    return str(eval(expression))

@tool
def get_weather(location: str) -> str:
    """指定した場所の現在の天気を取得する."""
    data = {
        "東京": "晴れ、気温 25°C、湿度 60%",
        "大阪": "曇り、気温 23°C、湿度 70%",
    }
    return f"{location}: {data.get(location, '晴れ、気温 20°C')}"

ReActエージェントの構築

ChatLiteLLMでLLMを初期化し、LangGraphでllm_nodeshould_continuetool_nodellm_node...というReActパターンのループを組み立てます。

demo.py
from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage
from langchain_litellm import ChatLiteLLM
from langgraph.graph import StateGraph, MessagesState, START, END

TOOLS = [web_search, calculator, get_weather]
TOOLS_BY_NAME = {t.name: t for t in TOOLS}

# LiteLLM 経由で LLM を初期化。モデル名を変えるだけでプロバイダー切替が可能
LLM = ChatLiteLLM(model="gpt-4o-mini", temperature=0)
LLM_WITH_TOOLS = LLM.bind_tools(TOOLS)

SYSTEM_PROMPT = (
    "あなたはツールを使ってユーザーの質問に回答するアシスタントです。"
    "必要に応じてツールを呼び出してください。"
)

def llm_node(state: MessagesState):
    msgs = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
    return {"messages": [LLM_WITH_TOOLS.invoke(msgs)]}

def tool_node(state: MessagesState):
    results = []
    for tc in state["messages"][-1].tool_calls:
        fn = TOOLS_BY_NAME[tc["name"]]
        out = fn.invoke(tc["args"])
        results.append(ToolMessage(content=str(out), tool_call_id=tc["id"]))
    return {"messages": results}

def should_continue(state: MessagesState):
    if state["messages"][-1].tool_calls:
        return "tool_node"
    return END

def build_agent():
    graph = StateGraph(MessagesState)
    graph.add_node("llm", llm_node)
    graph.add_node("tool_node", tool_node)

    graph.add_edge(START, "llm")
    graph.add_conditional_edges("llm", should_continue, ["tool_node", END])
    graph.add_edge("tool_node", "llm")
    return graph.compile()

AGENT = build_agent()

LiteLLMをLangChainから呼び出すための薄いラッパーがlangchain-litellmパッケージで、ChatLiteLLMmodel=gpt-4o-minianthropic/claude-sonnet-4-6のようなLiteLLM形式のモデル名を渡せばそのままLangGraphから利用できます。

agent-sreでエージェントを保護する

構築したエージェントにagent-sreの各コンポーネントを組み込んでいきます。

Agent SREコンポーネントの初期化

agent-sreパッケージから必要なクラスをインポートします。

demo.py
from agent_sre.anomaly import (
    RiskLevel,
    RogueAgentDetector,
    RogueDetectorConfig,
)
from agent_sre.cascade.circuit_breaker import (
    CircuitBreaker,
    CircuitBreakerConfig,
    CircuitOpenError,
)
from agent_sre.cost import CostGuard
from agent_sre.slo import SLI, SLO, ErrorBudget, SLIValue
from agent_sre.slo.objectives import ExhaustionAction

AGENT_ID = "research-agent"

ここから各コンポーネントを順に初期化していきます。

CircuitBreakerの設定

demo.py
breaker = CircuitBreaker(
    agent_id=AGENT_ID,
    config=CircuitBreakerConfig(
        failure_threshold=3,
        recovery_timeout_seconds=10.0,
        half_open_max_calls=1,
    ),
)

設定項目は以下の通りです。

パラメータ 説明
failure_threshold 3 連続失敗がこの回数に到達した時点で CLOSED → OPEN に遷移
recovery_timeout_seconds 10.0 OPEN → 復旧確認(HALF_OPEN) に遷移するまでの待機秒数
half_open_max_calls 1 復旧確認状態で通す試行リクエスト数

SLO / ErrorBudgetの設定

まずSLIを継承してタスク成功率を計測する SLI を定義します。

demo.py
class TaskSuccessRateSLI(SLI):
    """タスク成功率 SLI."""

    def collect(self) -> SLIValue:
        values = self.values_in_window()
        if not values:
            return self.record(1.0)
        good = sum(1 for v in values if v.is_good)
        return self.record(good / len(values))

collect()メソッドでは、ウィンドウ内のイベントのうち成功したものの割合を返しています。

次にSLOとErrorBudgetを組み合わせて初期化します。

demo.py
slo = SLO(
    name=f"{AGENT_ID}-reliability",
    indicators=[
        TaskSuccessRateSLI(name="task_success_rate", target=0.95, window="1h"),
    ],
    error_budget=ErrorBudget(
        total=0.05,
        window_seconds=3600,
        burn_rate_alert=2.0,
        burn_rate_critical=10.0,
        exhaustion_action=ExhaustionAction.THROTTLE,
    ),
    agent_id=AGENT_ID,
)

SLI(TaskSuccessRateSLI)の設定:

パラメータ 説明
name task_success_rate SLIの識別名
target 0.95 目標値(タスク成功率 95%)
window 1h 評価対象の時間窓(1時間)

ErrorBudgetの設定:

パラメータ 説明
total 0.05 許容する失敗率(1 - SLO目標 = 5%)
window_seconds 3600 バジェット計算のウィンドウ秒数(1時間)
burn_rate_alert 2.0 warningアラートを出すバーンレート閾値
burn_rate_critical 10.0 criticalアラートを出すバーンレート閾値
exhaustion_action THROTTLE バジェット枯渇時のアクション

exhaustion_actionには他にALERT(通知のみ)・CIRCUIT_BREAK(サーキットブレーク発動)・FREEZE_DEPLOYMENTS(デプロイ凍結)を指定できます。前述の通り、これらは自動発動ではなく、SLO.to_dict()等で参照して呼び出し側で実際のアクションを実装する想定です。

なおagent-sre 3.5.0 のErrorBudget実装では、失敗イベント1件ごとにconsumed += 1.0でカウントされる仕様になっています。そのためtotal=0.05のような小さい値で運用すると最初の1件で枯渇します。実運用では想定イベント数に対する絶対値(例: 100イベント中5件まで許容ならtotal=5.0)で設計したほうが直感的です。後述のデモ3ではtotal=0.05のままにしているので、最初の失敗でバジェット残が一気に0%になります。

RogueAgentDetectorの設定

demo.py
detector = RogueAgentDetector(
    config=RogueDetectorConfig(
        frequency_z_threshold=2.0,
        quarantine_risk_level=RiskLevel.HIGH,
    ),
)
detector.register_capability_profile(
    agent_id=AGENT_ID,
    allowed_tools=["web_search", "calculator", "get_weather"],
)

設定項目は以下の通りです。

パラメータ 説明
frequency_z_threshold 2.0 呼び出し頻度のZスコア閾値(これを超えるとバースト異常として扱う)
quarantine_risk_level HIGH このRiskLevel以上でquarantine_recommended=Trueが立つ
allowed_tools ["web_search", ...] エージェントに許可するツールのホワイトリスト

register_capability_profile()でホワイトリストに入れなかったツールを使うと、capability_scoreが上昇します。

CostGuardの設定

demo.py
cost_guard = CostGuard(
    per_task_limit=0.10,
    per_agent_daily_limit=1.00,
    org_monthly_budget=100.00,
    auto_throttle=True,
    kill_switch_threshold=0.95,
)

設定項目は以下の通りです。

パラメータ 説明
per_task_limit $0.10 1タスクあたりのコスト上限(check_task()のプリフライトで判定)
per_agent_daily_limit $1.00 1エージェントの1日あたりの上限
org_monthly_budget $100.00 組織全体の月次予算
auto_throttle True 利用率が閾値を超えたら自動でthrottled状態に遷移
kill_switch_threshold 0.95 日次上限のこの割合を超えたらキルスイッチ発動(killed状態に遷移)

これらをまとめて初期化する関数が以下になります。failure_thresholdrecovery_timeoutは引数で上書き可能にしてあり、後述のデモ2ではrecovery_timeout=2.0で再初期化して動作確認時間を短縮しています。

demo.py
def init_sre_components(
    failure_threshold: int = 3,
    recovery_timeout: float = 10.0,
) -> tuple[CircuitBreaker, SLO, RogueAgentDetector, CostGuard]:
    breaker = CircuitBreaker(
        agent_id=AGENT_ID,
        config=CircuitBreakerConfig(
            failure_threshold=failure_threshold,
            recovery_timeout_seconds=recovery_timeout,
            half_open_max_calls=1,
        ),
    )
    slo = SLO(
        name=f"{AGENT_ID}-reliability",
        indicators=[
            TaskSuccessRateSLI(name="task_success_rate", target=0.95, window="1h"),
        ],
        error_budget=ErrorBudget(
            total=0.05,
            window_seconds=3600,
            burn_rate_alert=2.0,
            burn_rate_critical=10.0,
            exhaustion_action=ExhaustionAction.THROTTLE,
        ),
        agent_id=AGENT_ID,
    )
    detector = RogueAgentDetector(
        config=RogueDetectorConfig(
            frequency_z_threshold=2.0,
            quarantine_risk_level=RiskLevel.HIGH,
        ),
    )
    detector.register_capability_profile(
        agent_id=AGENT_ID,
        allowed_tools=["web_search", "calculator", "get_weather"],
    )
    cost_guard = CostGuard(
        per_task_limit=0.10,
        per_agent_daily_limit=1.00,
        org_monthly_budget=100.00,
        auto_throttle=True,
        kill_switch_threshold=0.95,
    )
    return breaker, slo, detector, cost_guard

エージェントをラップするSREラッパー

初期化した各コンポーネントでエージェント呼び出しを保護するラッパー関数を実装します。CostGuardはLLM API呼び出しを自動でフックしないので、LLMレスポンスからトークン数を取り出してlitellm.cost_per_token()で実コストに変換し、record_cost()に渡します。

demo.py
import time

import litellm
from langchain_core.messages import AIMessage, HumanMessage

MODEL = "gpt-4o-mini"

def run_with_sre(
    question: str,
    *,
    breaker: CircuitBreaker,
    slo: SLO,
    detector: RogueAgentDetector,
    cost_guard: CostGuard,
    estimated_cost: float = 0.005,
    simulate_failure: bool = False,
) -> dict:
    # 1. コストチェック(プリフライト)
    allowed, reason = cost_guard.check_task(AGENT_ID, estimated_cost=estimated_cost)
    if not allowed:
        return {"status": "cost_blocked", "reason": reason}

    # 2. サーキットブレーカーでエージェント実行を包む
    def _invoke():
        if simulate_failure:
            raise RuntimeError("simulated_fault")
        return AGENT.invoke({"messages": [HumanMessage(content=question)]})

    try:
        result = breaker.call(_invoke)
    except CircuitOpenError as e:
        slo.indicators[0].record(0.0)
        slo.record_event(good=False)
        return {"status": "circuit_open", "retry_after": round(e.retry_after, 1),
                "cb_state": breaker.state}
    except Exception as e:  # CircuitBreaker.call() は任意の例外を失敗として再送出する
        slo.indicators[0].record(0.0)
        slo.record_event(good=False)
        return {"status": "failed", "error": str(e), "cb_state": breaker.state}

    # 3. ツール使用状況を収集して不正検出へ
    tool_calls = []
    for msg in result["messages"]:
        if hasattr(msg, "tool_calls") and msg.tool_calls:
            tool_calls.extend(tc["name"] for tc in msg.tool_calls)
    for name in tool_calls:
        detector.record_action(
            agent_id=AGENT_ID, action="tool_call", tool_name=name,
            timestamp=time.time(),
        )
    assessment = detector.assess(AGENT_ID)

    # 4. SLO 記録(SLI に値 → SLO 経由で error_budget と evaluate をまとめて更新)
    slo.indicators[0].record(1.0)
    slo.record_event(good=True)

    # 5. LLM のトークン使用量から実コストを算出して CostGuard に記録
    input_tokens = 0
    output_tokens = 0
    for msg in result["messages"]:
        if isinstance(msg, AIMessage) and msg.usage_metadata:
            input_tokens += msg.usage_metadata.get("input_tokens", 0)
            output_tokens += msg.usage_metadata.get("output_tokens", 0)
    prompt_cost, completion_cost = litellm.cost_per_token(
        model=MODEL,
        prompt_tokens=input_tokens,
        completion_tokens=output_tokens,
    )
    actual_cost = prompt_cost + completion_cost
    cost_guard.record_cost(
        agent_id=AGENT_ID,
        task_id=f"task-{int(time.time()*1000)}",
        cost_usd=actual_cost,
        breakdown={"input": prompt_cost, "output": completion_cost},
    )

    return {
        "status": "success",
        "response": result["messages"][-1].content,
        "tool_calls": tool_calls,
        "risk_level": assessment.risk_level.value,
    }

処理フローは以下のようになります。

  1. コストチェック: cost_guard.check_task()で予算オーバーを事前ブロック
  2. サーキットブレーカー: breaker.call()で包むだけで成功/失敗が自動記録される。breaker.call()は任意の例外を失敗としてカウントしてから再送出するため、except Exceptionで受ける必要があります(RuntimeErrorだけだとlitellm例外などをすり抜けます)
  3. 不正エージェント検出: detector.record_action()でツール使用を記録し、assess()でリスクレベルを評価
  4. SLO記録: slo.indicators[0].record()でSLIに値を入れた上で、slo.record_event()を呼ぶことでerror_budget記録とevaluate()がまとめて走る。slo.error_budget.record_event()を直接呼ぶとSLO.evaluate()が呼ばれず、ステータスが古い値のままになる点に注意
  5. 実コストの計算と記録: AIMessageのusage_metadataからトークン使用量を集計し、litellm.cost_per_token()で実コストに変換してcost_guard.record_cost()へ。usage_metadataが空のプロバイダーではコストが0として記録される点には注意

手書き実装ではrecord_success() / record_failure()を明示的に呼ぶ必要がありますが、breaker.call()を使うと例外を投げれば失敗・正常終了なら成功として自動計上されるので非常にシンプルに書けます。コストについてもagent-sre側は自動フックしませんが、LiteLLMのcost_per_token()と組み合わせることでトークンベースで正確に計上できます。

エージェントの動作確認

実装したコードを実行して動作を確認していきます。

$ python demo.py

正常系の動作確認

まずは正常系です。LangGraph + LiteLLMエージェントが実際にツールを呼び出してタスクを完了し、各メトリクスが記録される様子を確認します。

============================================================
デモ 1: エージェント実行と Agent SRE 監視
============================================================

  質問 1: 東京タワーの高さは何メートルですか?
  回答: 東京タワーの高さは333メートルです。1958年に完成しました。
  レイテンシ=3.47s  ツール=['web_search']  リスク=low
  SLO バジェット残=100.0%  累積コスト=$0.0001

  質問 2: 123 * 456 を計算してください。
  回答: 123 * 456 の計算結果は 56088 です。
  レイテンシ=1.72s  ツール=['calculator']  リスク=low
  SLO バジェット残=100.0%  累積コスト=$0.0001

  質問 3: 東京の天気を教えてください。
  回答: 東京の天気は晴れで、気温は25°C、湿度は60%です。
  レイテンシ=1.78s  ツール=['get_weather']  リスク=low
  SLO バジェット残=100.0%  累積コスト=$0.0002

各タスクでLLMが適切なツールを1つ選択し、リスクレベルlow、SLOバジェット100%、累積コストも3タスクで$0.0002と微小(gpt-4o-miniはトークン単価が低いため)で健全な状態で動作していることが確認できます。コストはLiteLLMが持つcost_per_token()で計算した実値がCostGuardに記録されています。

サーキットブレーカーの動作確認

動作確認の待ち時間を短縮するため、ここではrecovery_timeout=2.0で再初期化します(前述のinit_sre_components()は引数で上書き可能)。

demo.py
breaker, slo, detector, cost_guard = init_sre_components(
    failure_threshold=3, recovery_timeout=2.0,
)

その上で3回連続失敗を注入し、サーキットブレーカーの動作を確認します。

============================================================
デモ 2: サーキットブレーカー (失敗シミュレーション)
============================================================
  リクエスト 1: failed          CB=CLOSED
  リクエスト 2: failed          CB=CLOSED
  リクエスト 3: failed          CB=OPEN
  リクエスト 4: circuit_open    CB=OPEN
  リクエスト 5: circuit_open    CB=OPEN

  (recovery_timeout=2秒 待機...)
  リクエスト 6 (復帰): success         CB=CLOSED

failure_threshold=3で設定したため、3回連続失敗でCLOSED → OPENに遷移しました。以降はCircuitOpenErrorが投げられてLLM呼び出しは行われません。recovery_timeout=2秒経過後にHALF_OPEN(復旧確認)に遷移し、試行が成功するとCLOSEDに復帰することが確認できます。

重要なのはOPENの間はLLM API呼び出しが発生しない点で、劣化したエージェントによるAPI費用の浪費を防げます。

SLOとエラーバジェットの動作確認

成功と失敗が混在するシナリオで、SLO達成率とエラーバジェットの消費状況をリアルタイムに追跡します。

============================================================
デモ 3: SLO トラッキングとエラーバジェット
============================================================
  SLO: タスク成功率 >= 95%  エラーバジェット: 5%

  [1] success         バジェット残=100.0%  バーンレート= 0.00  SLO=healthy    アラート=[]
  [2] success         バジェット残=100.0%  バーンレート= 0.00  SLO=healthy    アラート=[]
  [3] failed          バジェット残=  0.0%  バーンレート=24000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [4] success         バジェット残=  0.0%  バーンレート=18000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [5] failed          バジェット残=  0.0%  バーンレート=28800.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [6] success         バジェット残=  0.0%  バーンレート=24000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [7] success         バジェット残=  0.0%  バーンレート=20571.43  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']
  [8] success         バジェット残=  0.0%  バーンレート=18000.00  SLO=exhausted  アラート=['burn_rate_warning', 'burn_rate_critical']

タスク3で最初の失敗が発生した時点でバジェット残0.0%になり、SLO=exhaustedに遷移、burn_rate_warningburn_rate_criticalの両アラートが発火していることがわかります。前述のとおりagent-sre 3.5.0 の実装では失敗1件でconsumed += 1.0になるため、total=0.05だと1件で即枯渇します。バーンレート値が大きくなっているのは、本来30日〜1時間スケールの消費を秒単位のデモで計測しているためで、実運用では長時間のイベントで測るのでもっと現実的な値に収まります。

不正エージェント検出の動作確認

RogueAgentDetectorの3シグナル(capability / frequency / entropy)がどう動くかをシナリオ別に確認します。

============================================================
デモ 4: 不正エージェント検出(3シグナル別)
============================================================
  A) 正常              リスク=low       スコア=0.00  隔離推奨=False
    capability=0.00  frequency=0.00  entropy=0.00
  B) 許可外ツール多用  リスク=low       スコア=0.50  隔離推奨=False
    capability=0.50  frequency=0.00  entropy=0.00
  C) ループ (低entropy)  リスク=medium    スコア=1.00  隔離推奨=False
    capability=0.00  frequency=0.00  entropy=1.00
  D) バースト攻撃     リスク=critical  スコア=18.30  隔離推奨=True
    capability=0.00  frequency=18.30  entropy=0.00

シナリオごとの読み方は以下の通りです。

  • A) 正常: 複数ツールを適度に使用 → 全スコア0、リスクlow
  • B) 許可外ツール多用: calculatorallowed_tools外として登録した状態で50%使用 → capability=0.50
  • C) ループ: 同一アクションを繰り返し → エントロピーがlow_threshold=0.3を下回ってentropy=1.00、リスクmedium
  • D) バースト攻撃: ベースラインに対して突然呼び出し頻度を急増 → Zスコアでfrequency=18.30、リスクcriticalで隔離推奨

composite_score >= 2.0HIGH>= 3.0CRITICALに分類され、quarantine_risk_level=HIGH以上になるとquarantine_recommended=Trueとなることが確認できます。

コストガードの動作確認

デモ用に上限を低めに再設定したエージェントで、コスト超過をシミュレートします(前述のinit_sre_components()per_agent_daily_limit=1.00ですが、ここでは動作を見やすくするために$0.50に下げています)。

demo.py
guard = CostGuard(
    per_task_limit=0.10,        # 1タスク上限 $0.10
    per_agent_daily_limit=0.50, # 1エージェントの1日上限 $0.50
    auto_throttle=True,
    kill_switch_threshold=0.95,
)
============================================================
デモ 5: コストガード
============================================================
  タスク1(通常): allowed=True  支出=$0.05/0.50  利用率=10%
  タスク2(通常): allowed=True  支出=$0.10/0.50  利用率=20%
  タスク3(通常): allowed=True  支出=$0.15/0.50  利用率=30%

  大型タスク: allowed=False  reason=Estimated cost $0.50 exceeds per-task limit $0.10
  タスク1: 支出=$0.20  利用率= 40%  状態=ok  アラート=[]
  タスク2: 支出=$0.25  利用率= 50%  状態=ok  アラート=['warning']
  タスク3: 支出=$0.30  利用率= 60%  状態=ok  アラート=[]
  タスク4: 支出=$0.35  利用率= 70%  状態=ok  アラート=[]
  タスク5: 支出=$0.40  利用率= 80%  状態=ok  アラート=['warning']
  タスク6: 支出=$0.45  利用率= 90%  状態=throttled  アラート=['warning']
  タスク7: 支出=$0.50  利用率=100%  状態=throttled,killed  アラート=['critical', 'critical', 'critical']
  タスク8: 支出=$0.55  利用率=110%  状態=throttled,killed  アラート=['critical']

以下のように段階的に保護機構が働いていることがわかります。

  • 大型タスク: check_task()で事前判定して、1タスク上限$0.10を超える見積もりを即ブロック
  • 利用率50% / 80%: warningアラートが定期的に発火
  • 利用率90%: auto_throttle=Trueによりthrottledに遷移
  • 利用率100%kill_switch_threshold=0.95超過): killed状態に遷移し、criticalアラート発火

本番ではper_agent_daily_limitにもっと現実的な値を設定することになります。

運用Tips: 永続化と再起動時の状態管理

ここまで紹介した4コンポーネントはすべてインメモリで状態を保持しているため、プロセス再起動で累積値がリセットされます。本番運用では「再起動でリセットされてもよいもの」と「永続化しないと意味をなさないもの」を区別する必要があります。

コンポーネント 再起動で失う情報 実運用への影響 永続化要否
CircuitBreaker failure_countstate 軽微(劣化が継続していれば数リクエストで再OPEN、健全ならCLOSEDのまま) 不要
RogueAgentDetector 行動履歴・頻度ベースライン 軽微(数百サンプルで自然に再構築される) 不要
SLI 個別の測定値 致命的(評価窓内のデータが空になり、SLOステータスがUNKNOWNに戻る) 必須
ErrorBudget consumed・イベント履歴 致命的(月次SLO違反の追跡が一回の再起動で消える) 必須
CostGuard spent_today_usdthrottledkilledorg_spent_monthorg_killed_cost_history 致命的(日次・月次・組織キルスイッチがすべて素通り、Z-score異常検知も盲点期間が発生) 必須

SLI の永続化(公式サポート)

agent-sre は SLI レベルで SQLiteMeasurementStore を標準提供しており、SLI のコンストラクタに store= で渡すだけで永続化できます。

demo.py
from agent_sre.slo.persistence import SQLiteMeasurementStore

store = SQLiteMeasurementStore(db_path="~/.agent_sre/sli.db")
sli = TaskSuccessRateSLI(
    name="task_success_rate", target=0.95, window="1h", store=store,
)

db_pathはホームディレクトリ・カレントディレクトリ・システムtempの配下のみ許可されており、それ以外のパスを渡すとValueErrorになります(パスインジェクション対策)。:memory:を渡せばインメモリSQLiteとしてテスト用途にも使えます。

ErrorBudget・CostGuard の永続化(自前実装が必要)

agent-sre 3.5.0 時点では、ErrorBudgetCostGuard には from_dict() のような復元APIが提供されていません。代わりに to_dict() でスナップショットを取得し、起動時に手動で内部状態を書き戻す形になります。

ErrorBudget.to_dict()の出力例:

{
  "total": 5.0,
  "consumed": 1.0,
  "remaining_percent": 80.0,
  "is_exhausted": false,
  "burn_rate": 360.0,
  "exhaustion_action": "throttle",
  "firing_alerts": ["burn_rate_warning", "burn_rate_critical"]
}

CostGuard.get_budget(agent_id).to_dict() の出力例:

{
  "agent_id": "ag1",
  "daily_limit_usd": 1.0,
  "per_task_limit_usd": 0.1,
  "spent_today_usd": 0.15,
  "remaining_today_usd": 0.85,
  "utilization_percent": 15.0,
  "task_count_today": 2,
  "avg_cost_per_task": 0.075,
  "throttled": false,
  "killed": false
}

実運用では以下のように定期スナップショットを取り、再起動時に手動復元する形が現実的です(疑似コード、redisjson は別途初期化済み前提)。

demo.py
# シャットダウン時 / 定期的にスナップショットを保存
snapshot = {
    "error_budget": slo.error_budget.to_dict(),
    "cost_budgets": {aid: cg.get_budget(aid).to_dict() for aid in agent_ids},
}
redis.set("agent-sre:state", json.dumps(snapshot))

# 起動時に復元(from_dict が無いので内部状態を直接書き戻す)
saved = json.loads(redis.get("agent-sre:state"))
# consumed は失敗イベント件数の累積(record_event ごとに +1.0 される値)
slo.error_budget.consumed = saved["error_budget"]["consumed"]
slo.evaluate()  # consumed を書き戻した後に SLOStatus を明示的に再評価しておく
for aid, b in saved["cost_budgets"].items():
    budget = cost_guard.get_budget(aid)
    budget.spent_today_usd = b["spent_today_usd"]
    budget.task_count_today = b["task_count_today"]
    budget.throttled = b["throttled"]
    budget.killed = b["killed"]
# 注: _cost_history(Z-scoreベースライン)は private 属性のため公式の書き戻し手段が無く、
# 再起動直後は履歴 10 件未満の間 Z-score 異常検知が一時的に無効化される点に留意。

特に CostGuard を永続化しない場合、Pod再起動のたびに日次上限が初期化されて予算ガードが事実上無効になるので注意が必要です。

reset_daily() は自前でスケジュールが必要

なお、コスト管理については LLM プロバイダー側の Usage API(OpenAIの/v1/usage等)を真の値として取得する方が信頼性が高いので、CostGuardを完全自前永続化するよりも、プロバイダーAPIで起動時にその日の累計を取得し直してAgentBudget.spent_today_usdにセットする運用の方が実用的かもしれません。ただし/v1/usageは集計の反映に数時間〜24時間程度の遅延が出る場合があるため、プリフライト判定には引き続きlitellm.cost_per_token()を使い、起動時の累積復元と日次リコンサイル(突き合わせ)の用途に限って Usage API を利用するのが現実解です。

まとめ

Microsoftが公開しているagent-sreパッケージをLangGraph + LiteLLMエージェントに統合して、Agent SREの各機能を試してみました。

  • CircuitBreaker: breaker.call()でラップするだけで障害の連鎖を自動遮断、OPEN中はLLM API呼び出しが発生しないのでコストも保護される
  • SLO / ErrorBudget: SLIを継承してタスク成功率を定義し、slo.record_event()経由でerror_budgetevaluate()をまとめて更新。ExhaustionActionは自動発動ではなくヒント値なので、呼び出し側でアクション発動を実装する
  • RogueAgentDetector: capability / frequency / entropyの3シグナルで暴走・不正使用・バースト攻撃を横断的に検出
  • CostGuard: プリフライトのcheck_task()と事後のrecord_cost()で、予算超過を自動スロットル/キル。LiteLLMのcost_per_token()と組み合わせるとトークン使用量ベースの実コストで正確に管理できる

単発のLLM呼び出しにはオーバーエンジニアリングになりますが、ツール呼び出しをループ的に繰り返すエージェントにはAgent SREの仕組みが有効に働きます。自前で同等の機能を実装するとかなりの工数になりますが、uv pip install agent-sreだけで実用水準の信頼性管理を手に入れられるので、エージェントを本番運用する際にはぜひ導入を検討してみてください。

ただし本記事の運用Tipsセクションで触れた通り、SLI/ErrorBudget/CostGuardは永続化しないと再起動でリセットされてしまい本来の役割を果たせなくなる点には注意が必要です。SLIはSQLiteMeasurementStoreで公式サポートされていますが、ErrorBudget/CostGuardto_dict()スナップショットから手動復元する形になるので、ここは自前の運用設計が必要になります。

最後まで読んで頂いてありがとうございました。


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事