亚马逊广告监控 × Open Claw:用 AI Agent 盯住竞品广告位,一个实战框架

15 阅读4分钟

本文聚焦工程实现,从数据架构到完整代码,拆解如何用 Open Claw + Pangolinfo SERP API 构建具有实际价值的亚马逊广告监控系统。

amazon-ad-monitor-workflow-diagram.png

TL;DR

亚马逊广告监控的工程核心:

  1. 用 Pangolinfo SERP API 实时抓取核心关键词的 SP 广告位(98% 覆盖率)
  2. 对比历史基准线,检测 Top3 入场/退场、第1位变化、价格大幅调整
  3. Open Claw LLM 对告警做业务语义解读
  4. 关键词分 A/B/C 三层,告警按优先级分级分渠道推送

数据入口:Pangolinfo Scrape API(支持 Amazon SERP,JSON 输出,分钟级实时)


一、为什么现有监控工具都不够用

广告后台的根本局限

广告账号后台给的是内视角数据——投放成本、曝光量、点击率——这些数据告诉你"自己做得怎么样",但完全不告诉你"竞品在干什么"。而竞品的动作,才是决定你需不需要调整出价策略的关键信号。

当你的 ACoS 突然恶化,你需要知道:是竞品新入场把竞价抬高了,还是你核心词的竞品忽然大幅降价导致你的点击量下降?这两种情况的应对策略截然相反。

Helium 10 等 SaaS 工具的缺陷

主流工具的竞品广告追踪数据通常有 24-48 小时延迟,且不开放 API 接口,无法集成进自己的自动化工作流。更关键的是,它们提供的是固定维度的预制报表,而不是你可以自由定义告警逻辑的数据管道。Open Claw 的价值正在于此:它让你把广告监控需求翻译成自然语言,由 Agent 决定调哪些工具、怎么分析、触发什么告警。


二、数据层:Pangolinfo SERP API 接入

核心 Tool 注册——这段代码把 Pangolinfo SERP 调用封装成 Open Claw 可用的工具:

# Open Claw MCP Tool:亚马逊 SERP 广告位实时查询

SERP_MONITOR_TOOL = {
    "name": "get_amazon_ad_positions",
    "description": (
        "实时查询亚马逊搜索结果页面的 SP 广告位数据。"
        "当用户询问某个关键词当前有哪些 ASIN 在投广告、位置如何时使用。"
        "返回 Top of Search 广告位的 ASIN、品牌、价格、排名。"
        "不适用于查询自然搜索排名(需使用其他 Tool)。"
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "keyword": {
                "type": "string",
                "description": "亚马逊搜索关键词,如 'wireless earbuds under $30'"
            },
            "marketplace": {
                "type": "string",
                "enum": ["US", "UK", "DE", "JP", "CA"],
                "default": "US",
                "description": "目标站点"
            },
            "top_n": {
                "type": "integer",
                "description": "返回前N个广告位,默认5",
                "default": 5
            }
        },
        "required": ["keyword"]
    }
}

# 对应的工具实现函数
async def get_amazon_ad_positions(
    keyword: str, marketplace: str = "US", top_n: int = 5
) -> dict:
    """Tool 实现:调用 Pangolinfo SERP API,返回结构化广告位数据"""
    import aiohttp

    headers = {"Authorization": f"Bearer {PANGOLINFO_API_KEY}"}
    payload = {
        "source": "amazon_search",
        "query": keyword,
        "marketplace": marketplace,
        "page": 1,
        "include_sponsored": True,
        "include_organic": False,
        "output_format": "json"
    }

    async with aiohttp.ClientSession() as session:
        async with session.post(
            "https://api.pangolinfo.com/v1/serp",
            headers=headers, json=payload,
            timeout=aiohttp.ClientTimeout(total=25)
        ) as resp:
            data = await resp.json()

    sponsored = data.get("sponsored_results", [])
    top_ads = [
        {
            "rank": item.get("ad_rank"),
            "asin": item.get("asin"),
            "brand": item.get("brand", ""),
            "price": item.get("price"),
            "placement": item.get("ad_placement", "unknown")
        }
        for item in sorted(sponsored, key=lambda x: x.get("ad_rank", 999))
        if "top" in item.get("ad_placement", "").lower()
    ][:top_n]

    return {
        "keyword": keyword,
        "marketplace": marketplace,
        "captured_at": datetime.utcnow().isoformat() + "Z",
        "top_of_search_ads": top_ads,
        "total_sponsored_count": len(sponsored)
    }

