🧑‍💻 開発者向けPython / 広告API / Claude Code

広告運用レポートをClaude Codeで自動化する方法【2026】Google広告・Meta広告API×スクリプト×Skills(設計と実装)

毎週月曜の朝、Google広告とMeta広告の管理画面からCSVを落とし、スプレッドシートに貼って前週比を出し、所感を書いてSlackに流す——この作業をまるごとClaude Code(コーディングエージェント)と広告媒体のAPIで自動化します。ポイントは「計算はスクリプト、解釈はAI、判断は人」の分担を仕組みで強制すること。この記事では、広告運用を本業とする運営会社が支援現場で採っている設計の考え方と、そのまま動かせるコード・設定ファイルを公開します。

🔬
調査・編集:AIエージェント研究所 編集部
運営=広告運用・生成AIエージェント導入支援の現役コンサル。このサイト自体も、AIエージェントと一緒に作り・公開・運用しています。

広告運用の現場で実際に採っている設計の考え方をもとに、エンジニア向けに実装まで解説します。→ 運営者・編集方針について

公開:2026年9月26日/最終更新:2026年9月26日
この記事の結論(要点まとめ)
  • レポート自動化は「取得・集計=Pythonスクリプト(決定的)」「解釈・文章化・掘り下げ=Claude Code」「判断・実行=人」に分ける。AIに四則演算をさせない
  • AIへの指示はCLAUDE.md(指標定義と厳守ルール)とSkills(.claude/skills/weekly-ad-report/SKILL.md=週次手順)で渡し、claude -p のヘッドレス実行を cron / GitHub Actions で定期起動する
  • ガードレールはpermissions(allow/ask/deny)とhooksで強制。PreToolUseで書き込み系コマンドを止め、Stopフックで「レポート中の数値が集計結果に実在するか」を検証する。入札・予算の変更は提案まで、実行は人
この記事の前提 Python(pandas)とターミナル操作、Git/GitHubの基本が分かる方向けです。API・サーバー・Gitなどの基礎から学びたい方は、AIエージェント時代のIT基礎ロードマップから読むのがおすすめです。料金・API仕様は執筆時点(2026年9月)の情報です。各媒体・Claude Codeの公式ドキュメントで最新情報を確認してください。

課題:レポート作成のどこをAIに任せ、どこを人が見るか

広告運用の週次・月次レポートは、どの現場でもだいたい次の流れで作られています。

  1. Google広告・Meta広告・(必要ならGA4)の管理画面からCSVをダウンロード
  2. スプレッドシートに貼り付け、媒体ごとに違う列名を揃えて合計・前週比・CPA・ROASを計算
  3. 数字を眺めて「何が起きたか」「なぜか」「次に何をするか」の所感コメントを書く
  4. Slackやメールでクライアント・チームに共有

このうち1と2は完全に機械的な作業で、ミスが起きると致命的です(貼り付け範囲のズレ、期間の取り違え、比率の平均を取ってしまう、など)。3は経験が出る部分ですが、「CPAが上がった媒体を見つけてキャンペーン別に分解し、候補を挙げる」ところまでは定型化できます。一方で、予算配分や入札の変更を決めることは、クライアントの事業状況や在庫・キャンペーン予定など、データの外側にある情報が効くため、人が判断すべき領域です。

工程担当理由
媒体APIからのデータ取得Pythonスクリプト毎回同じ処理。再現性と監査性が必要
指標名の正規化・集計・前週比・異常値検知Pythonスクリプト(pandas)LLMは桁の多い計算で誤りうる。計算は決定的なコードで
数値の解釈・所感の文章化・原因候補の洗い出しClaude Code言語化と「どこを掘るか」の探索はAIが得意
数値の突き合わせ検証Pythonスクリプト(hooksで強制)AIの出力をAIに検証させない
レビュー・クライアント共有の可否判断人最終責任を持つのは運用者
入札・予算・配信ON/OFFの変更人(AIは提案まで)お金が動く操作。誤実行のコストが大きい
助手アイ
助手アイAIに管理画面のCSVを丸ごと渡して「レポート書いて」じゃダメなんですか? そのほうが早そうです。
エージェント博士
エージェント博士試すだけならそれでもよいのじゃ。じゃが毎週クライアントに出すなら話は別じゃぞ。AIはそれらしい数字を書いてしまうことがある。CPAを1桁間違えたレポートが1回出れば、自動化で浮いた時間より信頼回復のほうが高くつく。だから数字はコードで作り、AIには「読んで書く」だけをさせるんじゃ

全体アーキテクチャ:6つの工程と役割分担

全体の流れは次の6工程です。色付きの工程だけがAI(Claude Code)の担当で、それ以外はコードと人が担います。

