AI Agents Marketing

Google広告の文案を書くClaudeスキル

Alejandro Rioja
Alejandro Rioja
3 分で読める
TL;DR

GAQLでGoogle広告APIを直接照会し、CTRが弱いか CPAが高いレスポンシブ検索広告を検出し、Googleの正確な文字数制限(30文字と90文字)内で見出しと説明文を書き直すClaudeスキルを作った。新しい広告は一時停止状態で作成され、自分で確認してから有効化する。本当に重要なのは、Googleの文字数制限こそが実際の制約であり、文案そのものではないという点だ。文字数を正しく数えられないモデルは、Googleが黒黙に切り詰めるか拒否する広告を生成してしまう。

無料ニュースレター

毎週水曜。28,400人以上の読者。無駄なし。

[運用者の視点] Facebook広告を管理するClaudeスキルはすでに持っている — その構築内容はこちら。Google広告はその続編として、より難しかった。APIが劣っているからではなく、レスポンシブ検索広告にはFacebookにはない厳格な文字数制限があるからだ:見出しは30文字、説明文は90文字。読んだ感じは良くても配信時に拒否されたり切り詰められたりする書き直しは、書き直しではなくバグだ。このスキルの作業の大半は、モデルがその制約を守るよう強制することに費やされた — 守ってくれると信頼するのではなく。

Table of contents

Open Table of contents

なぜGoogle広告はFacebookとは別の構築が必要だったか

Facebookスキルはパフォーマンスデータを読み、文案を書き直し、新しい広告セットを公開する。Google広告も同じ3つの仕事をするが、2点が異なる。

  1. クエリ言語がGAQLであり、RESTフィルタではない。 Google広告API v18はGoogle Ads Query Languageで動く — SQL形式、読み取り専用で、まさにこの種のレポート抽出のために作られている。
  2. レスポンシブ検索広告には厳格な制限がある。 広告1つにつき最大15個の見出し(各30文字)と4個の説明文(各90文字)。Googleは丁寧に警告してくれない — アセットを送信時に拒否するか、もっと悪いことに、実際の顧客には変に見える切り詰められたバージョンを配信する。

2番目の点が、素朴な実装を壊す原因だ。モデルに「もっとインパクトのある見出しを書いて」と頼むと、気前よく34文字を返してくる。Google広告はそのアセットを拒否するか、単語の途中で切れた状態で表示する。実際に稼働中の広告が「ご予約は今すぐ—」で終わっているのを見るまで気づかない。

APIアクセスの取得(誰もが過小評価する部分)

3つのことが必要で、3番目を計画に入れないと1週間を無駄にする。

  1. OAuth2認証情報を持つGoogle Cloudプロジェクト — クライアントID、クライアントシークレット、そしてhttps://www.googleapis.com/auth/adwordsスコープのリフレッシュトークン。リフレッシュトークンはOAuthプレイグラウンドか短いローカルスクリプトで一度生成すれば、その後は保存された認証情報になり、ログインフローではなくなる。
  2. デベロッパートークン — Google広告の管理(MCC)アカウントの「管理」→「APIセンター」から申請する。
  3. アクセスレベル。 すべての新しいデベロッパートークンはテストアカウントアクセスから始まる — Google広告のテストアカウントにしか機能せず、実際のアカウントには使えない。ベーシックアクセス(1日最大15,000操作、今回の用途には十分)は、Googleが手動で審査するフォームから申請する。実際のアカウントで本番稼働させる前に、承認まで数営業日かかることを見込んでおく。

GOOGLE_ADS_CLIENT_ID、GOOGLE_ADS_CLIENT_SECRET、GOOGLE_ADS_REFRESH_TOKEN、GOOGLE_ADS_DEVELOPER_TOKEN、GOOGLE_ADS_CUSTOMER_ID(ハイフンなしの10桁のアカウントID)を.envに保存する。MCC経由でアカウントを管理している場合は、管理アカウントのIDを設定したGOOGLE_ADS_LOGIN_CUSTOMER_IDも必要だ — これがないとクエリは権限エラーで拒否され、エラーメッセージは理由を教えてくれない。

スキルのファイル構成

code
.claude/skills/google-ads/
  SKILL.md          ← Claudeが読む指示
  index.ts          ← 実際のツール実装
  limits.ts         ← 文字数制限の検証

SKILL.md:

markdown
# Google Ads Manager Skill

Use this skill when the user says "check my Google Ads", "run ads
report", or "rewrite underperforming RSAs". Never run this without
explicit user instruction — it touches live ad spend.

## What it can do
- Pull campaign and ad group performance via GAQL (last 7 or 30 days)
- Flag responsive search ads with CTR < 3% or CPA above the stored
  target as underperformers
