導入:急増する 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):
- GPT-4.1:$8.00
- Claude Sonnet 4.5:$15.00
- Gemini 2.5 Flash:$2.50
- DeepSeek V3.2:$0.42
為替レートが公式レート ¥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 つのパネルで構成しています:
- モデル別 p50 / p95 / p99 レイテンシ折れ線 — 直近 1 時間と過去 7 日平均を同時表示
- モデル別 1 分あたりトークン消費コスト — USD ベース積み上げ棒
- 5xx エラー率ヒートマップ — status × llm_calls.model
- trace_id 検索パネル —