```
docs(health-profile): 添加防编造加固修订记录到导出健康档案设计文档 补充了关于导出摘要出现虚构病例问题的详细分析和修复方案, 包括检索策略优化、空数据兜底处理和prompt重写等三层防护措施。 ```
This commit is contained in:
@@ -413,3 +413,18 @@ Output:
|
||||
| **合计** | **~14h ≈ 2 个工作日** |
|
||||
|
||||
也是 W3「AskService 基础 RAG」的前置铺路工作,工程上一举两得。
|
||||
|
||||
---
|
||||
|
||||
## 14. 修订记录:防编造加固(2026-05-30)
|
||||
|
||||
**现象**:导出摘要出现整份虚构病例(疲劳/盗汗/血红蛋白98/阿司匹林…),不符任何真实记录。
|
||||
|
||||
**根因(双重)**:① §数据范围里「Diary 由关键词过滤后入 prompt」在泛化请求(无症状词,如「最近身体异常」)下把日记**全部清空** → 真实记录没进 prompt;② 数据稀疏时,1.7B 在固定 6 段模板上**凭训练先验脑补**完整病例(对「只用数据/缺失写无记录」这类约束遵循差)。
|
||||
|
||||
**修复(三层,客户端硬保证为主)**:
|
||||
1. **检索**:`retrieve` 改为——有症状词→按词过滤(保留隐私);无症状词→纳入时间窗内最近 5 条日记,确保真实记录进 prompt。
|
||||
2. **空数据硬兜底**:`isEffectivelyEmpty` 判定无任何记录且 profile 空时,**跳过 LLM**,用 `fallbackReport` 产出确定性「6 段全无记录、主诉仅照搬原话」的摘要,从根上杜绝空数据编造。
|
||||
3. **prompt 重写**:从「撰写」改为「抽取/搬运」框架;反编造铁律首尾各一遍;加一条**稀疏 few-shot** 教模型「缺失写无记录、数值原样照搬」。
|
||||
|
||||
**残留限制**:部分数据(如仅 1 条日记)仍走 LLM,强约束 + few-shot 大幅降低但不能 100% 杜绝小模型臆造;后续可加生成后数值校验。
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
| 决策点 | 选择 |
|
||||
|---|---|
|
||||
| 模型 | 新建独立 `CustomReminder` @Model,不动现有 `MetricReminder` |
|
||||
| 周期粒度 | 每天 / 每周选几天(复用 weekday 约定,覆盖示例)。不做间隔/按月/一次性 |
|
||||
| 周期粒度 | **每日 / 每周选几天 / 每月某日 / 每年某月某日**(2026-05-30 用户反转原「不做按月/按年」决策)。仍不做「每 N 天间隔」/一次性 |
|
||||
| 时间选择 | 常用时间快捷预设(8:00/12:00/18:00/22:00 chip)+ 保留 `DatePicker` 精调 |
|
||||
| 入口 | 新建 → 开启一个提醒 → `RemindersListView`(提醒中心),顶部「+ 新建提醒」打开编辑 sheet |
|
||||
| 列表范围 | 自由提醒 + 指标提醒**合展**(上次删了「我的」入口,指标提醒也只能从这里管) |
|
||||
| 量词(5公里/2片) | 写在自由文本 `title` 里,不单设字段 |
|
||||
@@ -32,20 +33,34 @@
|
||||
|
||||
```swift
|
||||
@Model final class CustomReminder {
|
||||
enum Frequency: String { case daily, weekly, monthly, yearly } // 嵌套枚举
|
||||
@Attribute(.unique) var id: UUID
|
||||
var title: String // 用户文案:"跑步5公里"
|
||||
var note: String // 可选备注 → 通知正文
|
||||
var hour: Int // 0...23
|
||||
var minute: Int // 0...59
|
||||
var weekdays: [Int] // 1=日…7=六,全 7 = 每天(复用 MetricReminder 约定)
|
||||
var title: String // 用户文案:"跑步5公里"
|
||||
var note: String // 可选备注 → 通知正文
|
||||
var hour: Int // 0...23
|
||||
var minute: Int // 0...59
|
||||
var weekdays: [Int] // 1=日…7=六,仅 weekly 用(复用 MetricReminder 约定)
|
||||
var frequencyRaw: String = "daily" // Frequency 原始值(内联默认 → 走轻量迁移)
|
||||
var dayOfMonth: Int = 1 // monthly / yearly 用,1...31
|
||||
var month: Int = 1 // yearly 用,1...12
|
||||
var enabled: Bool
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
// computed: isEveryDay / frequencyLabel / timeLabel(与 MetricReminder 同款,复用同一批本地化 key)
|
||||
// computed: frequency(get/set 包 frequencyRaw)/ isEveryDay / frequencyLabel(分档)/ timeLabel
|
||||
}
|
||||
```
|
||||
|
||||
Schema 注册:`App/KangkangApp.swift` 加 `CustomReminder.self`(additive 变更,无需迁移)。
|
||||
Schema 已含 `CustomReminder.self`。**本轮只给已存在的 `CustomReminder` 加 3 个带内联默认值的属性 → SwiftData 自动轻量迁移,不触发删库兜底(见 §10)。**
|
||||
|
||||
四档语义 → iOS `UNCalendarNotificationTrigger(repeats:true)`:
|
||||
| 频率 | DateComponents | 通知数 | id 后缀 |
|
||||
|---|---|---|---|
|
||||
| daily | hour,minute | 1 | `.daily` |
|
||||
| weekly | hour,minute,weekday ×N | N | `.w<weekday>` |
|
||||
| monthly | day,hour,minute | 1 | `.monthly` |
|
||||
| yearly | month,day,hour,minute | 1 | `.yearly` |
|
||||
|
||||
边界:iOS 重复触发**不顺延**。monthly 选 29/30/31 → 无此日的月份跳过(UI 给浅色提示);yearly 的「日」选项按所选月份最大天数动态收口(避免「4月31日」永不触发),仅闰年 2/29 给提示。
|
||||
|
||||
---
|
||||
|
||||
@@ -95,12 +110,12 @@ static func sync(_ metric: MetricReminder) async // 现有,内部改走共享
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `Models/Models.swift` | +`CustomReminder` |
|
||||
| `App/KangkangApp.swift` | schema +`CustomReminder.self` |
|
||||
| `Services/ReminderService.swift` | 泛化共享核心 + custom sync/cancel |
|
||||
| `Features/Me/CustomReminderEditSheet.swift` | **新增** 编辑表单 |
|
||||
| `Features/Me/RemindersListView.swift` | 提醒中心:新建按钮 + 合展两类 |
|
||||
| `Localizable.xcstrings` | 新增文案四语言 |
|
||||
| `Models/Models.swift` | `CustomReminder` +`Frequency` 枚举 +`frequencyRaw/dayOfMonth/month`(均带内联默认)+ 分档 `frequencyLabel` |
|
||||
| `App/KangkangApp.swift` | **持久化兜底改造**:迁移失败时由「删库」改为「挪到 `StoreBackups/<时间戳>/` 再重建」(见 §10) |
|
||||
| `Services/ReminderService.swift` | 调度核心泛化为 `Slot(suffix,DateComponents)` 列表;custom sync 按 frequency 分档;`cancelBase` 覆盖 daily/monthly/yearly/w1-7 |
|
||||
| `Features/Me/CustomReminderEditSheet.swift` | 频率分段 Picker + 各档子控件(周几 / 日 / 月+日)+ 时间快捷预设行 |
|
||||
| `Features/Me/RemindersListView.swift` | 不变(`frequencyLabel` 来自模型) |
|
||||
| `Localizable.xcstrings` | 新增 11 个 key × en/ja/ko |
|
||||
|
||||
---
|
||||
|
||||
@@ -114,4 +129,18 @@ static func sync(_ metric: MetricReminder) async // 现有,内部改走共享
|
||||
|
||||
## 9. 验收(真机)
|
||||
|
||||
① 新建「每天 20:00 跑步 5 公里」→ 列表出现 → 到点收到本地通知(标题=跑步5公里);② 改时间/周几即时重排;③ 关闭 Toggle 取消通知;④ 删除清除 pending;⑤ 切换语言后固定文案随之变化(用户输入文案不变);⑥ 指标提醒仍在同一列表可管。
|
||||
① 新建「每天 20:00 跑步 5 公里」→ 列表出现 → 到点收到本地通知(标题=跑步5公里);② 改时间/周几即时重排;③ 关闭 Toggle 取消通知;④ 删除清除 pending;⑤ 切换语言后固定文案随之变化(用户输入文案不变);⑥ 指标提醒仍在同一列表可管;⑦ **每月/每年**:切频率后子控件随之变化,边界提示出现;改频率后旧档 pending 通知被清掉(不留孤儿);⑧ **时间预设**:点 8:00/12:00/18:00/22:00 即填,精调仍可用。
|
||||
|
||||
---
|
||||
|
||||
## 10. 顺带修复:重打包数据丢失(根因 + 方案)
|
||||
|
||||
**问题**:Demo 期每次改 schema 重打包,SwiftData 数据被清空。
|
||||
|
||||
**根因(单点)**:`App/KangkangApp.swift` 的 `ModelContainer` 创建 catch 块**直接删 store 文件**。SwiftData 只对**纯增量**改动自动轻量迁移;一旦某次改动超纲(最常见:给已存在的 `@Model` 新增「非可选且无内联默认值」的属性),自动迁移抛错 → 落入 catch → 删库。W2 几乎每次都在改 schema,故体感「每次都丢」。
|
||||
|
||||
**方案(两层)**:
|
||||
1. **治本**:新增 `@Model` 属性一律「可选」或「内联默认值」(本轮 3 个新字段都给了 `= "daily"` / `= 1`)→ 走轻量迁移、不进 catch、数据保留。
|
||||
2. **兜底**:catch 不再删库,改为把旧 store(含 `-wal`/`-shm`)**挪到 `Application Support/StoreBackups/<时间戳>/`** 再重建——App 仍能启动,旧数据可手动恢复;挪不动才降级删除。
|
||||
|
||||
⚠️ 正式发布前仍应升级为 `VersionedSchema` + `SchemaMigrationPlan` 的正式迁移(注释已就地标注)。
|
||||
|
||||
Reference in New Issue
Block a user