コードAI人①取得広告APIGA4 API②正規化指標名を共通の形に③計算前週比・CPAROAS・異常値④文章化解釈して書くreports/*.md⑤レビュー運用者が下書きを確認⑥共有Slackに投稿履歴を保存計算済みの数字数値検証hooks下書き確認済み
AI(Claude Code)が担うのは④の解釈と文章化だけです。①取得(Google Ads・Meta・GA4のAPI)②正規化 ③計算(前週比・CPA・ROAS・異常値フラグ)はPythonで決定的に行い、hooksの数値検証を通った下書きを運用者が確認してから、⑥Slackに投稿しreports/に履歴を残します。

リポジトリ構成は次のとおりです。Claude Codeは「このリポジトリの中で、決められたスクリプトを実行し、決められた場所にMarkdownを書く作業者」という位置づけです。

ディレクトリ構成text
ad-report/
├── CLAUDE.md                     # 指標定義と厳守ルール(毎回読み込まれる)
├── .claude/
│   ├── settings.json             # permissions と hooks
│   ├── skills/weekly-ad-report/SKILL.md   # 週次レポートの手順書
│   └── agents/ad-investigator.md # 掘り下げ調査用のサブエージェント
├── scripts/
│   ├── fetch_ads.py              # ① 取得+② 正規化
│   ├── aggregate.py              # ③ 集計・前週比・異常値
│   ├── breakdown.py              # 掘り下げ用(読み取り専用)
│   ├── verify_report.py          # 数値の突き合わせ検証
│   ├── guard_bash.py             # 書き込み系コマンドのブロック
│   └── post_slack.py             # ⑥ Slack投稿
├── data/                         # 取得・集計結果(Git管理しない)
├── reports/                      # ④ Claude Codeが書く週次レポート
├── proposals/                    # 変更の「提案」置き場(実行は人)
├── run_weekly.sh                 # ヘッドレス実行の入口
├── google-ads.yaml / .env        # 認証情報(Git・Claudeの読み取り対象外)
└── .gitignore

なぜ「Claude APIを直接呼ぶ」ではなく「Claude Code」なのか

Claude APIをPythonから呼んでレポートJSONを生成する作り方もあります(後半で補足します)。それでも運用チームの業務自動化では、Claude Codeを中心に据える構成を当社では第一候補にしています。判断基準は次のとおりです。

観点Claude Code+スクリプトClaude APIをアプリに組み込む
手順の変更SKILL.md(日本語の手順書)を書き換えるだけ。運用者でも直せるプロンプトとコードの修正・デプロイが必要
掘り下げ分析同じリポジトリで対話モードに切り替えれば、そのままスクリプトを使って深掘りできるツール定義とエージェントループを自前で実装
スクリプトの保守API仕様変更時のコード修正もClaude Codeに頼める別途開発が必要
向いている規模社内チーム・数十アカウント程度の定型業務自社SaaSとして多数の顧客に提供する場合
弱み実行ごとの出力の揺れ。形式の強制はスキーマではなく検証スクリプト頼み開発・運用コストが大きい
当社の考え方

「AIに任せる範囲」は、まず狭く決めてから広げる

当社が広告運用×生成AIの導入を支援する際は、最初から「AIが運用する」構成にはしません。次の順で段階的に広げる設計を採っています。

  • 段階1:取得と集計をスクリプト化し、数字が管理画面と一致することを人が確認する(ここまではAIなし)
  • 段階2:所感の下書きをAIに書かせ、人が直して出す。直した箇所をSKILL.mdやCLAUDE.mdのルールに反映する
  • 段階3:異常時の掘り下げ(キャンペーン別・検索語句別の分解)をAIに任せ、変更は「提案ファイル」として出させる

この「機械チェックで止める」発想は、このサイト自体の運用とも共通です。本サイトでは、共通フッター(広告表記)を流し込めないページがあると build.js が exit 1 で止まり、景表法まわりの表記を keihyo-check.js で機械チェックしています。人の注意力に頼らず仕組みで止めるのが、AIと一緒に運用するときの基本です(詳しくはAIエージェントでメディアサイトを運用する仕組み)。

①データ取得:Google広告・Meta・GA4のAPIと認証

まずは管理画面からのCSVダウンロードをAPI取得に置き換えます。主要3媒体の認証方式の概要は次のとおりです(申請手順やアクセスレベルは変わることがあるため、必ず公式ドキュメントで確認してください)。

API認証に必要なもの(概要)Python
Google Ads API開発者トークン(MCCアカウントで申請。アクセスレベルあり)+OAuth 2.0(クライアントID/シークレット、リフレッシュトークン)。MCC経由でアクセスする場合は login_customer_id公式ライブラリ google-ads。GAQL(SQL風のクエリ言語)で取得
Meta Marketing API(Insights)アクセストークン(運用ではシステムユーザーのトークンが推奨)。APIバージョンは定期的に更新・廃止されるGraph APIをHTTPで叩く、または facebook_business SDK
GA4 Data APIサービスアカウントに、GA4プロパティの閲覧権限を付与google-analytics-data

日本の案件ではYahoo!広告APIも候補になります。構造は同じで、「取得関数を1つ足して共通スキーマに揃える」だけです。

scripts/fetch_ads.pyPython
# scripts/fetch_ads.py : 媒体APIから取得し「共通スキーマ」に揃えてCSV保存する
import json
import os

import pandas as pd
import requests
from google.ads.googleads.client import GoogleAdsClient

# 共通スキーマ(1行 = 媒体 × 日付 × キャンペーン)
COMMON_COLUMNS = [
    "date", "platform", "account_id", "campaign_id", "campaign_name",
    "impressions", "clicks", "cost", "conversions", "conversion_value",
]


def fetch_google_ads(customer_id: str, start: str, end: str) -> pd.DataFrame:
    """Google Ads API(GAQL)でキャンペーン別・日別の実績を取得する"""
    # 認証情報(開発者トークン・OAuthクライアント・リフレッシュトークン)は google-ads.yaml に置く
    client = GoogleAdsClient.load_from_storage("google-ads.yaml")
    service = client.get_service("GoogleAdsService")
    query = f"""
        SELECT segments.date, campaign.id, campaign.name,
               metrics.impressions, metrics.clicks, metrics.cost_micros,
               metrics.conversions, metrics.conversions_value
        FROM campaign
        WHERE segments.date BETWEEN '{start}' AND '{end}'
    """
    rows = []
    for batch in service.search_stream(customer_id=customer_id, query=query):
        for r in batch.results:
            rows.append({
                "date": r.segments.date,
                "platform": "google_ads",
                "account_id": customer_id,
                "campaign_id": str(r.campaign.id),
                "campaign_name": r.campaign.name,
                "impressions": r.metrics.impressions,
                "clicks": r.metrics.clicks,
                # Google広告の費用は micros(100万分の1単位)で返る
                "cost": r.metrics.cost_micros / 1_000_000,
                "conversions": r.metrics.conversions,
                "conversion_value": r.metrics.conversions_value,
            })
    return pd.DataFrame(rows, columns=COMMON_COLUMNS)


# Metaの「何をCVとみなすか」はアカウント設定次第。案件ごとに決めて設定ファイルで管理する
META_CONVERSION_ACTION = os.environ.get("META_CONVERSION_ACTION", "purchase")
META_API_VERSION = os.environ["META_API_VERSION"]  # 例: "vXX.0"(最新は公式で確認)


def _sum_action(actions: list | None, action_type: str) -> float:
    return sum(float(a["value"]) for a in (actions or []) if a["action_type"] == action_type)


def fetch_meta(ad_account_id: str, start: str, end: str) -> pd.DataFrame:
    """Meta Marketing API(Insights)でキャンペーン別・日別の実績を取得する"""
    url = f"https://graph.facebook.com/{META_API_VERSION}/act_{ad_account_id}/insights"
    params = {
        "level": "campaign",
        "fields": "campaign_id,campaign_name,impressions,clicks,spend,actions,action_values",
        "time_range": json.dumps({"since": start, "until": end}),
        "time_increment": 1,
        "access_token": os.environ["META_ACCESS_TOKEN"],
    }
    rows = []
    while url:
        res = requests.get(url, params=params, timeout=30)
        res.raise_for_status()
        body = res.json()
        for r in body.get("data", []):
            rows.append({
                "date": r["date_start"],
                "platform": "meta",
                "account_id": ad_account_id,
                "campaign_id": r["campaign_id"],
                "campaign_name": r["campaign_name"],
                # Metaは数値が文字列で返るので明示的に変換する
                "impressions": int(r.get("impressions", 0)),
                "clicks": int(r.get("clicks", 0)),
                "cost": float(r.get("spend", 0)),
                "conversions": _sum_action(r.get("actions"), META_CONVERSION_ACTION),
                "conversion_value": _sum_action(r.get("action_values"), META_CONVERSION_ACTION),
            })
        # 次ページのURLにはパラメータが含まれるので params は空にする
        url = body.get("paging", {}).get("next")
        params = {}
    return pd.DataFrame(rows, columns=COMMON_COLUMNS)


def load_all(start: str, end: str) -> pd.DataFrame:
    frames = [
        fetch_google_ads(os.environ["GOOGLE_ADS_CUSTOMER_ID"], start, end),
        fetch_meta(os.environ["META_AD_ACCOUNT_ID"], start, end),
    ]
    df = pd.concat(frames, ignore_index=True)
    df["date"] = pd.to_datetime(df["date"])
    return df


def fetch_ga4_channels(property_id: str, start: str, end: str) -> pd.DataFrame:
    """GA4 Data API:チャネル別のセッション・キーイベント(サイト側の答え合わせ用)"""
    from google.analytics.data_v1beta import BetaAnalyticsDataClient
    from google.analytics.data_v1beta.types import DateRange, Dimension, Metric, RunReportRequest

    client = BetaAnalyticsDataClient()  # サービスアカウント(GOOGLE_APPLICATION_CREDENTIALS)で認証
    res = client.run_report(RunReportRequest(
        property=f"properties/{property_id}",
        dimensions=[Dimension(name="date"), Dimension(name="sessionDefaultChannelGroup")],
        metrics=[Metric(name="sessions"), Metric(name="keyEvents")],
        date_ranges=[DateRange(start_date=start, end_date=end)],
    ))
    return pd.DataFrame([{
        "date": r.dimension_values[0].value,
        "channel": r.dimension_values[1].value,
        "sessions": int(r.metric_values[0].value),
        "key_events": float(r.metric_values[1].value),
    } for r in res.rows])


if __name__ == "__main__":
    import argparse

    p = argparse.ArgumentParser()
    p.add_argument("--start", required=True)
    p.add_argument("--end", required=True)
    a = p.parse_args()
    os.makedirs("data/raw", exist_ok=True)
    load_all(a.start, a.end).to_csv("data/raw/ads_daily.csv", index=False)
    print("saved: data/raw/ads_daily.csv")

設計上のポイントは3つです。

  • 単位と型を取得時に揃える:Google広告の費用は cost_micros(100万分の1単位)、Metaの数値は文字列で返ります。後工程に持ち越すとAIもスクリプトも混乱するので、ここで円・数値型に変換します。
  • 「何をCVとするか」は設定で持つ:Metaの actions はアクション種別ごとの配列で、どの action_type を成果とみなすかは案件次第です。コードに埋め込まず環境変数や設定ファイルで管理します。
  • GA4は「答え合わせ」用:媒体の計測CVとサイト側のキーイベントは定義も計測方法も違うため、一致しないのが普通です。レポートで両者を混ぜて計算せず、別の表として並べます。
認証情報の置き場所 google-ads.yaml・.env・サービスアカウントの鍵ファイルは .gitignore でコミット対象から外し、後述の permissions でClaude CodeのRead対象からも除外します。AIに秘密情報を「読ませない」ことが、プロンプトインジェクション対策としても最も確実です。

②③正規化と集計は必ずコードで(pandas)

媒体ごとに呼び名が違う指標を、共通スキーマに揃えます。

共通スキーマGoogle Ads APIMeta Insights
impressionsmetrics.impressionsimpressions
clicksmetrics.clicksclicks
cost(円)metrics.cost_micros ÷ 1,000,000spend
conversionsmetrics.conversionsactions のうち指定 action_type の合計
conversion_valuemetrics.conversions_valueaction_values のうち指定 action_type の合計
CPA / ROAS / CTR / CVR取得せず、上の基本指標から自分で計算する(定義を全媒体で揃えるため)

集計スクリプトは、今週と前週を媒体別+全体(ALL)で集計し、前週比・異常値フラグを付けて summary.json に書き出します。このJSONが、AIが引用してよい数値の「唯一の出典」になります。

scripts/aggregate.pyPython
# scripts/aggregate.py : 計算はすべてコードで行う(Claudeに四則演算させない)
import json

import numpy as np
import pandas as pd

BASE = ["impressions", "clicks", "cost", "conversions", "conversion_value"]
RATIOS = ["ctr", "cvr", "cpa", "roas"]


def _safe_div(a: pd.Series, b: pd.Series) -> pd.Series:
    # 0除算は NaN にする(「CV0件のCPA」を無限大や0として扱わない)
    return a / b.replace(0, np.nan)


def add_ratios(t: pd.DataFrame) -> pd.DataFrame:
    t = t.copy()
    t["ctr"] = (_safe_div(t["clicks"], t["impressions"]) * 100).round(2)   # %
    t["cvr"] = (_safe_div(t["conversions"], t["clicks"]) * 100).round(2)   # %
    t["cpa"] = _safe_div(t["cost"], t["conversions"]).round(0)             # 円
    t["roas"] = (_safe_div(t["conversion_value"], t["cost"]) * 100).round(1)  # %
    return t


def weekly_summary(df: pd.DataFrame, week_start: str) -> pd.DataFrame:
    """今週と前週を媒体別+全体で集計し、前週比(%)を付ける"""
    start = pd.Timestamp(week_start)
    prev_start, end = start - pd.Timedelta(days=7), start + pd.Timedelta(days=7)

    # 媒体横断の合計行(ALL)を作る。比率は必ず「合計してから割る」
    both = pd.concat([df, df.assign(platform="ALL")], ignore_index=True)
    cur = both[(both["date"] >= start) & (both["date"] < end)]
    prev = both[(both["date"] >= prev_start) & (both["date"] < start)]

    cur = add_ratios(cur.groupby("platform")[BASE].sum())
    prev = add_ratios(prev.groupby("platform")[BASE].sum())
    out = cur.join(prev, rsuffix="_prev", how="outer")

    for m in ["cost", "conversions"] + RATIOS:
        out[f"{m}_wow_pct"] = (_safe_div(out[m] - out[f"{m}_prev"], out[f"{m}_prev"]) * 100).round(1)
    out["cost"] = out["cost"].round(0)
    out["cost_prev"] = out["cost_prev"].round(0)
    return out


def flag_cost_anomalies(df: pd.DataFrame, window: int = 28, z: float = 3.0) -> pd.DataFrame:
    """日別費用が過去window日の平均から z×標準偏差 以上ずれた日を検出する"""
    daily = df.groupby(["platform", "date"], as_index=False)[BASE].sum().sort_values("date")

    def _zscore(s: pd.Series) -> pd.Series:
        hist = s.shift(1).rolling(window, min_periods=14)  # 当日を含めない
        return (s - hist.mean()) / hist.std().replace(0, np.nan)

    daily["cost_z"] = daily.groupby("platform")["cost"].transform(_zscore).round(2)
    return daily[daily["cost_z"].abs() >= z]


def rule_flags(summary: pd.DataFrame, min_conv: float = 10, cpa_worse_pct: float = 20.0) -> list[dict]:
    """運用ルールに基づくフラグ。サンプルが少ない週は判定しない"""
    flags = []
    for platform, r in summary.iterrows():
        if r["conversions_prev"] >= min_conv and pd.notna(r["cpa_wow_pct"]) and r["cpa_wow_pct"] >= cpa_worse_pct:
            flags.append({"platform": platform, "type": "cpa_worse",
                          "cpa": float(r["cpa"]), "cpa_prev": float(r["cpa_prev"]),
                          "cpa_wow_pct": float(r["cpa_wow_pct"])})
        if r["conversions"] == 0 and r["cost"] > 0:
            flags.append({"platform": platform, "type": "zero_conversion", "cost": float(r["cost"])})
    return flags


def build_payload(df: pd.DataFrame, week_start: str) -> dict:
    """AIに渡す入力JSON。ここにある数値だけをAIに引用させる"""
    summary = weekly_summary(df, week_start)
    anomalies = flag_cost_anomalies(df)
    anomalies = anomalies[anomalies["date"] >= pd.Timestamp(week_start)]
    anomalies = anomalies.assign(date=anomalies["date"].dt.strftime("%Y-%m-%d"))
    return {
        "period": {"week_start": week_start,
                   "week_end": str((pd.Timestamp(week_start) + pd.Timedelta(days=6)).date())},
        "currency": "JPY",
        # to_json は NaN を null にしてくれる
        "summary": json.loads(summary.reset_index().to_json(orient="records", force_ascii=False)),
        "flags": rule_flags(summary),
        "cost_anomalies": json.loads(
            anomalies[["platform", "date", "cost", "cost_z"]].to_json(orient="records")),
    }


if __name__ == "__main__":
    import argparse
    import os

    p = argparse.ArgumentParser()
    p.add_argument("--week-start", required=True)
    a = p.parse_args()
    df = pd.read_csv("data/raw/ads_daily.csv", parse_dates=["date"])
    payload = build_payload(df, a.week_start)
    out_dir = f"data/weekly/{a.week_start}"
    os.makedirs(out_dir, exist_ok=True)
    with open(f"{out_dir}/summary.json", "w", encoding="utf-8") as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)
    weekly_summary(df, a.week_start).to_csv(f"{out_dir}/summary.csv")
    print(f"saved: {out_dir}/summary.json, summary.csv")

「なぜそう書くか」を補足します。

  • 比率は「合計してから割る」:日別CPAの平均は全体CPAになりません。AIに任せるとやりがちな誤りなので、集計ロジックはコードに固定します。
  • 0除算はnull:CV0件のCPAを0円や無限大にすると、AIが「CPAが大幅改善」と誤読します。nullにしてCLAUDE.mdで意味を定義します。
  • サンプルが少ない週は判定しない:前週CVが数件の状態での「CPA+50%」はノイズです。min_conv のような閾値はアカウントの規模に合わせて調整します。
  • 異常値は「当日を含めない」過去平均と比較:shift(1) を入れないと、急増した当日が平均と標準偏差を押し上げて検知が鈍ります。z値の閾値3はあくまで出発点です。

出力される summary.json はこのような形です(値はテスト用のダミーデータ)。

data/weekly/2026-09-14/summary.json(抜粋)JSON
{
  "period": {
    "week_start": "2026-09-14",
    "week_end": "2026-09-20"
  },
  "currency": "JPY",
  "summary": [
    {
      "platform": "ALL",
      "cost": 1545921.0,
      "conversions": 195.0,
      "cpa": 7928.0,
      "roas": 113.5,
      "cost_prev": 1477231.0,
      "conversions_prev": 314.0,
      "cpa_prev": 4705.0,
      "cost_wow_pct": 4.6,
      "cpa_wow_pct": 68.5
    }
  ],
  "flags": [
    {
      "platform": "ALL",
      "type": "cpa_worse",
      "cpa": 7928.0,
      "cpa_prev": 4705.0,
      "cpa_wow_pct": 68.5
    },
    {
      "platform": "google_ads",
      "type": "cpa_worse",
      "cpa": 8988.0,
      "cpa_prev": 4690.0,
      "cpa_wow_pct": 91.6
    }
  ],
  "cost_anomalies": [
    {
      "platform": "google_ads",
      "date": "2026-09-17",
      "cost": 256139.0,
      "cost_z": 15.24
    }
  ]
}

④Claude Codeへの指示:CLAUDE.mdとSkills

Claude Codeは、ターミナルで動くAnthropicのコーディングエージェントです(npm install -g @anthropic-ai/claude-code などでインストールし、プロジェクトのディレクトリで claude を起動)。ファイルの読み書きとコマンド実行ができるので、「スクリプトを実行し、結果を読み、Markdownを書く」という作業をそのまま任せられます。

指示は2層に分けます。

  • CLAUDE.md:プロジェクト直下に置くと毎回読み込まれる「このリポジトリのルール」。指標定義と厳守ルールを書く(/init で雛形も作れます)
  • Skills:.claude/skills/<name>/SKILL.md に置く定型業務の手順書。フロントマターの name と description を見て、Claudeが必要なときに読み込みます
CLAUDE.mdMarkdown
# 広告レポート自動化リポジトリ

## このリポジトリの役割
Google広告・Meta広告の実績を取得・集計し、週次レポート(Markdown)を作る。
Claudeの担当は「集計結果の解釈・文章化・掘り下げ分析・スクリプト保守」。数値計算はしない。

## ディレクトリ
- scripts/fetch_ads.py   … 媒体APIから取得 → data/raw/ads_daily.csv
- scripts/aggregate.py   … 集計・前週比・異常値 → data/weekly/<週初日>/summary.json
- scripts/breakdown.py   … 掘り下げ用の読み取り専用クエリ
- scripts/verify_report.py … レポート中の数値が集計結果に存在するか検証
- reports/<週初日>.md    … 週次レポートの出力先
- proposals/             … 入札・予算などの変更「提案」の出力先(実行は人)

## 指標定義(summary.json)
- cost=広告費(円・税抜)、conversions=CV数、conversion_value=CV価値(円)
- ctr=clicks/impressions×100(%)、cvr=conversions/clicks×100(%)
- cpa=cost/conversions(円、CV0件はnull)、roas=conversion_value/cost×100(%)
- *_prev=前週、*_wow_pct=前週比(%)、platform="ALL"=媒体合計

## 厳守ルール
- 数値を自分で計算・合算・換算しない。必要な数値は scripts/ を修正・実行して出す
- レポートには summary.json の数値を表記どおり引用する(「約」「万」への丸め禁止)
- summary.json にない事実・原因は「推測:」と明記し、確認方法を添える
- 成果を保証・断定する表現(「必ず改善」「確実に増える」等)を使わない
- 入札・予算・配信ON/OFFの変更は実行しない。proposals/ に提案を書くまで
- .env、google-ads.yaml、*.key は読まない・表示しない

システムプロンプトに相当する「厳守ルール」は、広告運用ならではの観点を入れています。特に「約」「万」への丸め禁止は、後述の数値検証を機械的に通すための工夫です(「約84万円」と書かれると、入力の835,845と突き合わせられない)。また、成果を保証・断定する表現を禁じているのは、レポートの文面がそのまま広告主や社内の意思決定に使われるためです。

.claude/skills/weekly-ad-report/SKILL.mdMarkdown
---
name: weekly-ad-report
description: 週次の広告運用レポートを作成する。「週次レポート」「先週の広告の振り返り」などと頼まれたときに使う。引数は週初日(YYYY-MM-DD、月曜)。
---

# 週次広告レポートの作成手順

1. 週初日(月曜)を確認する。指定がなければ直近の完了週の月曜を使う
2. 前週比と異常値検知のため、週初日の35日前〜週末日の範囲でデータを取得する
   `python scripts/fetch_ads.py --start <週初日-35日> --end <週末日>`
3. 集計する: `python scripts/aggregate.py --week-start <週初日>`
4. `data/weekly/<週初日>/summary.json` を読み、以下の構成で `reports/<週初日>.md` を書く
   - `## サマリー`(3文以内)
   - `## 媒体別の数値`(summary.json の値をそのまま表にする)
   - `## ハイライト` / `## リスク・要確認`(flags と cost_anomalies は必ずどちらかで触れる)
   - `## 次のアクション(提案)`(優先度 high/medium/low、理由つき)
5. flags に cpa_worse がある媒体は、`python scripts/breakdown.py campaign ...` で
   キャンペーン別に分解し、悪化の主因候補を1〜3点に絞って「リスク」に書く
6. `python scripts/verify_report.py reports/<週初日>.md` を実行し、OK になるまで直す
7. Slack投稿はしない(投稿は run_weekly.sh が人のレビュー用チャンネルに行う)

## 書き方の注意
- 箇条書きは「-」を使う(番号付きリストは数値検証の邪魔になる)
- 数値には単位を付ける(円・%・件)

SKILL.mdは運用者が読んで分かる手順書として書くのがコツです。レビューで「リスク欄に在庫切れの可能性も書いてほしい」といった指摘が出たら、この手順書に1行足せば翌週から反映されます。プロンプトをコードに埋め込む作り方より、運用チーム自身で改善を回しやすくなります。

助手アイ
助手アイCLAUDE.mdとSKILL.md、どちらに書くか迷いそうです。
エージェント博士
エージェント博士目安はこうじゃ。どの作業でも守るべきこと(指標の定義、数値を計算しない、秘密情報を読まない)はCLAUDE.md。特定の作業の段取り(週次レポートの構成や手順)はSkill。月次レポートや媒体別の入稿チェックなど、業務が増えたらSkillを増やしていけばよいのじゃぞ

ガードレール:permissionsとhooksで「できないこと」を決める

CLAUDE.mdのルールは「お願い」にすぎません。守られなかったときに止める仕組みを、.claude/settings.json の permissions と hooks で用意します。

.claude/settings.jsonJSON
{
  "permissions": {
    "allow": [
      "Bash(python scripts/fetch_ads.py:*)",
      "Bash(python scripts/aggregate.py:*)",
      "Bash(python scripts/breakdown.py:*)",
      "Bash(python scripts/verify_report.py:*)",
      "Read(./data/**)",
      "Read(./reports/**)",
      "Write(./reports/**)",
      "Write(./proposals/**)"
    ],
    "ask": [
      "Edit(./scripts/**)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./google-ads.yaml)",
      "Read(./**/*.key)",
      "Bash(curl:*)",
      "Bash(python scripts/apply_changes.py:*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "python scripts/guard_bash.py" }]
      }
    ],
    "Stop": [
      {
        "hooks": [{ "type": "command", "command": "python scripts/verify_report.py" }]
      }
    ]
  }
}
  • allow:承認なしで実行してよいコマンドを、取得・集計・内訳・検証のスクリプトに絞る
  • ask:スクリプト自体の編集は、人の確認を挟む
  • deny:認証情報の読み取り、任意のHTTP送信(curl)、変更反映用スクリプトの実行を禁止

