Files
smartmate/backend/agent/scheduleplan/state.go
Losita e5b27df80d Version: 0.7.2.dev.260322
feat(schedule-plan):  重构智能排程链路并修复粗排双节对齐问题

-  新增“对话级排程状态持久化”能力:引入 `agent_schedule_states` 模型/DAO,并接入启动迁移
-  智能排程图升级:补齐小幅微调(quick refine)分支,完善预算/并发/状态字段流转
-  预览链路增强:完善排程预览服务读写与桥接逻辑,新增本地预览页 `infra/schedule_preview_viewer.html`
- ♻️ 缓存治理统一:将相关缓存处理收口到 DAO + `cache_deleter` 联动清理,移除旧散落逻辑
- 🐛 修复粗排核心 bug:禁止单节降级,强制双节并按 `1-2/3-4/...` 对齐;修复结束日扫描边界问题
-  新增粗排回归测试:覆盖孤立单节、偶数起点双节、Filler 对齐等关键场景
2026-03-22 13:50:10 +08:00

288 lines
12 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 (
"strings"
"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
// schedulePlanAdjustmentScopeSmall 表示“小改动微调”。
// 语义:优先走快速路径,只做轻量周级调整。
schedulePlanAdjustmentScopeSmall = "small"
// schedulePlanAdjustmentScopeMedium 表示“中等改动微调”。
// 语义:跳过日内拆分,直接进入周级配平。
schedulePlanAdjustmentScopeMedium = "medium"
// schedulePlanAdjustmentScopeLarge 表示“大改动重排”。
// 语义:必要时重新走全量路径(日内并发 + 周级配平)。
schedulePlanAdjustmentScopeLarge = "large"
)
// 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
// RestartRequested 标记本轮是否要求“放弃历史快照并重新排程”。
//
// 语义:
// 1. true强制清空 Previous* 并走全新构建;
// 2. false允许按同会话历史快照做增量微调。
RestartRequested bool
// AdjustmentScope 表示本轮改动力度分级small/medium/large
//
// 分流语义:
// 1. small走快速微调节点再进入周级优化
// 2. medium跳过 daily直接周级优化
// 3. large优先走全量路径多任务类时会经过 daily 并发)。
AdjustmentScope string
// AdjustmentReason 是模型给出的力度判定理由,用于日志排障与 review。
AdjustmentReason string
// AdjustmentConfidence 是模型给出的力度判定置信度0-1
AdjustmentConfidence float64
// 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
// PreviousCandidatePlans 是上一版预览保存的周视图结构化结果。
//
// 用途:
// 1. 连续微调时可直接复用,避免重复转换;
// 2. 兜底展示层(即使本轮未走全量粗排,仍可给前端稳定结构)。
PreviousCandidatePlans []model.UserWeekSchedule
// ── 最终输出 ──
// 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,
AdjustmentScope: schedulePlanAdjustmentScopeLarge,
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)
}
// normalizeAdjustmentScope 归一化排程微调力度字段。
//
// 兜底策略:
// 1. 只接受 small/medium/large
// 2. 任何未知值都回退为 large保证不会误走“过轻”路径。
func normalizeAdjustmentScope(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case schedulePlanAdjustmentScopeSmall:
return schedulePlanAdjustmentScopeSmall
case schedulePlanAdjustmentScopeMedium:
return schedulePlanAdjustmentScopeMedium
default:
return schedulePlanAdjustmentScopeLarge
}
}