Files
smartmate/backend/agent/scheduleplan/state.go
Losita f3f9902e93 Version: 0.7.1.dev.260321
feat(agent):  重构智能排程分流与双通道交付,补齐周级预算并接入连续微调复用

- 🔀 通用路由升级为 action 分流(chat/quick_note_create/task_query/schedule_plan),路由失败直接返回内部错误,不再回落聊天
- 🧭 智能排程链路重构:统一图编排与节点职责,完善日级/周级调优协作与提示词约束
- 📊 周级预算改为“有效周保底 + 负载加权分配”,避免有效周零预算并提升资源利用率
- ⚙️ 日级并发优化细化:按天拆分 DayGroup 并发执行,低收益天(suggested<=2)跳过,单天失败仅回退该天结果并继续全局
- 🧵 周级并发优化细化:按周并发 worker 执行,单周“单步动作”循环(每轮仅 1 个 Move/Swap 或 done),失败周保留原方案不影响其它周
- 🛰️ 新增排程预览双通道:聊天主链路输出终审文本,结构化 candidate_plans 通过 /api/v1/agent/schedule-preview 拉取
- 🗃️ 增补 Redis 预览缓存读写与清理逻辑,新增对应 API、路由、模型与错误码支持
- ♻️ 接入连续对话微调复用:命中同会话历史预览时复用上轮 HybridEntries,避免每轮重跑粗排
- 🛡️ 增加复用保护:仅当本轮与上轮 task_class_ids 集合一致才复用;不一致回退全量粗排
- 🧰 扩展预览缓存字段(task_class_ids/hybrid_entries/allocated_items),支撑微调承接链路
- 🗺️ 更新 README 5.4 Mermaid(总分流图 + 智能排程流转图)并补充决策文档

- ⚠️ 新增“连续微调复用”链路我尚未完成测试,且文档状态目前较为混乱,待连续对话微调功能真正测试完成后再统一更新
2026-03-21 22:08:35 +08:00