さらに PreToolUse フックで、Bashコマンドの中身を検査します。フックは標準入力でツール呼び出しの内容(JSON)を受け取り、exit code 2 で終了するとツール実行をブロックし、標準エラーの内容がClaudeに返されます。

scripts/guard_bash.pyPython
# scripts/guard_bash.py : PreToolUse フック。広告アカウントへの書き込み系コマンドを止める
import json
import re
import sys

BLOCK_PATTERNS = [
    r"apply_changes",      # 変更反映用スクリプトは人が手で実行する
    r"\bmutate",           # Google Ads API の更新系(Mutate)呼び出し
    r"requests\.post|curl\s.*-X\s*POST",  # 媒体APIへのPOST
    r"\.env\b|google-ads\.yaml",          # 認証情報ファイルの参照
]

event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")

for pattern in BLOCK_PATTERNS:
    if re.search(pattern, command):
        print(f"ブロック: '{pattern}' に該当するコマンドは実行できません。"
              "入札・予算・配信の変更は proposals/ に提案として書き出し、人の承認を待ってください。",
              file=sys.stderr)
        sys.exit(2)  # exit 2 = ツール実行をブロックし、理由をClaudeに返す
sys.exit(0)

そして Stop フック(Claudeが応答を終えようとしたときに発火)で、数値検証スクリプトを走らせます。レポート中の数値が summary.json に存在しなければ exit 2 で「終了」をブロックし、どの数値が怪しいかをClaudeに返して直させます。

