AI活用を推進する企業が増える中、「実際にどれだけAIを使っているのか」を定量的に示せる企業はほとんどない。京谷商会では、Claude Codeの全セッションのトークン消費量を自動計測し、公開ダッシュボードとしてウェブサイト上に表示するシステムを構築した。本記事では、その設計思想から実装詳細までを解説する。

なぜAI活用を数値化する必要があるのか

「AIを活用しています」という宣言は簡単だが、それを裏付けるデータを持つ企業は少ない。京谷商会では93名以上のAIスタッフがkuevicoシステム上で稼働しており、日常業務のかなりの部分がClaude Codeによって実行されている。しかし「かなりの部分」では説明にならない。

数値化が必要な理由は3つある。

  1. 社内KPIとしての活用度管理AI活用が進んでいるのか停滞しているのか、トレンドを把握する必要がある
  2. 対外的な信頼性の担保 — クライアントやパートナーに対して、AI活用の実績を客観的に示せる
  3. コスト対効果の基礎データ — トークン消費量は直接的なコスト指標であり、投資対効果の分析に不可欠

ダッシュボードの概要

上のスクリーンショットが実際に公開しているダッシュボードである。2026年2月23日の計測開始以降、以下の数値を記録している。

  • 累計トークン数: 695億トークン(69.5B tokens)
  • 総セッション数: 373セッション
  • 稼働日数: 31日
  • 稼働時間: セッションの合計時間を分単位で自動集計
  • 30日間トレンドチャート: 日別のトークン消費量を折れ線グラフで表示

このダッシュボードは京谷商会のコーポレートサイト(www.kyotanishokai.co.jp)および全18ポータルサイトに表示されており、誰でもリアルタイムにAI活用状況を確認できる。

アーキテクチャ全体像

データの流れは以下の通りだ。

Claude Code セッション (.jsonl)
    ↓ Python解析スクリプト
Cloudflare D1 (ai_usage_sessions テーブル)
    ↓ Hono API エンドポイント
公開API (GET /api/ai-usage/stats)
    ↓ JavaScript fetch
ウェブサイトウィジェット (チャート描画)

全体がCloudflare Workers + D1 + Honoスタックで統一されており、サーバーレスで完全に動作する。外部サービスへの依存はゼロだ。

Maxプランの制約と解決策

ここで一つ重要な前提がある。京谷商会ではClaude CodeのMaxプランを利用しているが、MaxプランではAnthropic APIから利用量データを取得できない。APIプランであれば /v1/usage エンドポイントで月間トークン使用量を参照できるが、Maxプランはそもそもトークン課金制ではないため、この仕組みが使えない。

そこで採用したのが、Claude Codeがローカルに生成するセッションJSONLファイルを直接解析するというアプローチだ。Claude Codeは ~/.claude/projects/ 配下にセッションごとのJSONLファイルを保存しており、各メッセージのusageフィールドにトークン使用量が記録されている。

実装詳細

1. セッションJSONLの解析

Claude Codeのセッションファイルは1行1JSONのJSONL形式で、各行にメッセージオブジェクトが含まれる。トークン情報は message.usage フィールドに格納されている。

def parse_session(filepath):
    """セッションJSONLファイルを解析してトークン使用量を集計する"""
    totals = {
        "input_tokens": 0,
        "output_tokens": 0,
        "cache_creation_tokens": 0,
        "cache_read_tokens": 0,
    }
    message_count = 0
    models = set()
    first_ts = None
    last_ts = None

    with open(filepath, "r", encoding="utf-8", errors="replace") as f:
        for line in f:
            try:
                obj = json.loads(line)
            except json.JSONDecodeError:
                continue

            if "message" in obj and "usage" in obj.get("message", {}):
                usage = obj["message"]["usage"]
                totals["input_tokens"] += usage.get("input_tokens", 0)
                totals["output_tokens"] += usage.get("output_tokens", 0)
                totals["cache_creation_tokens"] += usage.get(
                    "cache_creation_input_tokens", 0
                )
                totals["cache_read_tokens"] += usage.get(
                    "cache_read_input_tokens", 0
                )
                message_count += 1
                model = obj["message"].get("model")
                if model:
                    models.add(model)

            ts = obj.get("timestamp")
            if ts:
                if first_ts is None:
                    first_ts = ts
                last_ts = ts

    total_all = sum(totals.values())
    return {
        "session_id": os.path.basename(filepath).replace(".jsonl", ""),
        "total_tokens": total_all,
        "message_count": message_count,
        # ... 省略
    }

