RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

LangfuseによるPromptOpsの実践[入門編]

この記事はノバセル テクノ場 出張版2025 Advent Calendar 2025の22日目の記事です。

目次

はじめに

Novasellで主にAIプロダクトの設計・開発を行っている浅田です。

今回は、NotionによるPromptOps運用で直面した課題と、その解決策として検討しているLangfuseについてお話しします。

私たちのチームでは、プロンプトのテンプレートをNotionのデータベースで管理し、タグやバージョン情報を付与して運用してきました。しかし、本番運用が進むにつれ、この体制に限界を感じるようになりました。

以下は実際の管理イメージです。

プロンプトごとにバージョン(version)、ステータス(Status)、変更意図、評価結果サマリーなどを記録しています。また、対象のApp・Module・使用モデル・カテゴリといったタグを付与し、検索性を確保しています。

参考にした記事


Notion運用で直面した課題

まず前提として、弊社ではプロンプトを改善するのはドメイン知識を持つビジネスサイドのメンバーが中心です。そのため、LLMアプリケーションの構築にはDifyを採用し、ビジネスサイドが自分たちでプロンプトを変更・公開できるようにしています。

課題:DifyとNotionのプロンプト二重管理

その上で直面している課題が、Dify上のプロンプトとNotion上のプロンプトの二重管理です。

理想は「Difyでプロンプトを修正 → Notionでバージョン管理」という流れですが、実際は以下のようになりがちです。

  1. Difyでプロンプトを修正し、そのままデプロイ
  2. Notionの更新を忘れる(または後回しにする)
  3. 時間が経つと、どちらが正なのかわからなくなる
  4. Notion DBが過去の遺物と化す

この問題は、チームや運用の規模が大きくなるほど顕著になり、PromptOpsの仕組み自体が回らなくなります。

理想の状態

私たちが求めているのは、プロンプトを一元管理しつつ、ビジネスサイドも自由に編集できる環境です。

  • Gitのようなバージョン管理とロールバック機能
  • 非エンジニアでも使えるGUI
  • アプリケーションからの動的なプロンプト取得(二重管理の解消)

これらの要件を満たすツールとして、Langfuseの導入を検討しました。

LangfuseはオープンソースのLLMオブザーバビリティプラットフォームです。トレーシング、プロンプト管理、評価機能を備えており、LLMアプリケーションの開発・運用を包括的にサポートしてくれます。

なお、類似ツールとしてLangSmithなども存在しますが、今回はDifyとの連携プラグインが充実している点を重視し、Langfuseを選定しました。


1. Langfuse上でのプロンプト管理の仕組み

バージョン管理の仕組み

Langfuseでは、プロンプトを保存するたびにバージョン番号が自動的にインクリメントされます(1, 2, 3...)。各バージョンは完全に保持され、いつでも過去のバージョンを参照・復元できます。

ラベルによる環境管理

Langfuseの特徴的な機能がラベル(Labels)です。ラベルはバージョンへの「ポインタ」として機能し、デプロイ管理を柔軟に行えます。

ラベル 用途
production 本番環境で使用するバージョン
staging ステージング環境でテスト中
latest 最新バージョン(自動付与)
カスタム テナント別、実験用など自由に定義可能

ロールバックはラベルを付け替えるだけで実行できます。コード変更やデプロイは不要です(アプリケーション上で、常にproductionラベルが付与されたプロンプトを取得するよう事前に設定しておくイメージ。これによりアプリケーション側のコードを変更することなく、内部で使用するプロンプトを更新できます)。

Before: version 3 → production
After:  version 2 → production  // UIでポチッと変更するだけ

Config設定

プロンプトと一緒に、モデル名やパラメータも管理できます。

{
  "model": "gpt-4o",
  "temperature": 0.7,
  "max_tokens": 1000
}

これにより、「このプロンプトはどのモデルのどの設定で動かすべきか」という情報もセットで管理でき、二重管理の問題がさらに軽減されます。


2. Langfuse上でのプロンプト評価の仕組み

Scoreによる評価管理

Langfuseでは、LLMの出力に対してScoreを付けることで品質を追跡できます。

データ型 説明
NUMERIC 数値スコア 0.0〜1.0
CATEGORICAL カテゴリ "良い"、"普通"、"悪い"
BOOLEAN 真偽値 合格/不合格

Human Annotation(人間による評価)

Annotation Queues機能を使うと、大規模なアノテーション作業を効率化できます。

1. 評価基準(Score Config)を事前定義

まず、評価基準を事前に定義しておきます。 以下に例として、クレーム対応メール生成Botで使用するプロンプトを評価するための基準を載せておきます。

2. 評価対象のトレースをキューに追加

続いて、Langfuseでトレースしているアプリケーションのログをキューに追加します。

3. アノテーターがGUI上で順次評価

評価者は、Human Annotationからステータスが「Pending」になっているものを選択し、

以下のように、事前に定義した評価基準に基づいて評価を行います。

4. 評価結果がScoreとして記録される

評価が完了すると、結果がScoreとしてLangfuseに記録されます。

LLM-as-a-Judge(自動評価)

人間の評価と併用して、LLMによる自動評価も可能です。

組み込みの評価テンプレート:

  • Hallucination(ハルシネーション検出)
  • Helpfulness(有用性)
  • Relevance(関連性)
  • Toxicity(有害性)
  • Correctness(正確性)

大量のトレースを評価する際に、まずLLM-as-a-Judgeでスクリーニングし、閾値以下のものを人間がレビューするといった運用が可能です。


3. アプリケーションからのプロンプト利用方法

Langfuseの最大の利点は、プロンプト名とラベル(タグ)を指定するだけで動的にプロンプトを取得できる点です。これにより、アプリ側のコード変更なしでプロンプトを更新できます。

Python SDKでの取得

from langfuse import Langfuse

langfuse = Langfuse()

# productionラベルのプロンプトを取得(デフォルト)
prompt = langfuse.get_prompt("claim_response_generation")

# ラベル指定で取得
prompt = langfuse.get_prompt("claim_response_generation", label="production")

# 特定バージョンを取得
prompt = langfuse.get_prompt("claim_response_generation", version=2)

変数の展開

# プロンプトを取得
prompt = langfuse.get_prompt("claim_response_generation")

# 変数を展開
compiled = prompt.compile(
   customer_inquiry="ログインできません"
)

# LLMに渡す
response = openai.chat.completions.create(
    model=prompt.config["model"],
    messages=compiled,
)

ポイントは、アプリケーションコードは一切変更せずに、Langfuse上でプロンプトを更新するだけで本番に反映されるという点です。これでNotionとの二重管理問題が解消されます。

補足:Difyとの連携

Dify 0.6.12以降、Langfuseとのトレーシング統合がビルトインで提供されています。

