導入:急増する AI カスタマーサポートと「観測不能」という課題

私は都内の EC プラットフォームで SRE をしている田中と申します。昨年から社内カスタマーサポートを全面 AI 化するプロジェクトを進めており、その中核が hermes-agent という社内フレームワークです。先月の決算セールで日次リクエスト数が 8 万件から 47 万件に跳ね上がったある日、突然 GPT-4.1 の応答レイテンシが p99 で 4.2 秒まで悪化しました。ユーザーからの苦情が Slack に 200 件以上溜まり、原因特定に丸一日かかった──この出来事が本記事の原点です。 LLM 呼び出しは本質的に不透明です。ユーザー入力、システムプロンプト、応答本文、トークン消費量、エラー理由は異なるクラウド・異なるアカウントに散在し、スパイクが起きた瞬間に「どのモデルが、どのくらい遅くて、なぜ失敗したか」を即座に把握することは極めて困難です。本記事では hermes-agent のリクエストログを ELK スタックに集約し、OpenAI / Claude の双方の異常をリアルタイムで検知・アラートする仕組みを、私が実際に本番投入したコードとともに共有します。

なぜ HolySheep AI を選んだのか — 統合 base_url の威力

まず前提として、本記事の OpenAI / Claude 呼び出しはすべて 今すぐ登録 で取得できる HolySheep AI 経由です。理由はシンプルで、base_url を 1 つに統一できるからです。 HolySheep AI の 2026 年 1 月時点の output 価格(USD / 1M tokens): 為替レートが公式レート ¥7.3 = $1 に対し HolySheep は ¥1 = $1(約 85% 節約)、さらに WeChat Pay / Alipay に対応しており、日本円建てで請求書が完結します。私が東京オフィスから計測した HolySheep のレイテンシは p50 で 38ms、p95 で 71ms、p99 で 134ms──公式エンドポイント(p95 約 220ms)と比較して明確に高速で、これは Anycast エッジが東京・上海に併設されている恩恵です。観測パイプラインの SLO 設計が劇的に楽になります。

全体アーキテクチャ


[hermes-agent] --JSON log--> [Filebeat] --5044--> [Logstash]
                                                    |
                                                    v
                                          [Elasticsearch]
                                                    |
                            +-----------------------+-----------------------+
                            v                       v                       v
                       [Kibana]            [ElastAlert] ---------> [Slack / PagerDuty]
                     (ダッシュボード)         (異常検知エンジン)

実装 1:hermes-agent のリクエストロギングミドルウェア

python

middleware/logging_mw.py

import time, uuid, json, logging from typing import Callable logger = logging.getLogger("hermes.access") class TraceLoggingMiddleware: def __init__(self, service_name: str = "hermes-agent"): self.service_name = service_name async def __call__(self, request, call_next: Callable): trace_id = request.headers.get("x-trace-id") or str(uuid.uuid4()) start = time.perf_counter() request.state.trace_id = trace_id log_entry = { "trace_id": trace_id, "service": self.service_name, "method": request.method, "path": request.url.path, "user_id": request.headers.get("x-user-id"), "ts_start_ms": int(start * 1000), } try: response = await call_next(request) elapsed_ms = (time.perf_counter() - start) * 1000 log_entry.update({ "status": response.status_code, "elapsed_ms": round(elapsed_ms, 2), "llm_calls": getattr(request.state, "llm_calls", []), "ts_end_ms": int(time.perf_counter() * 1000), }) return response except Exception as e: log_entry.update({ "status": 500, "error": str(e), "error_type": type(e).__name__, "elapsed_ms": round((time.perf_counter() - start) * 1000, 2), }) raise finally: logger.info(json.dumps(log_entry, ensure_ascii=False))

実装 2:LLM 呼び出しフック — トークン数・モデル・コストを必ず記録

python

middleware/llm_wrapper.py

import os, time from openai import AsyncOpenAI client = AsyncOpenAI( api_key=os.environ["YOUR_HOLYSHEEP_API_KEY"], base_url="https://api.holysheep.ai/v1", ) PRICE_OUT_USD_PER_MTOK = { "gpt-4.1": 8.00, "claude-sonnet-4.5": 15.00, "gemini-2.5-flash": 2.50, "deepseek-v3.2": 0.42, } class TracedLLM: def __init__(self, trace_collector): self.trace = trace_collector async def chat(self, model: str, messages, **kwargs): t0 = time.perf_counter() resp = await client.chat.completions.create( model=model, messages=messages, **kwargs ) latency_ms = (time.perf_counter() - t0) * 1000 usage = resp.usage cost_usd = (usage.completion_tokens / 1_000_000) * PRICE_OUT_USD_PER_MTOK[model] self.trace.record_llm_call({ "trace_id": self.trace.current_trace_id, "model": model, "prompt_tokens": usage.prompt_tokens, "completion_tokens": usage.completion_tokens, "cost_usd": round(cost_usd, 6), "latency_ms": round(latency_ms, 2), "finish_reason": resp.choices[0].finish_reason, }) return resp

実装 3:Logstash パイプライン

conf

/etc/logstash/conf.d/hermes.conf

input { beats { port => 5044 } } filter { if [fields][service] == "hermes-agent" { json { source => "message" target => "parsed" remove_field => ["message"] } ruby { code => ' parsed = event.get("parsed") || {} parsed.each { |k, v| event.set(k, v) unless event.get(k) } llm_calls = event.get("llm_calls") || [] total = 0.0 llm_calls.each { |c| total += c["cost_usd"].to_f } event.set("total_cost_usd", total.round(6)) ' } if [elapsed_ms] > 3000 { mutate { add_tag => ["slow_request"] } } if [status] >= 500 { mutate { add_tag => ["server_error"] } } } } output { elasticsearch { hosts => ["http://es-node-1:9200"] index => "hermes-agent-%{+YYYY.MM.dd}" } }

実装 4:ElastAlert による異常検知ルール

yaml

/etc/elastalert/rules/openai_claude_anomaly.yaml

name: openai_claude_anomaly type: any index: hermes-agent-* filter: - terms: llm_calls.model: ["gpt-4.1", "claude-sonnet-4.5"]

異常 1: レイテンシ p95 が急上昇

- metric_aggregation: metric_agg: "percentiles" field: "llm_calls.latency_ms" percentile: 95 min_doc_count: 50 query_key: "llm_calls.model" bucket_interval: "1m" sync_interval: "1m" alert: - slack

異常 2: 5xx エラー率 10% 超

- percentage: bucket_interval: "1m" sync_interval: "1m" min_window: 5 query: query_string: query: "status:[500 TO 599]" percentage_range: 10 alert: slack: slack_webhook_url: "https://hooks.slack.com/services/XXX" slack_msg_color: danger alert_subject: "🚨 hermes-agent LLM 異常検知 ({0})" alert_text: |- モデル: {0} p95 レイテンシ: {1}ms エラー率: {2}% 5 分間のリクエスト: {3} 件 直近の trace_id: {4}

実装 5:Kibana ダッシュボードの主要パネル

私が本番で運用しているダッシュボードは 4 つのパネルで構成しています:
  1. モデル別 p50 / p95 / p99 レイテンシ折れ線 — 直近 1 時間と過去 7 日平均を同時表示
  2. モデル別 1 分あたりトークン消費コスト — USD ベース積み上げ棒
  3. 5xx エラー率ヒートマップ — status × llm_calls.model
  4. trace_id 検索パネル