ポイントは4種類のトークンを全て集計していることだ。

トークン種別説明
`input_tokens`プロンプトに含まれるトークン数
`output_tokens`モデルが生成したトークン数
`cache_creation_input_tokens`キャッシュ作成時に消費されたトークン数
`cache_read_input_tokens`キャッシュから読み取ったトークン数

特にキャッシュ関連のトークンは見落としがちだが、1Mコンテキスト時代においてはキャッシュが総トークン数の大部分を占めることもあり、無視すると実態と大きく乖離する。

2. D1テーブル設計

集計データの格納先として、Cloudflare D1に ai_usage_sessions テーブルを作成した。

CREATE TABLE IF NOT EXISTS ai_usage_sessions (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id TEXT UNIQUE NOT NULL,
  session_date TEXT NOT NULL,           -- YYYY-MM-DD (JST)
  model TEXT,
  input_tokens INTEGER DEFAULT 0,
  output_tokens INTEGER DEFAULT 0,
  cache_creation_tokens INTEGER DEFAULT 0,
  cache_read_tokens INTEGER DEFAULT 0,
  total_tokens INTEGER DEFAULT 0,
  message_count INTEGER DEFAULT 0,
  duration_minutes INTEGER DEFAULT 0,
  started_at TEXT,
  ended_at TEXT,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX idx_ai_usage_date ON ai_usage_sessions(session_date);
CREATE INDEX idx_ai_usage_total ON ai_usage_sessions(total_tokens DESC);

設計のポイントは以下の通り。

  • session_id にUNIQUE制約を設定し、同一セッションの重複投入を防止。INSERT OR IGNOREで冪等性を確保
  • session_date はJSTベースの日付文字列。日別集計のGROUP BYで使用する
  • duration_minutes はセッション内の最初と最後のタイムスタンプから自動算出
  • インデックスは日付検索とトークン数ソートの2つ。ダッシュボードAPIのクエリパターンに最適化している

3. 公開APIエンドポイント

Hono上に認証不要の公開エンドポイントを実装した。

// GET /api/ai-usage/stats — 公開エンドポイント
aiUsageRoutes.get('/stats', async (c) => {
  const db = c.env.DB;
  const todayJST = getTodayJST();

  const [todayResult, cumulativeResult, dailyResult, recordResult] =
    await Promise.all([
      // 今日の集計
      db.prepare(`
        SELECT COUNT(*) as sessions,
          COALESCE(SUM(total_tokens), 0) as total_tokens
        FROM ai_usage_sessions WHERE session_date = ?1
      `).bind(todayJST).first(),

      // 全期間の累計
      db.prepare(`
        SELECT COUNT(*) as total_sessions,
          COALESCE(SUM(total_tokens), 0) as total_tokens,
          COUNT(DISTINCT session_date) as active_days,
          MIN(session_date) as since
        FROM ai_usage_sessions
      `).first(),

      // 直近30日の日別推移
      db.prepare(`
        SELECT session_date as date,
          COUNT(*) as sessions,
          SUM(total_tokens) as total_tokens
        FROM ai_usage_sessions
        WHERE session_date >= date(?1, '-30 days')
        GROUP BY session_date ORDER BY session_date ASC
      `).bind(todayJST).all(),

      // 1日あたりの最高記録
      db.prepare(`
        SELECT session_date as date, SUM(total_tokens) as total_tokens
        FROM ai_usage_sessions
        GROUP BY session_date ORDER BY total_tokens DESC LIMIT 1
      `).first(),
    ]);

  return c.json({
    today: { date: todayJST, sessions: todayResult?.sessions ?? 0, ... },
    cumulative: { total_sessions, total_tokens, active_days, since },
    daily: dailyResult?.results ?? [],
    record: recordResult,
    updated_at: new Date().toISOString(),
  });
});

4つのクエリを Promise.all で並列実行している点がポイントだ。D1は同一リクエスト内の並列クエリに対応しているため、レイテンシを最小限に抑えられる。

レスポンスは以下の構造で、実際に https://api.kyotanishokai.co.jp/api/ai-usage/stats で確認できる。

{
  "today": {
    "date": "2026-03-25",
    "sessions": 5,
    "total_tokens": 892341567
  },
  "cumulative": {
    "total_sessions": 373,
    "total_tokens": 69500000000,
    "active_days": 31,
    "since": "2026-02-23"
  },
  "daily": [
    { "date": "2026-02-24", "sessions": 8, "total_tokens": 1234567890 },
    ...
  ],
  "record": {
    "date": "2026-03-15",
    "total_tokens": 5678901234
  }
}

4. ウェブサイトウィジェットの実装

フロントエンドは純粋なJavaScript + Canvas APIで実装した。外部ライブラリには依存していない。

async function renderAiUsageDashboard() {
  const res = await fetch(
    'https://api.kyotanishokai.co.jp/api/ai-usage/stats'
  );
  const data = await res.json();

  // 累計トークン数の表示(人間が読みやすい形式に変換)
  const totalB = (data.cumulative.total_tokens / 1e9).toFixed(1);
  document.getElementById('ai-total-tokens').textContent = `${totalB}B`;
  document.getElementById('ai-total-sessions').textContent =
    data.cumulative.total_sessions.toLocaleString();
  document.getElementById('ai-active-days').textContent =
    data.cumulative.active_days;

  // 30日間トレンドチャートの描画
  const canvas = document.getElementById('ai-trend-chart');
  const ctx = canvas.getContext('2d');
  const daily = data.daily;
  const maxTokens = Math.max(...daily.map(d => d.total_tokens));

  daily.forEach((d, i) => {
    const x = (i / (daily.length - 1)) * canvas.width;
    const y = canvas.height - (d.total_tokens / maxTokens) * canvas.height;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  });
  ctx.stroke();
}

外部チャートライブラリを使わなかった理由は、ダッシュボードのために数十KBのバンドルを追加することがページ速度に影響するためだ。Canvas APIで直接描画することで、追加のHTTPリクエストもバンドルサイズの増加もゼロに抑えている。

5. セッション終了時の自動記録

データ投入は手動ではなく、Claude Codeのセッション終了ワークフローに組み込まれている。セッションが終了するたびにPythonスクリプトが起動し、当該セッションのJSONLを解析してD1に自動投入する。


python scripts/track-ai-usage.py --session <session_id>

このスクリプトは3つのモードを持っている。

  • --session <id>: 特定セッションのみ処理(セッション終了ワークフロー用)
  • --today: 当日の未処理セッションを一括処理
  • --all: 全履歴を一括投入(初回セットアップ用)

INSERT OR IGNOREを使用しているため、同じセッションが複数回処理されても重複は発生しない。

計測から始まる改善サイクル

現時点では「計測して公開する」段階にある。次のステップとして計画しているのは以下だ。

  1. KPI目標の設定 — 1日あたりのトークン消費量の目標値を定め、達成度を追跡する
  2. 部署別・プロジェクト別の内訳 — どの業務領域でAI活用が進んでいるかを可視化する
  3. コスト効率の分析 — output_tokensあたりの成果物(記事数、コード行数など)との相関を分析する

「測定できないものは改善できない」という原則に従えば、まず計測基盤を整えることが最優先だ。695億トークンという数値が多いのか少ないのか、それ自体は現時点では判断できない。しかしトレンドチャートを見れば活用が加速しているのか停滞しているのかは一目瞭然であり、それが行動の起点になる。

まとめ

本記事で紹介したAI活用計測ダッシュボードは、特別な技術を使っているわけではない。JSONLの解析、D1への格納、APIの公開、Canvas描画。どれも基本的な技術の組み合わせだ。

重要なのは「計測して公開する」という意思決定のほうだ。AI活用を進める企業が「どれだけ使っているか」を数値で示せる状態にしておくことは、社内の推進力を維持する上でも、対外的な信頼を得る上でも価値がある。

京谷商会のAI活用状況はリアルタイムで公開されている。興味のある方は以下のAPIエンドポイントから直接データを取得できる。

公開API: https://api.kyotanishokai.co.jp/api/ai-usage/stats