撰写我的谷歌广告文案的 Claude 技能
我构建了一个 Claude 技能,直接用 GAQL 查询谷歌广告 API,找出点击率弱或单次转化成本高的自适应搜索广告,并在谷歌精确的字符限制(30 和 90)内重写标题和说明。新广告以“已暂停”状态创建——我自己审核后再启用。真正关键的一点是:谷歌的字符限制才是真正的约束,而不是文案本身。一个不能正确计算字符数的模型,会生成被谷歌悄悄截断或拒绝的广告。
每周三。28,400+ 读者。纯干货。
✓ 请查收邮箱 — 点击确认链接以完成订阅。
✓ 订阅成功!
✓ 您已在订阅列表中。
[运营者视角] 我已经有一个管理 Facebook 广告的 Claude 技能——那个构建在这里。谷歌广告是更难的续作,不是因为它的 API 更差,而是因为自适应搜索广告有 Facebook 没有的硬性字符限制:每个标题 30 个字符,每条说明 90 个字符。一个读起来顺畅但在投放时被拒绝或截断的重写,不是重写,是个 bug。这个技能的大部分工作,都花在强迫模型遵守这个约束上,而不是相信它会自动遵守。
Table of contents
Open Table of contents
为什么谷歌广告需要和 Facebook 不同的构建方式
Facebook 技能读取表现数据、重写文案、发布新的广告组。谷歌广告做同样三件事,但有两点不同:
- 查询语言是 GAQL,不是 REST 过滤器。 谷歌广告 API v18 运行在 Google Ads Query Language 之上——类似 SQL、只读,正是为这种报表提取而设计的。
- 自适应搜索广告有硬性限制。 每个广告最多 15 个标题(每个 30 个字符)和 4 条说明(每条 90 个字符)。谷歌不会温和地提醒你——它要么在提交时拒绝该素材,要么更糟,投放一个对真实顾客来说读起来不对劲的截断版本。
第 2 点正是让简单实现崩溃的地方。让模型”写一个更有力的标题”,它会欣然给你 34 个字符。谷歌广告会拒绝那个素材,或者把它渲染成在单词中间被截断的样子。我往往要等到看到一个以”立即预约您的—“结尾的真实广告才会发现。
获取 API 访问权限(所有人都低估的部分)
三件事,第三件如果不提前规划会耽误你一周。
- 一个带 OAuth2 凭证的 Google Cloud 项目——客户端 ID、客户端密钥,以及一个范围为
https://www.googleapis.com/auth/adwords的刷新令牌。通过 OAuth playground 或一个简短的本地脚本生成一次刷新令牌;之后它就是一个存储的凭证,不再是登录流程。 - 开发者令牌——从你的谷歌广告管理(MCC)账户的”管理”→“API 中心”申请。
- 访问级别。 每个新的开发者令牌都从测试账户访问开始——它只能对谷歌广告的测试账户生效,不能用于你的真实账户。你需要通过一个由谷歌人工审核的表单申请基本访问权限(每天最多 15,000 次操作,足够这个用途)。在计划上线真实账户之前,预留几个工作日等待审批。
把 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——没有它,查询会因权限错误被拒绝,而错误信息不会告诉你原因。
技能的文件结构
.claude/skills/google-ads/
SKILL.md ← Claude 读取的指令
index.ts ← 实际的工具实现
limits.ts ← 字符限制验证SKILL.md:
# 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 代码
// .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 之前,都会在代码里按限制进行校验。模型的重写只是草稿。验证器才是关卡。
我日常如何使用它
> 检查我最近 7 天的谷歌广告Claude 执行 GAQL 查询,标记出点击率低于 3% 或超过我设定的单次转化成本目标的任何广告,并在重写前征求确认。我确认后,它生成新的标题和说明,通过字符验证器筛选,剔除不合格的,然后通过谷歌广告 API 的 mutate 操作创建一个状态为 PAUSED 的新广告。我在谷歌广告里直接对比新旧文案,启用那些我真的会投放的版本,其余的保持暂停。
和 Facebook 版本一样的上限:大约 20 分钟,而不是在界面里耗掉一整个下午,因为那些机械性的部分——提取数据、在硬性约束内撰写、准备好变更——正是模型应该代替我去做的事。
验证器无法捕捉的东西
字符计数是机械性的。相关性不是。一个标题可能只有 28 个字符,通过了每一项检查,但对它所匹配的搜索词来说仍然是错误的信息。这个技能无法判断一个表现不佳的广告组是因为报价不好、与落地页不匹配,还是文案本身确实弱——它只能告诉我,这条文案相对于周围的广告是弱的。如果一个广告系列里所有广告的点击率都低,那是定位或落地页的问题,给技能更多针对错配搜索意图的重写循环,只会产生更顺从、但同样被忽略的文案。
在批准一批重写之前,我会像在 Facebook 那边一样,自己读一遍 GAQL 的输出。技能省去了我打字和数字符的工作。它没有省去判断究竟哪里出了问题的那部分。
我用来做这件事的工具
Claude 是两个广告技能——Facebook 的那个和这个——共同运行的基础,它们共享同一种重写-验证的模式,只是针对不同的 API 和更紧的字符预算。
谷歌广告 API(npm 包 google-ads-api)——包装原始 gRPC/REST API 的 TypeScript 库。它处理 OAuth 刷新和 GAQL 查询的构建,这样你就不用手动拼装 protobuf。
常见问题
我需要基本访问权限,还是测试账户访问就够了?
测试账户访问只对谷歌广告的测试账户有效——对开发有用,对有真实支出的真实账户毫无用处。在这个技能能够触碰真实广告系列之前,你需要基本访问权限(在你的管理账户的 API 中心申请)。审批是人工审核,不是即时的,所以要在需要之前申请,而不是在想上线的那一周才申请。
为什么是 5 个标题和 4 条说明,而不是最多的 15 个和 4 个?
谷歌允许最多 15 个标题,但 Ad Strength 和实际测试都表明,超过 8 到 10 个真正不同的角度之后,收益会递减——大多数账户最后都会用一堆几乎重复的标题来凑数。我把模型的输出限制在每批重写 5 个新标题,这样我审核的是真正不同的选项,而不是在筛选同一句话的九种变体。
如果模型的重写每次都没通过字符检查,会怎样?
验证器会抛出错误,而不是悄悄截断或提交一个超限的素材。实际操作中,这意味着我会重新运行一次提示词,如果两次都失败,我就自己手写那条特定的标题。这种情况在说明文字上更常见——90 个字符用来容纳一句完整的话,比看起来要紧得多。
相关阅读: 如果不想为每个平台都搭一个专用技能,而是想在一个 Claude 连接器里对 GA4、Search Console、Bing、Ahrefs 和 Core Web Vitals 做只读报表,我用的是 SiteWide MCP——同样是把 Claude 接入真实营销数据的思路,只是覆盖面比单一广告平台更广。
运营者的结论
如果你在手动写自适应搜索广告的各种变体、自己数字符,那正是模型应该接管的那种机械、基于规则的任务。真正需要判断力的部分——这是文案问题还是定位问题——仍然是你的事。把技能搭建起来,让它针对谷歌的限制去撰写和验证,诊断留给自己。
每周三。28,400+ 读者。纯干货。
✓ 请查收邮箱 — 点击确认链接以完成订阅。
✓ 订阅成功!
✓ 您已在订阅列表中。
相关文章
将AI实战手册发送到您的邮箱
每周三。28,400+ 读者。纯干货。
请查收邮箱。
我们已向您发送确认邮件 — 点击其中的链接以完成订阅。如果一分钟内没收到,请检查垃圾邮件。
订阅成功。
欢迎 — 下一期很快就会送达您的邮箱。
您已在订阅列表中 — 每周三留意查收。