ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

天气概率播报机器人:从数据源选型到推送链路全解析

2026/9/13 5:52:02 拓冰建站 浏览量
天气概率播报机器人:从数据源选型到推送链路全解析 CloddsBot 这个名字我第一眼看到就记住了——Clodds 是 clouds 和 odds 的合成词云和概率拼在一起基本就把这个项目的灵魂说清楚了它不是一个只会报“今天 24 度、明天有小雨”的普通天气机器人而是一个帮你判断“今天这事儿靠不靠谱、成功率有多大”的天气概率播报机器人。我最早被这种思路吸引是因为日常通勤、户外活动真正需要的其实不是温度数字而是决策建议出门要不要带伞、晚上能不能拍晚霞、周末适不适合去郊外骑车。CloddsBot 要解决的就是把多源天气数据拉下来之后用一套规则模型换算成降雨概率、云量覆盖率、风力影响这些可以直接指导行动的值再通过定时任务推送到 IM 上。这篇文章就把我从数据源选型、概率模型设计、工程实现到踩坑修复的完整过程拆开讲一遍适合所有想自己写一个“有点判断力”的天气机器人、或者想熟悉定时任务加消息推送这套链路的人参考。1. 项目冷启动CloddsBot 到底想解决什么问题1.1 为什么不做普通天气提醒而做概率预判我先说一个结论传统的天气播报机器人在移动端的留存率其实很差因为用户看一次 App 就完事了机器人发推送反而显得打扰。CloddsBot 想避开这个局面所以它调整了一下产品立场——不要替用户播报天气而是替用户做一道选择题。举个例子。两个人在同一个城市同一天早上通勤族关心的是“7:30 到 9:00 出门这段有没有可能下雨”他需要的是一个超过 50% 就会提醒带伞的概率阈值。摄影爱好者关心的是“傍晚云量多少、会不会挡住落日”他需要的是云量覆盖率曲线而不是降水概率。如果机器人只是推送“今天多云最高 26 度”这两个人都不会有什么反应因为他们需要的信息被埋没在通用描述里了。CloddsBot 的核心思路是把每个用户最关心的“决策点”抽象出来把天气数据加工成概率和指数让机器人说的话天然带着行动价值。这也是名字里 odds 那一半的意义所在。我建议所有想做天气类机器人的读者动手之前先不要纠结用什么 API先想清楚自己的 bot 到底在帮用户回答什么问题。问题定义越具体后续的数据模型和推送文案越容易设计。1.2 最小可用版本的产品边界很多人一上来就想把 Bot 做成全功能的比如支持语音问答、接入大模型、做自然语言理解。我建议 MVP 阶段绝对不要碰这些。CloddsBot 的 1.0 版本边界就三条拉数据、算概率、推送结论。我当时把功能拆成了 P0、P1、P2 三级这里贴出来给新手参考优先级功能点说明P0获取指定经纬度的逐小时天气数据拉不到数据其他全部免谈P0计算降雨概率、云量、风力指数这是 CloddsBot 的核心逻辑P0定时推送每日天气结论到 IM完成从数据到触达的闭环P1支持多城市订阅让 bot 能服务超过一个地方P1用户自定义提醒时段早晚时段分开推送P2历史准确率统计用反馈数据反过来优化概率模型P2接入日历或活动信息让概率建议自动结合用户行程MVP 阶段只做 P0最多带一个 P1 的城市参数。这样整个服务就是“一个定时脚本 几个函数”部署在服务器上跑也很轻量不会因为需求膨胀把项目拖黄。1.3 目标用户与真实使用场景我在设计场景的时候特意把用户分成三类因为不同类型的用户对概率表达的接受度完全不同。第一类是通勤族。他们最典型的问题是“早上要不要带伞”。这类用户不需要太复杂的输出只要告诉他有雨概率超过多少给出明确行动建议就行。CloddsBot 对这类用户通常输出“今天 8 点前后有短时小雨概率约 65%建议带伞”。第二类是户外活动组织者比如徒步群、骑行群的管理员。他们需要提前一到两天判断活动要不要改期。这个场景下机器人不能只给单点时段的数据而要给出一个“活动友好度”比如把风力超过 5 级、降雨概率超过 50% 的时段标记为高风险区间。这类用户看重的是趋势不是某一个小数点后面的概率值。第三类是对数据敏感的“半技术型”用户比如摄影爱好者、航拍玩家。他们希望看到更原始的信息云量曲线、风速变化、湿度趋势。CloddsBot 可以在推送详情里放一个简短的“指数摘要”满足他们进一步判断的需要。清楚目标用户之后你会发现产品的表达方式完全不一样了。这也是为什么我强烈建议先在纸上把用户画像想清楚再开始写代码。2. 数据层设计天气数据源与概率模型怎么搭2.1 数据源选型对比任何天气机器人都绕不开数据源这个坎。CloddsBot 在选择数据源时我主要关注四件事是否免费、是否需要注册 Key、返回字段是否包含逐小时降水概率和云量、以及每天请求次数限制。我实测对比了几个主流来源做了一个比较直接的对照数据源免费额度逐小时数据降水概率云量稳定性感受Open-Meteo免费无需 Key支持支持支持稳定海外节点响应较快OpenWeatherMap免费档每分钟有限制支持部分版本支持支持数据较全但免费档限流明显和风天气免费版有每日请求上限支持支持支持国内访问快返回字段丰富高德天气免费但需 Key不支持逐小时不支持不支持适合做展示不适合概率计算如果你的服务部署在海外服务器而且用户主要在国内我个人更推荐 Open-Meteo因为它不需要注册 Key、请求简单、免费额度非常充裕经纬度直接传过去就能拿数据。但如果你主要服务国内用户且很在意国内天气源的本地化精度那和风天气会更顺手只要注意免费版的每日调用上限。CloddsBot 我最后采用的是双数据源策略主源用 Open-Meteo备份源用和风天气。主源请求失败时自动切换到备用源。这里有个经验——不要迷信任何单个数据源再稳定的服务也有偶发 5xx而天气机器人错过推送时机基本等于失效。2.2 多云天的概率计算思路大多数天气 API 会直接给一个降水概率字段但 CloddsBot 没有直接拿这个字段去做主力输出。原因很简单用户真正关心的“会不会下雨”是一个条件概率而 API 返回的降水概率是基于模型的粗略估计它没有完全反映出云量、湿度、风力之间的关系。我的处理方式是构建一个多因素概率评分把几个关键因子融合起来基础降雨概率 P_base直接取 API 返回的 hourly precipitation probability。云量修正系数 C云量越高出现降水或阴天的可能性就越大。设计成 0.8 到 1.2 之间的系数。湿度修正系数 H相对湿度高于 80% 时降雨概率上调低于 50% 时下调。风力衰减系数 W风力过大时云层被吹散连续性降雨的概率反而降低但阵雨概率可能增加这里我简化为一个扣分项。综合起来可以写成这样一个简化的评分公式score P_base * C * H * W这个 score 是 0 到 100 之间的相对概率不是严格意义上的真实概率但对决策来说已经足够。实际应用中我还会把连续两小时内 score 都大于 60 的时段标记为“大概率降雨窗口”这个标记对推送文案特别有用。2.3 概率不是算完就结束校准与归一化概率模型最怕的就是输出结果系统性偏大或系统性偏小。比如模型给出的“70% 降雨概率”如果实际十次里只有四次下雨那这个 70% 就是虚高的用户很快就会失去信任。所以在 CloddsBot 里我加了一个很简单的校准环节把算出来的 score 经过一次逻辑回归形式的映射把输出范围压缩到 5 到 95 之间避免出现绝对化的 0% 和 100%。代码大概长这样import math def calibrate_score(score: float) - float: # 把 0~100 的分数映射到概率空间并限制极值 x (score - 50) / 20 # 中心化并缩放 prob 1 / (1 math.exp(-x)) # sigmoid 映射 return round(prob * 100, 1) # 示例原始分数 65 分经过校准后概率约为 88.1% print(calibrate_score(65))这套映射的意义是把模型输出的“相对分数”转换成更像概率的数值避免用户看到一个 45 分就完全忽略也避免 98% 这种容易被打脸的绝对表述。当然校准系数不是拍脑袋定的是靠历史记录不断拟合出来的这点后面我会专门讲反馈闭环。3. 工程实现从数据获取到 IM 推送的完整链路3.1 服务架构与准备工作CloddsBot 的整体架构非常朴素只有一个 Python 脚本加一个定时调度器我不建议一上来就上消息队列、容器编排这些重型组件。能用最简单的方式跑起来才是 MVP 阶段该做的事。目录结构大概是这样的cloddsbot/ ├── main.py # 入口负责调度 ├── weather.py # 数据拉取与解析 ├── probability.py # 概率计算与校准 ├── notifier.py # 推送逻辑适配不同 IM 渠道 ├── config.json # 城市、经纬度、推送 token 配置 └── history.db # 运行后自动生成用于准确率统计准备阶段要做三件事第一准备一个经纬度配置精确到小数点后两位就够了。第二如果选和风天气这类需要 Key 的服务先把 Key 放在环境变量或者配置文件中绝对不要硬编码到代码里。第三确定要推送到的 IM 渠道。如果只是自己用Server 酱、钉钉机器人、企业微信机器人、飞书机器人这类 webhook 最省事不用自己搭建收消息的服务器。3.2 天气数据拉取与时间处理的关键细节这里重点说下时间处理。天气 API 给的 hourly 数据通常按照数据源的时区或者 UTC 时间排列如果不做转换直接取“当前小时”很可能会出现消息推送时间与实际时段对不上的问题。我以 Open-Meteo 为例请求的时候直接带上时区参数是最省心的方案import requests def fetch_weather(lat: float, lon: float, timezone: str) - dict: url https://api.open-meteo.com/v1/forecast params { latitude: lat, longitude: lon, hourly: temperature_2m,precipitation_probability,cloud_cover,relative_humidity_2m,wind_speed_10m, timezone: timezone, forecast_days: 2, } resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() # 将时间序列和各项数据组织成按小时排列的列表 times data[hourly][time] probs data[hourly][precipitation_probability] clouds data[hourly][cloud_cover] humidity data[hourly][relative_humidity_2m] wind data[hourly][wind_speed_10m] # 找到未来第 2 个小时的索引作为默认关注点 current_index 2 return { time: times[current_index], precip_prob: probs[current_index], cloud_cover: clouds[current_index], humidity: humidity[current_index], wind_speed: wind[current_index], }这里有个非常重要的经验请求返回的数组长度、字段顺序可能在极端情况下变化所以千万不要用硬编码下标访问我是通过匹配时间字符串来定位目标小时的代码里为了直观才简化成固定索引。时间字符串一般是 ISO 格式直接和业务时间做比较即可。3.3 概率计算核心函数接下来是最核心的概率计算函数。我会把前面提到的多因子修正和校准逻辑合到一起形成一个可以直接复用的模块def compute_weather_score(precip_prob: float, cloud_cover: float, humidity: float, wind_speed: float) - float: # 1. 基础分降水概率 score precip_prob # 2. 云量修正云量越高降水可能性越大系数从 1.0 起向上调整 cloud_factor 1.0 max(0.0, (cloud_cover - 50) / 100) score * cloud_factor # 3. 湿度修正高湿度更容易形成降水 humidity_factor 1.0 if humidity 80: humidity_factor 1.15 elif humidity 50: humidity_factor 0.9 score * humidity_factor # 4. 风力修正大风天连续性降雨概率下降适当打折 if wind_speed 30: score * 0.85 elif wind_speed 5: score * 1.05 # 5. 限制在 0~100 之间再做一次校准映射 score max(0.0, min(100.0, score)) calibrated calibrate_score(score) return round(calibrated, 1)这些系数的取值不是来自某个权威论文而是我根据历史数据反推出来的经验值。比如湿度大于 80% 时降水概率上调 15%是因为我比对了一个月数据发现高湿度时段 API 降水概率容易被低估。新手可以直接照抄但建议你自己跑一段时间把系数调整到适合本地气候的状态。3.4 定时任务与消息推送适配定时任务我推荐直接用系统自带的 cron而不是在 Python 里写 while True 循环原因很简单系统级 cron 更稳定进程崩溃后可以自动恢复而且不用额外引入 Celery 这类重组件。一台 Linux 服务器上crontab 里加一行30 7 * * * cd /opt/cloddsbot /usr/bin/python3 main.py logs/cloddsbot.log 21意思是每天早上 7:30 运行一次。如果你需要早晚各推一次就再加一行。注意时区问题cron 默认用系统时区服务器时区是 UTC 的话要换算成国内时间再写表达式。推送逻辑我封装成一个简单的 notifier 模块适配不同 IM 时只需改 webhook 地址和消息格式import requests def send_im_message(webhook: str, content: str, msg_type: str text) - None: payload {msgtype: msg_type, text: {content: content}} resp requests.post(webhook, jsonpayload, timeout10) resp.raise_for_status()如果你用的是 Server酱这类单用户推送服务只需要把 webhook 换成对应的 send key 地址逻辑完全一样。重要的是把推送动作单独隔离出来方便以后增加渠道。3.5 消息内容设计概率数字怎么说得像人话这是很多人会忽略但恰恰最重要的环节。直接把概率数字甩给用户是没有产品思维的体现你要做的是把数字翻译成决策。我早期试过推送“降雨概率 68.5%”结果用户根本不知道怎么行动。后来改成“建议带伞”反馈立刻好转。现在 CloddsBot 的消息模板大概长这样早上好今日天气摘要城市/地址 - 降雨概率约 68% - 云量覆盖中等偏高 - 风速4级对出行影响较小 结论8点到10点之间出现短时降雨的可能性较大建议出门带伞。 今日最佳出行时段下午2点到5点。这里的关键设计是“结论先行”先给概率值再给行动建议。不要指望用户自己解读概率。行动建议可以用简单的规则生成比如概率高于 60% 就建议带伞低于 30% 则提示可放心出行。规则不复杂但很有效。4. 踩坑实录与问题排查4.1 接口返回字段为空的坑这类问题最容易出现在天气 API 上表现形式是拉取成功后某个字段返回 null脚本没做类型判断就直接参与运算导致整个服务崩溃。我踩过一次比较典型的坑是早年用某个天气接口时免费档的降水概率字段只在特定预报时段返回其他时段一律为 null。我当时没看文档以为是网络问题排查了很久才发现是接口设计如此。解决方案是在解析层做统一防御任何字段缺失或为 None 时采用默认值并记录警告日志def safe_float(value, default: float 0.0) - float: try: return float(value) except (TypeError, ValueError): return default这类问题看似小但如果不处理你的定时任务会一直失败而日志可能被大量 ValueError 刷屏真正的异常反而被淹没。所有从外部接口拿到的字段都要默认它是不可信的。4.2 定时任务漂移与多时区问题我遇到过 Cron 时间对不上号的尴尬情况。服务器时区是 UTCcron 表达式写的是30 7 * * *结果每天消息在下午 3:30 才到整整晚了八个小时。这个问题的根源就是时区没统一。排查方式很简单先用date命令确认系统时区再决定 cron 时间。如果你希望国内时间早晨 7:30 推送服务器是 UTC 时区的话cron 表达式就要写成30 23 * * *或者直接给系统设置国内时区sudo timedatectl set-timezone Asia/Shanghai设置完再跑date验证。另外天气 API 返回的时间序列也要确认时区参数建议在请求里显式传递业务时区不要依赖服务器的默认时区避免以后迁移服务器时又踩一遍。4.3 概率输出偏高偏低反馈闭环怎么建概率系统最需要的是反馈。没有反馈校准的概率模型本质上就是一个“看起来科学”的拍脑袋。CloddsBot 的做法是记录每天的预测结论和当天实际天气定期计算准确率反过来调整前面那些系数。我先说一个最简单的统计方法把模型输出的概率值和实际是否下雨按月汇总算出平均概率和实际频率之间的差值。正常来说如果模型说“概率 70%”的日子历史上真的下雨的比例应该接近 70%。如果差距长期超过 10 个百分点说明校准有问题。更进阶一点我引入了 Beta 分布来平滑小样本问题def update_probability(prior_alpha: float, prior_beta: float, rain_count: int, total_count: int) - float: alpha prior_alpha rain_count beta prior_beta (total_count - rain_count) return alpha / (alpha beta)在历史数据不足 20 条时我宁可用一个比较保守的先验概率也不要让模型的数值被几天的极端天气带偏。等数据积累多了再逐渐放宽先验的影响。4.4 推送消息失败与限流处理IM webhook 推送最常见的失败原因是限流和内容格式不合法。比如企业微信机器人限制每分钟最多 20 条消息如果群里有多个用户订阅多个城市就容易触发限流。我的处理方式有两层第一层发送前对内容做长度校验超过 2000 字就截断核心部分。第二层对 429 或 5xx 响应做指数退避重试最多重试 3 次间隔分别是 1 秒、5 秒、30 秒。另外所有 webhook 消息里如果包含 Markdown 语法要注意转义问题机器人接口对特殊字符的处理各不相同。分享一个更省心的办法如果推送量不大直接把所有城市汇总成一条消息推送而不是每个城市发一条。这样既不会触发限流用户看起来也清爽很多只是对代码的封装要求高一点。5. 从能用走向好用扩展方向与经验沉淀5.1 多城市订阅与存储设计CloddsBot 从单城市扩展到多城市存储设计就变得重要了。最简单的方案是直接用 JSON 文件当配置城市列表放在数组里脚本每次遍历生成消息。但如果用户想自己订阅、自己设置提醒时段JSON 文件就不够用了。我推荐用 SQLite因为它零部署、单文件、支持并发查询对个人项目来说再合适不过。表结构可以设计得非常简单CREATE TABLE city_subscription ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_key TEXT NOT NULL, city_name TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL, morning_time TEXT DEFAULT 07:30, evening_time TEXT DEFAULT 18:00, enabled INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP );这类轻量存储的好处是哪怕你以后想把 CloddsBot 改造成带用户交互的完整服务这个表结构也能直接复用不用推倒重来。5.2 个性化阈值与场景细分按我自己的经验天气机器人最容易吸引用户的点不是“预报准”而是“懂我”。同样一个降雨概率有人 50% 就要带伞有人 30% 就决定取消户外跑。CloddsBot 可以在订阅表里加一个 sensitivity 字段用高、中、低三档表达用户对风险的容忍度生成消息结论时直接参考这个档位来调整建议措辞。更进一步还可以做场景细分。比如针对通勤用户只关心早晚高峰针对骑行用户要额外关注风速和体感温度针对摄影用户提供未来三天云量最低的时间窗口。这些功能本质上不是技术难题而是产品逻辑设计需要你花时间理解用户真正在什么场景下会看机器人推送的消息。尽早把场景细分考虑进去比事后重构要省力得多。5.3 数据隐私和接口使用边界最后这块可能很多人不会注意但我想特别提一句。天气机器人收集的数据看似只有经纬度但经纬度实际上可以直接定位到个人常驻位置它属于敏感程度比较高的数据。个人项目也要注意几条边界不记录用户聊天内容推送日志只保留摘要不保留原始 IM 文本。经纬度只用于调用天气接口不落库或落库时做模糊化处理比如保留小数点后一位。缓存天气数据时要设置过期时间比如 30 分钟失效避免长期持有第三方接口的数据。使用任何第三方天气服务前确认免费版允许的调用频率和数据用途不要在服务器上开高频轮询去刷接口既浪费资源也容易被封禁。我在 CloddsBot 里处理经纬度时最后是先把坐标逆地理编码成城市名再在城市粒度上去拉天气数据。虽然这样稍微损失一点精准度但在隐私和功能之间是更稳妥的平衡。最后再说一点个人感受。CloddsBot 这个项目给我最大的启发是一个看似很窄的“天气概率播报”需求只要把表达方式从“报数字”改成“给建议”价值感完全不同。我自己用下来最舒服的场景是傍晚准备出门跑步前看一眼它推送的“18 点到 20 点降雨概率 20%风速 2 级适合跑步”那种确定感是普通天气 App 给不了的。如果你也想做一个类似的项目我建议第一版不要追求功能多先把采集、概率计算、推送这条链路跑通然后持续用真实反馈去修正系数。等跑上一个月再回头看你会发现那些调参的日子才是最值钱的经验。