AI Agents Marketing

Навык Claude, Который Пишет Мои Объявления Google Ads

Alejandro Rioja
Alejandro Rioja
8 мин чтения
TL;DR

Я создал навык Claude, который напрямую запрашивает API Google Ads через GAQL, находит адаптивные поисковые объявления со слабым CTR или высоким CPA и переписывает заголовки и описания в пределах точных лимитов символов Google (30 и 90). Новое объявление создаётся в статусе ПАУЗА — я сам проверяю и включаю его. Главное здесь: лимиты символов Google — это реальное ограничение, а не сам текст. Модель, которая неправильно считает символы, выдаёт объявления, которые Google молча обрезает или отклоняет.

Бесплатная рассылка

Каждую среду. 28 400+ читателей. Никакой воды.

[Взгляд оператора] У меня уже есть навык Claude, который управляет моими объявлениями в Facebook — эта разработка здесь. Google Ads оказался сложнее в продолжении, не потому что API хуже, а потому что у адаптивных поисковых объявлений есть жёсткие лимиты символов, которых нет у Facebook: 30 символов на заголовок, 90 на описание. Переписанный текст, который хорошо читается, но отклоняется или обрезается при показе, — это не переписанный текст, это баг. Большая часть работы над этим навыком ушла на то, чтобы заставить модель соблюдать это ограничение, а не доверять, что она сделает это сама.

Table of contents

Open Table of contents

Почему Google Ads требовал другой сборки, чем Facebook

Навык для Facebook читает данные о производительности, переписывает текст и публикует новый набор объявлений. Google Ads делает те же три задачи, но два момента отличаются:

  1. Язык запросов — GAQL, а не REST-фильтры. API Google Ads v18 работает на Google Ads Query Language — в формате SQL, только для чтения, созданном именно для такой выгрузки отчётов.
  2. У адаптивных поисковых объявлений жёсткие лимиты. До 15 заголовков (по 30 символов каждый) и 4 описаний (по 90 символов каждое) на объявление. Google не предупреждает мягко — он либо отклоняет актив при отправке, либо, что хуже, показывает обрезанную версию, которая выглядит неправильно для реального клиента.

Пункт 2 — это то, что ломает наивные реализации. Попросите модель «написать более яркий заголовок», и она с радостью вернёт 34 символа. Google Ads отклонит этот актив или отрендерит его обрезанным посреди слова. Я узнаю об этом только когда вижу живое объявление, которое заканчивается на «Забронируйте визи—».

Получение доступа к API (часть, которую все недооценивают)