scripts/verify_report.pyPython
# scripts/verify_report.py : レポート中の数値が集計JSONに実在するかを突き合わせる
import glob
import json
import os
import re
import sys

NUM_RE = re.compile(r"\d[\d,]*(?:\.\d+)?")


def numbers_in(text: str) -> set[float]:
    return {float(m.replace(",", "")) for m in NUM_RE.findall(text)}


def collect_numbers(obj) -> set[float]:
    """集計JSON内の数値(文字列中の日付なども含む)をすべて集める"""
    if isinstance(obj, bool) or obj is None:
        return set()
    if isinstance(obj, (int, float)):
        return {abs(float(obj))}  # 「12.3%悪化」のように符号なしで書かれることがある
    if isinstance(obj, str):
        return numbers_in(obj)
    if isinstance(obj, dict):
        obj = list(obj.values())
    return set().union(*(collect_numbers(v) for v in obj)) if obj else set()


def unknown_numbers(report_md: str, payload: dict, tol: float = 0.05) -> list[float]:
    allowed = collect_numbers(payload)
    return [n for n in sorted(numbers_in(report_md))
            if n > 3  # 「3点」など小さな数は対象外(運用に合わせて調整)
            and not any(abs(n - a) <= tol for a in allowed)]


def latest_report() -> str | None:
    files = sorted(glob.glob("reports/*.md"))
    return files[-1] if files else None


