Skip to content

环境数据:天气、户外时长与空气质量,解释健康数据里的异常 #3

Description

@zjywill

一句话

「我昨天走路 1000 步,是不是外面下雨了」——这句话模型答不了,而且缺的两样东西都是
结构性不可知:它不知道用户在哪,也不可能知道昨天那个地方下没下雨。

为什么这件事比通用搜索(#2)更该先做

#2 里定了一条判据:一个工具要占一个槽,必须返回模型没有的东西。环境数据是这条判据下
最干净的一例——不是「模型可能不知道」,是不可能知道

更要紧的是它绕开了 #2 里标为「难的那一半」的问题。通用搜索难在模型判断不出自己知不知道,
所以要靠 app 侧的可观察信号去触发;环境数据不需要这层判断,它和 HealthKit 数据一样是结构性
缺失,工具描述可以写得和 daily_steps 一样具体:问某几天的活动量/睡眠为什么反常时,查那
几天的环境
。没有模糊地带。

而它接的正是 Vana 的本职工作。这个 app 做的就是解释健康数据里的异常,而天气和空气质量是
最常见的解释变量之一:下雨 → 步数掉;高温 → 睡眠差、静息心率抬高;AQI 差 → 户外锻炼停了。
现在这些它一个都够不着,只能说「你这几天活动量下降了,注意保持」——一句正确的废话。

分三档,越靠前越便宜

逐条在 iOS 26.5 SDK 里核过(WeatherKit.swiftinterfaceHKMetadata.hHKTypeIdentifiers.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(雨雪分开)、precipitationChanceuvIndex、能见度、风、
    sun 日出日落。
  • 没有用户要填的 key:绑 bundle id,在 Certificates & Identifiers 里配 Service ID + key,
    app 里直接用 Swift API。这比 Open-Meteo 还省——省掉了设置页那一栏。
  • 额度:500,000 次/月随 Apple Developer Program 会员,不滚存。这个 app 一天用不了几次。

三个前提条件,做之前先确认:

  1. 要 Apple Developer Program 付费会员,并且要开 WeatherKit capability(project.yml 要改)。
  2. attribution 是硬要求,不是建议:必须显示 Apple Weather 商标和一个指向数据来源页的法律
    链接。这对 UI 有实际影响——结果面板里要留位置。天气预警还有额外要求(不许改文案),
    但这个功能用不到预警。
  3. 模拟器上要签名才能跑 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 档)HealthStoretimeInDaylight 按天查询 + 从 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 档连网络都不用。这件先做。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions