1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

システム監視をしてくれるボットを作成した話【後編:段階的な進化とRCA自動分析の実装】

1
Posted at

前編はこちら

本記事ではオブザーバビリティ(o11y)ツールとして New Relic を例に紹介していますが、Datadog・Grafana・Splunk・Dynatraceなど、MCP サーバーを提供している(または自作できる)o11y ツールであれば同様のアプローチが可能です。ご自身の環境に合わせて読み替えてください。

前編ではMCPの概要と全体構成を紹介しました。後編では、3段階にわたる実装の進化を、実際のコードを交えて紹介します。ぜひ後を追ってAIとのペアプロで作成をしてみてください。

第1世代: ツール直接呼び出し型

最初に作ったBotは、LLM(AI)を一切使わないシンプルな構造でした。何も考えず、とりあえずMCPサーバーを使ってみたいという一心で作成しました。

Teams/Slack
    ↓  ユーザーが「ツール名 {引数JSON}」と入力
botHandler.js (ActivityHandler)
    ↓  テキストをパースしてツール名・引数を抽出
httpMcpClient.js (自作HTTPクライアント)
    ↓  JSON-RPC 2.0 over Streamable HTTP
New Relic MCP Server

ユーザーの操作

# ツール一覧を表示
ツール一覧

# NRQLを実行(ユーザーが自分でNRQLを書く)
query_nrql {"nrql": "SELECT average(cpuPercent) FROM SystemSample WHERE hostname = 'my-server' SINCE 1 hour ago"}

MCPクライアントのコア部分です。New Relic MCPサーバー(https://mcp.newrelic.com/mcp/)にHTTPSでJSON-RPCリクエストを送ります。
参考資料:NewRelic公式ドキュメント「New Relic MCP を設定する」

class McpClient {
  constructor(mcpUrl, apiKey) {
    this._url = new URL(mcpUrl);
    this._apiKey = apiKey;
    this._sessionId = null;
    this._reqId = 0;
  }

  async callTool(name, args = {}) {
    await this._ensureInitialized();
    return this._post({
      jsonrpc: '2.0',
      id: ++this._reqId,
      method: 'tools/call',
      params: { name, arguments: args }
    });
  }
}

Bot側では、ユーザー入力をスペースで分割してツール名と引数を抽出するだけです。

// <ツール名> [{json}] 形式のパース
const spaceIdx = text.search(/\s/);
const toolName = spaceIdx === -1 ? text : text.slice(0, spaceIdx);
const argText  = spaceIdx === -1 ? '' : text.slice(spaceIdx).trim();

let toolArgs = {};
if (argText) {
  try {
    toolArgs = JSON.parse(argText);
  } catch {
    toolArgs = { query: argText };
  }
}

第1世代の限界

  • NRQLを書けない人は使えない
  • JSONでの記述を手作業なんてありえない
  • 1回に1ツールしか実行できない
  • 結果は生データがそのまま返ってくるだけで、解釈は人間任せ

結局、New Relicのコンソールを開くのと手間が変わりません。 このままではMCPサーバーを直接叩けるBotを用意しただけで、なんの意味もないので、AIを組み込むことにしました。

第2世代: AI Agent化

OpenAI APIの Function Calling を組み込んで、自然言語での問い合わせに対応しました。

「xxxサーバーのメモリが上昇しているが原因を調べて」
    ↓ LLMが自動でNRQL生成・実行・分析
「○○プロセスのメモリが△△GBに増加しており、原因と考えられます」

ReActループの実装

第2世代の心臓部が ReActループ(Reasoning + Acting)です。LLMが「考える → ツールを呼ぶ → 結果を見る → また考える」を繰り返します。

// ReAct ループ: LLM が最終回答を返すまでツール呼び出しを継続
for (let i = 0; i < MAX_ITERATIONS; i++) {
  const response = await this._openai.chat.completions.create({
    model: this._model,
    messages,
    tools: this._openaiTools,    // MCPツール一覧をOpenAI Function形式に変換したもの
    tool_choice: 'auto'          // LLMにツール選択を任せる
  });

  const choice = response.choices[0];
  messages.push(choice.message);

  // ツール呼び出しなし → LLM の最終回答
  if (choice.finish_reason !== 'tool_calls') {
    return choice.message.content ?? '(回答なし)';
  }

  // LLM が選択したツールを MCP で実行
  for (const call of choice.message.tool_calls) {
    const toolName = call.function.name;
    const toolArgs = JSON.parse(call.function.arguments);

    // ツール名からルーティング先の MCP クライアントを特定
    const mcpClient = this._toolClientMap[toolName];
    const toolResult = await mcpClient.callTool(toolName, toolArgs);

    messages.push({
      role: 'tool',
      tool_call_id: call.id,
      content: JSON.stringify(toolResult)
    });
  }
}

ポイントは this._toolClientMap です。MCPサーバーが複数(New Relic / Azure)あるので、ツール名からどのサーバーに送るかを自動ルーティングしています。

async _executeBuildOpenAITools() {
  const allTools = [];
  this._toolClientMap = {};

  for (const { name, client } of this._mcpClients) {
    const result = await client.listTools();
    const tools = result?.tools ?? [];

    for (const t of tools) {
      allTools.push({
        type: 'function',
        function: {
          name: t.name,
          description: `[${name}] ${t.description ?? ''}`,
          parameters: t.inputSchema ?? { type: 'object', properties: {} }
        }
      });
      // ツール名 → MCPクライアントのマッピングを記録
      this._toolClientMap[t.name] = client;
    }
  }
  return allTools;
}

Azure MCP の stdio 接続

Azure MCPサーバーは New Relic MCPとは異なり、SaaS型のHTTPエンドポイントではなく、@azure/mcp パッケージをローカル子プロセスとして起動します。stdin/stdout経由で JSON-RPC をやりとりします。

// MCP SDK の StdioClientTransport を使用
const { Client, StdioClientTransport } = await this._loadSdk();

this._transport = new StdioClientTransport({
  command: this._command,      // 'node'
  args: this._args,            // ['stdoutFilter.js', 'npx', '-y', '@azure/mcp@latest', ...]
  env: this._buildEnv(),
  cwd: this._cwd,
  stderr: 'pipe'
});

await this._client.connect(this._transport);

ところが、ここでひとつハマりました。@azure/mcp は内部で .NETバイナリを起動するのですが、この .NETプロセスが Azure API呼び出し時に System.DllNotFoundException のスタックトレースを stdoutに混入 させてしまいます。stdoutはJSON-RPCのチャネルなので、MCP SDKのJSONパースが壊れてしまいます。

そこで フィルタースクリプト を間に挟みました。JSON として有効な行だけを通し、それ以外はstderrに逃がします。

// stdoutFilter.js — stdout の JSON-RPC フィルター
const stdoutRl = readline.createInterface({
  input: child.stdout,
  crlfDelay: Infinity
});

stdoutRl.on('line', (line) => {
  const trimmed = line.trim();
  if (!trimmed) return;

  try {
    JSON.parse(trimmed);
    // 有効な JSON 行のみ親プロセスに転送
    process.stdout.write(line + '\n');
  } catch {
    // JSON でない行(DLL エラー等)は stderr にリダイレクト
    process.stderr.write(`[filter] ${line}\n`);
  }
});

第1世代 vs 第2世代 の比較

第1世代(ツール直接呼び出し) 第2世代(AI Agent)
ユーザー入力 query_nrql {"nrql": "SELECT ..."} 「xxxサーバーのメモリを確認して」
クエリ生成 ユーザーが手書き LLM が自動生成
ツール選択 ユーザーが明示 LLM が自動選択
複数ツール連携 1回ずつ手動実行 ReActループで自動連鎖
結果の解釈 生データそのまま LLMが日本語で要約・分析
対応 MCP New Relic のみ Azure MCP + New Relic MCP

第3世代: RCA(根本原因分析)対応

第2世代で十分実用的になりましたが、もう一歩踏み込みました。アラートが来たときの 「原因は何?」 に自動で答えられるようにしたいと考えました。

RCA モードの自動起動

ユーザーの質問に「原因」「調査」「なぜ」などのキーワードが含まれると、RCA モードが自動起動します。

const RCA_KEYWORDS = /(原因|rca|root cause|根本原因|障害.*調査|アラート.*調べ|...|troubleshoot)/i;

async handle(userMessage, onProgress) {
  const isRcaMode = RCA_KEYWORDS.test(userMessage);
  if (isRcaMode) {
    console.log('[Agent] RCA モードで調査を開始します');
  }

  const messages = [
    { role: 'system', content: this._buildSystemPrompt(userMessage, isRcaMode) },
    { role: 'user', content: userMessage }
  ];
  // ...
}

調査戦略のシステムプロンプト埋め込み

RCAモード時は、アラート種別ごとの調査手順をシステムプロンプトに丸ごと埋め込みます。これが第3世代のキモです。

本来皆さんが使っている様々なアプリのAI機能もパフォーマンスを向上させるために同じようなことをしていると思います。

## 根本原因分析 (RCA) の調査戦略

### Step 1: アラート情報の収集
- list_recent_issues で直近のインシデント一覧を取得
- search_incident で該当アラートの詳細を確認
- list_alert_conditions でアラートの発火条件を確認

### Step 2: 仮説に基づくメトリクス検証
#### CPU 高負荷アラート
1. CPU 使用率の時系列推移を確認
2. プロセス別の CPU 消費を確認 (ProcessSample)
3. 同一ホスト上の他メトリクス (メモリ、ディスク I/O) も併せて確認
4. 直近のデプロイやスケール変更がないか確認

### Step 3: Azure 側の裏取り(Azure MCP が利用可能な場合)
- リソースの構成変更(スケール変更、設定変更)
- アクティビティログ(直近の操作履歴)
- デプロイ履歴(直近のリリース)

NRQLテンプレートも一緒に埋め込みます。LLMはこのテンプレートを参考にNRQLを自動生成します。

### CPU 使用率の推移 (直近30分 vs 1時間前)
SELECT average(cpuPercent) FROM SystemSample
WHERE hostname = '{hostname}'
SINCE 30 minutes ago COMPARE WITH 1 hour ago
TIMESERIES 1 minute

### プロセス別 CPU 消費 TOP10
SELECT average(cpuPercent) FROM ProcessSample
WHERE hostname = '{hostname}'
SINCE 30 minutes ago FACET processDisplayName LIMIT 10

MCP サーバー横断調査

第3世代のもう一つの進化が Azure MCP ↔ New Relic MCP の横断調査 です。ReActループの中で、どのサーバーのツールを使ったかを追跡しています。

// MCP サーバー横断のコンテキスト: このループで呼び出したツールのサーバー名を記録
const serversUsedThisLoop = new Set();

for (const call of toolCalls) {
  const serverName = this._toolServerMap[toolName];
  serversUsedThisLoop.add(serverName);
  // ...
}

システムプロンプトにも横断調査の指示を入れています。

## MCP サーバー横断調査の指示

- New Relic → Azure: New Relic でアラートやメトリクス異常を検出したら、
  そのホスト名を使って Azure 側のリソース構成・アクティビティログを確認
- Azure → New Relic: Azure でリソースの構成変更やデプロイを検出したら、
  その時刻前後の New Relic メトリクスやログを確認して影響を評価

中間報告コールバック

RCAは調査に時間がかかります。特にSlackには30秒のレスポンスタイムアウトがあるため、途中経過をプロアクティブに送信 する仕組みを入れました。

// 中間報告: 一定間隔で調査進捗をユーザーに通知
if (onProgress && (i + 1) % PROGRESS_REPORT_INTERVAL === 0) {
  const serversUsed = [...serversUsedThisLoop].join(', ');
  const toolNames = toolCalls.map((c) => c.function.name).join(', ');
  await onProgress(
    `🔍 [調査中] ステップ ${i + 1}/${MAX_ITERATIONS}${toolNames} を実行しました (${serversUsed})。引き続き調査を進めています...`
  );
}

Bot側では、コールバックでTeams/Slackにメッセージを送信します。

const onProgress = async (progressMessage) => {
  try {
    await context.sendActivity(MessageFactory.text(progressMessage));
  } catch (err) {
    console.warn('[Bot] 中間報告の送信に失敗:', err.message);
  }
};

const answer = await this._agent.handle(text, onProgress);

3段階の進化まとめ

第1世代 第2世代 第3世代
LLM なし OpenAI Function Calling 同左 + RCAプロンプト拡張
対応MCP New Relic のみ (HTTP) New Relic + Azure (HTTP + stdio) 同左 + 横断調査
操作方法 ツール名とJSON引数を手打ち 自然言語で質問 同左(RCA自動起動)
分析能力 なし(生データ返却) LLMによる要約・分析 体系的RCA + NRQLテンプレート
ReActループ なし 最大10回 最大15回 + 中間報告
ステータス 廃止 本番稼働中 開発完了

ハマったポイント

Azure MCP の .NET DLL エラー問題

前述の通り、@azure/mcp の .NETバイナリがstdoutにエラーを吐く問題です。App Service Linux 環境で /root が noexec マウントの場合、ネイティブDLLのロードに失敗します。

対処として環境変数で回避しました。

const env = {
  DOTNET_BUNDLE_EXTRACT_BASE_DIR: '/tmp/azure-mcp',  // /root の代わりに /tmp を使用
  DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: '1',         // libicu 非依存モード
  HOME: '/tmp',                                        // /root への書き込み回避
  DOTNET_NOLOGO: '1',                                  // .NET のロゴ出力を抑制
  NO_COLOR: '1',                                       // ANSIカラーコード抑制
};

Azure MCP のマネージドID認証が使えない

@azure/mcp の .NETバイナリに App Service の IDENTITY_ENDPOINT / IDENTITY_HEADER が引き継がれないため、マネージドID認証が使えません。代わりにサービスプリンシパル(EnvironmentCredential)で認証しています。

// App Service の環境変数 → 子プロセスの環境変数にマッピング
if (process.env.AZURE_MCP_SP_CLIENT_ID) {
  env.AZURE_CLIENT_ID = process.env.AZURE_MCP_SP_CLIENT_ID;
}
if (process.env.AZURE_MCP_SP_CLIENT_SECRET) {
  env.AZURE_CLIENT_SECRET = process.env.AZURE_MCP_SP_CLIENT_SECRET;
}

サブプロセスのクラッシュ検知

Azure MCPのstdioプロセスが予期せずクラッシュすることがあります。連続エラーを検知して自動復旧する仕組みを入れました。

this._transport.onerror = (error) => {
  this._consecutiveErrors++;
  if (this._consecutiveErrors >= 5 && this._initialized) {
    // サブプロセスクラッシュと判定して接続をリセット
    this._initialized = false;
    this._consecutiveErrors = 0;
    // 30秒間のクールダウンで連続クラッシュループを防止
    this._crashCooldownUntil = Date.now() + 30000;
    this._client.close().catch(() => {});
  }
};

ソースコード構成

最終的な第3世代のファイル構成は以下の通りです。

ファイル 役割
server.js サーバー起動・環境変数チェック・ヘルスチェック
botHandler.js Teams ActivityHandler。MCP設定の構築、認証・環境変数マッピング
aiAgent.js ReActループ実行。LLM Function Calling → MCP呼び出し → 結果返却の反復
httpMcpClient.js MCP HTTPクライアント(New Relic MCP用)。JSON-RPC 2.0 over Streamable HTTP
stdioMcpClient.js MCP stdioクライアント(Azure MCP用)。@modelcontextprotocol/sdk 使用
stdoutFilter.js @azure/mcp の stdout フィルター。DLLエラーを除去

依存パッケージは最小限です。

{
  "dependencies": {
    "@modelcontextprotocol/sdk": "latest",
    "botbuilder": "^4.23.0",
    "openai": "^4.89.0",
    "restify": "^11.1.0",
    "dotenv": "^16.4.0"
  }
}

今後やりたいこと

  • Adaptive Cards 対応: ReActループの中間結果をカード形式でリッチに表示したい
  • 会話の記憶: 現在は1問1答。過去の質問コンテキストを引き継いで深掘りできるようにしたい
  • 定期レポート: 毎朝「昨夜のアラート概要」を自動投稿する機能
  • PagerDuty連携: アラートの対処アクション(再起動、スケールアウト等)をBotから実行
  • コスト最適化: LLMの呼び出し回数を減らすためのキャッシュやプリフェッチ

まとめ

  • MCP(Model Context Protocol)を使うことで、New Relic と Azure の監視データに AI から統一的にアクセスできる環境を構築しました
  • 3段階の改良を経て、自然言語での質問 → AI による自動調査 → 日本語レポート という流れを実現しました
  • 複数MCPサーバーの横断調査は、障害原因の迅速な特定に非常に有効です
  • MCPはまだ発展途上ですが、AI × 監視の領域では大きなポテンシャルを感じました
  • 今回はAIとのペアプロで進めましたが、ここまでわずか1週間で構築を終えました。そのことにも、AIの大きなポテンシャルを感じました。

監視業務の効率化に興味がある方は、ぜひMCPサーバーの導入を検討してみてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?