一句话
「我昨天走路 1000 步,是不是外面下雨了」——这句话模型答不了,而且缺的两样东西都是
结构性不可知 :它不知道用户在哪,也不可能知道昨天那个地方下没下雨。
为什么这件事比通用搜索(#2 )更该先做
#2 里定了一条判据:一个工具要占一个槽,必须返回模型没有 的东西。环境数据是这条判据下
最干净的一例——不是「模型可能不知道」,是不可能知道 。
更要紧的是它绕开了 #2 里标为「难的那一半」的问题。通用搜索难在模型判断不出自己知不知道,
所以要靠 app 侧的可观察信号去触发;环境数据不需要这层判断,它和 HealthKit 数据一样是结构性
缺失,工具描述可以写得和 daily_steps 一样具体:问某几天的活动量/睡眠为什么反常时,查那
几天的环境 。没有模糊地带。
而它接的正是 Vana 的本职工作。这个 app 做的就是解释健康数据里的异常,而天气和空气质量是
最常见的解释变量之一:下雨 → 步数掉;高温 → 睡眠差、静息心率抬高;AQI 差 → 户外锻炼停了。
现在这些它一个都够不着,只能说「你这几天活动量下降了,注意保持」——一句正确的废话。
分三档,越靠前越便宜
逐条在 iOS 26.5 SDK 里核过(WeatherKit.swiftinterface、HKMetadata.h、HKTypeIdentifiers.h)。
第 1 档:HealthKit 里已经有的(零新权限、零网络、零依赖)
这一档先做,而且它可能已经够回答开头那个问题了。
HKQuantityTypeIdentifierTimeInDaylight (iOS 17+,分钟,累计)——在户外待了多久。
不需要定位、不需要联网,而且它测的正是「他有没有出门」这件事本身。「昨天走 1000 步」配上
「昨天户外 8 分钟」,话基本就说完了,连天气都还没查。
HKMetadataKeyWeatherCondition / WeatherTemperature / WeatherHumidity (iOS 10+)——
挂在 HKWorkout 上,是 Watch 在那次户外锻炼当时当地 记的。这是真值,不是按今天的
位置反推的,正好补掉下面第 2 档的主要误差源。
HKQuantityTypeIdentifierUVExposure(iOS 9+),覆盖率存疑,顺手看一眼。
覆盖面窄(只有户外锻炼那几个时刻 + 每天的户外时长),但准 ,而且成本几乎为零——
现有的 HealthStore 加两个查询就行,连 project.yml 都不用动。
第 2 档:WeatherKit(天气全覆盖,含历史,不需要用户填 key)
顶掉原方案里的 Open-Meteo 天气部分。 之前那版是在不知道 WeatherKit 能查历史的前提下写的。
历史原生支持 :WeatherQuery.daily(startDate:endDate:) / .hourly(startDate:endDate:)。
DayWeather 给:condition、最高/最低温(iOS 18+ 还给出现时刻)、最大/最小湿度、
precipitationAmountByType(雨雪分开)、precipitationChance、uvIndex、能见度、风、
sun 日出日落。
没有用户要填的 key :绑 bundle id,在 Certificates & Identifiers 里配 Service ID + key,
app 里直接用 Swift API。这比 Open-Meteo 还省——省掉了设置页那一栏。
额度:500,000 次/月 随 Apple Developer Program 会员,不滚存。这个 app 一天用不了几次。
三个前提条件,做之前先确认:
要 Apple Developer Program 付费会员 ,并且要开 WeatherKit capability(project.yml 要改)。
attribution 是硬要求 ,不是建议:必须显示 Apple Weather 商标和一个指向数据来源页的法律
链接。这对 UI 有实际影响——结果面板里要留位置。天气预警还有额外要求(不许改文案),
但这个功能用不到预警。
模拟器上要签名才能跑 WeatherKit,现有的 xcodebuild 流程可能要调。这条最可能卡住,
先验证再往下做。
「降水小时数」WeatherKit 没有直接给 ,但 .hourly(startDate:endDate:) 自己数一遍就有。
这个字段要保留——见下面「口径」。
第 3 档:Open-Meteo(只补空气质量)
Apple 完全不给 AQI 和花粉(在 WeatherKit.swiftinterface 里 grep airquality|pollen|aqi| particulate,零命中)。这是唯一必须走第三方的部分。
Air Quality API,非商用不需要 key ,past_days 支持 0–92 天。
给 PM2.5 / PM10 / CO / NO₂ / SO₂ / O₃,以及欧洲和美国两套 AQI,全球覆盖(CAMS global 45km)。
花粉只有欧洲 (Only available in Europe during pollen season)。所以第一版没有花粉 ,
过敏那条线要么另找源,要么不做。
(原方案里「历史天气不能走 Archive API,ERA5 延迟 5 天」那条坑,随着天气改走 WeatherKit
已经作废;空气质量走 past_days,不受影响。留在这里只是记一笔,免得有人回头去接 Archive。)
口径:按天聚合,对齐健康工具
工具返回的形状照 daily_steps / sleep_summary——逐日一行,这样模型能直接和步数并排着读。
走 HealthReport,面板里可点(逐小时序列只进面板,不进 modelText,同现有约定)。
「下雨了吗」的正确口径是降水小时数,不是天气类型。 一天里下两小时和下一整天,对步数的
影响完全是两回事;只给一个「雨」字,模型会把两者说成同一件事。所以即使 WeatherKit 没这个
字段,也要从 hourly 自己数出来。
只给数据,不做归因。 不要在 app 侧预先算「步数和降水的相关性」——模型看到「那天下了 8
小时雨、户外 8 分钟、步数 1000」自己就会说出来,而 app 算出来的一个相关系数既不好解释也
容易骗人。同 MEDICATIONS.md 里不做交互作用数据库的理由。
位置与隐私
第 1 档不需要位置。第 2、3 档需要:
城市级精度就够 ,不要精确定位。requestWhenInUseAuthorization + reduced accuracy。
发给模型的是天气,不是坐标。 位置在本地解析成一次天气查询,上下文里只出现
「08-08 雨 8 小时、最高 19°C」。这条要写死——它让「读位置」在隐私上便宜得多,
和「HealthKit 只读、只上传聚合值」是同一条线。
没授权定位不是错误 。照 HealthKit 那条处理:照实说没有,给出补上的办法,并退回第 1 档
——timeInDaylight 和 workout 上的天气 metadata 不需要定位,这时候正好顶上。
误差 :历史天气按当前 位置查,用户上周在别的城市就是错的。第 1 档的 workout metadata
是当时当地的真值,两者冲突时以 workout 上的为准 。仍然要在输出里说明这个假设
(「按当前所在地查」),别让模型把它当成确定的事讲。
隐私会话照常可用 :读的是外部环境,不往盘上写用户的任何东西。
开关与缓存
独立开关,不归在 memoryEnabled 底下 (同 medicationsEnabled 的理由)。
第 1 档默认开(没有新权限,没有成本)。第 2、3 档第一次用到时才请求定位授权,不在启动时问。
可以缓存,而且该缓存 :历史天气永不变。今天的会变,给个短 TTL。这条和 外部信息检索:补上权重之外的那部分 #2 里
「不缓存检索结果」不冲突——那是成篇的正文,这是每天几个数。
任务拆分(草稿)
T1 (第 1 档)HealthStore 加 timeInDaylight 按天查询 + 从 HKWorkout metadata 里取
当时的天气。挂进现有的 workouts / 新增 daily_daylight,或者合成一个 daily_environment。
形状定下来之前先跑一遍种子数据,看模拟器上这两项到底有没有值 ——timeInDaylight 要
Watch 才记得全,覆盖率可能很差,那样这一档的价值就得重估。
T2 验证 WeatherKit 能在现有构建流程里跑通(签名、capability、模拟器)。这是前置风险,
先做,别等到 T3 才发现跑不起来。
T3 (第 2 档)HealthChat/Environment/:粗定位 + WeatherKit 历史查询,按天聚合,
降水小时数从 hourly 数。attribution 的 UI 位置一并留出。
T4 (第 3 档)Open-Meteo 空气质量,past_days。
T5 capability(暂名 daily_environment),输出 HealthReport,挂进
CapabilityRegistry.healthChat,受新开关管。工具描述收在「解释某几天的健康数据为什么反常」。
T6 chip 文案 + 面板(图标 cloud.sun);设置页开关;定位未授权的空态文案;
系统提示补一段(照着 registry 里有没有这个工具拼,同现有做法)。
验收
造一天低步数 + 那天真下雨 → 问「昨天怎么才走了 1000 步」,模型把降水小时数和户外时长说出来。
问「我最近睡得怎么样」→ 不调 这个工具(主要回归风险,同 外部信息检索:补上权重之外的那部分 #2 )。
不给定位授权 → 照实说,退回第 1 档,回答照常继续,不报错。
一次户外锻炼的天气(Watch 记的)和按当前位置查到的对不上时,以前者为准。
WeatherKit attribution 在面板里显示。
和 #2 的关系
两件事,不要合。#2 是「模型可能不知道」的东西(时效、长尾),难点在触发,而且要配一把 key;
这件是「模型不可能知道」的东西,难点只在工程,而且第 1 档连网络都不用。这件先做。
一句话
「我昨天走路 1000 步,是不是外面下雨了」——这句话模型答不了,而且缺的两样东西都是
结构性不可知:它不知道用户在哪,也不可能知道昨天那个地方下没下雨。
为什么这件事比通用搜索(#2)更该先做
#2 里定了一条判据:一个工具要占一个槽,必须返回模型没有的东西。环境数据是这条判据下
最干净的一例——不是「模型可能不知道」,是不可能知道。
更要紧的是它绕开了 #2 里标为「难的那一半」的问题。通用搜索难在模型判断不出自己知不知道,
所以要靠 app 侧的可观察信号去触发;环境数据不需要这层判断,它和 HealthKit 数据一样是结构性
缺失,工具描述可以写得和
daily_steps一样具体:问某几天的活动量/睡眠为什么反常时,查那几天的环境。没有模糊地带。
而它接的正是 Vana 的本职工作。这个 app 做的就是解释健康数据里的异常,而天气和空气质量是
最常见的解释变量之一:下雨 → 步数掉;高温 → 睡眠差、静息心率抬高;AQI 差 → 户外锻炼停了。
现在这些它一个都够不着,只能说「你这几天活动量下降了,注意保持」——一句正确的废话。
分三档,越靠前越便宜
逐条在 iOS 26.5 SDK 里核过(
WeatherKit.swiftinterface、HKMetadata.h、HKTypeIdentifiers.h)。第 1 档:HealthKit 里已经有的(零新权限、零网络、零依赖)
这一档先做,而且它可能已经够回答开头那个问题了。
HKQuantityTypeIdentifierTimeInDaylight(iOS 17+,分钟,累计)——在户外待了多久。不需要定位、不需要联网,而且它测的正是「他有没有出门」这件事本身。「昨天走 1000 步」配上
「昨天户外 8 分钟」,话基本就说完了,连天气都还没查。
HKMetadataKeyWeatherCondition/WeatherTemperature/WeatherHumidity(iOS 10+)——挂在 HKWorkout 上,是 Watch 在那次户外锻炼当时当地记的。这是真值,不是按今天的
位置反推的,正好补掉下面第 2 档的主要误差源。
HKQuantityTypeIdentifierUVExposure(iOS 9+),覆盖率存疑,顺手看一眼。覆盖面窄(只有户外锻炼那几个时刻 + 每天的户外时长),但准,而且成本几乎为零——
现有的
HealthStore加两个查询就行,连project.yml都不用动。第 2 档:WeatherKit(天气全覆盖,含历史,不需要用户填 key)
顶掉原方案里的 Open-Meteo 天气部分。 之前那版是在不知道 WeatherKit 能查历史的前提下写的。
WeatherQuery.daily(startDate:endDate:)/.hourly(startDate:endDate:)。DayWeather给:condition、最高/最低温(iOS 18+ 还给出现时刻)、最大/最小湿度、precipitationAmountByType(雨雪分开)、precipitationChance、uvIndex、能见度、风、sun日出日落。app 里直接用 Swift API。这比 Open-Meteo 还省——省掉了设置页那一栏。
三个前提条件,做之前先确认:
project.yml要改)。链接。这对 UI 有实际影响——结果面板里要留位置。天气预警还有额外要求(不许改文案),
但这个功能用不到预警。
xcodebuild流程可能要调。这条最可能卡住,先验证再往下做。
「降水小时数」WeatherKit 没有直接给,但
.hourly(startDate:endDate:)自己数一遍就有。这个字段要保留——见下面「口径」。
第 3 档:Open-Meteo(只补空气质量)
Apple 完全不给 AQI 和花粉(在
WeatherKit.swiftinterface里 grepairquality|pollen|aqi| particulate,零命中)。这是唯一必须走第三方的部分。past_days支持 0–92 天。Only available in Europe during pollen season)。所以第一版没有花粉,过敏那条线要么另找源,要么不做。
(原方案里「历史天气不能走 Archive API,ERA5 延迟 5 天」那条坑,随着天气改走 WeatherKit
已经作废;空气质量走
past_days,不受影响。留在这里只是记一笔,免得有人回头去接 Archive。)口径:按天聚合,对齐健康工具
工具返回的形状照
daily_steps/sleep_summary——逐日一行,这样模型能直接和步数并排着读。走
HealthReport,面板里可点(逐小时序列只进面板,不进modelText,同现有约定)。「下雨了吗」的正确口径是降水小时数,不是天气类型。 一天里下两小时和下一整天,对步数的
影响完全是两回事;只给一个「雨」字,模型会把两者说成同一件事。所以即使 WeatherKit 没这个
字段,也要从 hourly 自己数出来。
只给数据,不做归因。 不要在 app 侧预先算「步数和降水的相关性」——模型看到「那天下了 8
小时雨、户外 8 分钟、步数 1000」自己就会说出来,而 app 算出来的一个相关系数既不好解释也
容易骗人。同
MEDICATIONS.md里不做交互作用数据库的理由。位置与隐私
第 1 档不需要位置。第 2、3 档需要:
requestWhenInUseAuthorization+ reduced accuracy。「08-08 雨 8 小时、最高 19°C」。这条要写死——它让「读位置」在隐私上便宜得多,
和「HealthKit 只读、只上传聚合值」是同一条线。
——
timeInDaylight和 workout 上的天气 metadata 不需要定位,这时候正好顶上。是当时当地的真值,两者冲突时以 workout 上的为准。仍然要在输出里说明这个假设
(「按当前所在地查」),别让模型把它当成确定的事讲。
开关与缓存
memoryEnabled底下(同medicationsEnabled的理由)。「不缓存检索结果」不冲突——那是成篇的正文,这是每天几个数。
任务拆分(草稿)
HealthStore加timeInDaylight按天查询 + 从HKWorkoutmetadata 里取当时的天气。挂进现有的
workouts/ 新增daily_daylight,或者合成一个daily_environment。形状定下来之前先跑一遍种子数据,看模拟器上这两项到底有没有值——
timeInDaylight要Watch 才记得全,覆盖率可能很差,那样这一档的价值就得重估。
先做,别等到 T3 才发现跑不起来。
HealthChat/Environment/:粗定位 + WeatherKit 历史查询,按天聚合,降水小时数从 hourly 数。attribution 的 UI 位置一并留出。
past_days。daily_environment),输出HealthReport,挂进CapabilityRegistry.healthChat,受新开关管。工具描述收在「解释某几天的健康数据为什么反常」。cloud.sun);设置页开关;定位未授权的空态文案;系统提示补一段(照着 registry 里有没有这个工具拼,同现有做法)。
验收
和 #2 的关系
两件事,不要合。#2 是「模型可能不知道」的东西(时效、长尾),难点在触发,而且要配一把 key;
这件是「模型不可能知道」的东西,难点只在工程,而且第 1 档连网络都不用。这件先做。