設定方法:

  1. Difyの「Orchestration Studio → Monitoring」タブを開く
  2. LangfuseのAPIキーを設定

これにより、Difyで実行されたワークフローのトレースがLangfuseに送信され、可観測性が向上します。

プロンプト管理の直接連携については、コミュニティの dify-plugin-langfuse を使うことで、Difyワークフロー/チャットフロー内からLangfuseのプロンプトを取得・利用することも可能です。

キャッシュによる可用性確保

SDKはクライアントサイドキャッシュを実装しており、Langfuse APIに障害が発生してもアプリケーションは動作を継続できます。

# キャッシュTTLを設定(デフォルト60秒)
prompt = langfuse.get_prompt("movie-critic", cache_ttl_seconds=300)

残る課題:評価基準の設計は人間の仕事

Langfuseを導入すれば、プロンプトの管理・配信・トレーシングは大幅に改善されます。しかし、評価基準そのものはドメインエキスパートが設計するしかないという点は変わりません。

  • 何をもって「良いプロンプト」とするのか
  • どの出力が「正解」なのか
  • 品質の閾値はどこに設定するのか

これらの判断は、ドメイン知識を持つ人間にしかできません。LLM-as-a-Judgeも結局は「人間が定義した評価基準」に基づいて動作するので、基準設計は依然として人間の仕事です。

良し悪しの判断——これこそが、人間が介在することで生まれる数少ない価値の一つです。ツールで効率化できる部分はツールに任せ、人間は本質的な価値判断に集中する。Langfuse導入後は、この役割分担を意識した運用を進めていきたいと考えています。


まとめ

課題 Langfuseによる解決
二重管理問題 アプリから動的取得、Single Source of Truth化
非エンジニアの編集 GUIでの直接編集
品質評価 Score機能 + Annotation Queues

Langfuseは、Notion運用で抱えていた課題の多くを解決できる可能性を持っています。引き続き検証を進め、本番導入に向けた知見を蓄積していく予定です。


参考リンク - Langfuse Documentation - Prompt Management - Evaluation / Scores - Dify x Langfuse Integration

n8n で整える会議室トラブル報告フローの実装例

はじめに

こんにちは。ラクスル Advent Calendar 2025 22 日目を担当する、Corporate Technology IT Service チーム / Helpdesk チームの内山です。

社内では AI の活用が広がり、業務整理や壁打ちとして AI が自然に使われるようになってきました。
ラクスルで導入している n8n も AI と統合しやすい一方で、AI を使わないシンプルな業務改善にも適しており、今回紹介する仕組みはそのよい例だと考えています。
この記事では、会議室のトラブル報告フローを n8n で実装した例を紹介します。

なぜ作ったのか

会議室では日々さまざまな小さなトラブルが発生しますが、報告導線には課題がありました。

  • 「ついで報告」で記録が残らない
  • DM や雑談チャンネルに届き、情報が散在する
  • 一部の人だけが報告する構造になりがち
  • 社内のチケットシステムは心理的ハードルが高い

会議中のトラブルは進行が止まるため影響が大きく、だれでも迷わず報告できる導線が必要でした。

設計方針

QR コードが自然で早い導線

読み取った瞬間に必要な情報を渡せるため、迷わず報告可能

ログイン不要・スマホで完結

PC を開く必要がなく、その場で短時間で入力可能

会議室数が多いため選択させない

会議室を選択する方式は手間やミスが発生しやすいため、QR に部屋情報を埋め込んで自動判別

全体フロー

  1. 従業員が各会議室に設置した QR コードを読み取る
  2. n8n がスマホ向けフォームを返す
  3. 従業員が送信フォームを入力し送信を行う
  4. n8n は入力情報を Slack に通知し、従業員に完了画面を返す

実際の n8n のノード

こちらが実際の n8n のノードです。非常にシンプルだと思います。 上のノードが全体フローの「1-2 」、下のノードが「3-4」になります。

各フローの詳細

1. QR コードに会議室情報を含める

QR に直接会議室名を含めることで、利用者が選択する必要をなくしています。

https://example.com/webhook/report?room=A&token=rksl_xxx_123

2. n8n がスマホ向けフォームを返す

Webhook(GET)で受け取った後、Code のノードでフォーム HTML を返します。

※サンプルコード

const token = $json.query.token;
if (token !== 'YOUR_SECRET_TOKEN') {
    return [{
        headers: { "Content-Type": "text/plain" },
        body: "Unauthorized"
    }];
}

const room = $json.query.room || 'UNKNOWN';

const escapeHtml = (s) =>
    String(s).replace(/[&<>"']/g, (c) =>
        ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])
    );

const html = `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
    <style>
        body {
            font-family: system-ui, sans-serif;
            padding: 24px;
            background: #f5f7fa;
        }
        .card {
            background: #fff;
            padding: 28px;
            border-radius: 14px;
            max-width: 440px;
            margin: 0 auto;
        }
    </style>
</head>
<body>
    <div class="card">
        <h2>会議室トラブル報告</h2>
        <div>会議室:${escapeHtml(room)}</div>

        <form action="https://example.com/webhook/room-trouble" method="POST">
            <input type="hidden" name="room" value="${escapeHtml(room)}" />
            <input type="hidden" name="token" value="${escapeHtml(token)}" />

            <input type="text" name="name" required />
            <select name="trouble" required>
                <option value="">選択してください</option>
                <option>モニターが映らない</option>
                <option>音が出ない</option>
                <option>Web会議に接続できない</option>
                <option>その他</option>
            </select>

            <textarea name="detail"></textarea>
            <button type="submit">送信</button>
        </form>
    </div>
</body>
</html>
`;

return [{ html }];

実際の画面

3. 従業員が送信フォームを入力し送信を行う

従業員は、上記の送信フォームに情報を入力し、送信を行います。

4. n8n は入力情報を Slack に通知し、授業員に完了画面を返す

利用者がフォーム画面の送信ボタンを押すとWebhook(POST) Node で受け取り、受け取ったテキストを整形し Slack Webhook に送信します。
形式は単純で、本文をそのまま Slack に渡すだけです。合わせて利用者には送信内容を整形したものを完了画面として返します。

実際の画面

セキュリティ面の工夫

token の照合

Webhook で受け取ったパラメーターのうち QR の URL に含めたトークンと一致しているかを確認します。

const token = $json.query.token;
if (token !== 'YOUR_SECRET_TOKEN') {
    return [{
        headers: { "Content-Type": "text/plain" },
        body: "Unauthorized"
    }];
}

Wehhook URL が漏洩しても不正投稿を防ぐために設定しています。

なぜ n8n を選んだのか

