Marketing

价格监控 Agent:每日追踪 50 个竞品,价格变动自动发到 Slack

价格监控 Agent:每日追踪 50 个竞品,价格变动自动发到 Slack
目录

周一的上午 8:23,Slack 的 #pricing-wars 频道弹了一条消息:"Acme Cloud 把 Pro 套餐从 $99 降到 $79,立刻生效。Q4 还额外赠送 2 个免费席位。"我的定价负责人还没喝完早上的咖啡,数据就已经在收件箱里了。到了周二上午我们跟进了这个 bundle,周三自己的反制促销上线,到周五这家竞品在博客上吹嘘降价的文章就再也没出现在我们漏斗顶端的转化数据里。如果没有那条 Slack 告警,我们要等两个礼拜后客户来问"你们为什么比 Acme 贵这么多"才会发现。

这条告警跑在一个周六花 90 分钟搭起来的工作流上,每天成本大约 0.10 美元,过去四个月每一个真正重要的竞品调价都被它抓到了。下面就是完整的流程。

我们到底在搭什么

一个小代理,做这几件事:

  1. 每天早上抓取 50 个竞品商品页上的价格
  2. 把新价格和昨天的数据一起塞进 Google Sheet
  3. 让 Claude 读这个 Sheet,算出百分比变化,把变动超过 5% 的标出来
  4. 把 diff 发到 Slack 频道——旧价、新价、变化幅度、URL

不用自己写爬虫代码。不用在某个被我遗忘的服务器上跑凌晨 2 点的 cron 任务。整件事就是把我已经付费的四个服务粘起来。

第一步——建监控清单(50 个 URL,30 分钟)

整个工作流里最重要的决定,是"到底要监控什么"。诱惑是直接扔 200 个商品页进去。忍住。每多一个 URL,就多一个失败模式——改版、验证码、登录后才看得到的价格。起步先选 50 个你已经知道竞品是谁、且价格变动会真的改变你动作的产品。

我把监控清单放在 Google Sheet 里。每个产品一行,列是:

  • product_name —— 内部称呼
  • competitor —— 公司名
  • url —— 商品页直链(不是首页)
  • price_selector_hint —— 价格元素的 CSS 选择器,如果页面简单就写 "auto"
  • currency —— 三字母代码
  • notes —— 需要记住的任何事(比如"这个页面会重定向到地区版本")

30 分钟基本都花在配价格选择器上。等下会解释为什么这一步很重要。

第二步——用 Apify 抓取(差点多花冤枉钱的环节)

我一开始用自己写的 Python 爬虫试过。两天下来,撞了三次反爬墙、两次改版、还有一个验证码一天吃掉 40 美元的代理费。我把代码全删了,切到 Apify——它对大部分我想抓的网站都提供了现成的 scraper(他们叫 "Actor")。

剩下的我用通用的 CheerioCrawler Actor,它能让你喂一个 URL 列表进去,跑一小段 JavaScript 提取数据:

javascriptasync requestHandler({ $, request }) {
  const priceText = $('[data-testid="price"]').text()
    || $('.price').first().text()
    || $('[itemprop="price"]').attr('content')
    || '';
  const price = parseFloat(priceText.replace(/[^0-9.]/g, '')) || null;

  await Actor.pushData({
    url: request.url,
    product: request.userData.product,
    competitor: request.userData.competitor,
    price,
    scraped_at: new Date().toISOString(),
  });
}

这就是清单里 price_selector_hint 这一列存在的理由。便宜的开箱即用 Actor 自带选择器——1000 页大概 1-3 美元。一旦需要自定义选择器,每行成本会涨到 0.005-0.01 美元。50 个 URL 一天一次跑下来,Apify 计算资源每月大概 0.75 美元。我第一次加到 200 个 URL 的时候,狠狠交了一次"compute units 到底是什么"的学费。

Apify 返回一个 Dataset——所有行的 JSON 数组。这就是第三步的交接物。

第三步——把行落到 Google Sheet(一个连接器,零胶水代码)

最便宜的"每日表格数据落地"位置就是 Google Sheet。我建了一个 Sheet,里面两个 tab:

  • raw_dumps —— 每次抓取一行,列名跟 Apify dataset 对齐(urlproductcompetitorpricescraped_at)。每天涨 50 行,半年下来大概 9000 行,Sheet 扛得住。
  • latest —— 公式驱动的视图。用 QUERY 函数把 raw_dumps 里每个 URL 最新的一行拉出来,这样我(和 Claude)随时看到的都是"当前价紧挨着昨天价"的视图。

我用 Apify 的 Google Sheets 集成做推送。除了 Apify 本身的计算费,没多收钱。失败模式也很明显——早上 9 点 Sheet 里没新数据,就是哪里断了。

"昨天价"那个 lookup 就是整张 Sheet 里唯一需要的公式逻辑:

=ARRAYFORMULA(
  IF(C2="", "",
    (latest_price - C2) / C2
  )
)