三、变化检测:告警策略的工程实现

上面的数据采集是原材料,变化检测才是价值核心。以下是一个设计合理的告警规则引擎:

from dataclasses import dataclass
from typing import List, Set, Optional, Dict
from enum import Enum

class AlertTier(Enum):
    CRITICAL = 1  # 立即通知,核心词+全新竞品
    HIGH     = 2  # 立即通知,重要变化
    MEDIUM   = 3  # 汇总日报,一般变化
    INFO     = 4  # 仅记录,不通知

@dataclass
class AdMonitorAlert:
    keyword: str
    tier: AlertTier
    event: str
    message: str
    asin: str
    captured_at: str
    context: Optional[str] = None   # LLM 生成的业务分析

class AdAlertEngine:
    """
    亚马逊广告监控告警引擎
    设计原则:宁可漏报不重要的,绝不淹没重要信号
    """

    def __init__(self, keyword_priority: Dict[str, List[str]],
                 price_drop_pct: float = 12.0):
        """
        keyword_priority: {"A": ["core kw1", ...], "B": [...], "C": [...]}
        price_drop_pct: 触发价格告警的降幅阈值(百分比)
        """
        self._tier_map: Dict[str, str] = {}
        for tier, kws in keyword_priority.items():
            for kw in kws:
                self._tier_map[kw.lower()] = tier
        self.price_threshold = price_drop_pct / 100

    def _kw_tier(self, keyword: str) -> str:
        return self._tier_map.get(keyword.lower(), "B")

    def _resolve_alert_tier(self, base: AlertTier, keyword: str) -> AlertTier:
        """关键词优先级对告警级别的影响"""
        kw_tier = self._kw_tier(keyword)
        if kw_tier == "A" and base == AlertTier.HIGH:
            return AlertTier.CRITICAL   # A类词HIGH → 升级为CRITICAL
        if kw_tier == "C":
            return AlertTier.INFO       # C类词 → 全部降为INFO
        return base

    def generate_alerts(
        self,
        current: dict,     # get_amazon_ad_positions 返回值
        baseline: dict     # 上一次相同关键词的返回值
    ) -> List[AdMonitorAlert]:
        """对比生成告警列表"""
        if baseline is None:
            return []  # 首次建立基准线

        kw = current["keyword"]
        ts = current["captured_at"]

        curr_ads = {ad["asin"]: ad for ad in current["top_of_search_ads"]}
        base_ads = {ad["asin"]: ad for ad in baseline["top_of_search_ads"]}

        curr_top3 = set(list(curr_ads.keys())[:3])
        base_top3 = set(list(base_ads.keys())[:3])
        curr_p1 = list(curr_ads.keys())[0] if curr_ads else None
        base_p1 = list(base_ads.keys())[0] if base_ads else None

        alerts = []

        # === 检测1:第一位置变化 ===
        if curr_p1 and curr_p1 != base_p1:
            is_new = curr_p1 not in base_ads
            base_tier = AlertTier.CRITICAL if is_new else AlertTier.HIGH
            alerts.append(AdMonitorAlert(
                keyword=kw, tier=self._resolve_alert_tier(base_tier, kw),
                event="top1_change",
                message=(
                    f"Top1广告位:{base_p1 or '无'}{curr_p1}"
                    + ("【全新竞品】" if is_new else "")
                ),
                asin=curr_p1, captured_at=ts
            ))

        # === 检测2:新竞品进入Top3 ===
        for asin in curr_top3 - base_top3 - {curr_p1}:
            rank = curr_ads[asin]["rank"]
            alerts.append(AdMonitorAlert(
                keyword=kw,
                tier=self._resolve_alert_tier(AlertTier.HIGH, kw),
                event="new_top3",
                message=f"新竞品进入Top3 #{rank}{asin}",
                asin=asin, captured_at=ts
            ))

        # === 检测3:Top3竞品消失 ===
        for asin in base_top3 - curr_top3:
            alerts.append(AdMonitorAlert(
                keyword=kw,
                tier=self._resolve_alert_tier(AlertTier.MEDIUM, kw),
                event="top3_exit",
                message=f"竞品 {asin} 退出Top3(可能缩减预算)",
                asin=asin, captured_at=ts
            ))

        # === 检测4:Top3内价格大降 ===
        for asin, curr_ad in list(curr_ads.items())[:3]:
            old_price = base_ads.get(asin, {}).get("price")
            new_price = curr_ad.get("price")
            if old_price and new_price and old_price > 0:
                drop = (old_price - new_price) / old_price
                if drop >= self.price_threshold:
                    alerts.append(AdMonitorAlert(
                        keyword=kw,
                        tier=self._resolve_alert_tier(AlertTier.HIGH, kw),
                        event="price_drop",
                        message=(
                            f"Top3竞品 {asin} 降价 {drop*100:.1f}%:"
                            f"${old_price:.2f}→${new_price:.2f}"
                        ),
                        asin=asin, captured_at=ts
                    ))

        return alerts

