前言
实时价格监控是跨境电商数据基础设施的核心场景之一。本文从系统设计角度,深入探讨如何构建一套面向生产的亚马逊竞品调价监控系统,重点解析 AI Agent 与数据采集 API 的协作模式、高并发场景下的性能优化策略,以及可靠的多渠道告警架构。
一、为什么传统方案定义了错误的目标
大多数卖家工具的价格监控是以"可视化探索"为目标设计的:用户主动去看价格走势图,而不是系统主动在价格发生变化时通知用户。这两种设计哲学导致了完全不同的用户体验:
- 探索型:适合分析历史趋势,不适合实时响应
- 推送型:适合运营决策,要求低延迟、高可靠性
我们要构建的是推送型竞品价格预警系统:系统感知变化,推送到人,人做决策。
二、系统架构设计
2.1 整体架构
┌──────────────────────────────────────────────────────┐
│ 定时调度层 │
│ APScheduler / Cron / AWS EventBridge │
│ ↓ 每10分钟 │
├──────────────────────────────────────────────────────┤
│ 数据采集层 │
│ Pangolinfo Scrape API │
│ · 结构化JSON输出 (Buybox + All Offers) │
│ · 内置反爬优化,无需自维护代理池 │
│ · 支持美/英/日/德等多站点 │
│ ↓ │
├──────────────────────────────────────────────────────┤
│ 分析处理层 │
│ OpenClaw AI Agent + 差分引擎 │
│ · 价格快照滑动窗口比对 │
│ · 阈值过滤 + 告警去重 │
│ · AI上下文分析(历史模式 + 威胁评级) │
│ ↓ 仅在触发告警时 │
├──────────────────────────────────────────────────────┤
│ 通知分发层 │
│ 飞书 Webhook + Slack Webhook │
│ · 富文本消息卡片 │
│ · 重试机制(3次,指数退避) │
│ · 告警日志持久化 │
└─────────────────────────────────────────────────────┘
2.2 关键设计决策
为什么选择 Pangolinfo Scrape API 而非自建爬虫?
自建亚马逊爬虫在生产环境中面临以下持续性挑战:
- 亚马逊频繁更新反爬策略(selector变化、JS渲染、验证码注入)
- IP封禁需要高质量代理池(成本高、维护复杂)
- 页面结构解析维护成本高,版本迭代时易断
Pangolinfo API 将这些复杂性封装为单一接口,返回标准化JSON,API层面的稳定性和可维护性大幅优于自建方案。
为什么引入 OpenClaw Agent 而不只做规则判断?
纯规则的价格告警("降价>5%就告警")存在上下文盲区:它告诉你"发生了什么",但不告诉你"这有多重要"、"历史上是否有模式"、"建议怎么做"。OpenClaw Agent 把这些判断外包给语言模型,让告警从"事件通知"升级为"决策辅助"。
三、核心实现
3.1 高并发批量采集
# async_collector.py
import asyncio
import aiohttp
from datetime import datetime
from typing import List, Optional
PANGOLINFO_API_URL = "https://api.pangolinfo.com/v1/amazon/product"
async def fetch_single_asin(
session: aiohttp.ClientSession,
asin: str,
api_key: str,
semaphore: asyncio.Semaphore,
marketplace: str = "US"
) -> Optional[dict]:
"""异步采集单个ASIN,通过信号量控制并发数"""
async with semaphore:
try:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"asin": asin,
"marketplace": marketplace,
"parse": True,
"include_offers": True,
"include_buybox": True
}
async with session.post(
PANGOLINFO_API_URL, json=payload,
headers=headers, timeout=aiohttp.ClientTimeout(total=30)
) as resp:
if resp.status != 200:
print(f"[WARN] {asin}: HTTP {resp.status}")
return None
data = await resp.json()
return {
"asin": asin,
"collected_at": datetime.utcnow().isoformat() + "Z",
"buybox_price": data.get("buybox", {}).get("price"),
"buybox_seller_name": data.get("buybox", {}).get("seller_name"),
"buybox_seller_id": data.get("buybox", {}).get("seller_id"),
"all_offers": data.get("offers", []),
"product_title": data.get("title", ""),
"in_stock": data.get("availability") == "In Stock"
}
except Exception as e:
print(f"[ERROR] {asin}: {e}")
return None
async def batch_collect_async(
asin_list: List[str],
api_key: str,
max_concurrency: int = 8
) -> List[dict]:
"""
异步批量采集,控制并发避免触发API限速
Args:
max_concurrency: 最大并发请求数(建议5-10)
"""
semaphore = asyncio.Semaphore(max_concurrency)
async with aiohttp.ClientSession() as session:
tasks = [
fetch_single_asin(session, asin, api_key, semaphore)
for asin in asin_list
]
results = await asyncio.gather(*tasks)
# 过滤采集失败的项
return [r for r in results if r is not None]
# 同步包装(兼容调度器)
def collect_prices(asin_list: List[str], api_key: str) -> List[dict]:
return asyncio.run(batch_collect_async(asin_list, api_key))
3.2 告警去重与冷却机制
# deduplicator.py
import time
from typing import Optional
class AlertDeduplicator:
"""
防止同一ASIN同类事件短时间内重复告警
冷却期内:只记录,不推送
冷却期后:正常推送
"""
def __init__(self, cooldown_seconds: int = 7200): # 默认2小时冷却
self.cooldown = cooldown_seconds
self._last_alert: dict = {} # {(asin, alert_type): timestamp}
def should_alert(self, asin: str, alert_type: str) -> bool:
key = (asin, alert_type)
now = time.time()
last = self._last_alert.get(key, 0)
if now - last > self.cooldown:
self._last_alert[key] = now
return True
remaining = int(self.cooldown - (now - last)) // 60
print(f"[DEDUP] {asin} ({alert_type}) 冷却中,剩余约 {remaining} 分钟")
return False
3.3 告警推送可靠性(重试 + 日志)
# reliable_notifier.py
import requests
import time
import json
import logging
from datetime import datetime
from pathlib import Path
logger = logging.getLogger("PriceAlertSystem")
class ReliableNotifier:
"""
带重试机制的告警推送器
- 失败自动重试3次(指数退避)
- 所有告警事件持久化到本地日志文件
"""
def __init__(self,
feishu_webhook: str = "",
slack_webhook: str = "",
log_dir: str = "./alert_logs"):
self.feishu = feishu_webhook
self.slack = slack_webhook
self.log_dir = Path(log_dir)
self.log_dir.mkdir(parents=True, exist_ok=True)
def _post_with_retry(self, url: str, payload: dict, max_retries: int = 3) -> bool:
"""带指数退避重试的HTTP POST"""
for attempt in range(max_retries):
try:
resp = requests.post(url, json=payload, timeout=10)
if resp.status_code == 200:
return True
logger.warning(f"HTTP {resp.status_code} on attempt {attempt+1}")
except requests.RequestException as e:
logger.warning(f"Request failed on attempt {attempt+1}: {e}")
if attempt < max_retries - 1:
sleep_seconds = 2 ** attempt # 1s, 2s, 4s
time.sleep(sleep_seconds)
return False
def _log_event(self, event: dict, channels_result: dict):
"""持久化告警记录"""
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"event": event,
"delivery_result": channels_result
}
log_file = self.log_dir / f"alerts_{datetime.utcnow().strftime('%Y%m%d')}.jsonl"
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
def send(self, event: dict):
"""发送告警到全部配置渠道"""
results = {}
if self.feishu:
results["feishu"] = self._post_with_retry(self.feishu, self._build_feishu_card(event))
if self.slack:
results["slack"] = self._post_with_retry(self.slack, self._build_slack_blocks(event))
self._log_event(event, results)
status = " | ".join(f"{k}: {'✅' if v else '❌'}" for k, v in results.items())
logger.info(f"[ALERT] {event['asin']} -{abs(event['change_pct']):.1f}% | {status}")
def _build_feishu_card(self, e: dict) -> dict:
# (卡片结构同主站版本,此处省略)
return {} # 完整实现见主站文章代码
def _build_slack_blocks(self, e: dict) -> dict:
# (Block Kit结构同主站版本)
return {} # 完整实现见主站文章代码
四、性能指标参考
| 场景 | 参数 | 实测表现 |
|---|---|---|
| 单ASIN采集延迟 | 美国站单次请求 | 800ms - 2s |
| 批量并发采集(20 ASIN) | 并发8 | 4-6s 完成 |
| 告警触发到通知到达 | Feishu/Slack | <30s |
| 完整10分钟轮询周期 | 50 ASIN | <90s |
五、监控系统的监控
生产系统需要"监控监控本身",防止采集静默失败:
# health_check.py
import time
from typing import Callable
class MonitoringHealthCheck:
"""检测连续采集失败,触发二级告警"""
def __init__(self, max_consecutive_failures: int = 3,
emergency_notify_fn: Callable = None):
self.max_failures = max_consecutive_failures
self.failure_count = 0
self.notify = emergency_notify_fn or print
def record_success(self):
self.failure_count = 0
def record_failure(self):
self.failure_count += 1
if self.failure_count >= self.max_failures:
self.notify(
f"🚨 监控系统告警:已连续 {self.failure_count} 次采集失败!"
f"请检查 Pangolinfo API Key 及网络连接。"
)
六、总结与扩展建议
本文实现的系统满足生产级别的以下要求:
- 时效性:10分钟级采集,<30秒告警到达
- 可靠性:重试机制 + 健康检查 + 本地日志
- 可扩展性:异步并发支持100+ASIN,AI分析层易于替换升级
- 运维友好:环境变量配置,模块化设计,易于单独测试
扩展方向:
- 将价格历史写入 TimescaleDB,支持长周期趋势查询
- 接入更多数据维度(BSR排名变化、Review数量异常)
- 使用 OpenClaw Agent 生成每日竞争情报摘要推送