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, params=params, timeout=10) 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 2>&1意思是每天早上 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, json=payload, timeout=10) 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 时间对不上号的尴尬情况。服务器时区是 UTC,cron 表达式写的是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 给不了的。如果你也想做一个类似的项目,我建议第一版不要追求功能多,先把采集、概率计算、推送这条链路跑通,然后持续用真实反馈去修正系数。等跑上一个月再回头看,你会发现那些调参的日子才是最值钱的经验。