- Rewrite headlines (≤30 chars) and descriptions (≤90 chars) for
  flagged ads in Ale's voice
- Create new ad variations as PAUSED — never enabled automatically

## What it will NOT do
- Change budgets or bids without explicit confirmation
- Enable a paused ad
- Submit any headline or description that fails the character check

「What it will NOT do」の最後の行は、初週にスキルが32文字の見出しを公開してしまった後に追加したものだ。モデル自身の文字数カウントが2文字分間違っていた — 一目見て通りそうなほど近く、アセットが拒否されるほど遠かった。それ以来、すべてはモデルの自己申告ではなく、厳格なバリデータを通る。

中核となるTypeScriptコード

typescript
// .claude/skills/google-ads/index.ts
import { GoogleAdsApi } from "google-ads-api";
import Anthropic from "@anthropic-ai/sdk";

const client = new GoogleAdsApi({
  client_id: process.env.GOOGLE_ADS_CLIENT_ID!,
  client_secret: process.env.GOOGLE_ADS_CLIENT_SECRET!,
  developer_token: process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
});

const customer = client.Customer({
  customer_id: process.env.GOOGLE_ADS_CUSTOMER_ID!,
  login_customer_id: process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
  refresh_token: process.env.GOOGLE_ADS_REFRESH_TOKEN!,
});

interface AdPerformance {
  adGroupAdId: string;
  campaignName: string;
  headlines: string[];
  descriptions: string[];
  impressions: number;
  clicks: number;
  costMicros: number;
  conversions: number;
  ctr: number;
  cpa: number;
}

async function getAdPerformance(days = 7): Promise<AdPerformance[]> {
  const rows = await customer.query(`
    SELECT
      ad_group_ad.ad.id,
      campaign.name,
      ad_group_ad.ad.responsive_search_ad.headlines,
      ad_group_ad.ad.responsive_search_ad.descriptions,
      metrics.impressions,
      metrics.clicks,
      metrics.cost_micros,
      metrics.conversions
    FROM ad_group_ad
    WHERE segments.date DURING LAST_${days === 30 ? "30" : "7"}_DAYS
      AND ad_group_ad.status = 'ENABLED'
  `);

  return rows.map((r: any) => {
    const impressions = Number(r.metrics.impressions ?? 0);
    const clicks = Number(r.metrics.clicks ?? 0);
    const cost = Number(r.metrics.cost_micros ?? 0) / 1_000_000;
    const conversions = Number(r.metrics.conversions ?? 0);
    return {
      adGroupAdId: r.ad_group_ad.ad.id,
      campaignName: r.campaign.name,
      headlines: (r.ad_group_ad.ad.responsive_search_ad?.headlines ?? []).map((h: any) => h.text),
      descriptions: (r.ad_group_ad.ad.responsive_search_ad?.descriptions ?? []).map((d: any) => d.text),
      impressions,
      clicks,
      costMicros: Number(r.metrics.cost_micros ?? 0),
      conversions,
      ctr: impressions > 0 ? (clicks / impressions) * 100 : 0,
      cpa: conversions > 0 ? cost / conversions : Infinity,
    };
  });
}

const HEADLINE_MAX = 30;
const DESCRIPTION_MAX = 90;

function validateAsset(text: string, max: number): boolean {
  return text.length <= max;
}

async function rewriteRsa(
  ad: AdPerformance,
  targetCpa: number,
): Promise<{ headlines: string[]; descriptions: string[] }> {
  const anthropic = new Anthropic();
  const msg = await anthropic.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages: [{
      role: "user",
      content: `Rewrite this Google Ads responsive search ad in Alejandro Rioja's voice: direct, operator-focused, specific. It's underperforming — CTR ${ad.ctr.toFixed(2)}%, CPA $${ad.cpa.toFixed(2)} vs a $${targetCpa} target.

Current headlines: ${JSON.stringify(ad.headlines)}
Current descriptions: ${JSON.stringify(ad.descriptions)}

Rules, non-negotiable:
- Each headline: 30 characters MAXIMUM, including spaces and punctuation
- Each description: 90 characters MAXIMUM, including spaces and punctuation
- Return 5 headlines and 4 descriptions
- Return JSON only: {"headlines": ["...", ...], "descriptions": ["...", ...]}`,
    }],
  });
  const text = (msg.content[0] as any).text.replace(/```json\n?/, "").replace(/```/, "").trim();
  const parsed = JSON.parse(text);

  const headlines = parsed.headlines.filter((h: string) => validateAsset(h, HEADLINE_MAX));
  const descriptions = parsed.descriptions.filter((d: string) => validateAsset(d, DESCRIPTION_MAX));

  if (headlines.length < 3 || descriptions.length < 2) {
    throw new Error("Model returned too many over-limit assets — rerun or revise manually.");
  }

  return { headlines, descriptions };
}

見出しと説明文はすべて、生成後、APIに触れる前にコード内で制限と照合される。モデルの書き直しは下書きにすぎない。バリデータが関所だ。

日々どう使っているか

code
> 過去7日間のGoogle広告を確認して

ClaudeはGAQLクエリを実行し、CTR3%未満か保存したCPA目標を超えるものをすべてフラグし、書き直す前に確認を求める。承認すると、新しい見出しと説明文を生成し、文字数バリデータに通し、不合格のものを除外し、Google広告APIのmutate操作で新しい広告をPAUSED状態で作成する。新しい文案を古いものとGoogle広告内で比較し、実際に使いたいものを有効化し、残りは一時停止のままにする。

Facebook版と同じ上限だ — UIでの半日作業の代わりに約20分。データを抽出し、厳格な制約の中で書き、変更を準備するという機械的な部分は、まさにモデルが自分の代わりにやるべきことだからだ。

バリデータが検出できないもの

文字数カウントは機械的だ。関連性はそうではない。見出しが28文字で、すべてのチェックに通っても、紐づいている検索語に対しては間違ったメッセージであることはある。このスキルは、低パフォーマンスの広告グループが悪いオファーなのか、ランディングページとのミスマッチなのか、本当に弱い文案なのかを判断できない — それが伝えられるのは、周囲の広告と比較して文案が弱いということだけだ。キャンペーン内のすべての広告のCTRが低い場合、それはターゲティングかランディングページの問題であり、マッチしていない検索意図に対してスキルにさらに書き直しサイクルを与えても、より従順で同じく無視される文案が増えるだけだ。

Facebook側と同じように、書き直しのバッチを承認する前にGAQLの出力は自分で読む。スキルはタイピングと文字数カウントを取り除く。実際に何が壊れているかという判断は取り除かない。

このために使っているツール

Claude — Facebook向けとこちらの両方の広告スキルの基盤で、異なるAPIとより厳しい文字数予算に対して同じ「書き直して検証する」パターンを共有している。

Google広告API(npmパッケージgoogle-ads-api) — 生のgRPC/REST APIをラップするTypeScriptライブラリ。OAuthのリフレッシュとGAQLクエリの構築を処理してくれるので、protobufを手作業で組む必要がない。

よくある質問

ベーシックアクセスが必要か、テストアカウントアクセスで十分か

テストアカウントアクセスはGoogle広告のテストアカウントにしか機能しない — 開発には有用だが、実際の支出がある本番アカウントには使えない。このスキルが本番キャンペーンに触れる前に、ベーシックアクセス(管理アカウントのAPIセンターから申請)が必要だ。承認は手動審査で即時ではないため、必要になる週ではなく、必要になる前に申請しておくこと。

なぜ最大の15個と4個ではなく、5個の見出しと4個の説明文なのか

Googleは最大15個の見出しを許可しているが、Ad Strengthも実際のテストも、本当に異なる角度が8〜10個を超えると収益が減少することを示している — ほとんどのアカウントは、数を埋めるためのほぼ重複した見出しで終わる。モデルの出力を書き直しバッチごとに新しい見出し5個に制限することで、同じ文の9つのバリエーションをふるい分けるのではなく、本当に異なる選択肢を検討できる。

モデルの書き直しが毎回文字数チェックに失敗した場合はどうなるか

バリデータは、制限超えのアセットを黙って切り詰めたり送信したりせず、エラーを発生させる。実際には、プロンプトを一度再実行し、2回失敗したらその特定の見出しを手で書く。説明文でより頻繁に起こる — 90文字は見た目よりも完全な文を収めるには狭い空間だ。

関連記事: プラットフォームごとに専用のスキルを作る代わりに、GA4、Search Console、Bing、Ahrefs、Core Web Vitalsを1つのClaudeコネクタで読み取り専用レポートするには、SiteWide MCPを使っている — Claudeを実際のマーケティングデータに接続するという同じ発想だが、1つの広告プラットフォームより広い範囲をカバーする。

運用者としての結論

レスポンシブ検索広告のバリエーションを手で書き、自分で文字数を数えているなら、それはまさにモデルが担うべき機械的でルールに基づいた作業だ。本当に判断力が必要な部分 — それが文案の問題なのかターゲティングの問題なのか — は自分のものとして残る。スキルを構築し、Googleの制限に対して書いて検証させ、診断は自分の手に留めておく。

広告
続きを読む

関連記事

続きを読む

AIプレイブックをメールでお届け

毎週水曜。28,400人以上の読者。無駄なし。

↵ ↵ すべての結果を見る esc esc で閉じる