def main() -> int:
    hook_input = {}
    if not sys.stdin.isatty():
        raw = sys.stdin.read()
        hook_input = json.loads(raw) if raw.strip() else {}

    path = sys.argv[1] if len(sys.argv) > 1 else latest_report()
    if not path:
        return 0
    week = os.path.basename(path).removesuffix(".md")
    summary_path = f"data/weekly/{week}/summary.json"
    if not os.path.exists(summary_path):
        return 0  # 週次レポート以外のMarkdownは対象外

    with open(path, encoding="utf-8") as f:
        report_md = f.read()
    with open(summary_path, encoding="utf-8") as f:
        payload = json.load(f)

    bad = unknown_numbers(report_md, payload)
    if not bad:
        print(f"OK: {path} の数値はすべて集計結果に存在します")
        return 0
    msg = (f"{path} に集計結果({summary_path})に存在しない数値があります: {bad}\n"
           "該当箇所を集計JSONの値そのままに直すか、数値を削除してください。")
    print(msg, file=sys.stderr)
    # Stopフックが2回目以降なら無限ループを避けて止める(最終確認は run_weekly.sh 側でも行う)
    if hook_input.get("stop_hook_active"):
        return 1
    return 2  # exit 2 = Claudeに理由を返して、修正させる


if __name__ == "__main__":
    sys.exit(main())