四、完整调度器与Slack通知

import asyncio, json, sqlite3, aiohttp
from anthropic import Anthropic

class AdMonitorOrchestrator:
    """端到端的亚马逊广告监控调度器"""

    def __init__(self, config: dict):
        self.config = config
        self.engine = AdAlertEngine(
            keyword_priority=config["keyword_tiers"],
            price_drop_pct=config.get("price_drop_pct", 12)
        )
        self.llm = Anthropic()
        self.db = config["db_path"]
        self._init_db()

    def _init_db(self):
        with sqlite3.connect(self.db) as c:
            c.executescript("""
                CREATE TABLE IF NOT EXISTS serp_history (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    keyword TEXT, marketplace TEXT,
                    captured_at TEXT, data_json TEXT
                );
                CREATE TABLE IF NOT EXISTS alert_history (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    keyword TEXT, asin TEXT, event TEXT,
                    tier TEXT, triggered_at TEXT, dispatched INTEGER DEFAULT 0
                );
                CREATE INDEX IF NOT EXISTS idx_kw_ts
                    ON serp_history(keyword, marketplace, captured_at DESC);
            """)

    async def run_cycle(self, tier: str = "A", marketplace: str = "US"):
        """执行一个完整监控周期"""
        keywords = self.config["keyword_tiers"].get(tier, [])
        if not keywords:
            return

        # 批量采集(复用前面的 batch_fetch_serp)
        snapshots = await batch_fetch_serp(keywords, marketplace)

        all_alerts = []
        for snap_data in snapshots:
            # 持久化快照
            with sqlite3.connect(self.db) as conn:
                conn.execute(
                    "INSERT INTO serp_history (keyword, marketplace, captured_at, data_json)"
                    " VALUES (?, ?, ?, ?)",
                    (snap_data["keyword"], marketplace,
                     snap_data["captured_at"], json.dumps(snap_data))
                )
                # 加载历史基准线
                row = conn.execute(
                    "SELECT data_json FROM serp_history"
                    " WHERE keyword = ? AND marketplace = ?"
                    " ORDER BY captured_at DESC LIMIT 1 OFFSET 1",
                    (snap_data["keyword"], marketplace)
                ).fetchone()

            baseline = json.loads(row[0]) if row else None
            alerts = self.engine.generate_alerts(snap_data, baseline)

            # LLM 增强
            for alert in alerts:
                if alert.tier in (AlertTier.CRITICAL, AlertTier.HIGH):
                    alert.context = self._llm_analyze(alert, snap_data)

            all_alerts.extend(alerts)

        # 分发告警
        await self._dispatch(all_alerts)

    def _llm_analyze(self, alert: AdMonitorAlert, snap: dict) -> str:
        top3_str = "\n".join([
            f"  #{ad['rank']}: {ad['asin']} - ${ad['price'] or 'N/A'}"
            for ad in snap["top_of_search_ads"][:3]
        ])
        resp = self.llm.messages.create(
            model="claude-3-7-sonnet-20250219",
            max_tokens=150,
            messages=[{"role": "user", "content":
                f"亚马逊广告监控告警:关键词「{alert.keyword}」\n"
                f"事件:{alert.message}\n当前Top3:\n{top3_str}\n"
                "请用80字以内说明可能原因和建议关注事项。直接输出,无需铺垫。"
            }]
        )
        return resp.content[0].text

    async def _dispatch(self, alerts: List[AdMonitorAlert]):
        webhook = self.config.get("slack_webhook")
        if not webhook:
            return

        to_send = [a for a in alerts
                   if a.tier in (AlertTier.CRITICAL, AlertTier.HIGH)]

        async with aiohttp.ClientSession() as session:
            for alert in to_send:
                if self._is_duplicate(alert):
                    continue
                emoji = "🚨" if alert.tier == AlertTier.CRITICAL else "⚠️"
                msg = (
                    f"{emoji} *亚马逊广告监控* `{alert.tier.name}`\n"
                    f"词:`{alert.keyword}` | ASIN:`{alert.asin}`\n"
                    f"事件:{alert.message}"
                    + (f"\n分析:_{alert.context}_" if alert.context else "")
                )
                await session.post(webhook, json={"text": msg})
                self._mark_dispatched(alert)

    def _is_duplicate(self, alert: AdMonitorAlert,
                       hours: int = 6) -> bool:
        """6小时内同类告警已发送则跳过"""
        cutoff = (datetime.utcnow() - timedelta(hours=hours)).isoformat()
        with sqlite3.connect(self.db) as conn:
            return bool(conn.execute(
                "SELECT 1 FROM alert_history WHERE keyword=? AND asin=?"
                " AND event=? AND triggered_at>? AND dispatched=1 LIMIT 1",
                (alert.keyword, alert.asin, alert.event, cutoff)
            ).fetchone())

    def _mark_dispatched(self, alert: AdMonitorAlert):
        with sqlite3.connect(self.db) as conn:
            conn.execute(
                "INSERT INTO alert_history (keyword, asin, event, tier,"
                " triggered_at, dispatched) VALUES (?,?,?,?,?,1)",
                (alert.keyword, alert.asin, alert.event,
                 alert.tier.name, alert.captured_at)
            )