要件は「QR → フォーム → Slack 通知」を短期間で実装することでした。
次の理由から n8n が適していると判断しました。

  • Webhook でフォーム HTML を直接返せる
  • URL パラメータを自然に扱える
  • Slack への通知が簡単
  • 修正や改善をすぐに反映できる

必要な範囲だけ実装するという今回の目的に合っていました。

今後の展望

送られてくる内容が蓄積されれば、次のような拡張も可能になります。

  • AI によるトラブル分類
  • 一次回答の自動化
  • 傾向を基にした機材改善の判断

段階的に取り入れていければと考えています。

おわりに

n8n は AI 連携だけでなく、現場のちょっとした不便を解消する用途にも向いています。
今回の QR → フォーム → Slack というシンプルな導線でも、会議室運用の負担を大きく軽減できました。

引き続き現場に合わせた改善を進めていく予定です。

React Compilerについて調べてみた

こんにちは。ノバセル事業本部エンジニアの宮本です。 本記事はノバセル テクノ場 出張版 2025 Advent Calendar 2025 の 21 日目の記事になります。

はじめに

Claude Code、GitHub Copilot、Cursor——AI アシスタントと一緒にコードを書く「バイブコーディング」が当たり前になりました。しかし、生成された React コードをレビューしていると、ある傾向に気づきます。

