Files
smartmate/docs/backend/第二阶段主动调度MVP功能预期.md
Losita ba23ebd201 Version: 0.9.62.dev.260502
后端:
1. 主动调度补齐 `unfinished_feedback` 定位闭环——用户补充信息先在滚动窗口内定位到可校验的日程块,定位失败则继续 ask_user,不再硬猜 target_id 或直接跑 graph。
2. 聊天占管重跑链路加并发保护——`waiting_user_reply -> rerunning` 改为 DB CAS 抢占,重复补充只返回可见等待提示,避免并发生成多份 preview。
3. rerun 结果回写继续收口——新 preview_id 同步回 trigger 审计指针,session 只在拿到新 preview 时更新当前预览,ready_preview 后清空追问状态并释放回普通聊天。
4. 主动调度事件校验放宽 unfinished_feedback 的空 target 场景,允许先触发、后定位,再进入 graph + preview 主链路。
2026-05-02 12:41:50 +08:00

615 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第二阶段主动调度 MVP 功能预期
## 0. 当前状态摘要2026-05-02
本文档仍作为主动调度 MVP 的产品边界说明;具体实现拆分、表结构和阶段进度以《第二阶段主动调度 MVP 实现方案.md》和《主动调度缺口分阶段实施计划.md》为准。
截至当前仓库状态MVP 主闭环已经从“功能预期”推进到可演示、可排障的实现状态:
1. `important_urgent_task` 已能从任务池生成主动调度 trigger写入 `active_schedule_previews`,并通过飞书 webhook 触达用户。
2. 飞书链接已统一进入 `/assistant/{conversation_id}`,不再使用独立 `/schedule-adjust/{preview_id}` 入口。
3. 用户进入助手页后,可以看到主动调度卡片,并打开日程预览与精排弹窗;确认前只展示 preview不写正式 `schedule_events`
4. 用户确认后才进入同步 apply 链路apply 失败必须回写可排障状态,不能半写正式日程。
5. `unfinished_feedback` 的“定位 -> ask_user -> 重跑 graph -> 刷新 preview”闭环已经落地阶段 5 不再重复全套主链路,只保留 1 条真实 chat 烟测确认入口未回归。
演示和验收时需要特别注意:主动调度窗口是“从当前时刻起滚动 24 小时”,不是自然日或整周。如果窗口落在周六且该用户没有课程,预览里只出现待安排任务是符合语义的;需要展示“已有课程 + 待确认任务”同屏时,应把 `mock_now` 固定到有课程的周四窗口。
## 1. 文档目的
本文档先讨论第二阶段最终想做成什么功能,不进入具体代码拆分和表结构实现。
第二阶段的核心目标不是让 Agent 获得直接改正式日程的能力,而是让系统具备一条可控的主动发现链路:
```text
课程 / 任务事实变化
-> 后台观测滚动 24 小时内的任务与日程风险
-> 生成结构化诊断和候选方案
-> 让 LLM 做解释、追问和有限选择
-> 写入预览 / 触达用户
-> 用户回系统确认后再进入正式应用
```
这里的关键是:**系统主动观测后端给候选并粗排LLM 做解释 / 追问 / 有限选择,用户掌握最终确认权**。
---
## 2. 产品边界
### 2.1 只处理滚动 24 小时内的局部问题
第一版主动调度只关注从当前时刻起滚动 24 小时内的问题。
不处理:
1. 未来 72 小时的全局计划。
2. 本周整体重排。
3. 多天任务均衡。
4. 为了优化指标而跨天反复搬动任务。
这样做的原因:
1. 滚动 24 小时内的问题最接近用户当下行动,主动触达更有必要。
2. 范围越小,误改日程的风险越低。
3. 第二阶段重点是验证“任务池与日程视图联通”,不是做完整排程引擎。
### 2.2 不给 Agent 直接写正式日程的权限
日程是高风险数据。第一版不计划让 Agent 绕过用户确认直接写 `schedule`
允许:
1. 读取任务与日程事实。
2. 生成风险诊断。
3. 生成候选方案。
4. 写入待确认预览。
5. 发布飞书触达事件。
不允许:
1. 后台 worker 直接修改正式日程。
2. 飞书通知直接应用日程变更。
3. LLM 根据自然语言自由构造正式日程写入。
4. 因为后台诊断认为存在风险,就绕过用户确认改动正式日程。
### 2.3 主动调度不是新增一批通用日程工具
现有日程工具已经有大量可复用能力,包括读取、分析、局部移动、交换、队列处理、结构化结果展示等。
第二阶段新增能力的重点不应是重做 `schedule` 工具,而是补上“任务池与日程视图联通”所缺的观测能力:
1. 哪些任务已经进入四象限“重要且紧急”池,应该被拉入日程视图。
2. 哪些任务尚未进入 schedule 视图但应该被提醒安排。
3. 用户明确反馈未完成后,哪些后继任务会被挤压。
4. 当前是否存在值得打扰用户的候选方案。
---
## 3. 设计理念
### 3.1 参考 `analyze_health`,做观测与候选生成
当前 `analyze_health` 的有效经验是:
1. 后端先做结构化观测。
2. 后端输出问题、指标、裁决和候选操作。
3. LLM 不做开放式全窗搜索,也不承担主排序;它只在后端候选里做有限选择与表达。
4. 候选必须是后端验证过合法性和收益的。
5. 如果没有值得继续处理的问题,后端明确返回 close / ask_user而不是继续诱导 LLM 硬调。
第二阶段应延续这个模式。
也就是说,新能力更像一个主动调度观测能力,而不是一个自由排程工具。具体工程工具名后续再确认,本阶段只固定职责边界。
### 3.2 让 LLM 做解释、追问和有限选择
LLM 的职责:
1. 把结构化诊断转换成用户能理解的解释。
2. 在后端候选非常接近时,做有限选择。
3. 在信息不足时,把后端 `missing_info` 转成自然追问。
4. 根据主动注入的上下文理解用户偏好,但不调用单独的 `user_preference.get` 工具。
后端的职责:
1. 注入用户上下文、偏好和必要任务事实。
2. 消费四象限“重要且紧急”任务池,不在主动调度内重复管理 DDL 阈值。
3. 枚举候选方案。
4. 校验候选方案是否可行。
5. 明确给出候选的风险、收益和确认要求。
---
## 4. 上下文注入方式
第二阶段不新增 `user_preference.get` 工具。
偏好、近期反馈、必要上下文应由主动调度入口在运行前注入,例如:
```text
ActiveScheduleContext
user_id
now
time_window = now ~ now+24h滚动 24 小时)
course_context
task_context
schedule_context
quadrant_context
injected_preferences
recent_user_feedback
trigger_source
```
这样可以保证:
1. LLM 不需要额外调用偏好读取工具。
2. worker 和 API 测试触发走同一套上下文构造逻辑。
3. 偏好采集仍归 memory / 上下文系统负责,主动调度只消费注入后的上下文。
4. 后续飞书入口、Web 入口、后台定时入口可以共享上下文协议。
---
## 5. MVP 触发来源
第二阶段触发基本由课程和任务本身驱动,暂不做泛化生活习惯型触发。
### 5.1 重要且紧急任务进入日程视图
触发条件:
1. 任务来自四象限“重要且紧急”池。
2. 任务未完成。
3. 当前日程视图中没有足够明确的完成安排,或任务尚未进入 schedule 视图。
4. 主动调度不单独维护 DDL 临近阈值,阈值判断交给四象限自动轮换机制。
系统目标:
1. 判断该任务是否应该被加入 schedule 预览。
2. 判断滚动 24 小时内是否存在合适的可用窗口。
3. 给出一个或多个“加入日程视图”的候选方案。
4. 若没有可用窗口,则给出 ask_user 或风险提醒。
5. 注意当前四象限轮换机制是懒加载:只有用户访问时才会轮换。第二阶段需要考虑在后台触发前刷新轮换结果,或补一个 worker 侧轮换入口,避免主动调度读到过期任务池。
### 5.2 用户反馈已排任务未完成
触发条件:
1. 任务已经出现在 schedule 视图中。
2. 用户明确反馈该任务没有完成,例如“刚才那个没做完”“这项要延后”。
3. 该任务仍然影响滚动 24 小时内的 DDL、后继任务或今日计划。
系统目标:
1. 默认情况下,动态任务计划时间过去后按已完成推进,不主动追问。
2. 用户明确反馈未完成时,再把该任务拉回主动调度链路。
3. 分析滚动 24 小时内后继任务是否被挤压。
4. 生成局部补救候选,例如今天补做、延后低优先级后继任务,或者暂不调整。
### 5.3 课程表变化触发后置
课程变化触发暂不进入第一版。
原因:
1. 当前课程基本不可移动,主动调度很难在课程层直接补救。
2. 调停课、补课、课程调整等机制还需要先完善。
3. 等课程调整机制稳定后,再把课程变化纳入主动调度触发源。
后续触发条件可以是:
1. 滚动 24 小时内课程时间发生变化。
2. 变化挤占了原本计划给任务的时间。
3. 被挤占任务尚未开始,或用户明确反馈未完成,且仍有 DDL 或后继影响。
系统目标:
1. 判断受影响任务是否需要重新进入候选。
2. 仅围绕受影响任务和直接后继任务生成局部方案。
3. 不做全局重排。
### 5.4 用户反馈疲劳
触发条件:
1. 用户在对话或反馈中明确表达“很累”“今天撑不住”“不想继续”等状态。
2. 滚动 24 小时内仍存在待执行任务、DDL 高压任务,或用户明确反馈未完成的任务。
系统目标:
1. 判断是否有低优先级任务可以移出今日预览。
2. 判断是否应只提醒用户确认而不是主动给移动候选。
3. 不因疲劳反馈自动取消 DDL 高风险任务。
---
## 6. 核心场景的功能预期
### 6.1 重要且紧急任务还没进入日程视图
这是“从任务池进入日程视图”的问题。
用户视角:
```text
系统发现你有一个重要且紧急任务还没有安排到接下来 24 小时的时间表里。
系统给出 1~3 个可选安排建议。
你确认后,它才进入正式日程。
```
系统预期:
1. 从四象限“重要且紧急”池识别应被拉入日程视图的任务。
2. 排除已完成、已取消、信息不足的任务。
3. 检查滚动 24 小时内可用时间。
4. 输出候选项,而不是直接调用正式 schedule 写入。
候选项示例:
```json
{
"candidate_id": "place_task_123_slot_a",
"action": "加入日程预览",
"target": {
"task_id": 123,
"source": "task_pool"
},
"preview_change": {
"type": "add_to_schedule_view",
"day": 0,
"slot_start": 7,
"slot_end": 8
},
"why": "任务已进入重要且紧急池,当前尚未进入日程视图;该时段未被课程占用。",
"risk": "需要用户确认预计耗时是否足够。",
"requires_user_confirm": true
}
```
### 6.2 Schedule 内任务由用户反馈未完成
这是“用户明确说没做完之后的后继调整”问题。
用户视角:
```text
系统默认刚才安排的动态任务已经完成,不会频繁追问。
如果你告诉系统“没做完”,它会基于接下来 24 小时给出补救候选。
```
系统预期:
1. 动态任务计划时间过去后默认按 `assumed_completed` 处理。
2. 不因为缺少完成证据就主动进入风险确认。
3. 用户反馈未完成后,才分析滚动 24 小时内后继任务是否被影响。
4. 输出“今天补做 / 延后后继 / 暂不调整”类型候选。
候选项示例:
```json
{
"candidate_id": "unfinished_task_456_repair_today",
"action": "未完成补救预览",
"target": {
"task_id": 456,
"source": "schedule_task"
},
"preview_change": {
"type": "repair_unfinished_task",
"day": 0,
"slot_start": 9,
"slot_end": 10
},
"why": "用户反馈该任务没有完成,且它仍影响 24 小时内的后续安排。",
"risk": "需要用户确认是否接受挤占后续低优先级任务。",
"requires_user_confirm": true
}
```
### 6.3 动态任务失败后的后继挤压补救
这是“当前动态任务炸了,但后面还有后继任务”的问题。
用户视角:
```text
如果你告诉系统当前任务没做完,而后面还有安排,系统会先尝试在滚动 24 小时内局部补救。
它会优先找不那么痛的方案;如果时间真的不够,会把选择交给你。
```
系统预期:
1. 先判断当前动态任务是否仍值得补救,而不是直接跳过。
2. 如果滚动 24 小时内空间足够,生成“把当前动态任务往后挤进去”的预览。
3. 这个预览允许在必要时打破部分用户偏好,例如原本偏好的学习时段、任务顺序或轻重搭配,但必须清楚说明代价。
4. 如果空间不够,优先询问用户是否能延后结束时间。
5. 如果用户不能延后,或延后仍然不够,再生成“强行融入下一个动态任务”的 rush 预览。
补救策略顺序:
```text
策略 A局部重排补救
直接复用并魔改粗排思路,但范围只限滚动 24 小时。
输入只传受影响的部分 task item而不是整批任务。
用户偏好 weekday / slot 优先遵守;如果无法达成,再打破偏好。
打破偏好的优先级低于“把任务压进时间表”。
目标是把失败的动态任务重新塞回时间表,并尽量少影响后继任务。
策略 B协商延后结束时间
如果当前 24 小时容量不够,询问用户能否延后结束时间或放宽边界。
策略 C压缩融合到下一个动态任务
如果不能延后,就把失败任务强行融入下一个动态任务。
第一版默认两个动态任务各 50% 压缩,让用户 rush 一下。
```
策略 C 的产品判断:
1. 它不是理想方案,但通常比直接跳过失败任务更合理。
2. 两节课各自减半的影响,往往小于完全放弃其中一项。
3. 第一版只把它作为兜底候选,不默认自动执行。
4. 第一版默认 50% / 50%暂不按优先级、DDL 或预计耗时动态分配。
5. 后续如果出现更好的补救策略,可以替换或降低它的优先级。
候选项示例:
```json
{
"candidate_id": "rush_merge_task_456_into_789",
"action": "压缩融合预览",
"target": {
"unfinished_task_id": 456,
"next_task_id": 789
},
"preview_change": {
"type": "rush_merge",
"unfinished_task_ratio": "50%",
"next_task_ratio": "50%"
},
"why": "用户反馈当前动态任务未完成,且 24 小时内没有足够独立空档;直接跳过会损失更大。",
"risk": "两个任务都会被压缩,需要用户接受 rush 模式。",
"requires_user_confirm": true
}
```
---
## 7. 产品能力边界
本阶段可以讨论“系统需要哪些能力”,但不在这里冻结具体工具名、入参 schema、handler 注册方式或内部实现路径。
也就是说,本章只定义产品级能力边界;工程级工具设计在产品预期敲定后另开文档。
### 7.1 主动观测能力
第二阶段需要一个类似 `analyze_health` 的主动观测能力。
它负责:
1. 汇总滚动 24 小时内任务 / 日程联通风险。
2. 区分 task DDL 临近和用户反馈未完成后的补救两类问题。
3. 输出结构化指标、问题、裁决和候选。
4. 告诉 LLM 是否应该继续、提醒用户、生成预览或收口。
它不负责:
1. 直接写正式 schedule。
2. 替代现有 `place` / `move` / `swap` 等日程工具。
3. 自行读取偏好工具。
4. 做 24 小时外全局重排。
5. 在飞书内完成复杂确认。
### 7.2 候选生成能力
候选生成能力的产品职责是“出选择题”,不是让 LLM 自由编写正式日程参数。
第一版候选只允许覆盖这些产品动作:
```text
加入日程预览
将任务池中的 DDL 临近任务加入预览,不直接写正式日程。
未完成补救预览
对用户明确反馈未完成的日程任务生成补救预览。
后继挤压重排预览
当前动态任务失败且影响后继时,在滚动 24 小时内做局部重排候选。
延后结束询问
当前容量不足时,询问用户是否允许延后结束时间或放宽边界。
压缩融合预览
无法延后时,把失败任务融入下一个动态任务,形成 rush 候选。
询问用户
信息不足、风险过高、用户反馈不够明确时询问用户。
仅提醒
只提醒用户查看,不建议调整。
收口
当前无值得打扰用户的问题。
```
第一版明确不支持这些产品动作:
```text
直接写正式日程
全局重排
自动标记完成
飞书内直接应用日程
后台自动进入 rush 模式
```
### 7.3 预览写入能力
预览写入能力只负责保存待确认方案,让用户能看到“改前 / 改后 / 为什么”。
它不代表正式日程已经变化,也不承担复杂排程判断。
### 7.4 用户确认能力
用户确认能力负责把用户选择的候选项转成正式应用请求。
第一版确认粒度建议按候选项确认,而不是整版黑盒确认。
正式应用时优先复用现有 service 逻辑,不在主动调度模块里绕过既有写入链路单独改状态或改日程。
### 7.5 通知触达能力
通知触达能力只负责把“系统发现问题并生成了建议”这件事告诉用户。
它不负责判断怎么调度,也不负责在飞书内完成确认。
### 7.6 前后对比与回滚能力
当前版本还没有完整的前后对比机制。第二阶段如果要让用户放心确认主动调度建议,需要补齐“改前 / 改后 / 可回滚”的预览能力。
建议方向:
1. 先为当前版本补上前后对比和回滚机制。
2. 主动调度生成的候选复用这套机制,不另起一套预览协议。
3. 用户确认前只看到预览,不修改正式日程。
4. 用户确认后进入现有 service 写入链路;失败时可根据预览快照回滚或提示未应用。
---
## 8. 用户确认与预览预期
第二阶段的用户确认粒度建议按候选项确认,而不是整版黑盒确认。
预览应至少展示:
1. 为什么触发。
2. 当前风险是什么。
3. 建议改变哪一个任务。
4. 改到哪里或准备问用户什么。
5. 不调整会有什么后果。
6. 这个建议是否会影响后继任务。
7. 如果打破了用户偏好,需要明确说明打破了什么、为什么值得这样做。
8. 如果进入 rush 模式,需要明确说明两个任务分别被压缩到什么程度。
确认后才进入正式应用链路。
如果候选是 `ask_user`,用户回答后可以重新触发一次同一链路,但必须带上新的上下文和幂等键,避免重复打扰。
---
## 9. 飞书触达预期
飞书第一版只负责提醒,不承载复杂调度判断。
触达内容应该是:
```text
我发现你接下来 24 小时内有一个任务可能需要安排 / 你反馈的未完成任务需要补救。
我已经生成了一个待确认建议,请回到系统查看。
```
飞书不做:
1. 直接确认日程调整。
2. 直接标记任务完成。
3. 多轮 Agent Chat。
4. 复杂卡片交互。
飞书事件仍建议走:
```text
notification.feishu.requested
```
后续如果要做飞书聊天入口,再单独走 `agent.channel.*`,不要污染通知投递模型。
---
## 10. 完成状态语义
第二阶段采用“默认完成,用户反馈纠偏”的口径。
建议拆成两个产品概念:
```text
assumed_completed
动态任务计划时间过去后,系统默认按已完成推进体验。
用户反馈未完成
用户明确告诉系统任务没有完成,系统再进入补救链路。
```
规则:
1. 动态任务计划时间过去后,默认按 `assumed_completed` 处理。
2. 系统不因为缺少完成证据而主动追问用户。
3. 用户明确反馈没完成后,作为触发上下文进入补救分析;它不一定需要落成新的数据库状态。
4. 补救分析仍只生成预览或候选,不能直接改正式日程。
---
## 11. MVP 验收标准与当前状态
第一版验收按“已落地口径、阶段 5 主验收、后置能力”三类看,避免把后续演进项混进本轮收口。
已落地并作为 MVP 口径的能力:
1. 只扫描滚动 24 小时内的问题。
2. 能识别四象限“重要且紧急”池中尚未进入日程视图的任务。
3. 能输出结构化 `metrics / issues / decision / candidates`
4. 候选项必须来自后端,不让 LLM 自由生成正式写库参数LLM 只做解释、追问和有限选择。
5. 确认前不直接写正式 `schedule_events / schedules`,只写 `active_schedule_previews` 或触达用户。
6. 能发布 `notification.feishu.requested`,并通过 `notification_records` 记录 `sent / failed / dead / skipped` 等状态。
7. 飞书触达后进入 `/assistant/{conversation_id}`,由助手会话页承载预览、微调和确认。
8. 用户确认后才允许进入同步 apply 链路;成功写正式日程,失败回写预览状态和错误原因。
9. `unfinished_feedback` 已支持用户补充事实后的 `ask_user -> rerun -> ready_preview` 闭环。
10. 动态任务计划时间过去后默认按 `assumed_completed` 推进,不主动追问。
阶段 5 主验收重点:
1. preview 过期后 confirm 必须拒绝。
2. 同一 preview 重复 confirm 只能生效一次。
3. 错误 `candidate_id`、跨用户 `preview_id`、preview 与 session 不匹配都必须拒绝。
4. 冲突或不可写入时apply 应失败并保留可排障状态。
5. 相同 `idempotency_key / dedupe_key` 不能重复生成有效 trigger / preview / notification。
6. notification 要覆盖未配置 webhook 的 `skipped`、临时失败的 `failed/retry`、永久失败的 `dead` 和成功的 `sent`
7. 重复消费同一 `notification.feishu.requested` 不能重复投递或重复写 `notification_records`
8. `api / worker / all` 三种启动边界相关 handler / job 注册不能缺失。
后置能力,不作为当前 MVP 完成阻塞:
1. 课程变化触发、疲劳反馈触发和更复杂的跨天全局重排。
2. `compress_with_next_dynamic_task` 压缩融合候选,当前只保留 schema / 口径,不默认生成。
3. apply 成功后的撤销按钮和完整回滚体验。
4. 更细颗粒的偏好打破策略,例如哪些偏好是硬约束、哪些偏好可在高风险任务前让位。
---
## 12. 已确定结论
1. 时间范围采用滚动 24 小时,不按自然日切分。
2. 主动调度不单独管理 DDL 临近阈值,只消费四象限“重要且紧急”任务池。
3. 当前四象限自动轮换是懒加载机制,这是第二阶段需要处理的工程风险。
4. 用户反馈未完成不是必须新增数据库状态;第一版可以先作为触发上下文。
5. 正式应用优先复用现有 service 逻辑,不在主动调度模块里另写一套状态更新权限。
6. 预览能力建议先补齐当前版本的前后对比和回滚机制,再由主动调度复用。
7. 后继挤压重排优先复用并魔改粗排:传入滚动 24 小时时间窗和部分 task item。
8. 局部重排中,用户偏好 weekday / slot 优先遵守;无法达成时允许打破,但要说明代价,并优先把任务压进去。
9. 压缩融合第一版默认 50% / 50%。
10. 候选项第一版限制为 1~3 个。
11. 课程变化触发后置,等调停课 / 补课 / 课程调整机制完善后再接入。
---
## 13. 已收口与后续演进问题
已收口:
1. 主动观测能力当前落在 `backend/active_scheduler` 准独立模块内,主链路走 graph / service pipeline不进入 ReAct 工具循环。
2. 主入口统一到 `/assistant/{conversation_id}`,飞书只负责触达和跳转,不在飞书内完成复杂确认。
3. 预览独立写入 `active_schedule_previews`,不塞进 `agent_schedule_states`;正式 apply 仍走后端重校验。
4. task_pool 任务进入正式日程时使用 `schedule_events.task_source_type=task_pool`,不创建孤儿 `task_item`
后续演进:
1. 四象限懒加载轮换仍建议继续评估后台刷新或抽公共轮换服务,避免主动调度读取到过期任务池。
2. 前后对比与回滚体验还可以继续增强,但不影响当前“确认前只预览、确认后同步 apply”的 MVP 语义。
3. 压缩融合和复杂偏好打破策略暂不打开,等主链路和失败注入脚本稳定后再评估。
4. 课程变化、疲劳反馈和跨天计划优化后置到后续版本。