# 入口示例(配合 APScheduler 或 Celery 定时调用)
async def main():
    config = {
        "db_path": "/data/ad_monitor.db",
        "slack_webhook": "https://hooks.slack.com/services/xxx",
        "price_drop_pct": 12,
        "keyword_tiers": {
            "A": ["wireless earbuds", "bluetooth speaker"],
            "B": ["earbuds under 30", "tws earbuds"],
            "C": []
        }
    }
    orch = AdMonitorOrchestrator(config)
    await orch.run_cycle("A", "US")

if __name__ == "__main__":
    asyncio.run(main())

五、性能与成本参考

指标参考值
50 词 / 次采集时间(async)30-60 秒
API 调用次数(50词 A+B类)~280 次/天
SQLite → PostgreSQL 切换阈值关键词 > 100 或频率 < 2h
告警去重窗口6 小时(同词+同ASIN+同事件)
LLM 调用成本(每次告警)~$0.001(claude-3-7-sonnet输入约200 token)

总结

  • 工具调用是核心:把 Pangolinfo SERP API 封装成 Open Claw Tool,描述清晰
  • 异步并发是性能关键:asyncio + semaphore 控制并发数,避免超速
  • 分层是可持续运营的保证:A/B/C 层频率不同,告警阈值不同
  • 去重是信噪比的保障:同事件6小时静默,不产生告警疲劳

参考