const UserList = ({ users }: { users: User[] }) => {
  const sortedUsers = useMemo(
    () => users.sort((a, b) => a.name.localeCompare(b.name)),
    [users]
  );

  const handleClick = useCallback((id: string) => {
    console.log(id);
  }, []);

  return (
    <ul>
      {sortedUsers.map((user) => (
        <li key={user.id} onClick={() => handleClick(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
};

AI はuseMemouseCallbackを多用します。「パフォーマンス最適化のベストプラクティス」として学習しているからでしょう。しかし、React 公式ドキュメントを読むと、これらの Hooks は計算コストの高い処理でなければ、むしろ初期化のオーバーヘッドが大きくなる可能性があると書かれています。

レビューのたびに「このメモ化は本当に必要か?」と悩むのは生産的ではありません。レビューの判断軸を求めてドキュメントを読んでいると、React Compilerという解決策に出会いました。これは手動でのメモ化を不要にする、ビルドタイムの最適化ツールです。

この記事では、React Compiler がどのような仕組みで動作するのかを、React Summit 2025 での Lydia Hallie の発表「React Compiler Internals」を参考にしながら解説します。


なぜメモ化が必要なのか

React Compiler の内部に入る前に、そもそもなぜメモ化が必要なのかを確認しましょう。

const users = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob", active: false },
  { id: 3, name: "Charlie", active: true },
];

function UserStats({ users }: { users: User[] }) {
  const activeUsers = users.filter((u) => u.active);
  return <div>Active: {activeUsers.length}</div>;
}

function App() {
  const [sort, setSort] = useState<"asc" | "desc">("asc");

  return (
    <>
      <UserStats users={users} />
      <button onClick={() => setSort((s) => (s === "asc" ? "desc" : "asc"))}>
        Sort: {sort}
      </button>
    </>
  );
}

このコードには大きなパフォーマンス問題があります。Appsortステートが変わると、Appが再レンダリングされ、子コンポーネントであるUserStatsも再レンダリングされます。React のデフォルト動作として、親が再レンダリングされれば子も再レンダリングされるからです。

UserStatsが再レンダリングされると、users.filter()が再実行されます。3 件のデータなら問題ありませんが、10,000 件だったらどうでしょうか。users配列は変わっていないのに、sortが変わっただけで重い処理が走ってしまいます。

従来の解決策は、React.memoでコンポーネントをラップするか、useMemoで計算結果をキャッシュすることでした。

function UserStats({ users }: { users: User[] }) {
  const activeUsers = useMemo(() => users.filter((u) => u.active), [users]);
  return <div>Active: {activeUsers.length}</div>;
}

しかし、この手動メモ化には問題があります。

  1. 依存配列の管理が煩雑: 依存を追加し忘れると、値が更新されないバグが生じる
  2. どこにメモ化を入れるべきか判断が難しい: 入れすぎても入れなさすぎても問題
  3. コードの可読性が下がる: ビジネスロジックとパフォーマンス最適化が混在する

React Compiler は、これらの問題を自動的に解決します。


React Compiler とは

React Compiler は、ビルドタイムに React コードを解析し、適切な場所に自動でメモ化を挿入するツールです。Babel プラグインとして実装されていますが、内部は Babel から独立した独自のコンパイラです。

2025 年 10 月に v1.0 の安定版がリリースされ、React 17 以上で利用可能になりました。Meta では既に Quest Store などで本番導入されており、初期ロードで最大 12%、特定のインタラクションで 2.5 倍以上の改善が報告されています。

開発者はメモ化を意識することなくコードを書け、コンパイラが最適化されたコードを生成します。しかも、手動では実現が難しい粒度の細かいメモ化も可能です。では、どのような仕組みでそれを実現しているのでしょうか。


コンパイルパイプラインの全体像

React Compiler は、以下のフェーズを経てコードを変換します。

JavaScript
    ↓
HIR(High-level Intermediate Representation)
    ↓
SSA(Single Static Assignment)
    ↓
Type Inference(型推論)
    ↓
Effect Analysis(エフェクト解析)
    ↓
Reactive Analysis(リアクティブ解析)
    ↓
Scope Discovery(スコープ発見)
    ↓
Code Generation(コード生成)

それぞれのフェーズを詳しく見ていきましょう。


Phase 1: HIR(High-level Intermediate Representation)

なぜ中間表現が必要か

JavaScript のコードをそのまま解析するのは困難です。変数のスコープ、条件分岐、クロージャなど、複雑な要素が絡み合っています。コンパイラはまず、コードをより扱いやすい形式に変換します。

HIR は、各操作を個別の命令として分離した中間表現です。

// 元のコード
function UserStats({ users }) {
  const activeUsers = users.filter((u) => u.active);
  return <div>{activeUsers.length}</div>;
}
// HIR(簡略化)
1: $users = LoadParam('users')
2: $filter = LoadProperty($users, 'filter')
3: $callback = CreateFunction(...)
4: $activeUsers = Call($filter, $users, $callback)
5: $length = LoadProperty($activeUsers, 'length')
6: $jsx = CreateJSX('div', {}, $length)
7: Return($jsx)

データフローの可視化

HIR の重要な点は、データの流れが明確になることです。命令 7 のReturnは命令 6 の$jsxを使い、命令 6 は命令 5 の$lengthを使い...というように、依存関係が追跡可能になります。

これにより、「この値が変わったら、どの計算をやり直す必要があるか」をコンパイラが判断できるようになります。


Phase 2: SSA(Single Static Assignment)

変数の再代入問題

HIR には一つ問題があります。変数が再代入される場合、どの値を参照しているか曖昧になることです。

function UserStats({ users, filters }) {
  let activeUsers = users.filter((u) => u.active);

  if (filters.limitResults) {
    activeUsers = activeUsers.slice(0, 10);
  }

  return <div>{activeUsers.length}</div>;
}

このコードでは、activeUsersが 2 つの異なる値を持つ可能性があります。filters.limitResultstrueならsliceされた配列、falseならフィルタされただけの配列です。

コンパイラはビルドタイムに実行時の値を知ることができないため、このままでは安全に最適化できません。

SSA 形式への変換

この問題を解決するのが SSA(Static Single Assignment)です。各変数が一度だけ代入される形式に変換します。

// 上記のactiveUsersをSSA形式で表すと
const activeUsers_1 = users.filter((u) => u.active);
const activeUsers_2 = activeUsers_1.slice(0, 10);

再代入のたびに新しい変数名が割り当てられるため、どの時点の値を参照しているかが常に明確です。これによりコンパイラは各変数の定義と使用箇所を正確に追跡でき、安全な最適化が可能になります。


Phase 3: Type Inference(型推論)

コンパイラは、各値の型を推論します。

  • プリミティブ値(number, string, boolean
  • オブジェクト / 配列
  • 関数
  • React 要素
  • Hook の戻り値

型情報は、次のエフェクト解析で重要になります。例えば、プリミティブは不変なのでキャッシュが安全ですが、オブジェクトは変更される可能性があります。


Phase 4: Effect Analysis(エフェクト解析)

エフェクトとは何か

エフェクトは、各操作がデータにどう影響するかを表します。メモ化が安全かどうかを判断するために必要な情報です。

React Compiler は 6 種類のエフェクトを定義しています。

Effect 説明
Read 値を読み取るだけ .length, .filter へのアクセス
Store 新しい値を格納 const x = ...
Capture 外部スコープから値を取り込む クロージャで親スコープの変数を参照
Mutate 値を変更する可能性 配列の.push(), オブジェクトのプロパティ変更
Freeze 値を不変にする キャッシュ後の値

キャッシュの安全性

  • Read: データを変更しないので、キャッシュは常に安全
  • Store: 格納する値が不変なら安全
  • Capture: 依存関係として追跡が必要
  • Mutate: キャッシュすると危険な可能性あり
  • Freeze: キャッシュ後に変更されないことを保証
// Read effect - 安全
const length = users.length;

// Mutate effect - 危険
users.push(newUser); // 元の配列を変更

エフェクト解析により、「この操作の結果をキャッシュしても安全か」をコンパイラが判断できます。


Phase 5: Reactive Analysis(リアクティブ解析)

React における「リアクティブ」とは

ここまでで、コンパイラは各操作の振る舞いを理解しました。しかし、まだ重要な情報が欠けています。それは、どの値がレンダー間で変化しうるかです。

React でメモ化が効果を発揮するのは、「前回のレンダーと今回のレンダーで値が同じなら、計算をスキップする」場合です。つまり、レンダー間で変化しない値をメモ化しても意味がありません。

リアクティブな値の伝播

リアクティブ解析は、以下のルールで値の「リアクティブ性」を判定します。

リアクティブの起点:

  1. 関数パラメータ(props): 親から渡されるので、常にリアクティブ
  2. Hook の戻り値: useState, useContextなどの値

伝播ルール:

  • リアクティブな値を入力に持つ操作の出力は、リアクティブになる
function UserStats({ users }) {
  // users はリアクティブ(props)
  const activeUsers = users.filter((u) => u.active); // リアクティブ
  const count = activeUsers.length; // リアクティブ
  return <div>{count}</div>; // リアクティブ
}

usersがリアクティブなので、それに依存するactiveUsersもリアクティブ、さらにそれに依存するcountもリアクティブ...と伝播していきます。

なぜこれが重要か

リアクティブでない値をメモ化しても意味がありません。例えば、コンポーネント外で定義された定数はレンダー間で変わらないので、キャッシュする必要がありません。

const ITEMS_PER_PAGE = 10; // リアクティブではない

function List({ items }) {
  // ITEMS_PER_PAGEはメモ化不要
  // itemsはリアクティブなのでメモ化対象
  const pagedItems = items.slice(0, ITEMS_PER_PAGE);
}

Phase 6: Scope Discovery(スコープ発見)

メモ化の粒度を決める

ここまでで、コンパイラは以下を把握しています。

  • 各操作の依存関係(HIR, SSA)
  • 各操作の副作用(Effect Analysis)
  • どの値がレンダー間で変化しうるか(Reactive Analysis)

次のステップは、どの操作をまとめてキャッシュするかを決めることです。

スコープとは

スコープは、一緒にキャッシュされる操作のグループです。同じ依存を持つ操作は同じスコープに入り、独立した依存を持つ操作は別のスコープになります。

function UserStats({ users }) {
  // Scope 1: usersに依存
  const activeUsers = users.filter((u) => u.active);

  // unscopedな計算
  const count = activeUsers.length;

  // Scope 2: countに依存
  return <div>{count}</div>;
}
  • Scope 1: usersが変わったら再計算、変わらなければキャッシュを使用
  • Scope 2: countが変わったら再生成、変わらなければキャッシュを使用

手動メモ化を超える最適化

ここで重要なのは、Scope 1 と Scope 2 が独立していることです。

users配列の内容が変わっても、filter後のlengthが同じなら(例: active なユーザーが 10 人 →10 人)、JSX は再生成されません。

これを手動で実現しようとすると、非常に複雑なコードになります。

// 手動で同じ最適化を実現しようとすると...
function UserStats({ users }) {
  const activeUsers = useMemo(() => users.filter((u) => u.active), [users]);

  const count = activeUsers.length;

  // countだけに依存するJSXのメモ化は...?
  // React.memoでラップした子コンポーネントに分離する必要がある
}

React Compiler は、このような細粒度の最適化を自動で行います。

純粋関数の最適化

外部の変数に依存しない純粋関数は特別な扱いを受けます。

function UserStats({ users }) {
  const activeUsers = users.filter((u) => u.active);
  //                              ^^^^^^^^^^^^^^^^
  //                              このコールバックは独立したスコープ
}

u => u.activeというコールバックは、外部の変数に依存していません。このような関数は一度だけ生成してすべてのレンダーで再利用できます。コンパイラはこれを検出し、関数を抽出します。


Phase 7: Code Generation(コード生成)

キャッシュスロットの確保

最終フェーズでは、HIR を最適化された JavaScript コードに変換します。

まず、スコープごとにキャッシュスロットを確保します。各スコープには 2 種類のスロットが必要です。

  1. 依存の値を保存するスロット: 前回の値と比較するため
  2. 出力の値を保存するスロット: キャッシュヒット時に返すため
function UserStats({ users }) {
  const $ = useMemoCache(4); // 4つのスロット
  // $[0]: users の前回の値
  // $[1]: activeUsers の前回の値
  // $[2]: count の前回の値
  // $[3]: JSX の前回の値
}

生成されるコード

最終的に生成されるコードは以下のようになります。

function UserStats({ users }) {
  const $ = _c(4); // 4つのキャッシュスロット
  let t0;
  // Scope 1: usersが変わったか?
  if ($[0] !== users) {
    t0 = users.filter((u) => u.active);
    $[0] = users; // 依存をキャッシュ
    $[1] = t0; // 出力をキャッシュ
  } else {
    t0 = $[1]; // キャッシュから取得
  }

  // Scopeのない値は毎回実行する
  const activeUsers = t0;
  const count = activeUsers.length;

  let t1;
  // Scope 2: countが変わったか?
  if ($[2] !== count) {
    t1 = <div>{count}</div>;
    $[2] = count;
    $[3] = t1;
  } else {
    t1 = $[3];
  }

  return t1;
}

各スコープは独立して動作します。usersが変わってもフィルタ後の count が同じなら、最初のスコープだけが再計算され、JSX のスコープはキャッシュを返します。


React Compiler の限界

React Compiler は万能ではありません。以下のケースでは最適化が効きません。

1. React のルール違反

// ❌ 条件付きHook呼び出し
function Bad({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // Hook呼び出しが条件内
  }
}

// ❌ レンダー中の副作用
function Bad({ items }) {
  items.push("new"); // propsを直接変更
  return <List items={items} />;
}

コンパイラは React のルールに則ったコードを前提としています。違反があると最適化をスキップします。

2. クラスコンポーネント

React Compiler は関数コンポーネントのみをサポートしています。クラスコンポーネントは対象外です。

3. 複雑な JavaScript パターン

コンパイラが安全性を判断できない複雑なパターンでは、メモ化しないという保守的な選択をします。安全でないメモ化よりも、メモ化しない方がマシだからです。


コードベースの準備状況をチェックする

React Compiler を導入する前に、コードベースが React ルールに従っているかチェックできます。

npx react-compiler-healthcheck

このコマンドは、プロジェクトを解析し、React ルール違反や互換性の問題を報告します。

また、eslint-plugin-react-compiler を使うと、コンパイラが最適化できないコードを検出できます。

npm install eslint-plugin-react-compiler
// .eslintrc.js
module.exports = {
  plugins: ["react-compiler"],
  rules: {
    "react-compiler/react-compiler": "error",
  },
};

まとめ

React Compiler は、AI が生成しがちな過剰なuseMemo/useCallbackの問題を根本から解決します。 コンパイラの恩恵を最大限に受けるには React ルールに則ったコードが前提ですが、バイブコーディングのルールにこれを上手く組み込むことでレビュー時の悩みを一つ減らすことができるかもしれません。


参考リンク

ググるよりも先にAIへ。AI前提の開発しか知らない新卒エンジニアの「迷い」と「リアル」

はじめに

こんにちは。ラクスル Advent Calendar 2025 21 日目を担当する、プラットフォーム統括部でエンジニアをしている鍛治です。

今年の4月に新卒として入社し、配属されてからは主に Claude Code を使って開発をしています。

正直に言うと、これまでの開発で自分の手だけでガッツリとコードを書いてきた経験はほとんどありません

日々の開発ではググるよりも先に AI へ質問を投げ、ドキュメントを読む前にまず AI から噛み砕いた説明をもらう、というスタイルが当たり前になっています。

この記事では、そんなAI 前提の開発しかしてこなかった新卒エンジニアが日々の業務の中で感じていることや、うまく言語化しきれない違和感・迷いについて整理せずに書いてみようと思います。

AI の是非や結論を出したいわけではありません。

むしろ、今はまだモヤっとしている部分をそのまま残しておきたくて、この文章を書いています。

AI前提の開発で感じているメリット

オンボーディングがスムーズ

配属されてから感じている一番大きなメリットは、オンボーディングがかなりスムーズなことです。

知らないコードを読むときの心理的な負荷が小さく、初めて触るリポジトリでも AI に質問しながら処理の流れを追うことができます。

タスクを進める中で、触ったことのないリポジトリやこれまで経験のない言語を調査しなければならない場面でも、「まず全体像を掴む」というところまでは比較的早くたどり着けていると感じています。

アウトプットが早い(※アウトカムとは別)

AI を使うことで、コードを書くスピード自体は圧倒的に速くなります。

ここで言うアウトプットは、あくまで「コードが書ける」「形になる」という意味でのアウトプットで、それがそのままアウトカム(価値)につながっているかは別の話だと考えています。

ただ少なくとも、手が止まらず前へ進めるという点では大きな助けとなっています。

コーディング以外でも役に立つ

AI はコーディング以外の場面でもかなり役に立っています。

  • ドキュメントを書くとき
  • 英語の意味や使い方が曖昧なとき

こういった場面で、自然な形で質問できるのはありがたいです。

「文章を書く」「英語を読む」といった作業への心理的ハードルも下がっていると感じています。

面白い

単純に、面白いというのも正直な感想です。

AI を用いた開発がまだ過渡期にある今、その進化をリアルタイムで見られるのは非常に楽しいです。

Claude Code に関して言えば、ほぼ毎日のようにアップデートがあり、changelog を見に行くのが習慣になっています。

モデル自体も各社が鎬を削っていて、テキストだけでなく Nano Banana のような画像生成も含め、出力の精度が急速に上がっていくのを実感します。

迷い①:理解と学習についての違和感

AI を使って開発する中で、最近よく感じているのが理解や学習の仕方についての違和感です。

難しい概念の理解

難しい概念にぶつかったとき、今の私はとりあえず AI に聞いてしまいます。

噛み砕いた説明をしてくれるので、「なるほど」と思える状態まではすぐに到達できます。

ただ、それが本当に理解できている状態なのかには、正直自信がありません。

本来であれば、自分であれこれ考えたり、調べたりしながら時間をかけて理解していたはずの部分をかなりショートカットしている感覚があります。

この使い方で良いのか、今も判断がついていません。

わかった気になることが多い

AI の説明を読んで、「ふーん」と思った時点で理解したつもりになってしまうことがよくあります。

少し時間が経つと説明できなかったり、別の文脈で同じ話題が出たときにうまく繋がらなかったりして、そのとき初めて「あまり腹落ちしていなかったな」と気づきます。

AI がそれらしい説明をしてくれる分、自分の中で理解が浅いままでも前に進めてしまうのが怖いと感じています。

自分の言葉で説明できない

普段の開発は AI に聞きながら進めているため、ペアプロや対面でレビューをしてもらう場面ではパッと正確に説明できないときがあります。

その場で言葉にできないということは、やはりどこかで「わかった気になっている」だけなのかもしれません。

迷い②:主体性・負荷・アウトプットの問題

理解や学習の違和感と並んで感じているのが、自分がどれだけ主体的にアウトプットしているのか分からなくなる感覚です。

自らアウトプットする機会が少ない

AI を使っていると、そもそも自分の手でコードを書く機会がかなり少なくなります。

もちろん、生成されたコードをそのまま使うわけではありませんが、「白紙の状態から自分で考えて書く」という時間は、以前より明らかに減っています。

AI が書いたコードを理解できれば十分なのか、それとも自分で書ける状態まで持っていくべきなのか、このあたりの基準がまだ自分の中で定まっていません。

AIの圧倒的な出力に滅入る

AI はどちらかというと発散が得意な印象です。

複数の案や、それっぽく正しそうなコードを一気に出してくれる一方で、正確な知識が十分でない状態では、何を基準に選べばいいのか分からなくなることがあります。

生成されたコードを読むのにも意外と時間がかかり、結局間違っていてもう一度生成し直す、ということも少なくありません。

「書く時間」は短くなっている一方で、「判断するための力」はより求められていると強く感じています。

負荷をかけなくても仕事が回る

AI の性能が向上して、動くコードや最低限のドキュメントは一発で出せることが増えてきました。

極端な話ですが、

  • AI にコードを書かせる
  • レビュー依頼を出す
  • 返ってきたレビューをまた AI に食わせて直す

これを繰り返せば仕事としては一応成り立ってしまうものの、自分自身にはほとんど負荷をかけておらず、情報を横流ししているだけにも思えます。

AI の出力に対して、意識的に批判的な思考を持ち、深掘りしないといけない。そうしないと何も身につかないまま時間だけが過ぎていく気がしています。

迷い③:成長・評価・チームへの影響

ここまで書いてきた迷いは、個人の学習や主体性の話に見えるかもしれませんが、最近は成長の実感や評価、チームとの関わり方にも影響していると感じています。

個人の能力を評価される難しさ

AI を使うことで、ある程度のスピードや品質はすでに担保されてしまいます。

その結果、個人としてアウトプットの品質をさらに高めようとすると、AI の出力以上の理解や判断力が求められるようになります。

正直なところ、AI の出力を明確に上回るまでは、「誰がやってもアウトプットの精度はあまり変わらない」という状態になっているようにも感じています。

成長している気になっているだけじゃないか

最近は、エピックをある程度任せてもらい、少しずつ自分で進められるようになってきました。

ただ、それが本当に自分の能力が上がった結果なのか確信を持てません。

AI がなければ、アウトプットの品質が上がることと、自分の成長はある程度結びついていたと思います。

一方で AI があると、

  • AI の精度が上がった
  • たまたま良い出力(AI は確率で出力するので)を引いた

といった要素も絡み、アウトプットの品質向上が必ずしも自分の成長と一致しなくなります。

このズレが積み重なって、将来思ったほど成長できていないといった状況にならないか不安です。

人との関わりの減少

分からないことがあったとき、今はとりあえず AI に聞く、という行動が当たり前になっています。

本来であれば周りの誰かに聞いていたはずのことも、AI で自己完結してしまう場面が増えました。

その結果、

  • 周辺の業務知識を雑談の中で知る機会
  • ちょっとした会話から生まれる信頼関係

といったものを得られにくくなっている気がします。

質問される側からすると、集中を遮られる回数が減るというメリットはあると思います。

ただ、チームで開発している以上、コミュニケーションが減ることの負の側面がやはり大きいのではないかと感じています。

おわりに

この記事を書いてみて、自分はまだ AI とどう向き合うのが正解なのか答えを持っていないことを改めて実感しました。

AI は間違いなく便利で、今の開発を支えてくれている存在です。

一方で、理解の仕方や成長の実感、評価やチームとの関わり方についてうまく言語化できないモヤっとした感覚も残っています。

この文章は、そのモヤっとを整理するためというより、今の時点で感じている迷いをそのまま残すためのメモです。

「自分はどう感じているか」「どう向き合っているか」を考えるきっかけになれば幸いです。

BigQuery MCP x Claude Skills で実現するデータ分析

この記事はノバセル テクノ場 出張版2025 Advent Calendar 2025の20日目の記事です。

qiita.com

はじめに

ノバセルで PdM をしていた山中です。現在は親会社のラクスルへ出向しており、全社の AI 活用推進を担当しています。

データ分析の現場では、SQL を書ける人材が限られていることや、ドメイン知識が属人化していることが課題となっています。「このカラムの値 1 は何を意味するのか」「このテーブルとあのテーブルはどう結合すればいいのか」といった暗黙知が散在しており、データ分析のたびに有識者へ確認しなければならない状況が生まれがちです。

一方で、ChatGPT や Claude に「売上データを分析して」と投げかけるとどうでしょうか。テーブル構造やカラムの意味を理解していないため、的外れなクエリが生成されたり、誤った結合条件で集計されたりと、実用的でない結果になりがちです。

かといって、データカタログツールやセマンティックレイヤー(Looker 等)を導入するのは追加ライセンスコストがかかることもあり、なかなか踏み切れないこともあります。

そこで今回は、最近提供が開始された BigQuery MCP と Claude Skills を組み合わせて、ドメイン知識をクイックに AI へ注入しながらデータ分析を自動化してみます。

準備編

BigQuery MCP とは

BigQuery MCP は、BigQuery のテーブル定義やメタデータを AI から参照できるようにする MCP サーバーです。Claude などのアシスタントが MCP ツール経由でテーブル一覧の取得、スキーマ情報の照会、クエリの実行といった操作を行えるようになります。公式が提供しているリモート MCP サーバーになります。

BigQuery MCP では、以下の 5 つのツールが利用可能です(詳細はこちら):

ツール名 機能
list_dataset_ids プロジェクト内のデータセット ID 一覧を取得
get_dataset_info 特定のデータセットの詳細情報を取得
list_table_ids データセット内のテーブル ID 一覧を取得
get_table_info テーブルのスキーマとメタデータを取得
execute_sql SQL クエリを実行して結果を取得


BigQuery MCP を利用するには、Google Cloud のプロジェクト設定と OAuth 認証の構成が必要です。基本的な手順は 公式ドキュメント に詳しく記載されています。 今回は、Claude Desktop と接続しました。チャットで利用するモデルは Sonnet 4.5 です。

補足:MCP Toolbox for Databases について

なお、MCP Toolbox for Databases を利用すると、標準の BigQuery MCP に加えて、貢献度分析や予測分析などの高度な機能が利用可能になります。 Conversational Analytics API も含めて呼び出し可能なので、リモート MCP に拘らない場合には、こちらの利用も検討してください。

Claude Skills とは

Claude Skills は、特定のドメイン知識や作業手順を Claude に教え込むための仕組みです。Markdown 形式でスキルファイルを作成し、Claude Desktop に読み込ませることで、AI が文脈に応じて適切な知識を参照しながら作業を進められるようになります。

今回作成したスキル

データ分析の領域ではデータセットに関する情報が膨大に存在し、AI に渡すコンテクストの選別が難しいという課題があります。 そこで、今回は必要なドメイン知識だけがロードされる Progressive Disclosure の効果も検証します。 以下の 6 つのスキルを作成しました。

スキル 役割
bigquery-mcp MCP ツールの使い方
bigquery-ec-values EC データのカラム値定義(order_status, user_type など)
bigquery-ec-relations EC データのテーブル結合パターン
bigquery-marketing-values マーケティングデータのカラム値定義(channel, status など)
bigquery-marketing-relations マーケティングデータのテーブル結合パターン
artifact-charts グラフ描画用テンプレート

検証用データセット

EC 売上データ(demo_ec_sales)

テーブル 説明
orders 注文情報
order_items 注文明細
products 商品マスタ
users ユーザー情報


ドメイン知識の例:

  • order_status = 4 → 配達完了
  • user_type = 2 → プレミアム会員
  • 売上集計時はキャンセル(order_status = 9)を除外

マーケティングデータ(demo_marketing)

テーブル 説明
campaigns 広告キャンペーン情報
ad_impressions 広告表示ログ
ad_clicks 広告クリックログ
conversions コンバージョン情報


ドメイン知識の例:

  • status = 3 → 終了済みキャンペーン
  • channel = 'google_ads' → Google 広告
  • ROAS = revenue / cost

スキルの構築

スキルは、Claude と対話しながら作成することが可能です。

youtu.be

各スキルのソースコードは、以下のリポジトリをご覧ください。

github.com

検証編

以下の依頼文を使って、スキルの有無による精度の違いを検証しました。

プレミアム会員の先月の完了注文の売上を商品カテゴリ別に集計してください

検証 1: bigquery-mcp のみ

BigQuery MCP だけを有効化した状態で依頼を投げてみました。 以下は、bigquery-mcp.md の概要です。

---
name: bigquery-mcp
description: "BigQuery MCP ツールのエキスパート。データ探索、スキーマ確認、SQL クエリ実行に使用。BigQuery のデータセット、テーブル、分析クエリの実行時にトリガー。"
---

# BigQuery MCP ツールガイド

## 概要

BigQuery MCP は以下の 5 つのツールを提供:
- **list_dataset_ids** - プロジェクト内のデータセット一覧
- **get_dataset_info** - データセットのメタデータ取得
- **list_table_ids** - データセット内のテーブル一覧
- **get_table_info** - テーブルスキーマとメタデータ取得
- **execute_sql** - SQL クエリ実行

## ツール選択ガイド

| 目的 | ツール | 使用場面 |
|------|--------|----------|
| データ探索 | `list_dataset_ids` | 最初のステップ:どのデータセットがあるか確認 |
| データセット詳細 | `get_dataset_info` | 説明、ラベル、ロケーション取得 |
| テーブル探索 | `list_table_ids` | 特定データセット内のテーブル一覧 |
| スキーマ確認 | `get_table_info` | カラム名、型、説明を取得 |
| データ取得 | `execute_sql` | SELECT、集計、結合クエリ実行 |

## 推奨ワークフロー

1. list_dataset_ids
   └─> 対象データセットを特定

2. list_table_ids(dataset_id)
   └─> 関連テーブルを探索

3. get_table_info(dataset_id, table_id)
   └─> クエリ前にスキーマ確認

4. execute_sql(query)
   └─> 最適化したクエリを実行


...(以降省略)

かろうじて BigQuery を利用することは察してくれたようですが、参照するべきプロジェクト ID やデータセットを知らないため、回答できませんでした。

知らない Project Id を指定した

検証 2: bigquery-ec-relations を追加

EC データに関するリレーションスキルを追加してみました。

---
name: bigquery-ec-relations
description: "EC サイト BigQuery テーブルの結合パターン定義。orders, order_items, products, users テーブル間のリレーションと推奨 JOIN 条件を提供。売上集計、顧客分析時に使用。"
---

# BigQuery テーブル結合パターン

## 対象データセット

- **プロジェクト:** `demo-project`
- **データセット:** `demo_ec_sales`

| テーブル | 説明 |
|----------|------|
| orders | 注文情報 |
| order_items | 注文明細 |
| products | 商品マスタ |
| users | ユーザー情報 |

## テーブル構成

users (1) ──── (N) orders (1) ──── (N) order_items (N) ──── (1) product

## 基本の結合条件

| From | To | 結合キー | 関係 |
|------|----|---------|------|
| orders | order_items | order_id | 1:N |
| order_items | products | product_id | N:1 |
| orders | users | user_id | N:1 |

...(中略)...

## 注意事項

- 売上集計時は必ず `order_status != 9` でキャンセルを除外
- 売上金額は `oi.quantity * oi.price` で計算
- order_items は必ず orders 経由で結合(直接 users と結合しない)

スキルをロードして、今回は適切にプロジェクトIDやデータセットの中を確認することができています。

-- 生成されたクエリ
SELECT 
  p.category_name,
  COUNT(DISTINCT o.order_id) AS order_count,
  SUM(oi.quantity) AS total_quantity,
  SUM(oi.quantity * oi.price) AS total_sales
FROM `demo-project.demo_ec_sales.orders` o
INNER JOIN `demo-project.demo_ec_sales.order_items` oi ON o.order_id = oi.order_id
INNER JOIN `demo-project.demo_ec_sales.products` p ON oi.product_id = p.product_id
INNER JOIN `demo-project.demo_ec_sales.users` u ON o.user_id = u.user_id
WHERE u.user_type = 2
  AND o.order_status != 9
  AND o.order_date >= '2025-11-01'
  AND o.order_date < '2025-12-01'
GROUP BY p.category_name
ORDER BY total_sales DESC

JOIN は正しく生成されましたが、いくつか問題があります。

  • 「プレミアム会員」の条件(user_type = 2)を推定している。またそのために多くのクエリを発行している
  • 「完了注文」の条件(order_status = 4)ではなく、キャンセル除外(!= 9)のみ

絞り込み条件について推定している様子

検証 3: bigquery-ec-values を追加

EC データに関する値の定義スキルを追加してトライしてみます。

---
name: bigquery-ec-values
description: "EC サイト BigQuery テーブルのカラム値定義。order_status や user_type などの数値コードとビジネス用語の対応を提供。売上分析、注文ステータス、会員種別の質問時に使用。"
---

# BigQuery カラム値定義

## orders テーブル

### order_status
| 値 | 意味 | ビジネス用語 |
|----|------|-------------|
| 1 | 受注済み | 新規注文 |
| 2 | 出荷準備中 | 処理中 |
| 3 | 出荷済み | 配送中 |
| 4 | 配達完了 | 完了 |
| 9 | キャンセル | キャンセル |

**よく使う条件:**
- 完了注文: `order_status = 4`
- キャンセル除外: `order_status != 9`
- アクティブ注文: `order_status IN (1, 2, 3)`

## users テーブル

### user_type
| 値 | 意味 |
|----|------|
| 1 | 一般会員 |
| 2 | プレミアム会員 |
| 3 | 法人会員 |

**よく使う条件:**
- プレミアム会員: `user_type = 2`
- 個人会員(一般+プレミアム): `user_type IN (1, 2)`

今回は正しいクエリが生成されました。

-- 生成されたクエリ
SELECT
  p.category_name,
  COUNT(DISTINCT o.order_id) AS order_count,
  SUM(oi.quantity) AS total_quantity,
  SUM(oi.quantity * oi.price) AS total_sales
FROM `demo-project.demo_ec_sales.orders` o
INNER JOIN `demo-project.demo_ec_sales.order_items` oi ON o.order_id = oi.order_id
INNER JOIN `demo-project.demo_ec_sales.products` p ON oi.product_id = p.product_id
INNER JOIN `demo-project.demo_ec_sales.users` u ON o.user_id = u.user_id
WHERE o.order_status = 4
  AND u.user_type = 2
  AND o.order_date >= '2025-11-01'
  AND o.order_date < '2025-12-01'
GROUP BY p.category_name
ORDER BY total_sales DESC

いくつかエラーが途中出てしまったが無事完了した

検証 4: マーケティングデータセットを追加する

次に、マーケティング用スキルを追加して別ドメインの分析を試しました。

終了済みの Google 広告キャンペーンの ROAS をキャンペーン別に計算してください

今回は、適切にLEFT JOINを判断できていたり、終了のステータス(3)で正しくフィルタできています。

-- 生成されたクエリ
SELECT
  c.campaign_name,
  c.channel,
  COUNT(DISTINCT i.impression_id) as impressions,
  COUNT(DISTINCT cl.click_id) as clicks,
  COUNT(DISTINCT cv.conversion_id) as conversions,
  SUM(i.cost) as total_cost,
  SUM(cv.revenue) as total_revenue,
  ROUND(SUM(cv.revenue) / NULLIF(SUM(i.cost), 0), 2) as roas
FROM `demo-project.demo_marketing.campaigns` c
LEFT JOIN `demo-project.demo_marketing.ad_impressions` i 
  ON c.campaign_id = i.campaign_id
LEFT JOIN `demo-project.demo_marketing.ad_clicks` cl 
  ON i.impression_id = cl.impression_id
LEFT JOIN `demo-project.demo_marketing.conversions` cv 
  ON cl.click_id = cv.click_id
WHERE c.status = 3  -- 終了済み
  AND c.channel = 'google_ads'  -- Google 広告のみ
GROUP BY 1, 2
ORDER BY roas DESC

また、EC 側のスキルはロードされませんでした。Progressive Disclosure が期待通りに機能しています。

正しくマーケティングデータが分析できた

検証 5: 複数ドメインをまたぐ分析

EC とマーケティング両方のデータが必要なケースを試しました。

キャンペーンデータと売上データを見比べて、キャンペーン以外からの収益を集計してください

この依頼では、全てのスキルがロードされ、両方のデータセットを結合した正しいクエリが生成されました。

--- 生成されたクエリ
WITH campaign_orders AS (
  SELECT DISTINCT order_id
  FROM `demo-project.demo_marketing.conversions`
  WHERE order_id IS NOT NULL
)
SELECT 
  o.order_id,
  o.order_date,
  SUM(oi.quantity * oi.price) as order_amount
FROM `demo-project.demo_ec_sales.orders` o
INNER JOIN `demo-project.demo_ec_sales.order_items` oi 
  ON o.order_id = oi.order_id
LEFT JOIN campaign_orders co 
  ON o.order_id = co.order_id
WHERE o.order_status != 9  -- キャンセル除外
  AND co.order_id IS NULL  -- キャンペーン経由ではない注文のみ
GROUP BY o.order_id, o.order_date
ORDER BY o.order_date DESC, o.order_id
LIMIT 20

ECとマーケティングの各スキルを呼び出して分析している

検証 6: グラフによる可視化

分析の場合、可視化まで行いたいことが多いのではないでしょうか。 グラフ描画に関するskillを設定していない場合、リッチなダッシュボードを生成する傾向があります。

スキルがないとリッチなダッシュボードを作成してくれる

artifact-charts スキルはシンプルなチャートを生成するスキルになっており、Chart.js のテンプレートにデータを差し込むだけで即座にグラフを描画できます。

---
name: artifact-charts
description: "データ分析結果を高速にグラフ化。アーティファクトで棒グラフ、折れ線グラフ、円グラフを即座に描画。可視化、グラフ作成、チャート描画時に使用。"
---

# 高速グラフ描画

データを受け取ったら、以下のテンプレートにデータを差し込んでアーティファクトを生成する。

## ポリシー: シンプルさを最優先

- **1 リクエスト = 1〜2 グラフ**: ダッシュボードは作らない
- **装飾は最小限**: アニメーション、グラデーション、影は使わない
- **凡例・ラベルは必要な場合のみ**: データが自明なら省略
- **色は単色 or 3 色まで**: 視認性を優先
- **インタラクティブ機能は不要**: ホバー、ズーム、フィルタは追加しない

**禁止事項:**
- 複数グラフを並べたダッシュボード
- タブ切り替え UI
- リアルタイム更新機能
- 外部データ読み込み

## グラフ選択

| データの性質 | グラフ | 例 |
|-------------|--------|-----|
| カテゴリ比較 | 棒グラフ | カテゴリ別売上 |
| 時系列推移 | 折れ線 | 月別推移 |
| 構成比 | 円グラフ | 売上構成比 |

...(以降省略)

スキルによってシンプルなグラフを素早く生成する

まとめ

Claude Skills を活用することで、BigQuery MCP 単体では実現できなかったドメイン知識の注入が可能となり、実用的なデータ分析を短時間で実行できることが確認できました。 また、グラフ描画まで一気通貫で実行してくれるので、レポーティング資料の作成まで自動化することも可能ですね。 クイックに AI での分析環境を立ち上げる際には選択肢の一つになるのではないでしょうか。