C2 是昨天的价,latest_price 是今天的。结果是一个"百分比变化"列。这就是 Claude 接下来要看的字段。

第四步——让 Claude 读 Sheet、标变化、写 diff

这是 Claude 真正进入工作流的唯一一环。我跑的提示词(可以走 Anthropic API,也可以走 Claude.ai Projects 加 Sheets 连接器):

You are a price-monitoring analyst. Read the "latest" tab of the
attached Google Sheet. Each row has a competitor, product, current
price, and previous price.

Return ONLY a JSON object with this structure:
{
  "alerts": [
    {
      "competitor": "Acme Cloud",
      "product": "Pro plan",
      "old_price": 99.00,
      "new_price": 79.00,
      "currency": "USD",
      "pct_change": -19.2,
      "url": "https://..."
    }
  ],
  "unchanged_count": 47,
  "scan_date": ""
}

Flag any row where |pct_change| > 5%.

Hard rules:
- Never invent prices. If a price is null or unreadable, set pct_change to null and skip.
- Only flag moves that are clear and likely intentional. A 0.3% wiggle is not news.
- If nothing crossed the threshold, return {"alerts": [], "unchanged_count": 50, "scan_date": ""}.

两个设计决定值得点一下。第一,5% 这个阈值比选哪个大模型更重要。我一开始是"只要有变动就告警",一天能收到 14 条。调了一周改成 5%,每天就 1-3 条真正有用的告警。阈值才是真正的产品,模型只是管道。

第二,"never invent prices" 这条规则。我抓到过 Claude 信心满满地报"$59 → $54",结果 Sheet 上其实是"$59 → null"。模型就是想要"有用"一点。真正有用的,是空的 alerts 数组,而不是一个瞎编的百分比。

成本:Sonnet 4 读 50 行加 prompt,每次大概 4000 输入 token、800 输出 token。按 $3 / $15 每百万 token 算,每次扫描大约 0.025 美元。Slack 调用加几分钱,就是前面说的 0.10 美元一次。

第五步——把 diff 发到 Slack(8 行代码)

Slack 这条消息,是团队真正会去看的环节。别过度设计。我迭代了三个月定下来的格式:纯文本,不用 Block Kit,不放按钮:

*Price Monitoring — 2025-10-06*
3 products moved more than 5% overnight:

• *Acme Cloud* — Pro plan: $99 → $79 (-19.2%) ⚠️
• *Globex* — Starter: $49 → $42 (-14.3%)
• *Initech* — Enterprise: $499 → $549 (+10.0%)

All 47 other products unchanged.

"⚠️" 标记的是 15% 以上的变化——这个阈值我希望在 1 小时内有人介入。低于这个阈值的,每日汇总发一次就行。

第六步——定时调度(注意几个失败模式)

整个流程我跑在 GitHub Actions 的 cron 上,UTC 时间早上 7 点触发。这个 action 会调用 Apify 的 run 端点、等它跑完、跑 Claude 那一段、然后 POST 到 Slack。整个任务大概 4 分钟。

我实际撞到过的三个失败模式:

  • Apify 偶尔会限流。 50 个 URL 一天一次基本不会触发,但加到 200 个 URL 就会看到零星的 429 响应。修法是 GitHub Actions 那一步加个单次重试。别搞复杂。
  • 竞品改了商品页的版式。 昨天能用的选择器今天返回 null。Claude 的 prompt 直接处理了——这行进 unchanged_count,不会误报。我每个月检查一次清单,把反复出问题的 price_selector_hint 改一下。
  • 大站撞了反爬墙。 把那个 URL 换到另一个 Actor(Apify 每个站有 3-4 个备选)就行,或者接受"这家的价格会黑几天"。别让一个卡住的 URL 拖累整条流水线——每条抓取包一层 try/catch,继续往下走。

如果让我重新开始,我会怎么做

第一版我踩的坑是告警阈值设成了 0%。第一天就收到 6 条 Slack,其中 4 条是"商品页刷新了,价格现在显示两位小数而不是零位小数"。我会直接从 5% 开始,如果两周后频道太安静再降到 3%。别低于 2%——你会陷入"四舍五入噪音"的追逐战。

另一点——前三天我会完全跳过 LLM,先用脚本算 diff、把原始数字直接发到 Slack。让数据先流起来。让自己习惯收到告警。然后再把 Claude 加进来做综合判断和"这条变化到底有没有意义"的过滤——这一步其实是人很难持续做好的。

一个范围上的提醒:这个工作流是给可见、稳定、商品页、价格是简单数字的场景设计的。它处理不了登录后才能看价格的 B2B 定价、处理不了按用户动态定价(酒店、机票、保险)、也处理不了货币换算的边界情况。

对其他所有人——50 个商品页、一个早晨、0.10 美元——这就是我找到的最便宜的可用版本。第一次值回搭建成本的告警,大概就是那种:竞品在周五下午调价,你赶在销售团队周一早上被"为什么你们突然这么贵"这个问题砸到之前,先把价格跟进到位。