SM-Personal-Learning 个性化学习功能设计
版本: v2.0 日期: 2026-07-14 状态: 简化设计版 关联: SA_主动整备_Agent设计.md、SA_空间控制面板设计.md、SM-Weather-API_天气接入设计.md 核心思路: 不聚合、不分类、不建偏好评级,只选最近 30 天内室外温度最相似的 5 条历史快照喂给 LLM
1. 背景与设计原则
1.1 要解决的问题
智能整备 Agent 当前基于知识库规则做全局统一的决策——无论谁来开会、外面什么天气,LLM 推荐的方案都一样。实际上,不同用户对环境有不同的偏好:有人喜欢 22°C 的强劲冷气,有人觉得 26°C 就够。全局规则无法覆盖个人差异。
1.2 设计原则
| 原则 | 说明 |
|---|---|
| 隐式采集 | 用户不需要配置偏好,系统通过深度调节等行为自动学习 |
| 不搞分类 | 不做季节/温度带/天气状况的分类标签——直接用原始室外温度排序 |
| 不搞聚合 | 不做统计均值、置信度计算——直接喂原始快照,LLM 自己看规律 |
| 可降级 | 无记录时退化为知识库规则,冷启动无影响 |
| 手工信号优先 | 用户手动调整过的是强信号,未动的虽隐含认可但不作为强依据 |
1.3 与智能整备的关系
整备触发 → LLM 出建议(含天气+知识库规则)
↓
推送通知卡片(通知型自动执行 / 确认型等用户确认)
↓
用户可随时打开面板"深度调节"
↓
每次操作都记录快照 → 成为下次 prep 的历史参考2. 数据模型
2.1 整备会话表(prep_sessions)
一次会议的一次整备生命周期 = 一条记录。
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
session_id | VARCHAR(64) PK | 整备会话唯一 ID | s_20260714_A101 |
meeting_id | VARCHAR(64) | 会议 ID | mtg_001 |
room_id | VARCHAR(64) | 空间 ID | A-101 |
booker_id | VARCHAR(64) | 预定人(用户身份) | u_zhangsan |
meeting_start | DATETIME | 会议开始时间 | 2026-07-14 14:00 |
meeting_end | DATETIME | 会议结束时间 | 2026-07-14 15:00 |
prep_trigger_at | DATETIME | prep 触发时间 | 2026-07-14 13:45 |
prep_type | ENUM | 通知型 auto_notify / 确认型 confirm | auto_notify |
weather_temp | INT | 触发时的室外温度 | 35 |
weather_condition | VARCHAR(32) | 触发时的天气状况 | 晴 |
2.2 整备快照表(prep_snapshots)
每次整备生命周期中的"事件"都记录一条全量快照。
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
id | BIGINT AUTO_INCREMENT | 主键 | |
session_id | VARCHAR(64) | 所属会话 | s_20260714_A101 |
recorded_at | DATETIME | 记录时间 | 2026-07-14 13:45 |
source | ENUM | 见下方来源定义 | |
operator_id | VARCHAR(64) | 操作人(LLM 推荐时为 null) | u_zhangsan |
snapshot | JSON | 全量控制快照 | 见下方示例 |
source 来源定义:
| 来源 | 含义 | 触发场景 |
|---|---|---|
llm_recommend | LLM 刚出的推荐值 | prep 触发 → LLM 输出 recipes 后 |
auto_executed | 通知型·已自动下发 | 通知型 prep,设备指令已下发 |
user_confirmed | 确认型·用户点了确认 | 确认型 prep,用户点击"确认执行" |
user_adjust | 用户手动调整 | 用户打开深度调节面板修改数值并生效 |
user_later_adjust | 会议中再次手动调整 | 会议进行中用户又唤出面板调整 |
snapshot 结构示例(全量,包含所有设备语义的最终值):
json
{
"hvac_power": true,
"hvac_mode": "制冷",
"hvac_target_temp": 22,
"light_power": true,
"light_brightness": 60,
"curtain_power": true,
"curtain_position": 100
}2.3 一个完整 session 的快照示例
场景:A-101, 张三, 14:00~15:00 会议, 通知型 prep
时间 | 来源 | 操作人 | snapshot
──────────┼────────────────────┼────────┼────────────────────────────
13:45 | llm_recommend | null | hvac_power=true, hvac_target_temp=24, hvac_mode=制冷, light_power=true, light_brightness=80, curtain_power=true, curtain_position=50
13:45 | auto_executed | null | (同上,自动下发执行)
13:50 | user_adjust | 张三 | hvac_target_temp=22, 其余同上
14:00 | user_later_adjust | 张三 | hvac_target_temp=23, light_brightness=60, curtain_position=100, 其余同上
14:30 | user_later_adjust | 张三 | hvac_target_temp=22, 其余同上3. 召回逻辑——给 LLM 喂什么
3.1 核心思路
不做任何聚合/统计/分类。查最近 30 天同一用户的整备记录,按室外温度接近度排序,取 top 5,直接拿"用户最终决定"的快照喂给 LLM。
3.2 查询逻辑
1. 查 prep_sessions WHERE booker_id = 当前预定人
AND room_id = 当前会议室
AND prep_trigger_at >= NOW() - 30 天
2. 对每个 session,归约为"用户最终状态":
- 该 session 中同一语义 key 取最后一次 user_adjust 的值
- 如果该语义 key 用户从未手动调整过 → 跳过(不输出)
3. 按室外温度接近度排序:
ORDER BY ABS(weather_temp - 当前室外温度) ASC
4. 取 TOP 5为什么只输出"用户改过的 key":没改过的说明用户接受了推荐值,那是全局规则的功劳,不是个人偏好的体现。
3.3 注入 LLM 的格式
【张三近期操作习惯(近30天·按气温接近排序)】
日期 室外 状态 控制快照
────────────────────────────────────────────────────────
7/12 33°C 你手动调过 空调24→22°C 灯开→亮度70% 窗帘关
7/10 35°C 你确认了推荐 空调24°C 灯开·亮度80% 窗帘开
7/08 32°C 你手动调过 空调24→21°C 灯开→亮度60% 窗帘关
7/05 28°C 你手动调过 空调24→26°C 灯开·亮度80% 窗帘开
7/01 27°C 确认了推荐(未改) 空调24°C 灯开·亮度80% 窗帘开
提示:今天室外 34°C,与 7/12(33°C)、7/10(35°C) 接近。
历史中这种天气下,用户倾向比推荐值调低 2°C 并关窗帘。
上述记录供参考,最终请结合当前 RO 读数和约束判断。说明:
| 状态 | 对应 source | 含义 |
|---|---|---|
| 你手动调过 | user_adjust / user_later_adjust | 强信号,用户明确改了 |
| 你确认了推荐 | user_confirmed | 中信号,用户看了并点了同意 |
| 自动执行(未干预) | auto_executed 后无 user_adjust | 弱信号,可能是没看到 |
关键设计点:每个 session 只贡献一条经过归约的快照(即"用户最终决定了什么"),而不是原始的多条事件记录。保持 LLM 输入简洁。
4. 数据采集
4.1 采集时机
| 时机 | 上报方 | 记录为 |
|---|---|---|
| prep 触发 → LLM 出推荐值 | Dify 工作流 | llm_recommend |
| 通知型·自动下发执行 | Hub(执行后) | auto_executed |
| 确认型·用户点击确认 | 前端控制面板 | user_confirmed |
| 用户深度调节→立即生效 | 前端控制面板 | user_adjust |
| 用户会议中再次调节 | 前端控制面板 | user_later_adjust |
4.2 前端上报 API
POST /internal/preparation/snapshots
{
"session_id": "s_20260714_A101",
"recorded_at": "2026-07-14 13:50",
"source": "user_adjust",
"operator_id": "u_zhangsan",
"snapshot": {
"hvac_power": true,
"hvac_target_temp": 22,
"light_power": true,
"light_brightness": 60,
"curtain_power": true,
"curtain_position": 100
}
}调用方:前端控制面板的"立即生效"/"确认执行"按钮回调。
4.3 所需的前端上下文
| 数据 | 来源 | 说明 |
|---|---|---|
operator_id | Chatbot 用户身份 | 当前登录用户 ID(已有,Chatbot 内可获取) |
session_id | 面板打开时携带 | 从通知卡片 AppLink 参数带过来 |
snapshot | 面板当前所有语义的最终值 | 从 PrepStore 实时获取 |
recorded_at | 当前时间 | 前端本地时间 |
5. Dify 工作流变动
5.1 新增节点
在"构建LLM用户消息"与"LLM推理"之间插入 Code: 召回个人操作记录。
输入:
booker_id:从 Hub Context 新增的字段获取room_id:会议空间 ID(已有)current_weather_temp:当前室外温度(天气节点已有)user_message:"构建LLM用户消息"节点的输出
逻辑:
python
def main(user_message, booker_id, room_id, current_weather_temp):
sessions = query_prep_sessions(booker_id, room_id, days=30)
if not sessions:
return {"user_message": user_message}
# 对每个 session 归约为"用户最终决定"
history = []
for s in sessions:
snapshots = query_snapshots(s.session_id,
sources=['user_adjust', 'user_later_adjust', 'user_confirmed', 'auto_executed'])
if not snapshots:
continue
# 按语义 key 取最后一次用户修改的值
final_state = {}
touched_keys = set()
has_user_adjust = False
for snap in snapshots:
if snap.source in ('user_adjust', 'user_later_adjust'):
for k, v in snap.snapshot.items():
final_state[k] = v
touched_keys.add(k)
has_user_adjust = True
elif snap.source == 'user_confirmed':
for k, v in snap.snapshot.items():
final_state[k] = v
# 只保留用户明确调过的 key
user_final = {k: final_state[k] for k in touched_keys}
# 判断状态标签
if has_user_adjust:
status_label = "user_adjusted"
else:
status_label = "user_confirmed"
history.append({
"date": s.prep_trigger_at.strftime("%m/%d"),
"weather_temp": s.weather_temp,
"weather_condition": s.weather_condition,
"status": status_label,
"final_state": user_final
})
# 按气温接近度排序,取 top 5
history.sort(key=lambda h: abs(h["weather_temp"] - current_weather_temp))
history = history[:5]
if not history:
return {"user_message": user_message}
# 构建注入段落
lines = ["", "【{}近期操作习惯(近30天·按气温接近排序)】".format(booker_id)]
lines.append("日期 室外 状态 控制快照")
lines.append("─" * 60)
for h in history:
status_text = {
"user_adjusted": "你手动调过",
"user_confirmed": "你确认了推荐",
"auto_no_intervention": "自动执行(未干预)"
}.get(h["status"], h["status"])
state_parts = []
for k, v in h["final_state"].items():
if isinstance(v, bool):
v_text = "开" if v else "关"
else:
v_text = str(v)
state_parts.append(f"{k}={v_text}")
state_str = " ".join(state_parts)
lines.append(f"{h['date']} {h['weather_temp']}°C {status_text:<12} {state_str}")
# 加一句提示
nearest = history[0]
lines.append("")
lines.append(f"提示:今天室外 {current_weather_temp}°C,与 {nearest['date']}({nearest['weather_temp']}°C) 最接近。")
lines.append("上述记录供参考,最终请结合当前 RO 读数和量程约束自行判断。")
return {"user_message": user_message + "\n" + "\n".join(lines)}5.2 Hub Context API 需补充字段
当前 GET /internal/preparation/tasks/{taskId}/context 返回值需增加:
json
{
"bookerId": "u_zhangsan",
"bookerName": "张三"
}6. 边界情况
| 场景 | 处理 |
|---|---|
| 冷启动(无历史) | 不注入偏好段落,LLM 纯用知识库规则 |
| 不足 5 条 | 有几条给几条 |
| 无相同温度匹配 | 按温差升序取,温差再大也有最接近的 |
| 用户改过调回 | 最后一次 user_adjust 的值就是最终值,如实记录。如果等于推荐值,状态标记为"确认了推荐"而非"手动调过"(不视为强信号) |
| 同一会议多次调整 | 按语义 key 取最后一次 user_adjust 值,一个 session 归约为一行 |
| 窗帘类已纳入 | curtain_power(bool 开关) 和 curtain_position(number 位置) 均作为标准 RW 语义,与 hvac/light 同等对待 |
7. 实施计划
| 序号 | 任务 | 说明 |
|---|---|---|
| 1 | Hub 新增 prep_sessions 表 | 记录整备会话 |
| 2 | Hub 新增 prep_snapshots 表 | 记录每次快照事件 |
| 3 | Hub Context API 补充 booker_id 字段 | Dify 需要知道是谁的会议 |
| 4 | Hub 新增 POST /snapshots 接收端点 | 前端/后端上报快照 |
| 5 | Dify 新增 Code: 召回个人操作记录 节点 | 查询 top 5 并注入 user_message |
| 6 | 前端控制面板补充快照上报 | 立即生效/确认执行回调中 POST 快照 |
| 7 | 端到端联调 |
8. 附录:相关文档
| 文档 | 关系 |
|---|---|
| SA_主动整备_Agent设计.md §9.4 | 本设计即对该节"个人偏好习惯表(预研)"的正式落地 |
| SA_空间控制面板设计.md | 面板的"立即生效"是快照的主要采集入口 |
| SM-Weather-API_天气接入设计.md | 室外温度是快照排序的核心维度 |
| 空间语义管理.md §10 | 空间逻辑映射定义了哪些语义可以被操作,窗帘等语义纳入其中 |
| SA_主动整备_消息通知通道设计.md | 用户身份(booker_id)的来源定义 |
