我删除了按钮 — 将 Flutter AI 功能从 UI 触发迁移到每小时 Cron 批处理

发布: (2026年4月18日 GMT+8 08:20)
7 分钟阅读
原文: Dev.to

I’m sorry, but I don’t have the ability to retrieve the contents of the linked article. If you can provide the text you’d like translated, I’ll be happy to translate it into Simplified Chinese while preserving the formatting and code blocks as you requested.

介绍

我从我的应用中移除了 “Run AI Prediction” 按钮。听起来像是倒退一步——但实际上这是我本周做出的最佳架构决策之一。

通过将 AI 推理从 UI 触发的调用迁移到自动化的每小时 cron 任务(GitHub Actions),我:

  • 消除了所有用户等待时间
  • 将 API 配额浪费削减约 90%
  • 添加了断路器,以防止 429 错误在应用其余部分级联

本文使用我的个人项目 “Jibun Kabushiki Kaisha”(自我公司),一个 Flutter Web + Supabase 生活管理应用,作为案例研究,阐述 什么、为什么以及如何

适用场景:

  • 使用 Supabase Edge Functions + AI API 构建 Flutter 应用
  • 遇到过 OpenAI、Anthropic 或 Gemini 的速率限制(429 错误)
  • 想要降低 AI 功能的感知延迟

原始设计与问题

最初的设计相当直接:

  1. 用户点击 “生成 AI 预测”
  2. 调用 Supabase Edge Function
  3. OpenAI/Gemini API 生成预测
  4. 显示结果

在生产环境运行后,出现了三个问题。

1. 高延迟 & 配额浪费

  • AI 预测耗时 3–8 秒。用户只能盯着加载动画——在赛马预测应用中,这非常痛苦,因为用户往往在比赛前几分钟查看结果。
  • 快速连续点击按钮会导致重复请求,浪费配额。

2. 级联的 429 错误

当 OpenAI/Anthropic/Gemini 配额在高峰时段耗尽,429 错误会在所有依赖 AI 的功能中级联出现:

ai-assistant: 400 → 429 → 429 → 429 (级联故障)

缺少断路器(circuit‑breaker),导致整个 AI 助手界面崩溃。

3. 空状态放弃

如果用户在任何 AI 运行之前打开预测页面,会看到空状态。分析显示 约 30 % 的新用户在此阶段离开。

解决方案:每小时 Cron 批处理 与 熔断器

每小时运行预测

问题 1 和 3 的解决办法很简单:每小时自动运行 AI 预测,这样用户打开应用时预测已经准备好。

# .github/workflows/horse-racing-update.yml
on:
  schedule:
    - cron: '0 * * * *'  # 每小时的 :00

工作流会执行 fetch_horse_racing.py,生成 AI 预测,并 UPSERT 结果到 Supabase 的 horse_races 表。当用户打开应用时,数据已经在那儿 —— 零等待时间

UI 简化

  • 删除了 “Run AI Prediction” 按钮。
  • 将 AppBar 中的脑袋图标替换为信息提示框和赛号徽章。
// Before
ElevatedButton(
  onPressed: _runAiPredictions,  // DELETED
  child: const Text('Run AI Prediction Now'),
),

// After — race number badge (no button needed)
Container(
  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  decoration: BoxDecoration(
    color: AppTheme.orange,
    borderRadius: BorderRadius.circular(12),
  ),
  child: Text('${race.raceNumber}R',
    style: const TextStyle(color: Colors.white, fontSize: 11)),
),

空状态提示 已更新:

  • 之前:“点击脑袋图标生成 AI 预测”
  • 之后:“预测已由每小时批处理(cron)自动生成”

我还在 horse_races 表中添加了 race_number 列(从 race_id_ext 的后两位迁移而来),因此每张赛马卡现在会显示清晰的 “1R / 2R / …” 徽章。

其他 AI 功能的熔断器

其他 AI 功能(例如 ai-assistant)仍然由 UI 触发,但现在使用 冷却守护 来防止 429 错误连锁。

// lib/services/ai_service.dart
class AiQuotaGuard {
  static DateTime? _cooldownUntil;
  static const _cooldownDuration = Duration(seconds: 60);

  static bool get isInCooldown {
    if (_cooldownUntil == null) return false;
    return DateTime.now().isBefore(_cooldownUntil!);
  }

  static Duration get remainingCooldown {
    if (_cooldownUntil == null) return Duration.zero;
    final r = _cooldownUntil!.difference(DateTime.now());
    return r.isNegative ? Duration.zero : r;
  }

  static void triggerCooldown() =>
      _cooldownUntil = DateTime.now().add(_cooldownDuration);

  static bool isQuotaError(dynamic error) {
    final msg = error.toString().toLowerCase();
    return msg.contains('429') ||
        msg.contains('quota') ||
        msg.contains('rate limit') ||
        msg.contains('too many requests');
  }
}

在 AI 调用包装器中的使用

Future callAiAssistant(String prompt) async {
  if (AiQuotaGuard.isInCooldown) {
    final secs = AiQuotaGuard.remainingCooldown.inSeconds;
    throw AIServiceException(
      errorType: 'RATE_LIMIT',
      message: 'AI service is temporarily limited. Retry in ${secs}s.',
      retryAfter: secs,
    );
  }
  try {
    return await _invokeFunction('ai-assistant', {'prompt': prompt});
  } catch (e) {
    if (AiQuotaGuard.isQuotaError(e)) {
      AiQuotaGuard.triggerCooldown();
      rethrow;
    }
    rethrow;
  }
}

UI 可以读取 retryAfter 来显示倒计时,而不是通用的错误信息。

指标

指标之前之后
预测显示延迟3–8 秒0 秒(预生成)
峰值时重复的 API 调用40–60 请求/小时0 (仅请求批处理)
级联 429 错误3–5 次/月0(断路器)
空状态放弃~30 %< 5 %

要点

  • 从按钮触发 AI 是一种自然的首个实现方式,但在生产环境中会产生真实成本:

    • 用户等待时间 → 将推理迁移到批处理,预先生成结果
    • 配额浪费 → 使用 cron 有计划地调度调用
    • 级联故障 → 添加带有冷却窗口的断路器
  • 这些模式 并非 Flutter 特有;它们适用于任何集成 AI API 的应用。

  • 移除按钮并不是功能删除——而是将责任从 UI 层转移到自动化层,使应用更具弹性且更友好。

公开构建

Jibun Kabushiki Kaisha – Flutter Web + Supabase 生活管理应用

标签: FlutterWeb Supabase buildinpublic AI CircuitBreaker GitHubActions

0 浏览
Back to Blog

相关文章

阅读更多 »

地球日的活力

我构建的 History 按日历天在浏览器中保存;每个部分旁边的照片是真实的捆绑图像。可选的 Gemini API 路由可以添加温暖的教练……