検証ロジックの割り切り この検証は「数値が入力に存在するか」しか見ていません。「Google広告のCPA」として「MetaのCPA」の値を書く、といった取り違えは検出できません。完璧な検証ではなく、捏造された数値(入力にない数値)を確実に落とす安価な網と位置づけ、取り違えは人のレビューと後述の評価セットで拾います。無限ループを避けるため、Stopフックの入力にある stop_hook_active が真のとき(2回目以降)は止めるのを諦め、最終判定は定期実行スクリプト側で行います。

⑤⑥定期実行と共有:ヘッドレス実行・cron・GitHub Actions・Slack

Claude Codeは claude -p "プロンプト" で非対話(ヘッドレス)実行ができます。--output-format json を付けると実行結果がJSONで返るのでログに残しやすく、--allowedTools で許可するツールを明示できます。Skillを呼ぶスラッシュコマンドをそのままプロンプトに渡します。

run_weekly.shShell
#!/usr/bin/env bash
# run_weekly.sh : 毎週月曜の朝に cron から起動する
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p logs

# 直近の完了週(月〜日)の月曜日(GNU date 前提)
WEEK_START=$(date -d "-$(( $(date +%u) + 6 )) days" +%F)

claude -p "/weekly-ad-report ${WEEK_START}" \
  --allowedTools "Read,Write,Bash(python scripts/fetch_ads.py:*),Bash(python scripts/aggregate.py:*),Bash(python scripts/breakdown.py:*),Bash(python scripts/verify_report.py:*)" \
  --output-format json > "logs/claude-${WEEK_START}.json"

test -f "reports/${WEEK_START}.md"   # レポートが無ければここで失敗させる

# フックとは別に、最後にもう一度コードで検証してから共有する
if python scripts/verify_report.py "reports/${WEEK_START}.md" < /dev/null; then
  python scripts/post_slack.py "reports/${WEEK_START}.md"
else
  python scripts/post_slack.py "reports/${WEEK_START}.md" "数値検証NG。公開前に必ず人が確認してください"
fi

自社サーバーやVMで動かすなら cron に登録します。cronは環境変数やPATHが対話シェルと異なるため、ANTHROPIC_API_KEY などの読み込みとコマンドのフルパスに注意してください。

crontab -eShell
# 毎週月曜 8:45(サーバーのタイムゾーン)に実行。APIキー等は run_weekly.sh 側で読み込む
45 8 * * 1 /home/ops/ad-report/run_weekly.sh >> /home/ops/ad-report/logs/cron.log 2>&1

サーバーを持たないなら GitHub Actions の schedule で動かせます。秘密情報は GitHub の Secrets に置き、実行時にだけファイルへ書き出します。Claude Code用の公式アクション(anthropics/claude-code-action)もありますが、ここではCLIを直接インストールして run_weekly.sh を共通化する形にしています。

.github/workflows/weekly-report.ymlYAML
# .github/workflows/weekly-report.yml
name: weekly-ad-report
on:
  schedule:
    - cron: "0 0 * * 1"   # 毎週月曜 9:00 JST(UTCで指定)
  workflow_dispatch:

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: npm install -g @anthropic-ai/claude-code
      - name: Write credentials
        run: echo "$GOOGLE_ADS_YAML" > google-ads.yaml
        env:
          GOOGLE_ADS_YAML: ${{ secrets.GOOGLE_ADS_YAML }}
      - name: Generate report
        run: ./run_weekly.sh
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          META_ACCESS_TOKEN: ${{ secrets.META_ACCESS_TOKEN }}
          META_API_VERSION: ${{ vars.META_API_VERSION }}
          META_AD_ACCOUNT_ID: ${{ vars.META_AD_ACCOUNT_ID }}
          GOOGLE_ADS_CUSTOMER_ID: ${{ vars.GOOGLE_ADS_CUSTOMER_ID }}
          SLACK_REVIEW_WEBHOOK_URL: ${{ secrets.SLACK_REVIEW_WEBHOOK_URL }}
      - uses: actions/upload-artifact@v4
        with:
          name: weekly-report
          path: reports/

共有は Slack の Incoming Webhook で行います。投稿先はクライアント向けチャンネルではなく社内のレビュー用チャンネルにし、見出しに「下書き・要レビュー」と明記します。人が確認してから外部に出す、という⑤の工程を飛ばさないための設計です。

scripts/post_slack.pyPython
# scripts/post_slack.py : 保存済みのMarkdownレポートをSlackのレビュー用チャンネルへ投稿する
import os
import sys

import requests


def post(path: str, warning: str = "") -> None:
    with open(path, encoding="utf-8") as f:
        body = f.read()
    text = f"*【下書き・要レビュー】{os.path.basename(path)}*\n"
    if warning:
        text += f":warning: {warning}\n"
    text += "\n" + body
    # Incoming Webhook のURLは秘密情報。コードに直書きせず環境変数から読む
    res = requests.post(os.environ["SLACK_REVIEW_WEBHOOK_URL"], json={"text": text}, timeout=10)
    res.raise_for_status()


if __name__ == "__main__":
    post(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else "")
ヘッドレス実行の注意
  • CIで使うAPIキーはレポート専用に発行し、利用上限を設定しておく(料金・プランは公式で確認)
  • Actionsの実行ログにレポート本文や顧客名が残る点に注意。公開リポジトリでは動かさない
  • レポートを reports/ にコミットして履歴を残す場合も、社外秘の数値を扱うリポジトリはプライベートにする

掘り下げ分析:対話モードとサブエージェント

週次レポートで「Google広告のCPAが前週比で大きく悪化」とフラグが立ったら、次は原因の掘り下げです。ここは対話モードのClaude Codeが最も力を発揮する場面です。同じリポジトリで claude を起動し、「先週のGoogle広告のCPA悪化の原因を調べて」と頼みます。

AIに渡す「道具」は、読み取り専用の内訳スクリプト1本にまとめます。キャンペーン別・検索語句別・クリエイティブ別の実績を、期間とキャンペーンを指定して返すだけのものです(検索語句・クリエイティブ別の明細は、fetch_ads.py と同じ要領で取得スクリプトを追加して data/raw/ に保存しておきます)。

scripts/breakdown.pyPython
# scripts/breakdown.py : 掘り下げ分析用の読み取り専用クエリ(Claude Codeから呼ばせる)
import argparse