237 lines
9.9 KiB
Go
Raw 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.
package scheduleplan
import (
"time"
"github.com/LoveLosita/smartflow/backend/model"
)
const (
// schedulePlanTimezoneName 是排程链路默认业务时区。
// 与随口记保持一致,固定东八区,避免容器运行在 UTC 导致"明天/今晚"偏移。
schedulePlanTimezoneName = "Asia/Shanghai"
// schedulePlanDatetimeLayout 是排程链路内部统一的分钟级时间格式。
schedulePlanDatetimeLayout = "2006-01-02 15:04"
// schedulePlanDefaultDailyRefineConcurrency 是日内并发优化默认并发度。
// 这里给一个保守默认值,避免未配置时直接把模型并发打满导致限流。
schedulePlanDefaultDailyRefineConcurrency = 3
// schedulePlanDefaultWeeklyAdjustBudget 是周级配平默认调整额度。
// 额度存在的目的:
// 1. 防止周级 ReAct 过度调整导致震荡;
// 2. 控制 token 与时延成本;
// 3. 让方案改动更可解释。
schedulePlanDefaultWeeklyAdjustBudget = 5
// schedulePlanDefaultWeeklyTotalBudget 是周级“总尝试次数”默认预算。
//
// 设计意图:
// 1. 总预算统计“动作尝试次数”(成功/失败都记一次);
// 2. 有效预算统计“成功动作次数”(仅成功时记一次);
// 3. 通过双预算把“探索次数”和“有效改动次数”分离,降低模型无效空转成本。
schedulePlanDefaultWeeklyTotalBudget = 8
// schedulePlanDefaultWeeklyRefineConcurrency 是周级“按周并发”默认并发度。
// 说明:
// 1. 周级输入规模通常比单天更大,默认并发度不宜过高,避免触发模型侧限流;
// 2. 可在运行时按请求状态覆盖。
schedulePlanDefaultWeeklyRefineConcurrency = 2
)
// DayGroup 是“按天拆分后”的最小优化单元。
//
// 设计目的:
// 1. 把全量周视角数据拆成“单天小包”,降低日内 ReAct 输入规模;
// 2. 支持并发优化不同天的数据,缩短整体等待;
// 3. 通过 SkipRefine 让低收益天数直接跳过,节省模型调用成本。
type DayGroup struct {
Week int
DayOfWeek int
Entries []model.HybridScheduleEntry
SkipRefine bool
}
// SchedulePlanState 是“智能排程”链路在 graph 节点间传递的统一状态容器。
//
// 设计目标:
// 1) 收拢排程请求全生命周期的上下文,降低节点间参数散落;
// 2) 支持“粗排 -> 日内并发优化 -> 周级配平 -> 终审校验”的完整链路追踪;
// 3) 支持连续对话微调:保留上版方案 + 本次约束变更,便于增量重排。
type SchedulePlanState struct {
// ── 基础上下文 ──
TraceID string
UserID int
ConversationID string
RequestNow time.Time
RequestNowText string
// ── plan 节点输出 ──
// UserIntent 是模型对用户排程意图的结构化摘要(如"帮我安排高数复习计划")。
UserIntent string
// Constraints 是用户提出的硬约束列表(如 ["早八不排", "周末休息"])。
Constraints []string
// TaskClassIDs 是本次请求携带的任务类集合(统一主语义)。
//
// 设计说明:
// 1. 这里明确不再维护单值 task_class_id避免“单值和切片同时存在”导致语义漂移
// 2. 分流依据统一为 len(TaskClassIDs)
// 2.1 len==1跳过 daily 并发,直接进入 weekly refine
// 2.2 len>=2进入 daily 并发后再 weekly refine
// 3. 输入清洗(去重、过滤非法值)由 plan 节点完成,这里只承载最终状态。
TaskClassIDs []int
// Strategy 是排程策略steady/rapid默认 steady。
Strategy string
// TaskTags 是“任务项 ID -> 认知类型标签”的映射。
// 使用 ID 而不是名称,目的是规避“同名任务”带来的映射冲突。
TaskTags map[int]string
// TaskTagHintsByName 是“任务名称 -> 认知类型标签”的临时映射。
// 该字段只作为 plan 输出兼容层:
// 1. 若模型暂时给不出 task_item_id只给名称
// 2. 后续在 hybridBuild/dailySplit 阶段再转换为 TaskTags(ID 维度)。
TaskTagHintsByName map[string]string
// ── preview 节点输出 ──
// CandidatePlans 是粗排算法生成的候选方案(展示型结构,供后续节点做预览与总结)。
CandidatePlans []model.UserWeekSchedule
// AllocatedItems 是粗排算法已分配的任务项EmbeddedTime 已回填),供 ReAct 精排使用。
AllocatedItems []model.TaskClassItem
// HasPlanningWindow 标记是否成功解析出“任务类时间窗”的相对周/天边界。
//
// 语义:
// 1. truePlanStart*/PlanEnd* 字段可用于 Move 工具的硬边界校验;
// 2. false表示当前运行未拿到窗口信息例如依赖未注入工具层将仅做基础校验。
HasPlanningWindow bool
// PlanStartWeek / PlanStartDay 表示全局排程窗口起点(相对周/天)。
PlanStartWeek int
PlanStartDay int
// PlanEndWeek / PlanEndDay 表示全局排程窗口终点(相对周/天)。
PlanEndWeek int
PlanEndDay int
// ── 日内并发优化阶段 ──
// DailyGroups 是按 (week, day) 拆分后的单日优化输入。
// 结构week -> day -> DayGroup。
DailyGroups map[int]map[int]*DayGroup
// DailyResults 是单日优化输出。
// 结构week -> day -> []HybridScheduleEntry。
DailyResults map[int]map[int][]model.HybridScheduleEntry
// DailyRefineConcurrency 是日内并发优化的并发度。
// 说明:该值由配置注入,可按环境调节。
DailyRefineConcurrency int
// ── 周级 ReAct 精排阶段 ──
// HybridEntries 是混合日程条目列表包含既有日程existing和粗排建议suggested
// 周级 ReAct 工具直接在此切片上操作(内存修改,不涉及 DB
HybridEntries []model.HybridScheduleEntry
// MergeSnapshot 是 merge 后快照。
// 终审失败时回退到该快照,确保至少保留“日内优化成果”。
MergeSnapshot []model.HybridScheduleEntry
// ReactRound 当前周级 ReAct 循环轮次。
ReactRound int
// ReactMaxRound 周级 ReAct 最大循环轮次。
ReactMaxRound int
// ReactSummary 周级 ReAct 输出的优化摘要。
ReactSummary string
// ReactDone 标记周级 ReAct 是否已完成。
ReactDone bool
// WeeklyAdjustBudget 是周级跨天调整额度上限。
// 语义:有效动作预算(仅工具调用成功时扣减)。
WeeklyAdjustBudget int
// WeeklyAdjustUsed 是周级跨天调整已使用额度。
// 语义:有效动作已使用次数(仅成功调用时递增)。
WeeklyAdjustUsed int
// WeeklyTotalBudget 是周级总动作预算。
// 语义:总尝试次数预算(成功/失败都扣减)。
WeeklyTotalBudget int
// WeeklyTotalUsed 是周级总动作已使用次数。
// 语义:成功/失败每执行一次工具调用都递增。
WeeklyTotalUsed int
// WeeklyRefineConcurrency 是周级“按周并发”并发度。
WeeklyRefineConcurrency int
// WeeklyActionLogs 记录周级优化阶段的关键动作流水。
//
// 设计目的:
// 1. 供 final_check 的总结模型理解“优化过程”,而非只看最终静态结果;
// 2. 供调试排查时快速回放“每轮做了什么动作、是否成功、为何失败”。
WeeklyActionLogs []string
// ── 连续对话微调 ──
// PreviousPlanJSON 是上一版已落库方案的 JSON 序列化,用于增量微调。
// 从对话历史中提取,不做持久化。
PreviousPlanJSON string
// IsAdjustment 标记本次是否为微调请求(而非全新排程)。
IsAdjustment bool
// HasPreviousPreview 标记是否命中“同会话上一次排程预览快照”。
//
// 语义:
// 1. true可以尝试复用上次 HybridEntries 作为本轮优化起点;
// 2. false按全新排程路径构建粗排底板。
HasPreviousPreview bool
// PreviousTaskClassIDs 是上一次预览对应的任务类集合。
//
// 用途:
// 1. 本轮未显式传 task_class_ids 时作为兜底;
// 2. 仅会话内承接,不改动数据库。
PreviousTaskClassIDs []int
// PreviousHybridEntries 是上一次预览保存的混合日程条目。
//
// 用途:
// 1. 连续对话微调时直接复用,避免重新粗排;
// 2. 若为空则回退到粗排构建路径。
PreviousHybridEntries []model.HybridScheduleEntry
// PreviousAllocatedItems 是上一次预览保存的任务块分配结果。
//
// 用途:
// 1. 保持 final_check 的数量核对口径稳定;
// 2. return_preview 阶段可继续回填 embedded_time。
PreviousAllocatedItems []model.TaskClassItem
// ── 最终输出 ──
// FinalSummary 是 graph 最终给用户的回复文案。
FinalSummary string
// Completed 标记整个排程链路是否成功完成。
Completed bool
}
// NewSchedulePlanState 创建排程状态对象并初始化默认值。
func NewSchedulePlanState(traceID string, userID int, conversationID string) *SchedulePlanState {
now := schedulePlanNowToMinute()
return &SchedulePlanState{
TraceID: traceID,
UserID: userID,
ConversationID: conversationID,
RequestNow: now,
RequestNowText: now.In(schedulePlanLocation()).Format(schedulePlanDatetimeLayout),
Strategy: "steady",
TaskTags: make(map[int]string),
TaskTagHintsByName: make(map[string]string),
DailyRefineConcurrency: schedulePlanDefaultDailyRefineConcurrency,
WeeklyRefineConcurrency: schedulePlanDefaultWeeklyRefineConcurrency,
ReactMaxRound: 2,
WeeklyAdjustBudget: schedulePlanDefaultWeeklyAdjustBudget,
WeeklyTotalBudget: schedulePlanDefaultWeeklyTotalBudget,
}
}
// schedulePlanLocation 返回排程链路使用的业务时区。
func schedulePlanLocation() *time.Location {
loc, err := time.LoadLocation(schedulePlanTimezoneName)
if err != nil {
return time.Local
}
return loc
}
// schedulePlanNowToMinute 返回当前时间并截断到分钟级。
func schedulePlanNowToMinute() time.Time {
return time.Now().In(schedulePlanLocation()).Truncate(time.Minute)
}