Files
memind/docs/memind-health-architecture.md
T
john f96fdcb3b9 feat(health): add P0 health channel kernel and encrypted health zone.
Lock health recording behind confirm-only state machines, reject public publishes for the health category, and ship the 14-day experiment Page Data templates with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:04:26 +08:00

51 KiB
Raw Blame History

MeMind Health 架构与通道设计

日期:2026-09-02 状态:架构设计稿。未改动业务代码、未碰生产、未读写用户数据。 适用范围:MeMind H5、微信服务号、MindSpace、Page Data、Memory V2、Agent Gateway


0. 一句话定位

MeMind Health 不是「另一个健康 App」,而是:

持续理解本人健康基线,并在长期变化中发现「偏离」的个人健康 Agent。

核心数据链路:

本人 → 录入/设备/报告 → Health Timeline → Personal Baseline
     → 偏离事件 → Agent 推理 → 行动建议 → 医疗资料沉淀

与传统健康产品的根本差异:

传统健康 App MeMind Health
140/90 → 标红 该用户 90 天基线 119126/7077,近 14 天持续漂移 +8%
数据看板 「最近和以前不一样」的事件叙事
用户自己盯曲线 只在偏离时介入,无事静默
单次录入 时间序列 + 关联假设 + 长期学习

MVP 验证命题

MeMind 能不能比我自己更早发现:我最近和以前不太一样?

若成立,血压计只是传感器;Personal Baseline + Health Timeline + Event Engine 才是产品。


1. 已确认的产品决策(不可随意推翻)

以下决策已确认,后续实现必须遵守。变更需重新评审本文档。

D1 单人模型:只监测账号本人

  • 监测对象 默认且仅为账号本人HealthProfileuser_id 严格 1:1。
  • 不做 FamilyGroup / 多 Subject / 子女代录代管。
  • 理由:录入标准、测量情境、回顾深度必须统一,否则 Personal Baseline 失真(「参差不齐」)。
  • 未来若做「关注家人」,只能是 本人主动授权只读分享摘要P4),录入权永远在本人。

D2 他人健康数据走辅轨,不进引擎

用户说「我妈上周体检血压 150/95」这类内容:

  • 写入 Memory V2fact / experience)或普通备忘,Agent 可对话召回。
  • 禁止进入 HealthObservation、Baseline、Trend、Event Engine。
  • Agent 话术须明确告知:「这不会进入你的健康分析」。

D3 存储走 Page DataP0 决策)

  • 结构化健康数值存入 用户隔离的 PostgreSQL schemaPage Data 体系,private_data_execute 建表 + private_data_register_dataset 注册)。
  • 复用现成的用户级 schema 隔离、dataset 授权、操作日志、导出、软删除恢复能力。
  • P1 若基线/趋势聚合出现性能瓶颈,再评估迁移到专用表;迁移方案见 §12.3。

D4 服务号允许规则命中 + 强制确认

  • 自由文本先走确定性规则(正则/关键词);命中即进入对应流程。
  • 规则未命中 → 回交互式菜单,不让 LLM 硬猜
  • 所有由识别/解析得到的数值,一律经用户确认后才落库

D5 健康会话完全独立

  • 健康会话与普通聊天会话完全隔离(独立 session、独立工具白名单)。
  • 普通聊天中识别到健康意图,只做引导跳转,给入口链接/按钮。
  • 绝不在普通聊天里直接落库健康数据。

2. 四条不可违背的工程原则

  1. 健康通道是「锁定态会话」,不是「聊天里聊健康」。进入后 skill 被 pin,意图分类器不再参与判断。
  2. 拍照/上传不直接落库,一律「抽取 → 确认 → 提交」三段式。未确认数据不进基线引擎。
  3. 意图不明确时不猜,直接给选项。 菜单式交互在健康场景比自然语言理解更精准、更快、更可审计。
  4. 健康区数据默认加密,无口令不允许发布。 服务端硬阻断,不靠前端自觉。

配套的分层职责:

规则判断事实 → Rule Engine(确定性、可审计)
算法发现趋势 → Trend Engine(统计、可复现)
LLM 负责解释 → Agent(不可作为 primary detector

禁止的反模式:血压 128/85 → LLM → "你觉得危险吗?"


3. 系统总览

                    MeMind Health(本人)
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
  Health Connector    Health Timeline    Medical Documents
  (P2: Apple/蓝牙)     (结构化时序)        (报告 OCR + 归档)
        │                   │                   │
        └───────────────────┼───────────────────┘
                            ↓
                  Personal Baseline Engine
                            ↓
        ┌───────────────────┴───────────────────┐
        ↓                                       ↓
   Rule Engine                            Trend Engine
  (医学阈值 / 漏测 / 药物变化)              (个体偏移 / 变异性 / 联动)
        │                                       │
        └───────────────────┬───────────────────┘
                            ↓
                      Event Engine
                  (Health Event Cluster)
                            ↓
                MeMind Agent(解释 + 追问 + 建议)
                            ↓
        提醒 / 复测 / 就医建议 / 就诊摘要 / Memory 沉淀

3.1 与现有 MeMind 能力的映射

设计层 复用的现有能力 关键文件
通道锁定不漂移 selectedChatSkill 短路 + enforceSelectedSkillRuntime chat-intent-router.mjsagent-run-routes.mjs
结构化存储 Page Data 用户隔离 PG schema page-data-service.mjsmindspace-userdata-postgres.mjs
健康区与发布策略 h5_space_categoriesvisibility/ai_access/publish_policy mindspace.mjsSYSTEM_CATEGORIES
加密发布与口令 发布 access_mode + password_hash mindspace-pages.mjspage-data-public-service.mjs
私有页不被收录 INDEXABLE_ACCESS_MODE = 'public',非 public 一律 noindex mindspace-index-policy.mjs
报告原文件存储 MindSpace 资产(h5_assets+ 分区归属 mindspace.mjs
服务号图文/语音接入 msgType 分流 + eventKey 菜单事件 wechat-mp.mjs
图片轮次隔离 带图轮次摘掉 read_image、污染触发会话轮换 chat-image-turn-scope.mjs
用药/复诊提醒 h5_schedule_items + schedule-assistant schedule-service.mjs
长期关联假设 Memory V2 candidate → promote memory-v2.mjsmemory-v2-lifecycle.mjs
灰度 / fail-open 现有 canary + fail-open 模式 memory-v2-runtime.mjs

3.2 需要新增的模块(隔离在健康域内)

health-channel-state.mjs        通道状态机(纯函数,可单测)
health-observation-service.mjs  Observation 读写 + 合理性校验 + 幂等去重
health-extraction.mjs           图片/报告结构化抽取 + confidence 归一
health-baseline-engine.mjs      滚动基线计算(P1
health-event-engine.mjs         Rule + Trend + Event 聚合(P1
health-document-service.mjs     报告归档 + 指标映射
health-share-service.mjs        就诊摘要快照与令牌(P3
skills/health-assistant/        skill 定义
src/pages/health/*              H5 健康入口页面

4. 入口设计:H5 主入口 + 服务号轻入口

两个入口共用同一个 Health Channel 内核,只是壳不同。

                 ┌──────────────────────────┐
   H5 首页 ──────→│ 健康助手入口(卡片 / Tab) │
                 └────────────┬─────────────┘
                              │
  服务号菜单 / 关键词 ─────────→│
  「健康助手」                  │
                              ↓
                 ┌────────────────────────────┐
                 │   Health Channel 内核       │
                 │  (skill pin + 状态机)       │
                 └────────────┬───────────────┘
                              ↓
        ┌────────────┬────────┴───────┬────────────┐
        ↓            ↓                ↓            ↓
    健康录入      健康评估         报告归档      健康档案
   (值/照片)    (基线/趋势)      (OCR→档案)   (MindSpace 健康区)

4.1 H5 主入口(功能完整,主战场)

H5 是主入口,因为需要结构化表单、数值键盘、Timeline 图表、可编辑确认卡、档案浏览。

建议做成独立页面而非普通聊天里的一个 skill 按钮:

/health              健康首页(今日录入状态 + 最近趋势 + 待确认项)
/health/chat         健康助手会话(锁定通道)
/health/records      Health Timeline
/health/documents    报告档案
/health/profile      基线 / 既往史 / 长期用药

/health/chat 使用独立会话空间,不与普通聊天会话混用(见 §7)。

4.2 服务号轻入口(随手录入,主打拍照/语音)

服务号的价值是随手:量完血压直接拍照发送。

进入方式两条,均有现成钩子:

  • 自定义菜单点击msgType === 'event' + eventKey === 'MEMIND_HEALTH'
  • 关键词命中 → 文本命中「健康助手」「健康」「录血压」等

进入后回复固定菜单(服务号仅文本/图文,用数字选择):

已进入【健康助手】专用通道
本通道只处理健康相关内容,数据自动归入你的健康档案。

请回复数字:
1  健康录入(血压/心率/体重/血氧/体温/睡眠/症状)
2  上传报告(直接发送照片即可)
3  健康评估(看看最近和平时比有什么变化)
4  查看健康档案
0  退出健康通道

关键约束:

  • 进入后所有消息(含图片)只走健康链路,直到用户回复 0 或超时。
  • 建议 30 分钟无交互自动退出,退出时必须明确告知,避免用户以为还在通道内。
  • 通道状态与现有 canary/stable 会话钉选逻辑并存,不改动现有分流

5. 防漂移三层保障

这是本设计的核心,对应用户最关心的「不允许漂移,要精准」。

5.1 第一层:Skill Pin(复用现有短路)

现有 chat-intent-router.mjs 已具备完整短路能力:

  • readSelectedChatSkillId(message) 读取 metadata.memindRun.selectedChatSkill
  • buildSkillSelectionClassification() 返回 route: agent_orchestrationconfidence: 1reason: '用户已选择 skill'
  • 结果:LLM 意图分类器根本不会被调用

健康通道发消息时携带:

{
  "metadata": {
    "memindRun": {
      "selectedChatSkill": "health-assistant",
      "healthChannel": {
        "sessionKind": "health",
        "step": "await_value",
        "pendingDraftId": "drf_...",
        "metricSet": "blood_pressure"
      }
    }
  }
}

服务号侧从会话状态读出「当前在健康通道」,注入相同 metadata。

agent-run-routes.mjsenforceSelectedSkillRuntime 进一步固定 toolMode / taskType / requiredExecutor,保证工具集不越界。

这一层解决「会不会被路由到别的 skill」。

5.2 第二层:Channel 状态机(解决「这句话是什么意思」)

Pin 住 skill 不够 —— 用户说「137」,是血压还是体重?必须有显式状态机,不靠 LLM 猜。

IDLE
 │  选择「健康录入」
 ↓
AWAIT_METRIC_TYPE ──→ 展示可录入项(血压/心率/体重/血氧/体温/睡眠/症状/用药)
 │  选择「血压」
 ↓
AWAIT_CONTEXT ──────→ 晨起 / 睡前 / 服药后 / 其他(可跳过取默认)
 │
 ↓
AWAIT_VALUE ────────→ 期望数值;接受手输 / 拍照 / 语音
 │
 ↓
CONFIRM ────────────→ 结构化确认卡(可编辑)
 │  确认
 ↓
COMMITTED ──────────→ 写入 Observation + 返回 Timeline 即时反馈

完整状态转移表:

当前状态 期望输入 事件 下一状态
IDLE 菜单选择 / 规则命中文本 / 图片 select_action 对应流程首状态
IDLE 无法高置信解析 ambiguous AWAIT_ACTION_CHOICE
AWAIT_ACTION_CHOICE 1/2/3/4 choose 对应流程首状态
AWAIT_METRIC_TYPE 指标名 pick_metric AWAIT_CONTEXT
AWAIT_CONTEXT 情境 / 跳过 pick_context AWAIT_VALUE
AWAIT_VALUE 数值 / 图片 / 语音 submit_value CONFIRM
AWAIT_VALUE 无关输入 unexpected AWAIT_VALUE(回提示,不猜)
CONFIRM 确认 commit COMMITTED
CONFIRM 修改 revise AWAIT_VALUE
CONFIRM 放弃 discard IDLE
任意 0 / 「退出」 exit 通道退出
任意 30 分钟无交互 timeout 通道退出(告知用户)

状态机每一步都有明确的期望输入类型。用户在 AWAIT_VALUE 说「137/82」语义完全确定,不存在歧义。

若在某状态收到完全无关输入,通道不猜测,回:

当前正在录入「血压」。
- 直接发送数值,如 137/82
- 或拍照上传血压计读数
- 回复「取消」放弃本次录入
- 回复「0」退出健康通道

状态机应实现为纯函数health-channel-state.mjs),输入 (state, event) 输出 (nextState, actions),便于完整单测覆盖。

5.3 第三层:意图不明时强制选择(D4)

IDLE 状态自由发言的判定顺序,确定性优先

1. 确定性规则匹配(正则 / 关键词)→ 命中即执行,不调 LLM
2. 规则未命中 → LLM 做候选动作分类,要求返回 top-2 + confidence
3. confidence < 0.75 或 top-2 差距 < 0.15 → 不执行,返回选择卡
4. 用户选择后 → 进入状态机,此后无歧义

选择卡内容:

我不太确定你想做什么,请选择:
1  记录健康数据
2  查看健康评估
3  上传 / 归档报告
4  查看健康档案

规则库设计(示例,需持续维护并单测):

意图 规则示例 落地动作
血压录入 (\d{2,3})\s*[/]\s*(\d{2,3}),或含「血压」+ 两个数 metric_set=blood_pressureCONFIRM
体重录入 含「体重」+ (\d{2,3}(\.\d)?) metric_set=weight
心率录入 含「心率/脉搏」+ (\d{2,3}) metric_set=heart_rate
血氧 含「血氧/SpO2」+ (\d{2,3}) metric_set=spo2
体温 含「体温/发烧」+ (3\d(\.\d)?) metric_set=temperature
睡眠 含「睡了」+ 时长表达 metric_set=sleep
症状 含「头晕/胸闷/乏力/心慌/疼」 metric_set=symptom
评估 「最近怎么样」「帮我看看」「有没有异常」 评估流
档案 「我的档案」「历史记录」「报告」 档案流

关键原则:宁可多问一次,也不要错记一条。错误的 Observation 会污染 Personal Baseline,代价远高于多一次交互。

歧义值必须强制确认,例如「体重 137」显然异常但格式合法 → 走合理性校验(§6.2)标记 suspect


6. 拍照精准记录设计

最易漂移的环节,单独设计。

6.1 先分清两类图片

类型 内容 归属
设备读数照 血压计、体重秤、血氧仪、体温计屏幕 HealthObservation(数值)
医疗报告 体检报告、化验单、影像报告、处方 HealthDocument(档案)+ 抽取指标

判定策略分层,成本递增:

  1. 用户已在 AWAIT_VALUE(先选「血压」再拍照)→ 类型已确定,直接按血压解析。这是最精准路径,产品应主推。
  2. IDLE 直接发图 → 轻量视觉判断「设备屏幕 vs 文档」,然后回确认:「这看起来是血压计读数,要记录为血压吗?」
  3. 判断不确定 → 直接给选项:「这张图是:1 设备读数 2 体检/化验报告 3 其他」

产品引导上把「先选类型再拍」做成主路径:

健康录入 → 选「血压」 → 「请输入数值或拍照上传」 → 拍照

先验极强,几乎不可能错。

6.2 设备读数:抽取 → 确认 → 提交

绝不允许「拍照即入库」。

图片
 ↓
① 图片指纹(内容 hash)去重          ← 防止同一照片重复录入
 ↓
② 结构化抽取(视觉模型 + 强约束 schema)
 ↓
③ 合理性校验(Rule Engine,确定性)
 ↓
④ 生成 ObservationDraft(草稿态,不进基线)
 ↓
⑤ 用户确认卡(H5 可编辑表单;服务号文本 + 回复确认)
 ↓
⑥ 确认后 → HealthObservation(正式)+ 原图存档 + 抽取留痕

第 ② 步要求模型输出严格 schema:

{
  "metric_set": "blood_pressure",
  "systolic": 137,
  "diastolic": 82,
  "pulse": 72,
  "device_time": "2026-09-02 07:20",
  "confidence": { "systolic": 0.97, "diastolic": 0.95, "pulse": 0.88 },
  "unreadable": []
}

第 ③ 步校验规则(确定性,不经 LLM):

校验 规则 不通过处理
量程 收缩压 60–260;舒张压 40160;脉搏 30200SpO₂ 50100;体温 3343 标记 unreadable,要求重拍或手输
逻辑 收缩压 > 舒张压 提示可能识别颠倒,要求确认
基线偏离 与个人基线偏离 > 50% 标记 suspect强制人工确认
置信度 任一字段 confidence < 0.9 该字段标记待确认,高亮展示

第 ⑥ 步的留痕至关重要:raw_payload 保留原始抽取 JSON、图片引用、用户是否修改过。将来审计「这条 137 是怎么来的」可完整回溯,也是 Event Engine evidence chain 的基础。

6.3 时间戳:隐藏的漂移源

「什么时候测的」与「值是多少」同样重要,且极易出错。优先级:

1. 设备屏幕显示时间(OCR 得到)   ← 最准
2. 图片 EXIF 拍摄时间             ← 次准
3. 用户手动指定                   ← 兜底
4. 消息到达时间                   ← 最后手段,必须提示用户

若只能用第 4 种,确认卡必须明确写:「测量时间按当前时间记录,如非刚测请修改」。

必须支持补录改时间 —— 否则 Timeline 全错,基线随之失真。

6.4 报告类:归档与指标抽取分离

报告图片 / PDF
 ↓
① 归档(必做,成功率高)
   → HealthDocument:类型、机构、报告日期、原文件、OCR 全文
   → 原文件落入 MindSpace 健康分区资产
 ↓
② 指标抽取(尽力而为,允许失败)
   → 化验项映射:血脂 / 血糖 / 肝功 / 肾功 / 血常规 ...
   → 每项带 confidence + 参考范围
   → 高置信项 → ObservationDraft(待确认)
   → 低置信项 → 仅存 OCR 文本,不入结构化

归档必须成功,抽取允许失败。 即使指标识别不出,用户报告也已安全存进档案随时可查 —— 这是产品底线价值。

6.5 与现有视觉守卫的兼容(硬约束)

docs/regression-guards/vision-turn-read-image-isolation.md 的守卫必须继续生效

  • 带图轮次必须摘掉 read_image
  • 工具图片污染必须触发会话轮换

健康通道的图片处理必须走同一套 chat-image-turn-scope.mjs禁止另开绕过守卫的路径。健康通道只是在其之上叠加「结构化 schema 约束 + 确认闸门」,底层图片轮次隔离不变。


7. 会话隔离(D5

「不影响现有功能」的关键。

普通会话                          健康会话
─────────                        ─────────
sessionKind: 'chat'              sessionKind: 'health'
完整 skill 集                     仅 health-assistant + 白名单工具
Memory V2 全量 resolve            健康上下文 + 有限 profile
可发布任意分区页面                 仅健康区(强制加密)

具体隔离点:

  1. 会话独立:健康会话使用独立 session_id 命名空间。普通聊天历史不注入健康会话;健康数据不会出现在普通聊天上下文。
  2. 工具白名单:健康会话内由 enforceSelectedSkillRuntime 限定工具集 —— 仅允许健康数据读写、文档归档、健康区页面生成。不允许 edit_file 写非健康区路径,不允许公开发布。
  3. Memory 分轨Observation 走独立数据面;仅「本人对睡眠敏感」这类关联假设晋升 Memory V2,带 health_correlation 标记。他人健康数据只进备忘(D2)。
  4. 引导跳转:普通聊天识别到健康意图时,只回引导,不落库:
你想记录健康数据吗?健康数据需要在健康助手通道里录入,
这样才能保证记录准确并纳入你的健康基线分析。

[进入健康助手 →]
  1. 服务号状态:健康通道状态存于服务号会话状态,与现有 canary/stable 钉选并存。

8. 数据模型(Page Data 落地,D3

8.1 分层存储决策

数据 存储 理由
健康数值(Observation Page Data 用户隔离 PG schema 结构化、可聚合、已有隔离与审计
报告原文件(图片/PDF MindSpace 健康分区资产(h5_assets Page Data 不适合大文件
报告元数据 + OCR 全文 Page DataPG 需检索与关联
基线 / 事件 Page DataPG),P1 由引擎写入
关联假设 Memory V2 需对话召回
提醒 / 复诊 h5_schedule_items 复用现有提醒链路

注意:报告文件与其元数据分离存储,通过 asset_id 关联。

8.2 表设计(PostgreSQL,用户隔离 schema

-- 健康档案(本人,1 行)
CREATE TABLE IF NOT EXISTS health_profile (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  birth_year INT NULL,
  sex TEXT NULL,
  height_cm NUMERIC(5,1) NULL,
  chronic_conditions JSONB NOT NULL DEFAULT '[]'::jsonb,
  long_term_medications JSONB NOT NULL DEFAULT '[]'::jsonb,
  notes TEXT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 原子观测(所有体征 / 行为 / 主观状态的唯一写入格式)
CREATE TABLE IF NOT EXISTS health_observations (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  observed_at TIMESTAMPTZ NOT NULL,          -- 测量时刻,非录入时刻
  metric_type TEXT NOT NULL,                 -- bp_systolic | bp_diastolic | hr | spo2 | weight | temperature | sleep_minutes | symptom | ...
  value_num NUMERIC(10,2) NULL,
  value_text TEXT NULL,                      -- 症状等枚举/文本
  unit TEXT NULL,
  context TEXT NULL,                         -- morning | evening | post_medication | fasting | other
  source TEXT NOT NULL,                      -- manual | photo | voice | ocr_report | apple_health | ble_device
  source_ref TEXT NULL,                      -- 图片 hash / asset_id / 设备标识
  time_source TEXT NOT NULL,                 -- device_screen | exif | user_specified | message_arrival
  confidence NUMERIC(4,3) NULL,
  quality_flag TEXT NULL,                    -- ok | suspect | user_corrected
  raw_payload JSONB NULL,                    -- 原始抽取结果 + 审计留痕
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMPTZ NULL
);

CREATE INDEX IF NOT EXISTS idx_health_obs_metric_time
  ON health_observations (metric_type, observed_at DESC);

-- 幂等:同一图片 + 同一指标 + 同一测量时刻不重复入库
CREATE UNIQUE INDEX IF NOT EXISTS uq_health_obs_dedup
  ON health_observations (metric_type, observed_at, source_ref)
  WHERE source_ref IS NOT NULL AND deleted_at IS NULL;

-- 待确认草稿(不进基线引擎)
CREATE TABLE IF NOT EXISTS health_observation_drafts (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  draft_key TEXT NOT NULL UNIQUE,
  metric_set TEXT NOT NULL,                  -- blood_pressure | weight | spo2 | ...
  extracted JSONB NOT NULL,                  -- 抽取结果 + confidence
  validation JSONB NOT NULL,                 -- 校验结果 + 标记
  status TEXT NOT NULL DEFAULT 'pending',    -- pending | committed | discarded | expired
  channel TEXT NOT NULL,                     -- h5 | wechat
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMPTZ NOT NULL,
  committed_at TIMESTAMPTZ NULL
);

-- 医疗资料档案
CREATE TABLE IF NOT EXISTS health_documents (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  doc_type TEXT NOT NULL,                    -- checkup | lab | imaging | prescription | discharge | other
  report_date DATE NULL,
  institution TEXT NULL,
  title TEXT NOT NULL,
  asset_id TEXT NULL,                        -- MindSpace 健康分区资产引用(原文件)
  ocr_text TEXT NULL,
  extracted_metrics JSONB NOT NULL DEFAULT '[]'::jsonb,
  extraction_status TEXT NOT NULL DEFAULT 'pending',  -- pending | partial | done | failed
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted_at TIMESTAMPTZ NULL
);

-- 个人基线(P1,引擎写入)
-- 注意:基线必须按 context 分层(见 §15 O3)。晨间与晚间血压生理上不同,
--       混算基线会同时掩盖晨峰异常并制造假偏离。
CREATE TABLE IF NOT EXISTS health_baselines (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  metric_type TEXT NOT NULL,
  context TEXT NOT NULL,                     -- morning | evening | any(不分层指标用 any
  window_days INT NOT NULL,                  -- 7 | 30 | 90
  computed_at TIMESTAMPTZ NOT NULL,
  sample_count INT NOT NULL,
  mean_val NUMERIC(10,2) NULL,
  std_val NUMERIC(10,2) NULL,
  p25_val NUMERIC(10,2) NULL,
  p75_val NUMERIC(10,2) NULL,
  typical_low NUMERIC(10,2) NULL,
  typical_high NUMERIC(10,2) NULL,
  maturity TEXT NOT NULL,                    -- cold | weak | stable
  UNIQUE (metric_type, context, window_days)
);

-- 健康事件(P1
CREATE TABLE IF NOT EXISTS health_events (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  event_type TEXT NOT NULL,                  -- threshold_breach | baseline_drift | missed_measurement | symptom_cluster | correlation_hypothesis
  severity TEXT NOT NULL,                    -- info | watch | alert
  detected_at TIMESTAMPTZ NOT NULL,
  rule_id TEXT NULL,                         -- 可审计:哪条规则触发
  metrics_involved JSONB NOT NULL DEFAULT '[]'::jsonb,
  evidence_observation_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
  agent_summary TEXT NULL,                   -- LLM 生成,不可作为 detection 依据
  status TEXT NOT NULL DEFAULT 'open',       -- open | acknowledged | resolved | dismissed
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- 关联假设(P1;晋升到 Memory V2 后记录 memory_item_id
CREATE TABLE IF NOT EXISTS health_correlations (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  factor_a TEXT NOT NULL,                    -- sleep_deterioration
  factor_b TEXT NOT NULL,                    -- bp_elevation
  confidence NUMERIC(4,3) NOT NULL,
  occurrence_count INT NOT NULL DEFAULT 1,
  first_seen_at TIMESTAMPTZ NOT NULL,
  last_seen_at TIMESTAMPTZ NOT NULL,
  user_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
  memory_item_id TEXT NULL
);

设计要点:

  • health_observations唯一写入格式。血压拆成 bp_systolic / bp_diastolic 两行,便于统一基线计算;确认卡在 UI 层合并展示。
  • 所有含软删除的表都带 deleted_atPage Data soft_delete 硬要求)。
  • 唯一索引实现幂等:重复发送同一张照片不会产生重复观测。
  • time_source 显式记录时间来源,用于后续数据质量分析。

8.3 Dataset 注册

每个表独立注册 dataset,遵守 page-data-collect 的「一任务一表一 dataset」原则。

{
  "name": "health_observations",
  "table": "health_observations",
  "description": "MeMind Health 个人健康观测记录",
  "actions": ["read", "insert", "update", "softDelete"],
  "columns": {
    "read": ["id", "observed_at", "metric_type", "value_num", "value_text", "unit", "context", "source", "quality_flag", "created_at"],
    "insert": ["observed_at", "metric_type", "value_num", "value_text", "unit", "context", "source", "source_ref", "time_source", "confidence", "quality_flag", "raw_payload"],
    "update": ["observed_at", "value_num", "value_text", "context", "quality_flag"]
  }
}

关键安全约束raw_payloadsource_refocr_text 不得出现在任何公开页的 read 白名单中(见 §9.4)。

8.4 写入路径(重要澄清)

Page Data 有两条 API:公开页 API 与 Owner Admin API。健康数据的写入不走公开页 API

H5 健康录入 / 服务号录入
  → 平台内登录态请求
  → health-observation-service.mjs
  → Owner 侧 Page Data 访问(/api/admin/page-data/* 或服务端直连 dataset 授权)
  → 用户隔离 PG schema

MindSpace 健康区页面(Timeline / 档案展示)
  → 走公开页 API,但仅 read
  → 且只读「摘要快照 dataset」,不直连原始 observations(见 §9.4

这样原始健康数据永不通过公开页 API 暴露。


9. MindSpace 健康区:强制加密

9.1 新增系统分区

mindspace.mjsSYSTEM_CATEGORIES 增加一项(设计,尚未实现):

{
  code: 'health',
  name: '健康档案区',
  visibilityPolicy: 'private',
  aiAccessPolicy: 'explicit_asset_grant',   // AI 需显式授权才能读具体资料
  publishPolicy: 'password_required',       // 新增策略值
  sortOrder: 30,
}

h5_space_categories.publish_policyVARCHAR(64),新增枚举值不需要改表结构

现有系统分区(oa / public / draft / archive)策略保持不变。

9.2 发布闸门(服务端硬阻断)

publish(pageId, accessMode, password)
  ↓
category = resolveCategory(pageId)
  ↓
if (category.publishPolicy === 'password_required') {
    if (accessMode === 'public')  → 拒绝:健康区禁止完全公开
    if (!password && !hasExistingPasswordHash) → 拒绝:必须设置口令
    if (accessMode not in ['password', 'login_required']) → 拒绝
}
  ↓
继续现有发布流程(不改动其他分区行为)

必须是服务端阻断,不是前端禁用选项。前端仅额外做「不展示完全公开选项」的体验优化。

口令须满足现有平台约束:≥ 6 位bind 时必须传入并写入 password_hash。禁止只改 access_mode 不写 password_hash

9.3 访问体验:本人免密,外部必密

访问健康页
 ↓
是 owner 且已登录?
 ├─ 是 → 直接放行(不要求输口令)
 └─ 否 → 要求口令 → 校验 → 发 page-data token → 放行
  • 健康区默认 password 模式(可分享给医生)。
  • owner 登录态访问短路口令校验。
  • 口令可重置、令牌可撤销,复用现有能力:
    • POST /api/admin/page-data/policies/:pageId/password/reset
    • POST /api/admin/page-data/policies/:pageId/tokens/revoke

9.4 分享安全:快照而非直连(关键设计)

password 模式无法区分访客身份 —— 拿到口令的人能读该页授权的所有行。因此:

禁止健康区公开页直接 read health_observations / health_documents

正确做法:为分享场景生成只读摘要快照 dataset

CREATE TABLE IF NOT EXISTS health_share_snapshots (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  snapshot_key TEXT NOT NULL UNIQUE,
  scope TEXT NOT NULL,             -- clinic_summary | trend_30d | lab_selected
  period_start DATE NOT NULL,
  period_end DATE NOT NULL,
  payload JSONB NOT NULL,          -- 已脱敏、已聚合的展示数据
  created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMPTZ NULL,
  revoked_at TIMESTAMPTZ NULL
);
  • 分享页只 read health_share_snapshots,且 columns.read 仅含 payload 等必要字段。
  • 快照由用户显式生成,范围、时间窗、包含项由用户勾选。
  • 快照可设过期时间、可撤销。
  • 原始 observations / ocr_text / raw_payload 永不进入任何公开页授权。

这解决了「分享给医生」与「数据最小暴露」的矛盾。

9.5 SEO 自动安全(无需改动)

mindspace-index-policy.mjsINDEXABLE_ACCESS_MODE = 'public'isPublicationIndexable() 要求 accessMode === 'public'

健康区强制 password / login_required 后 → 自动 noindex,无需额外改 SEO 逻辑。

此行为落在现有回归守卫 docs/regression-guards/mindspace-seo-geo.md 保护范围内,改动健康区时须一并跑 npm run verify:seo-geo

9.6 健康区内容结构

健康档案区/
  timeline/       健康时间线页(加密,owner 免密)
  documents/      报告归档
    2026-08-体检报告/
    2026-09-血脂化验/
  assessments/    评估摘要
  shares/         就诊摘要快照页(口令 + 可撤销 + 可过期)

10. Health Timeline 与 Personal Baseline

10.1 Health Timeline(日聚合视图,非存储层)

2026-09-01
  BP       128/76
  HR       68
  Weight   67.2kg
  Sleep    6h32m
  Symptom  none

2026-09-02
  BP       137/82   ↑ vs 30d baseline +9.8%
  HR       72
  Weight   67.4kg
  Sleep    5h10m    ↓ vs 30d baseline -17%

7-day trend:
  BP baseline deviation    +9.8%
  Sleep baseline deviation -17%

实现:health_observations 按日 rollup。P0 用查询时聚合;数据量增长后再引入物化视图或定时 job。

10.2 Personal BaselineP1

滚动窗口,每日凌晨重算 7 / 30 / 90 天基线。

冷启动策略(maturity 字段):

阶段 样本量 maturity 行为
冷启动 < 7 个有效点 cold 只展示原始值,不产生偏离事件
弱基线 729 weak 可产生 info 级提示,不发 alert
稳定 ≥ 30 stable 全量启用 Trend Engine

这条很重要:基线未成熟就报警会摧毁用户信任。

有效点定义:quality_flag != 'suspect'deleted_at IS NULLsuspect 只是待裁决状态 —— 用户确认后必须落到 okuser_corrected,不得长期停留,否则真实的恶化会被永久过滤掉。

样本量按 context 分层计算(O3 决策的直接后果):晨间血压基线只统计 context='morning' 的观测。这意味着若用户每天只测一次晨间血压,达到 stable 需 30 天;若晨晚各一次,两条基线各自独立计数,不会因为总数翻倍而提前成熟

weightsleep_minutes 等本身不分晨晚的指标使用 context='any'

10.3 Rule Engine(确定性)

规则类 示例 严重度
绝对阈值 SpO₂ < 90% alert
绝对阈值 收缩压 ≥ 180 或 舒张压 ≥ 110 alert
漏测 连续 3 天无晨间血压 watch
新症状 连续 3 天报告同一症状 watch
药物变化 新增/停用药物后 7 天内加强监测 info

输出 health_events(event_type='threshold_breach', rule_id=...)

10.4 Trend Engine(统计,不用 LLM

检测项 方法
基线漂移 近 14 天均值 vs 90 天基线,偏离 > 阈值且持续 ≥ N 天
变异性增大 近 7 天 std > 历史 std × 1.5
多指标联动 睡眠 ↓ + 静息 HR ↑ + BP ↑ 同期发生 → cluster

输出 health_events(event_type='baseline_drift' | 'symptom_cluster')

10.5 Event Engine → Agent

Event Engine 合并 Rule + Trend 结果,去重、分级、聚合为 Cluster,然后才调用 Agent。

Agent 的输入是结构化 Event,不是原始时序

{
  "cluster_id": "...",
  "window": "14d",
  "baseline_maturity": "stable",
  "findings": [
    { "metric": "bp_systolic", "deviation": "+8%", "baseline": "119-126", "recent": "132-138", "days_above": 5 },
    { "metric": "sleep_minutes", "deviation": "-17%" }
  ],
  "open_questions": ["recent_insomnia", "medication_change", "alcohol", "stress"]
}

Agent 职责:

  1. 用自然语言解释「发生了什么变化」(不是诊断
  2. 提出 12 个高价值追问
  3. 给出行动建议:复测计划 / 记录给医生 / 何时就医
  4. 将用户回答与关联假设写入 health_correlations → 满足条件后晋升 Memory V2

Agent 不能反向修改 Event 严重度 —— 那是 Engine 的职责。

理想输出示例:

过去 30 天整体生命体征较稳定。最近 7 天晨间收缩压较过去 90 日个人基线上升约 8 mmHg,其中 5 天高于近期常态;同期睡眠时间有所下降。建议未来 7 天按规范继续早晚测量,如持续偏高,可将趋势记录提供给医生评估。

10.6 关联假设的长期学习

第一次失眠期 → BP +7%
第二次失眠期 → BP +9%
第三次失眠期 → BP +8%
     ↓
occurrence_count = 3, confidence ↑
     ↓
晋升 Memory V2label: health_correlation
     ↓
"这个人的血压可能对睡眠变化比较敏感"

术语纪律:必须称「关联假设」,禁止表述为医疗诊断或因果结论。


11. 零侵入保证与灰度

11.1 四条保证

  1. 全局 Feature FlagMEMIND_HEALTH_ENABLED=false 时,健康入口不展示、路由不注册、服务号关键词不生效,现有链路行为不变
  2. 只做加法:意图路由不改判定逻辑,健康会话天然携带 selectedChatSkill,走的是已存在的短路分支。
  3. 发布闸门只对 health 分区生效:其他分区 publish_policy 不变,现有发布行为不受影响。
  4. fail-open:抽取/基线服务不可用时,退化为「仅归档原图 + 提示稍后重试」,不阻塞会话、不影响普通聊天。

11.2 灰度

沿用仓库既有 canary 模式:按 user_id 灰度,先内部账号 → 小流量 → 全量。健康引擎(P1)应支持 off / shadow / active 三态,shadow 模式下只记录会产生哪些事件、不实际通知用户。

11.3 必须遵守的既有守卫

改动触及以下路径时,按 AGENTS.md 规则必跑对应 verify

npm run verify:mindspace-publish-guards
npm run verify:mindspace-publish-guards:full
npm run verify:seo-geo
npm run verify:seo-discovery
npm run verify:page-data

特别注意:

  • chat-image-turn-scope.mjs — 带图轮次 read_image 隔离
  • mindspace-index-policy.mjs — 私有页 noindex
  • mindspace-page-sync-service.mjs — remote 模式也必须 sync public HTML

11.4 新增测试

health-channel-state.test.mjs        状态机全转移覆盖 + 无关输入不猜测
health-extraction.test.mjs           schema 约束 / 量程校验 / 置信度门限
health-observation-service.test.mjs  幂等去重 / 时间来源优先级 / suspect 标记
health-baseline-engine.test.mjs      冷启动 maturity / 有效点过滤
health-event-engine.test.mjs         规则触发 + evidence chain 完整性
health-publish-guard.test.mjs        健康区禁止 public 发布(硬阻断)
health-share-snapshot.test.mjs       分享页不得授权原始 observations

12. 合规与产品边界

  1. 非医疗器械声明MeMind Health 是健康记录与趋势提醒,不提供诊断
  2. 关联假设 ≠ 诊断Agent 输出统一附 disclaimer 模板。
  3. Evidence chain 可追溯:每条 alert 可展开到 evidence_observation_ids + rule_id。监管与客诉必备。
  4. 数据分级:健康观测为敏感数据。分享须用户显式生成快照、可过期、可撤销。
  5. LLM 边界detection 层零 LLMexplanation 层 LLM 输入仅为结构化 Event。
  6. 删除权:用户可删除任意 Observation / Document;删除后基线须重算。

12.1 隐私默认值

  • 健康区默认 privateaiAccessPolicy: explicit_asset_grant
  • Agent 读取具体报告内容需显式授权,不默认全量注入上下文。
  • 健康数据不参与 SEO/GEO 收录(§9.5 自动保证)。

12.2 数据保留

复用 Memory V2 生命周期思路:Observation 长期保留(健康趋势需要长历史);但抽取中间产物(原始视觉输出)可设较短保留期。

12.3 P1 存储迁移预案

若 Page Data 聚合性能不足,迁移到专用表的前提:

  • 保持 health_observations 列结构不变,仅换存储位置
  • 提供双写 + 校验期
  • 迁移前后 Timeline 与基线结果必须一致(回归对账脚本)

13. 分阶段路线

P0 — 验证入口与录入精度(不做引擎)

  • H5 /health 入口 + 通道状态机
  • 手动录入 + 拍照录入(三段式确认)
  • Health Timeline 展示
  • MindSpace 健康区(强制加密)+ 报告归档
  • 服务号:关键词/菜单进入 + 拍照上传 + 确认落库
  • health-assistant skill v0(读 Timeline 摘要回答「最近怎么样」)

不做:基线、Event Engine、设备接入、分享。

验证指标

  • 拍照识别经确认后准确率 ≥ 98%
  • 通道内漂移事件(被路由到非健康 skill)= 0
  • 健康区无法以 public 模式发布(自动化测试保证)
  • 连续 14 天录入完成率

P1 — Personal Baseline + Event Engine(核心命题验证)

  • Baseline Engine7/30/90 天 + maturity
  • Trend Engine + Rule Engine v1
  • Event Engine + Agent 解释
  • 关联假设 v0 → Memory V2 晋升

验证方式:回溯已有 30 天数据,Event Engine 能否在用户主观感知前 3–7 天产生 watch 事件;并访谈「这条提醒有用吗?会不会太吵?」

P2 — 设备接入与提醒

  • Apple Health / 蓝牙设备 Connector(设备可信 → 免确认路径,但异常值仍标 suspect
  • 分级通知策略(info 静默 / watch 日摘要 / alert 即时)

P3 — 医疗纵深

  • 用药 Timeline(整合 schedule-assistant
  • 检验指标长期趋势
  • 就诊摘要加密分享页(§9.4 快照机制)

P4 — 授权分享

  • 本人授权家人/医生只读摘要(非代录、非原始数据)
  • 机构侧只读接口(如有需求)

14. P0 前置最小实验(先做,成本极低)

在写任何平台代码前,用 14 天人工实验验证核心假设。

完整可执行方案见 memind-health-p0-experiment.md,此处仅摘要。

参与者:8–10 人(最低 5 人);有意纳入更可能出现波动的人群
工具:  现有 Page Data 匿名问卷页(方案 A),无需新代码

D1–D7 : 基线建设期,每日录入晨间 BP + 睡眠 + 症状 + 扰动因素
D8    : 手工计算 7 天基线(Excel 即可)
D8–D14: 继续录入,研究者每日手工标记偏离(不告知参与者)
D14   : 盲态访谈(必须在揭盲前完成)

主结局 H1(一定可测):14 天晨测完成率 ≥ 80% 的参与者 ≥ 3/5。

次结局 H2(可能不可判定):手工偏离标记早于主观察觉 ≥ 3 天的参与者 ≥ 3/5。

关键设计取舍:

  • H1 是主结局,不是 H2。 若用户坚持不下来,再精妙的基线算法都无意义;此时正确反应是把设备自动同步提到最高优先级,而不是优化算法。
  • 14 天内可能无人偏离,这是最可能的失败模式,方案中已预设无偏离处理路径,不得因 H2 无结论就否定项目方向。
  • 漏检比误报更值得研究:用户明确感到变化而基线检测不到,说明指标组合或窗口有问题。
  • 顺带产出照片校验集:参与者同时提供设备屏幕照片 + 手工录入值,形成 (图片 → 正确值) 标注对,直接用作 P0 拍照识别的回归用例。这是很难另外获得的资产。

15. 已决事项(O1O6

以下六项已决策,实现必须遵守。变更需重新评审本章。

O1 症状录入:固定枚举 + 严重度 + 可选备注

决策:采用固定枚举,附三级严重度,允许自由备注,但备注不参与结构化分析。

枚举(可多选;无症状必须显式选「无」):
  无 / 头晕 / 头痛 / 胸闷 / 心慌心悸 / 乏力 / 气短 / 下肢水肿 / 失眠 / 其他

严重度(每个勾选项必填):
  轻 = 1 / 中 = 2 / 重 = 3

自由备注:可选,存 value_text,仅供 Agent 解释时参考

理由symptom_cluster 检测需要可比较的离散值。自由文本会产生「有点晕」「头晕晕的」「晕」等语义漂移,无法计数、无法判断「连续 3 天同一症状」。

存储:每个症状一行 health_observationsmetric_type='symptom'value_text= 枚举码,value_num= 严重度。这样症状严重度也可做趋势(如「头晕从轻转中」)。

「无症状」必须显式记录,不能靠「没有记录」推断 —— 否则无法区分「今天没症状」与「今天忘了记」。

O2 语音录入:必须确认,不得直接落库

决策:语音识别结果一律进入 CONFIRM 状态,禁止跳过确认。

理由:数字类语音识别错误率显著高于文本,且错误形态危险 —— 「一百三十七比八十二」可能识别为「137、82」也可能是「130、782」。血压场景一次错记就污染基线。

附加要求:语音录入的确认卡必须同时展示识别原文,让用户能判断是识别错了还是自己说错了:

听到:「血压一百三十七比八十二」
识别:收缩压 137,舒张压 82
确认保存 / 修改 / 重说

O3 血压强制记录情境,且基线按情境分层

决策:血压录入强制记录情境,三选一,按测量时刻自动预选,用户可改。

morning  晨间(默认:测量时刻 < 10:00)
evening  晚间(默认:测量时刻 ≥ 18:00)
other    其他(10:0018:00 默认,或用户手动指定)

这不只是一个字段 —— 基线必须按 context 分层计算(已反映到 §8.2 health_baselinesUNIQUE (metric_type, context, window_days))。

理由(这是本次评审中最重要的修正):晨间血压与晚间血压生理上是不同指标,个体内可差 10–20 mmHg。若混算基线:

  • 晨峰升高会被晚间的低值稀释 → 漏掉最有临床意义的异常
  • 用户某天只测了晚间 → 与混合基线比较 → 产生假偏离

两种错误方向相反,但都由同一个原因造成。因此分层是必需的,不是优化项。

代价:达到 stable 所需天数不因多测而减少(见 §10.2)。这个代价可以接受 —— 宁可基线成熟慢,不要基线本身是错的。

other 情境的观测记录但不参与晨/晚基线计算,仅在 Timeline 展示。

O4 报告 OCR:复用现有图片/文件分析链路,不新建 OCR 服务

决策:健康侧不实现独立 OCR。复用仓库现有的图片与文件分析能力(服务号侧 media analysis、Agent 侧图片/文件读取),健康域只在其之上叠加两层:

现有图片/文件分析链路(不改动)
        ↓
health-extraction.mjs
  ├─ 强约束输出 schemametric_set + 字段 + confidence
  └─ 合理性校验(量程 / 逻辑 / 基线偏离 / 置信门限)
        ↓
ObservationDraft(待确认)

理由:重复实现 OCR 会造成两套图片处理路径,直接威胁 chat-image-turn-scope.mjs 的回归守卫(带图轮次 read_image 隔离、工具图片污染触发会话轮换)。健康通道必须走同一条底层路径。

实施前置检查:动工前需确认现有链路对「多页 PDF」「长图化验单」的支持程度。若不支持,应作为独立需求补齐通用能力,而不是在健康域内私建分支。

O5 alert 通知:不承诺推送,主机制为「下次进入即见」

决策alert 触达不作为产品承诺。核心机制是进入即见,推送为 best-effort 补充。

分级策略:

严重度 触达方式
info 静默,仅健康首页与 Timeline 可见
watch 进入即见 + 可选日摘要(用户须主动开启)
alert 进入即见(置顶未读)+ 48 小时内有交互时发服务号客服消息 + 已订阅者发订阅通知

理由:服务号客服消息受 48 小时交互窗口限制,订阅通知需用户主动订阅且有频次场景限制;H5 无主动推送能力。恰恰是长期不互动的用户最需要提醒,而这类用户技术上最难触达。

因此产品措辞必须修正

禁止:「异常时我们会通知你」
应为:「异常会记录在你的健康档案,并在你下次进入时优先展示」

实现要求:任何入口进入健康通道时(无论何种原因),必须先展示未读 alert,且未读状态在用户明确确认前不清除。

同时:应引导用户主动订阅服务号通知,并在引导文案中说明原因,而不是默认用户已可触达。

O6 偏离阈值:P0 不定死,由实验数据标定

决策:不在设计阶段固化阈值。P0 采用下列初始建议值,并在前置实验与 P0 数据积累阶段完成标定。

初始建议值(仅供实验期手工判定与 P1 起点,非最终值):

指标 单日标记 持续偏离(触发事件)
晨间收缩压 dev% ≥ +5%z ≥ +1.5 连续 ≥ 2 天同向 → watch;≥ 5 天或 dev% ≥ +10%alert 候选
晨间舒张压 同上 同上
睡眠时长 dev% ≤ -15%z ≤ -1.5 连续 ≥ 3 天 → 参与 cluster 判定
静息心率 dev% ≥ +10%z ≥ +1.5 连续 ≥ 3 天 → 参与 cluster 判定
体重 7 日内变化 ≥ 1.5kg 直接 watch

双重判定(相对偏差 OR 标准分)的理由:相对偏差直观但忽略个体变异性 —— 变异大的人频繁误报;z 分数考虑变异性但小样本下 std 不稳定。两者取「或」,宁可多标记后人工/规则复核,也不要漏检。

标定流程

前置实验(14 天,手工判定)
   → 记录首次偏离日 vs 主观察觉日
   → 计算不同阈值下的提前天数与误报数
   → 选择「提前 ≥ 3 天且误报可接受」的阈值组合
   → 写回本节,作为 P1 Trend Engine 初始配置

阈值必须可配置,不得硬编码在检测逻辑中 —— 后续需按真实数据持续调整。


16. 相关文档