Три вещи, и третья стоит вам недели, если не спланировать заранее:

  1. Проект Google Cloud с учётными данными OAuth2 — client ID, client secret и refresh token с правом доступа https://www.googleapis.com/auth/adwords. Сгенерируйте refresh token один раз через OAuth playground или короткий локальный скрипт; после этого это сохранённые учётные данные, а не процесс входа.
  2. Developer token, запрошенный из вашего управляющего аккаунта (MCC) Google Ads в разделе Администрирование → Центр API.
  3. Уровень доступа. Каждый новый developer token начинается с доступа Test Account — он работает только с тестовыми аккаунтами Google Ads, а не с вашим реальным. Вы запрашиваете Basic Access (до 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, вам также нужен GOOGLE_ADS_LOGIN_CUSTOMER_ID с 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 символа. Собственный подсчёт модели был неверен на два символа — достаточно близко, чтобы пройти при беглом взгляде, и достаточно далеко, чтобы актив отклонили. С тех пор всё проходит через строгий валидатор, а не через самоотчёт модели.

Основной код на 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
> проверь мой Google Ads за последние 7 дней

Claude выполняет запрос GAQL, отмечает всё, что ниже 3% CTR или выше моей сохранённой цели по CPA, и спрашивает перед переписыванием. Я подтверждаю. Он генерирует новые заголовки и описания, пропускает их через валидатор символов, отбрасывает то, что не проходит, и создаёт новое объявление в статусе PAUSED через операцию mutate API Google Ads. Я сравниваю новый текст со старым прямо в Google Ads, включаю те, которые действительно стал бы использовать, и оставляю остальное на паузе.

Тот же потолок, что и у версии для Facebook: примерно 20 минут вместо целого дня в интерфейсе, потому что механическая часть — извлечение данных, составление текста в рамках жёсткого ограничения, подготовка изменения — это именно то, что модель должна делать вместо меня.

Чего валидатор не улавливает

Подсчёт символов — механический процесс. Релевантность — нет. Заголовок может быть на 28 символов, пройти все проверки и всё равно оказаться неправильным сообщением для поискового запроса, к которому он привязан. Навык не может определить, связана ли низкая эффективность группы объявлений с плохим предложением, несоответствием с посадочной страницей или действительно слабым текстом — он может только сказать мне, что текст слаб по сравнению с объявлениями рядом. Если у всех объявлений в кампании низкий CTR, это проблема таргетинга или посадочной страницы, и если дать навыку больше циклов переписывания для неправильно подобранного поискового намерения, это даст только более послушный, но столь же игнорируемый текст.

Я сам читаю вывод GAQL перед тем, как одобрить пакет переписываний, так же как делаю со стороны Facebook. Навык убирает набор текста и подсчёт символов. Он не убирает суждение о том, что на самом деле сломано.

Инструменты, которые я использую для этого

Claude — то, на чём работают оба рекламных навыка, и для Facebook, и этот, они разделяют один и тот же паттерн переписать-и-проверить, просто против другого API и более жёсткого бюджета символов.

Google Ads API (npm-пакет google-ads-api) — обёртка на TypeScript вокруг необработанного gRPC/REST API. Она обрабатывает обновление OAuth и построение запросов GAQL, чтобы не собирать protobuf вручную.

Часто задаваемые вопросы

Мне нужен Basic Access, или подойдёт доступ Test Account?

Доступ Test Account работает только с тестовыми аккаунтами Google Ads — полезен для разработки, бесполезен для реального аккаунта с реальными расходами. Вам нужен Basic Access (запрашивается в Центре API вашего управляющего аккаунта), прежде чем этот навык сможет коснуться живой кампании. Одобрение — это ручная проверка, а не мгновенная, так что запрашивайте его заранее, а не в ту неделю, когда хотите запустить.

Почему 5 заголовков и 4 описания, а не максимумы 15 и 4?

Google разрешает до 15 заголовков, но и Ad Strength, и реальные тесты показывают убывающую отдачу после 8–10 действительно разных подходов — большинство аккаунтов заканчивают с несколькими почти дублирующими заголовками, которые просто заполняют счёт. Я ограничиваю вывод модели пятью новыми заголовками на пакет переписывания, чтобы рассматривать действительно разные варианты, а не отсеивать девять вариаций одной и той же фразы.

Что происходит, если переписанный модель текст не проходит проверку символов при каждой попытке?

Валидатор выбрасывает ошибку, а не молча обрезает или отправляет актив с превышением лимита. На практике это означает, что я перезапускаю промпт один раз, и если он проваливается два раза, я пишу этот конкретный заголовок вручную. Чаще это случается с описаниями — 90 символов — более узкое пространство для полноценного предложения, чем кажется.

Также по теме: для отчётности только для чтения по GA4, Search Console, Bing, Ahrefs и Core Web Vitals в одном коннекторе Claude вместо создания отдельного навыка под каждую платформу, я использую SiteWide MCP — та же идея подключения Claude к реальным маркетинговым данным, но с более широким охватом, чем одна рекламная платформа.

Вывод оператора

Если вы пишете варианты адаптивных поисковых объявлений вручную и сами считаете символы, это именно та механическая, основанная на правилах задача, которую должна взять на себя модель. Часть, требующая реального суждения — проблема в тексте или в таргетинге — остаётся за вами. Создайте навык, дайте ему писать и проверять текст по лимитам Google, а диагностику оставьте себе.

Реклама
Читать дальше

Похожие статьи

Читать дальше

Получайте ИИ-руководство на почту

Каждую среду. 28 400+ читателей. Никакой воды.

↵ ↵ — все результаты esc esc — закрыть