Version: 0.7.0.dev.260319

 feat(agent): 新增智能排程 Agent 全链路 + ReAct 精排引擎

  🏗️ 智能排程 Graph 编排(阶段 1 基础链路)
  - 新增 scheduleplan 包:state / tool / prompt / nodes / runner / graph 六件套
  - 实现 plan → preview → materialize → apply → reflect → finalize 完整图编排
  - 通过函数注入解耦 agent 层与 service 层,避免循环依赖
  - 路由层新增 schedule_plan 动作,复用现有 SSE + 持久化链路

  🧠 ReAct 精排引擎(阶段 1.5 语义化微调)
  - 粗排后构建"混合日程"(既有课程 + 建议任务),统一为 HybridScheduleEntry
  - LLM 开启深度思考,通过 Swap / Move / TimeAvailable / GetAvailableSlots 四个 Tool 在内存中优化任务时间
  - reasoning_content 实时流式推送前端,用户可见 AI 思考过程
  - 精排结果仅预览不落库,向后兼容(未注入依赖时走原有 materialize 路径)

  📝 文档
  - 新增 ReAct 精排引擎决策记录

  ⚠️ 已知问题:深度思考模式耗时较长,超时策略待优化
This commit is contained in:
Losita
2026-03-19 23:16:35 +08:00
parent cd95aeeaaa
commit d3cec2a5b9
24 changed files with 2737 additions and 24 deletions

View File

@@ -0,0 +1,139 @@
package scheduleplan
import (
"time"
"github.com/LoveLosita/smartflow/backend/model"
)
const (
// schedulePlanTimezoneName 是排程链路默认业务时区。
// 与随口记保持一致,固定东八区,避免容器运行在 UTC 导致"明天/今晚"偏移。
schedulePlanTimezoneName = "Asia/Shanghai"
// schedulePlanDatetimeLayout 是排程链路内部统一的分钟级时间格式。
schedulePlanDatetimeLayout = "2006-01-02 15:04"
)
// SchedulePlanState 是"智能排程"链路在 graph 节点间传递的统一状态容器。
//
// 设计目标:
// 1) 收拢排程请求全生命周期的上下文降低节点间参数散<E695B0><E695A3><EFBFBD>
// 2) 支持"粗排 -> 校验 -> 修补重试 -> 落库"的完整链路追踪;
// 3) 支持连续对话微调:保留上版方案 + 本次约束变更,便于增量重排。
type SchedulePlanState struct {
// ── 基础上下文 ──
TraceID string
UserID int
ConversationID string
RequestNow time.Time
RequestNowText string
// ── plan 节点输出 ──
// UserIntent 是模型对用户排程意图的结构化摘要(如"帮我安排高数复习计划")。
UserIntent string
// Constraints 是用户提出的硬约束列表(如 ["早八不排", "周末休息"])。
Constraints []string
// TaskClassID 是目标任务类 ID由 Extra 字段或模型抽取获得。
TaskClassID int
// Strategy 是排程策略steady/rapid默认 steady。
Strategy string
// ── preview 节点输出 ──
// CandidatePlans 是粗排算法生成的候选方案(展示型结构,供 SSE 推送给前端预览)。
CandidatePlans []model.UserWeekSchedule
// AllocatedItems 是粗排算法已分配的任务项EmbeddedTime 已回填),供 materialize 直接转换。
AllocatedItems []model.TaskClassItem
// ── ReAct 精排阶段 ──
// HybridEntries 是混合日程条目列表包含既有日程existing和粗排建议suggested
// ReAct 工具直接在此切片上操作(内存修改,不涉及 DB
HybridEntries []model.HybridScheduleEntry
// ReactRound 当前 ReAct 循环轮次。
ReactRound int
// ReactMaxRound 最大循环轮次(建议 3
ReactMaxRound int
// ReactSummary LLM 输出的优化摘要。
ReactSummary string
// ReactDone 标记 ReAct 是否已完成。
ReactDone bool
// ── materialize 节点输出 ──
// ApplyRequest 是转换后的落库请求体。
ApplyRequest *model.UserInsertTaskClassItemToScheduleRequestBatch
// ── apply 节点输出 ──
// Applied 标记是否落库成功。
Applied bool
// ApplyError 记录落库失败的错误信息,供 reflect 节点分析。
ApplyError string
// ── reflect 节点状态 ──
// RetryCount 记录当前重试次数。
RetryCount int
// MaxRetry 是最大重试次数(建议 = 2
MaxRetry int
// ReflectAction 记录模型给出的修补动作retry_with_patch / partial_apply / give_up
ReflectAction string
// ── 连续对话微调 ──
// PreviousPlanJSON 是上一版已落库方案的 JSON 序列化,用于增量微调。
// 从对话历史中提取,不做持久化。
PreviousPlanJSON string
// IsAdjustment 标记本次是否为微调请求(而非全新排程)。
IsAdjustment bool
// ── 最终输出 ──
// 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),
MaxRetry: 2,
Strategy: "steady",
ReactMaxRound: 3,
}
}
// CanRetry 判断当前是否还能继续重试落库。
func (s *SchedulePlanState) CanRetry() bool {
return s.RetryCount < s.MaxRetry
}
// RecordApplyError 记录一次落库失败。
func (s *SchedulePlanState) RecordApplyError(errMsg string) {
s.RetryCount++
s.ApplyError = errMsg
}
// 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)
}