import pandas as pd

SOURCES = {
    "campaign": ("data/raw/ads_daily.csv", ["platform", "campaign_id", "campaign_name"]),
    "search_terms": ("data/raw/search_terms.csv", ["campaign_id", "search_term"]),
    "creative": ("data/raw/creative_daily.csv", ["platform", "campaign_id", "ad_id", "ad_name"]),
}


def main() -> None:
    p = argparse.ArgumentParser(description="期間を指定して内訳を集計する(読み取り専用)")
    p.add_argument("kind", choices=SOURCES.keys())
    p.add_argument("--start", required=True)
    p.add_argument("--end", required=True)
    p.add_argument("--campaign-id")
    p.add_argument("--top", type=int, default=30)
    a = p.parse_args()

    path, keys = SOURCES[a.kind]
    df = pd.read_csv(path, dtype={"campaign_id": str, "ad_id": str})
    df = df[(df["date"] >= a.start) & (df["date"] <= a.end)]
    if a.campaign_id:
        df = df[df["campaign_id"] == a.campaign_id]

    t = df.groupby(keys, as_index=False)[["clicks", "cost", "conversions"]].sum()
    t["cpa"] = (t["cost"] / t["conversions"].where(t["conversions"] > 0)).round(0)
    t = t.sort_values("cost", ascending=False).head(a.top)
    print(t.to_csv(index=False))  # Claudeが読みやすいようにCSVで標準出力へ


if __name__ == "__main__":
    main()

調査の型を固定したい場合は、サブエージェントとして役割を切り出します。.claude/agents/<name>.md に定義すると、メインの会話から「調査係」に作業を任せられます。

.claude/agents/ad-investigator.mdMarkdown
---
name: ad-investigator
description: CPAやCVの悪化原因を、キャンペーン→検索語句→クリエイティブの順に掘り下げる調査係。数値の変動理由を調べるときに使う。
tools: Read, Grep, Glob, Bash
---

あなたは広告運用の調査担当です。
- データは `python scripts/breakdown.py` の出力だけを根拠にする(campaign / search_terms / creative)
- 「前週と比べて何が変わったか」を、費用の大きい順に確認する
- 仮説ごとに「根拠となった出力」「確からしさ(高・中・低)」「人が追加で確認すべきこと」を書く
- 入札・予算・配信の変更は提案のみ。proposals/ に書き出し、実行しない

典型的な流れは、キャンペーン別に分解 → 悪化幅の大きいキャンペーンの検索語句を見る → 費用が増えてCVのない語句を特定 → 除外キーワードの追加を提案、です。このとき、Claudeは proposals/ に提案ファイルを書くところで止まります。

助手アイ
助手アイ除外キーワードの追加くらいなら、AIにそのまま実行させてもよさそうですけど……。
エージェント博士
エージェント博士そこがHuman-in-the-loopの線引きじゃ。除外キーワード1つでも、ブランド名や指名語句を誤って除外すれば売上に直結する。書き込み系は「提案ファイル → 人が確認 → 人が反映スクリプトを実行」にして、permissionsのdenyとPreToolUseフックの二重で止めておくのじゃ。任せる範囲を広げるのは、提案の質を何週か見てからでも遅くないぞ

コストとモデル選択/自社プロダクトに組み込む場合の補足

Claude Codeでのモデルの使い分け

Claude Codeでは /model コマンドや --model オプションで使うモデルを切り替えられます。執筆時点(2026年9月)の主なモデルと、APIで使う場合の料金(100万トークンあたり、入力/出力)は次のとおりです。

モデルAPI料金(入力/出力)広告レポートでの使いどころ
Claude Haiku 4.5$1 / $5多数アカウントの定型レポート、分類・タグ付けなど大量処理
Claude Sonnet 5$2 / $10週次レポートの所感作成。速度と品質のバランス
Claude Opus 5$5 / $25CPA悪化の原因調査など、複数の仮説を検証する掘り下げ分析

定期実行の週次レポートは手順が固定されているため中位モデルで十分なことが多く、掘り下げ分析のように探索が必要な作業では上位モデルが向きます。サブスクリプションプランで使う場合とAPIキーで使う場合で課金体系が異なるため、最新の料金・プランは公式サイトで確認してください。

コスト面でもう一つ効くのがプロンプトキャッシュです。Claude Codeは内部でキャッシュを活用しますが、キャッシュは「先頭から変わらない部分」が長いほど効きます。CLAUDE.mdやSKILL.mdに日付や毎回変わる値を書かない(変わる値はスクリプトの出力として読ませる)ようにしておくと、効率が落ちにくくなります。

補足:Claude APIで自社プロダクトに組み込む場合

レポート機能を自社のSaaSや社内システムに組み込む場合は、Claude APIを直接呼びます。このとき出力形式は structured outputs(output_config の format に JSON Schema を渡す)で固定すると、パース失敗がなくなります。集計部分(summary.json)と数値検証の考え方は、この記事の構成をそのまま流用できます。

api_supplement.py(補足)Python
# 補足:自社プロダクトに組み込む場合(Claude API の structured outputs)
import json

import anthropic

client = anthropic.Anthropic()  # 環境変数 ANTHROPIC_API_KEY を読む

schema = {
    "type": "object",
    "properties": {
        "summary": {"type": "string"},
        "highlights": {"type": "array", "items": {"type": "string"}},
        "risks": {"type": "array", "items": {"type": "string"}},
        "next_actions": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["summary", "highlights", "risks", "next_actions"],
    "additionalProperties": False,
}

with open("data/weekly/2026-09-14/summary.json", encoding="utf-8") as f:
    payload = json.load(f)

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=16000,
    system="集計JSONの数値だけを引用し、再計算しないこと。推測は「推測:」と明記すること。",
    messages=[{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}],
    output_config={"format": {"type": "json_schema", "schema": schema}},
)
text = next(b.text for b in response.content if b.type == "text")
report = json.loads(text)
print(report["summary"])

多数アカウントの月次レポートのように急がない処理は、Batch API(client.messages.batches.create())で非同期にまとめて投げると約50%安くなります。結果は順不同で返るので、custom_id にアカウントIDを入れて突き合わせます。固定のシステムプロンプトと指標定義を先頭に置き cache_control を付ければ、プロンプトキャッシュも併用できます。エージェントループやツール定義を含む本番運用の注意点はAIエージェントを本番運用するための設計で解説しています。

品質管理:ハルシネーション対策と評価セット

ここまでの対策を、防御の層として整理します。

層仕組み防げるもの
入力の限定AIには summary.json と内訳スクリプトの出力だけを読ませる古いCSVや別アカウントの数値の混入
計算の排除比率・前週比・異常値はpandasで算出済み計算ミス、比率の平均化
ルールCLAUDE.md(再計算禁止・丸め禁止・推測の明示・断定表現の禁止)根拠のない断定、成果の保証表現
機械検証Stopフック+定期実行後の verify_report.py入力にない数値(捏造・計算結果)の混入
人のレビューSlackのレビュー用チャンネルで確認してから外部共有取り違え、文脈の誤解、データ外の事情

評価セットを作って「手順の変更」を検証する

SKILL.mdやCLAUDE.mdを変えたとき、レポートの質が上がったのか下がったのかを感覚で判断すると、改善が迷走します。過去の週の summary.json を固定の評価セットとして保存し、手順を変えるたびに同じ入力でレポートを生成して比べます。チェック項目の例は次のとおりです。

  • 数値検証:verify_report.py を全件で通過するか(機械判定)
  • フラグの網羅:flags と cost_anomalies の各項目が、レポート中で言及されているか(媒体名と種別で機械判定できる)
  • 禁止表現:「必ず」「確実に」「保証」などの語が含まれないか(正規表現で機械判定)
  • 推測の明示:入力にない原因に言及する文に「推測:」が付いているか(人の目視、またはAIによる採点を補助的に)
  • 実務での有用性:運用者が「そのまま出せる」「少し直せば出せる」「使えない」の3段階で評価(人)

評価セットには、CV0件の週、媒体の片方が停止していた週、費用が急増した日を含む週など、境界条件のデータを意識して入れておくと、ルールの抜けが見つかりやすくなります。

業界動向:媒体のAI自動化と運用者の価値

広告媒体そのものも、AIによる自動化を急速に進めています。Google広告のP-MAX(パフォーマンス最大化)キャンペーンや、Meta広告のAdvantage+のように、入札・ターゲティング・クリエイティブの組み合わせを媒体側のAIに委ねるメニューが広がり、「管理画面で細かく調整する」作業の比重は下がっています。

その結果、運用者の価値は「設定作業」から「解釈・戦略・検証」へ移っているというのが、業界で広く共有されている見方です。媒体のAIが何を学習し、どこに配信を寄せたのかは、管理画面の数値を読み解かなければ分かりません。事業のKPIに照らして正しい方向に最適化されているか、計測は正しいか、次に何を検証するか——これらは依然として人の仕事です。

企業全体でも、AI活用を前提にした働き方への移行は進んでいます。たとえばShopifyは、AI活用を業務の前提とする方針をCEOが社内メモで示したと報じられました(2025年)。レポート作成のような定型作業をAIエージェントに任せ、浮いた時間を「解釈と検証」に回すというこの記事の構成は、その流れに沿ったものです。

MCP化の展望

この記事ではスクリプトをBashで実行する構成にしましたが、媒体データの取得部分をMCP(Model Context Protocol)サーバーにする方法もあります。MCPはAIアプリと外部ツール・データをつなぐ共通規格で、取得処理を「ツール」として公開すれば、Claude Codeだけでなく他のMCP対応クライアントからも同じ関数を使えます。

ターミナルShell
# 自作の媒体データ取得MCPサーバー(stdio)を、プロジェクト共有で登録
claude mcp add --scope project ads-data -- python mcp_server/ads_server.py

# リモートのMCPサーバー(Streamable HTTP)を登録する場合
claude mcp add --transport http ads-remote https://mcp.example.com/mcp

--scope project を付けると設定が .mcp.json に保存され、チームで共有できます。接続状況は /mcp で確認します。Google広告についてはGoogleがGoogle Ads API用のMCPサーバー(読み取り中心)を公開しています(執筆時点)。ただし、提供範囲や認証の扱いは変わりうるため、採用時は公式リポジトリで最新情報を確認してください。

MCP化のメリットは「対話的な分析のしやすさ」と「再利用性」、デメリットはサーバーの実装・保守の手間です。まずはこの記事のスクリプト構成で運用を固め、複数チームや複数ツールから使いたくなった段階でMCPに切り出すのが現実的です。自作する場合の手順はMCPサーバーの作り方で解説しています。

よくある質問

なぜAIに数値の計算をさせてはいけないのですか?

LLMは桁の多い計算や比率の集計で誤ることがあり、しかも誤りがもっともらしい形で出てくるためです。広告レポートでは1つの数値の誤りが信頼を大きく損ないます。前週比・CPA・ROAS・異常値検知はpandasなどのコードで決定的に計算し、AIには計算済みの数値を読んで解釈・文章化する役割だけを任せ、出力中の数値が入力に存在するかをスクリプトで検証するのが安全です。

Google Ads APIの開発者トークンはすぐに本番アカウントで使えますか?

開発者トークンはMCCアカウントから申請し、アクセスレベルによって利用できる範囲が異なります。申請直後は利用範囲が限られ、本番アカウントのデータを取得するには審査が必要になる場合があります。加えてOAuth 2.0のクライアントIDとリフレッシュトークンも必要です。手順や条件は変わることがあるため、公式ドキュメントで最新情報を確認してください。

入札や予算の変更までAIエージェントに任せてもよいですか?

おすすめしません。入札・予算・配信ON/OFFはお金が直接動く操作で、誤実行のコストが大きいためです。AIには原因の調査と変更案の作成までを任せ、提案ファイルを人が確認してから人が反映する形(Human-in-the-loop)にします。Claude Codeではpermissionsのdenyと、PreToolUseフックによるブロックの二重で、書き込み系の操作を実行できないようにしておくと安心です。

Claude CodeではなくClaude APIで作るべきなのはどんな場合ですか?

レポート機能を自社のSaaSや社内システムに組み込み、多数の利用者に提供する場合です。その場合はClaude APIを直接呼び、structured outputsで出力形式をJSON Schemaで固定し、多数アカウントの月次処理にはBatch APIを使うとコストを抑えられます。社内チームの定型業務であれば、手順書(SKILL.md)を運用者自身が直せるClaude Codeの構成のほうが改善を回しやすくなります。

まとめ|計算はコード、解釈はAI、判断は人

広告運用レポートの自動化で最も大切なのは、AIの賢さよりも役割分担を仕組みで強制することです。媒体APIからの取得と集計はPythonで決定的に行い、Claude CodeにはCLAUDE.mdとSkillsで「集計結果を読んで書く」手順を渡し、permissionsとhooksで書き込み操作と捏造された数値を止める。そのうえで、人はレビューと意思決定に集中する——この形なら、週次レポートを毎週安定して回しながら、異常時の掘り下げもAIと一緒に進められます。

まずは取得・集計スクリプトだけを作り、数字が管理画面と一致することを確認するところから始めてください。広告APIとClaude Codeの接続そのものについてはClaude Codeと広告APIをつなぐ入門記事、GA4やSearch Consoleの分析はClaude Code×GA4・Search Console分析もあわせてどうぞ。

業務自動化のスキルを、キャリアと収入につなげる

APIとAIエージェントで業務を自動化できる人材は、社内でも社外でも求められています。次の一歩に役立つ記事です。

AIで独立するには?未経験から案件を取る6ステップ【2026】自動化・開発スキルを案件にする。フリーランスエージェントの選び方も
記事を読む

次の一歩

姉妹サイトの記事で、次に気になりやすいことを調べられます。