Version: 0.7.1.dev.260320

🧠 agent智能编排:删除了落库相关逻辑。再次重申:agent智能编排旨在为用户预览排程结果,实际的落库由用户决定,并通过按钮触发常规接口进行落库。目前仅保留 ReAct 精排循环链路(待改进)。
📄 修改了 ReAct 智能精排决策文档相关内容。
🔄 undo:当前 agent 智能排程逻辑待改进。
This commit is contained in:
LoveLosita
2026-03-20 19:40:11 +08:00
parent d3cec2a5b9
commit 059b25872a
12 changed files with 115 additions and 661 deletions

View File

@@ -14,14 +14,6 @@ const (
schedulePlanGraphNodePlan = "schedule_plan_plan"
// 图节点:调用粗排算法生成候选方案
schedulePlanGraphNodePreview = "schedule_plan_preview"
// 图节点:将候选方案转换为可落库结构
schedulePlanGraphNodeMaterialize = "schedule_plan_materialize"
// 图节点:执行落库
schedulePlanGraphNodeApply = "schedule_plan_apply"
// 图节点:分析失败原因并生成修补方案
schedulePlanGraphNodeReflect = "schedule_plan_reflect"
// 图节点:生成最终回复文案
schedulePlanGraphNodeFinalize = "schedule_plan_finalize"
// 图节点:退出(用于提前终止分支)
schedulePlanGraphNodeExit = "schedule_plan_exit"
// 图节点构建混合日程ReAct 精排前置)
@@ -55,13 +47,9 @@ type SchedulePlanGraphRunInput struct {
//
// 图结构:
//
// START -> plan -> [branch] -> preview -> [branch] -> materialize -> [branch] -> apply -> [branch]
// | | | |
// exit exit exit finalize (成功)
// |
// reflect -> [branch] -> apply (重试)
// |
// finalize (放弃)
// START -> plan -> [branch] -> preview -> [branch] -> hybridBuild -> [branch] -> reactRefine -> returnPreview -> END
// | | |
// exit exit exit
//
// 该文件只负责"连线与分支",节点内部逻辑全部下沉到 nodes.go。
func RunSchedulePlanGraph(ctx context.Context, input SchedulePlanGraphRunInput) (*SchedulePlanState, error) {
@@ -105,18 +93,6 @@ func RunSchedulePlanGraph(ctx context.Context, input SchedulePlanGraphRunInput)
if err := graph.AddLambdaNode(schedulePlanGraphNodePreview, compose.InvokableLambda(runner.previewNode)); err != nil {
return nil, err
}
if err := graph.AddLambdaNode(schedulePlanGraphNodeMaterialize, compose.InvokableLambda(runner.materializeNode)); err != nil {
return nil, err
}
if err := graph.AddLambdaNode(schedulePlanGraphNodeApply, compose.InvokableLambda(runner.applyNode)); err != nil {
return nil, err
}
if err := graph.AddLambdaNode(schedulePlanGraphNodeReflect, compose.InvokableLambda(runner.reflectNode)); err != nil {
return nil, err
}
if err := graph.AddLambdaNode(schedulePlanGraphNodeFinalize, compose.InvokableLambda(runner.finalizeNode)); err != nil {
return nil, err
}
if err := graph.AddLambdaNode(schedulePlanGraphNodeExit, compose.InvokableLambda(runner.exitNode)); err != nil {
return nil, err
}
@@ -148,19 +124,18 @@ func RunSchedulePlanGraph(ctx context.Context, input SchedulePlanGraphRunInput)
return nil, err
}
// 8. preview -> [branch] -> hybridBuild | materialize | exit
// 8. preview -> [branch] -> hybridBuild | exit
if err := graph.AddBranch(schedulePlanGraphNodePreview, compose.NewGraphBranch(
runner.nextAfterPreview,
map[string]bool{
schedulePlanGraphNodeHybridBuild: true,
schedulePlanGraphNodeMaterialize: true,
schedulePlanGraphNodeExit: true,
},
)); err != nil {
return nil, err
}
// 8.1 hybridBuild -> [branch] -> reactRefine | exit
// 9. hybridBuild -> [branch] -> reactRefine | exit
if err := graph.AddBranch(schedulePlanGraphNodeHybridBuild, compose.NewGraphBranch(
runner.nextAfterHybridBuild,
map[string]bool{
@@ -171,62 +146,24 @@ func RunSchedulePlanGraph(ctx context.Context, input SchedulePlanGraphRunInput)
return nil, err
}
// 8.2 reactRefine -> returnPreview固定边
// 10. reactRefine -> returnPreview固定边
if err := graph.AddEdge(schedulePlanGraphNodeReactRefine, schedulePlanGraphNodeReturnPreview); err != nil {
return nil, err
}
// 8.3 returnPreview -> END
// 11. returnPreview -> END
if err := graph.AddEdge(schedulePlanGraphNodeReturnPreview, compose.END); err != nil {
return nil, err
}
// 9. materialize -> [branch] -> apply | exit
if err := graph.AddBranch(schedulePlanGraphNodeMaterialize, compose.NewGraphBranch(
runner.nextAfterMaterialize,
map[string]bool{
schedulePlanGraphNodeApply: true,
schedulePlanGraphNodeExit: true,
},
)); err != nil {
return nil, err
}
// 10. apply -> [branch] -> finalize | reflect
if err := graph.AddBranch(schedulePlanGraphNodeApply, compose.NewGraphBranch(
runner.nextAfterApply,
map[string]bool{
schedulePlanGraphNodeFinalize: true,
schedulePlanGraphNodeReflect: true,
},
)); err != nil {
return nil, err
}
// 11. reflect -> [branch] -> apply (重试) | finalize (放弃)
if err := graph.AddBranch(schedulePlanGraphNodeReflect, compose.NewGraphBranch(
runner.nextAfterReflect,
map[string]bool{
schedulePlanGraphNodeApply: true,
schedulePlanGraphNodeFinalize: true,
},
)); err != nil {
return nil, err
}
// 12. finalize -> END
if err := graph.AddEdge(schedulePlanGraphNodeFinalize, compose.END); err != nil {
return nil, err
}
// 13. exit -> END
// 12. exit -> END
if err := graph.AddEdge(schedulePlanGraphNodeExit, compose.END); err != nil {
return nil, err
}
// 14. 运行步数上限:原有链路 ~10 步 + ReAct 精排(hybridBuild + reactRefine + returnPreview = 3
// 加余量到 25防止异常分支导致无限循环。
maxSteps := 25
// 13. 运行步数上限:plan + preview + hybridBuild + reactRefine + returnPreview = 5 步,
// 加余量到 15防止异常分支导致无限循环。
maxSteps := 15
// 15. 编译图得到可执行实例。
runnable, err := graph.Compile(ctx,

View File

@@ -23,31 +23,6 @@ type schedulePlanIntentOutput struct {
Strategy string `json:"strategy"`
}
// ── materialize 节点模型输出结构 ──
type schedulePlanMaterializeOutput struct {
Assignments []materializeAssignment `json:"assignments"`
UnassignedItemIDs []int `json:"unassigned_item_ids"`
}
type materializeAssignment struct {
TaskItemID int `json:"task_item_id"`
Week int `json:"week"`
DayOfWeek int `json:"day_of_week"`
StartSection int `json:"start_section"`
EndSection int `json:"end_section"`
EmbedCourseEventID int `json:"embed_course_event_id"`
}
// ── reflect 节点模型输出结构 ──
type schedulePlanReflectOutput struct {
Action string `json:"action"`
Reason string `json:"reason"`
PatchedAssignments []materializeAssignment `json:"patched_assignments"`
RemoveItemIDs []int `json:"remove_item_ids"`
}
// ══════════════════════════════════════════════════════════════
// plan 节点
// ══════════════════════════════════════════════════════════════
@@ -191,292 +166,6 @@ func runPreviewNode(
return st, nil
}
// ══════════════════════════════════════════════════════════════
// materialize 节点
// ══════════════════════════════════════════════════════════════
// runMaterializeNode 负责将粗排已分配的任务项转换为可落库结构。
//
// 职责边界:
// 1) 纯代码转换,不调用模型——粗排算法已完成分配,每个 item 的 EmbeddedTime 已回填;
// 2) 直接将 AllocatedItems 转为 BatchApplyPlans 可消费的 SingleTaskClassItem 数组;
// 3) 跳过 EmbeddedTime 为空的项(未成功分配的任务项),并在回复中说明。
func runMaterializeNode(
ctx context.Context,
st *SchedulePlanState,
chatModel *ark.ChatModel,
deps SchedulePlanToolDeps,
emitStage func(stage, detail string),
) (*SchedulePlanState, error) {
if st == nil {
return nil, errors.New("schedule plan graph: nil state in materialize node")
}
if len(st.AllocatedItems) == 0 {
// 无已分配项preview 已设置了 FinalSummary直接透传。
return st, nil
}
emitStage("schedule_plan.materialize.converting", "正在将排程方案转换为可执行计划...")
// 1. 将已分配的任务项直接转换为 BatchApplyPlans 请求结构。
// 粗排算法已在 EmbeddedTime 中回填了 Week/DayOfWeek/SectionFrom/SectionTo
// 这里只做格式映射,不做二次分配。
items := make([]model.SingleTaskClassItem, 0, len(st.AllocatedItems))
skippedCount := 0
for _, allocated := range st.AllocatedItems {
if allocated.EmbeddedTime == nil {
// EmbeddedTime 为空说明粗排未能为该项找到可用槽位,跳过。
skippedCount++
continue
}
items = append(items, model.SingleTaskClassItem{
TaskItemID: allocated.ID,
Week: allocated.EmbeddedTime.Week,
DayOfWeek: allocated.EmbeddedTime.DayOfWeek,
StartSection: allocated.EmbeddedTime.SectionFrom,
EndSection: allocated.EmbeddedTime.SectionTo,
EmbedCourseEventID: 0, // 阶段 1 暂不支持嵌入水课,后续可扩展
})
}
if len(items) == 0 {
st.FinalSummary = "所有任务项均未能分配到可用时间槽,请检查课表或调整时间范围。"
return st, nil
}
st.ApplyRequest = &model.UserInsertTaskClassItemToScheduleRequestBatch{
TaskClassID: st.TaskClassID,
Items: items,
}
detail := fmt.Sprintf("已生成 %d 项排程安排。", len(items))
if skippedCount > 0 {
detail += fmt.Sprintf("%d 项因槽位不足未能安排)", skippedCount)
}
emitStage("schedule_plan.materialize.done", detail)
return st, nil
}
// ══════════════════════════════════════════════════════════════
// apply 节点
// ══════════════════════════════════════════════════════════════
// runApplyNode 负责将排程方案落库。
//
// 职责边界:
// 1) 调用 BatchApplyPlans 服务执行写库;
// 2) 成功时标记 Applied=true
// 3) 失败时记录错误信息,由分支逻辑决定是否进入 reflect 重试。
func runApplyNode(
ctx context.Context,
st *SchedulePlanState,
deps SchedulePlanToolDeps,
emitStage func(stage, detail string),
) (*SchedulePlanState, error) {
if st == nil {
return nil, errors.New("schedule plan graph: nil state in apply node")
}
if st.ApplyRequest == nil || len(st.ApplyRequest.Items) == 0 {
return st, nil
}
emitStage("schedule_plan.apply.persisting", "正在检查冲突并落库排程方案...")
err := deps.BatchApplyPlans(ctx, st.TaskClassID, st.UserID, st.ApplyRequest)
if err != nil {
st.RecordApplyError(err.Error())
if st.CanRetry() {
emitStage("schedule_plan.apply.conflict", fmt.Sprintf("落库失败(第%d次准备调整方案...", st.RetryCount))
} else {
emitStage("schedule_plan.apply.failed", "多次尝试后仍无法落库,请手动调整。")
}
return st, nil
}
st.Applied = true
st.ApplyError = ""
emitStage("schedule_plan.apply.done", "排程方案已成功落库!")
return st, nil
}
// ══════════════════════════════════════════════════════════════
// reflect 节点
// ══════════════════════════════════════════════════════════════
// runReflectNode 负责分析落库失败原因并生成修补方案。
//
// 职责边界:
// 1) 把后端错误信息喂给模型,让模型决定修补策略;
// 2) retry_with_patch重新构建 ApplyRequest 并回到 apply
// 3) partial_apply移除冲突项后重新构建 ApplyRequest
// 4) give_up设置 FinalSummary 并退出。
func runReflectNode(
ctx context.Context,
st *SchedulePlanState,
chatModel *ark.ChatModel,
emitStage func(stage, detail string),
) (*SchedulePlanState, error) {
if st == nil {
return nil, errors.New("schedule plan graph: nil state in reflect node")
}
emitStage("schedule_plan.reflect.analyzing", "正在分析失败原因并调整方案...")
// 1. 构造 prompt包含错误信息和当前方案。
currentPlanJSON, _ := json.Marshal(st.ApplyRequest)
prompt := fmt.Sprintf(`排程落库失败,错误信息:%s
当前排程方案(%d 个任务项):
%s
请分析失败原因并给出修补方案。`,
st.ApplyError,
len(st.ApplyRequest.Items),
string(currentPlanJSON),
)
raw, callErr := callScheduleModelForJSON(ctx, chatModel, SchedulePlanReflectPrompt, prompt, 1024)
if callErr != nil {
// 模型调用失败,直接放弃。
st.ReflectAction = "give_up"
st.FinalSummary = fmt.Sprintf("排程落库失败且无法自动修补:%s。请手动调整排程。", st.ApplyError)
return st, nil
}
parsed, parseErr := parseScheduleJSON[schedulePlanReflectOutput](raw)
if parseErr != nil {
st.ReflectAction = "give_up"
st.FinalSummary = fmt.Sprintf("排程落库失败:%s。请手动调整。", st.ApplyError)
return st, nil
}
st.ReflectAction = strings.TrimSpace(parsed.Action)
switch st.ReflectAction {
case "retry_with_patch":
// 2. 用模型给出的修补方案替换当前请求。
if len(parsed.PatchedAssignments) > 0 {
items := make([]model.SingleTaskClassItem, 0, len(parsed.PatchedAssignments))
for _, a := range parsed.PatchedAssignments {
items = append(items, model.SingleTaskClassItem{
TaskItemID: a.TaskItemID,
Week: a.Week,
DayOfWeek: a.DayOfWeek,
StartSection: a.StartSection,
EndSection: a.EndSection,
EmbedCourseEventID: a.EmbedCourseEventID,
})
}
st.ApplyRequest.Items = items
}
emitStage("schedule_plan.reflect.patched", "已调整方案,准备重新落库。")
case "partial_apply":
// 3. 移除冲突项后重试。
if len(parsed.RemoveItemIDs) > 0 {
removeSet := make(map[int]bool)
for _, id := range parsed.RemoveItemIDs {
removeSet[id] = true
}
filtered := make([]model.SingleTaskClassItem, 0)
for _, item := range st.ApplyRequest.Items {
if !removeSet[item.TaskItemID] {
filtered = append(filtered, item)
}
}
st.ApplyRequest.Items = filtered
}
if len(st.ApplyRequest.Items) == 0 {
st.ReflectAction = "give_up"
st.FinalSummary = "移除冲突项后没有剩余可安排的任务,请检查课表或调整时间范围。"
return st, nil
}
emitStage("schedule_plan.reflect.partial", fmt.Sprintf("已移除冲突项,剩余 %d 项准备落库。", len(st.ApplyRequest.Items)))
default:
// 4. give_up 或未知动作。
reason := strings.TrimSpace(parsed.Reason)
if reason == "" {
reason = st.ApplyError
}
st.FinalSummary = fmt.Sprintf("排程无法自动完成:%s。建议手动调整。", reason)
}
return st, nil
}
// ══════════════════════════════════════════════════════════════
// finalize 节点
// ══════════════════════════════════════════════════════════════
// runFinalizeNode 负责生成最终回复文案。
//
// 职责边界:
// 1) 落库成功时调用模型生成友好摘要;
// 2) 落库失败时透传已有的 FinalSummary
// 3) 将上版方案信息嵌入回复,支持前端在连续对话中回传。
func runFinalizeNode(
ctx context.Context,
st *SchedulePlanState,
chatModel *ark.ChatModel,
emitStage func(stage, detail string),
) (*SchedulePlanState, error) {
if st == nil {
return nil, errors.New("schedule plan graph: nil state in finalize node")
}
// 1. 如果已有 FinalSummary失败场景直接使用。
if strings.TrimSpace(st.FinalSummary) != "" {
return st, nil
}
// 2. 落库未成功时给兜底文案。
if !st.Applied {
st.FinalSummary = "本次排程未能成功落库,请检查任务类配置后重试。"
return st, nil
}
emitStage("schedule_plan.finalize.summarizing", "正在生成排程结果摘要...")
// 3. 调用模型生成友好摘要。
planJSON, _ := json.Marshal(st.ApplyRequest)
constraintsText := "无"
if len(st.Constraints) > 0 {
constraintsText = strings.Join(st.Constraints, "、")
}
prompt := fmt.Sprintf(`排程结果:
- 成功安排 %d 个任务项
- 排程方案:%s
- 用户约束:%s
- 排程意图:%s
请生成结果摘要。`,
len(st.ApplyRequest.Items),
string(planJSON),
constraintsText,
st.UserIntent,
)
raw, callErr := callScheduleModelForJSON(ctx, chatModel, SchedulePlanFinalizePrompt, prompt, 256)
if callErr != nil {
// 模型生成摘要失败,使用固定文案。
st.FinalSummary = fmt.Sprintf("排程完成!已成功安排 %d 个任务项。", len(st.ApplyRequest.Items))
} else {
summary := strings.TrimSpace(raw)
// 移除可能的 JSON 包裹或 markdown。
summary = strings.Trim(summary, "\"'`")
if summary == "" {
summary = fmt.Sprintf("排程完成!已成功安排 %d 个任务项。", len(st.ApplyRequest.Items))
}
st.FinalSummary = summary
}
st.Completed = true
emitStage("schedule_plan.finalize.done", "排程完成!")
return st, nil
}
// ══════════════════════════════════════════════════════════════
// 分支决策函数
// ══════════════════════════════════════════════════════════════
@@ -500,47 +189,6 @@ func selectNextAfterPlan(st *SchedulePlanState) string {
return schedulePlanGraphNodePreview
}
// selectNextAfterApply 根据 apply 节点结果决定下一步。
//
// 分支规则:
// 1) Applied=true -> finalize成功落库
// 2) CanRetry=true -> reflect尝试修补
// 3) CanRetry=false -> finalize重试耗尽由 finalize 输出失败文案)
func selectNextAfterApply(st *SchedulePlanState) string {
if st == nil {
return schedulePlanGraphNodeFinalize
}
if st.Applied {
return schedulePlanGraphNodeFinalize
}
if st.CanRetry() {
return schedulePlanGraphNodeReflect
}
// 重试耗尽,设置失败文案后进入 finalize。
if strings.TrimSpace(st.FinalSummary) == "" {
st.FinalSummary = fmt.Sprintf("排程落库多次失败:%s。请手动调整。", st.ApplyError)
}
return schedulePlanGraphNodeFinalize
}
// selectNextAfterReflect 根据 reflect 节点结果决定下一步。
//
// 分支规则:
// 1) give_up -> finalize
// 2) retry_with_patch / partial_apply -> apply重新落库
func selectNextAfterReflect(st *SchedulePlanState) string {
if st == nil {
return schedulePlanGraphNodeFinalize
}
if st.ReflectAction == "give_up" {
return schedulePlanGraphNodeFinalize
}
if st.ApplyRequest == nil || len(st.ApplyRequest.Items) == 0 {
return schedulePlanGraphNodeFinalize
}
return schedulePlanGraphNodeApply
}
// ══════════════════════════════════════════════════════════════
// 工具函数
// ══════════════════════════════════════════════════════════════
@@ -610,26 +258,6 @@ func parseScheduleJSON[T any](raw string) (*T, error) {
return &out, nil
}
// buildTaskItemsInfo 将任务项列表格式化为模型可读的文本。
func buildTaskItemsInfo(items []model.TaskClassItem) string {
if len(items) == 0 {
return "无任务项"
}
var sb strings.Builder
for i, item := range items {
content := "未命名"
if item.Content != nil && strings.TrimSpace(*item.Content) != "" {
content = strings.TrimSpace(*item.Content)
}
order := i + 1
if item.Order != nil {
order = *item.Order
}
sb.WriteString(fmt.Sprintf("- ID=%d, 序号=%d, 内容=%s\n", item.ID, order, content))
}
return sb.String()
}
// extractPreviousPlanFromHistory 从对话历史中提取上版排程方案。
//
// 策略:

View File

@@ -26,93 +26,6 @@ const (
"strategy": "steady"
}`
// SchedulePlanMaterializePrompt 用于 materialize 节点:
// 将粗排候选方案与任务项列表匹配,生成可落库的结构。
//
// 设计要点:
// 1) 模型负责"选择哪些任务项放到哪些时间槽"
// 2) 后端负责最终校验(冲突检测在 BatchApplyPlans 中执行);
// 3) 输出必须是严格 JSON 数组,每项包含 task_item_id + 时间坐标。
SchedulePlanMaterializePrompt = `你是 SmartFlow 的排程方案转换器。
你将收到两组数据:
1) 粗排算法推荐的可用时间槽列表(按周分组)。
2) 需要安排的任务项列表(每项有 ID 和内容)。
你的任务是把每个任务项分配到一个可用时间槽中。
约束规则:
1) 每个任务项只能分配到一个时间槽。
2) 同一个时间槽不能分配多个任务项。
3) 必须尊重用户约束(如有)。
4) 如果可用槽位不足,优先安排靠前的任务项,剩余的标记为 unassigned。
输出要求:
- 仅输出 JSON不要 markdown不要解释。
- 格式如下:
{
"assignments": [
{
"task_item_id": 1,
"week": 1,
"day_of_week": 1,
"start_section": 3,
"end_section": 4,
"embed_course_event_id": 0
}
],
"unassigned_item_ids": [5, 6]
}`
// SchedulePlanReflectPrompt 用于 reflect 节点:分析落库失败原因并生成修补方案。
//
// 设计要点:
// 1) 模型收到后端错误信息,决定修补策略;
// 2) 可选动作retry_with_patch换槽位重试、partial_apply跳过冲突项、give_up放弃
// 3) 修补方案必须是结构化 JSON后端直接消费。
SchedulePlanReflectPrompt = `你是 SmartFlow 的排程修补分析器。
排程方案落库失败了,请分析失败原因并给出修补方案。
你可以选择以下动作之一:
1) "retry_with_patch":修改冲突项的时间槽后重试。
2) "partial_apply":跳过冲突项,只落库不冲突的部分。
3) "give_up":放弃本次排程,向用户解释原因。
输出要求:
- 仅输出 JSON不要 markdown不要解释。
- 格式如下:
{
"action": "retry_with_patch|partial_apply|give_up",
"reason": "简短原因",
"patched_assignments": [
{
"task_item_id": 1,
"week": 1,
"day_of_week": 2,
"start_section": 5,
"end_section": 6,
"embed_course_event_id": 0
}
],
"remove_item_ids": [3]
}`
// SchedulePlanFinalizePrompt 用于 finalize 节点:生成用户友好的排程结果摘要。
//
// 设计要点:
// 1) 以事实为主(成功安排了几项、哪些时间段);
// 2) 提及用户约束是否被满足;
// 3) 若有未安排的项目,给出原因和建议。
SchedulePlanFinalizePrompt = `你是 SmartFlow 的排程结果播报员。
请根据排程结果,生成一段简洁友好的中文摘要回复给用户。
要求:
1) 说明成功安排了多少个任务项。
2) 简要描述时间分布(如"分布在第1~3周主要集中在工作日下午")。
3) 如果有未安排的项目,说明原因。
4) 如果用户有约束(如"早八不排"),确认是否已遵守。
5) 语气自然友好不超过100字。
6) 不要输出 markdown 或列表格式,只输出纯文本。`
// SchedulePlanReactSystemPrompt 用于 ReAct 精排节点:
// LLM 开启深度思考,通过 Tool 调用对粗排结果进行语义化优化。
//

View File

@@ -16,7 +16,7 @@ import (
// reactRoundTimeout 是单轮 ReAct 的超时时间。
// 深度思考模式下 reasoning 阶段可能耗时较长,需要给足时间。
const reactRoundTimeout = 5 * time.Minute
const reactRoundTimeout = 15 * time.Minute
// runReactRefineNode 执行 ReAct 精排循环。
//

View File

@@ -4,7 +4,6 @@ import (
"context"
"github.com/cloudwego/eino-ext/components/model/ark"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
@@ -60,27 +59,6 @@ func (r *schedulePlanRunner) previewNode(ctx context.Context, st *SchedulePlanSt
return runPreviewNode(ctx, st, r.deps, r.emitStage)
}
func (r *schedulePlanRunner) materializeNode(ctx context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
return runMaterializeNode(ctx, st, r.chatModel, r.deps, r.emitStage)
}
func (r *schedulePlanRunner) applyNode(ctx context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
return runApplyNode(ctx, st, r.deps, r.emitStage)
}
func (r *schedulePlanRunner) reflectNode(ctx context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
return runReflectNode(ctx, st, r.chatModel, r.emitStage)
}
func (r *schedulePlanRunner) finalizeNode(ctx context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
return runFinalizeNode(ctx, st, r.chatModel, r.emitStage)
}
func (r *schedulePlanRunner) exitNode(_ context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
// exit 节点不做任何业务逻辑,仅把当前状态原样透传到 END。
return st, nil
}
// ── ReAct 精排节点适配层 ──
func (r *schedulePlanRunner) hybridBuildNode(ctx context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
@@ -95,42 +73,27 @@ func (r *schedulePlanRunner) returnPreviewNode(ctx context.Context, st *Schedule
return runReturnPreviewNode(ctx, st, r.emitStage)
}
func (r *schedulePlanRunner) exitNode(_ context.Context, st *SchedulePlanState) (*SchedulePlanState, error) {
// exit 节点不做任何业务逻辑,仅把当前状态原样透传到 END。
return st, nil
}
// ── 分支决策适配层 ──
func (r *schedulePlanRunner) nextAfterPlan(_ context.Context, st *SchedulePlanState) (string, error) {
return selectNextAfterPlan(st), nil
}
func (r *schedulePlanRunner) nextAfterMaterialize(_ context.Context, st *SchedulePlanState) (string, error) {
// materialize 后:有 ApplyRequest 则去 apply否则去 exit。
if st == nil || st.ApplyRequest == nil || len(st.ApplyRequest.Items) == 0 {
return schedulePlanGraphNodeExit, nil
}
return schedulePlanGraphNodeApply, nil
}
func (r *schedulePlanRunner) nextAfterApply(_ context.Context, st *SchedulePlanState) (string, error) {
return selectNextAfterApply(st), nil
}
func (r *schedulePlanRunner) nextAfterReflect(_ context.Context, st *SchedulePlanState) (string, error) {
return selectNextAfterReflect(st), nil
}
// nextAfterPreview 根据 preview 结果决定下一步。
//
// 分支规则:
// 1) preview 失败(无候选方案)-> exit
// 2) HybridScheduleWithPlan 已注入 -> hybridBuild ReAct 精排路径)
// 3) 否则 -> materialize走原有落库路径向后兼容
// 2) 否则 -> hybridBuild进入 ReAct 精排路径)
func (r *schedulePlanRunner) nextAfterPreview(_ context.Context, st *SchedulePlanState) (string, error) {
if st == nil || len(st.CandidatePlans) == 0 {
return schedulePlanGraphNodeExit, nil
}
if r.deps.HybridScheduleWithPlan != nil {
return schedulePlanGraphNodeHybridBuild, nil
}
return schedulePlanGraphNodeMaterialize, nil
return schedulePlanGraphNodeHybridBuild, nil
}
// nextAfterHybridBuild 根据 hybridBuild 结果决定下一步。
@@ -140,8 +103,3 @@ func (r *schedulePlanRunner) nextAfterHybridBuild(_ context.Context, st *Schedul
}
return schedulePlanGraphNodeReactRefine, nil
}
// nextAfterFinalize 用于 finalize 分支——固定结束。
func (r *schedulePlanRunner) nextAfterFinalize(_ context.Context, _ *SchedulePlanState) (string, error) {
return compose.END, nil
}

View File

@@ -44,7 +44,7 @@ type SchedulePlanState struct {
// CandidatePlans 是粗排算法生成的候选方案(展示型结构,供 SSE 推送给前端预览)。
CandidatePlans []model.UserWeekSchedule
// AllocatedItems 是粗排算法已分配的任务项EmbeddedTime 已回填),供 materialize 直接转换
// AllocatedItems 是粗排算法已分配的任务项EmbeddedTime 已回填),供 ReAct 精排使用
AllocatedItems []model.TaskClassItem
// ── ReAct 精排阶段 ──
@@ -61,27 +61,6 @@ type SchedulePlanState struct {
// 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 序列化,用于增量微调。
@@ -107,23 +86,11 @@ func NewSchedulePlanState(traceID string, userID int, conversationID string) *Sc
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)

View File

@@ -18,22 +18,10 @@ type SchedulePlanToolDeps struct {
// SmartPlanningRaw 调用粗排算法,同时返回展示结构和已分配的任务项。
// 返回值:
// - []UserWeekSchedule展示型结构供 SSE 阶段推送给前端预览;
// - []TaskClassItem已分配的任务项EmbeddedTime 已回填),供 materialize 直接转换
// - []TaskClassItem已分配的任务项EmbeddedTime 已回填),供 ReAct 精排使用
SmartPlanningRaw func(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)
// BatchApplyPlans 将排程方案批量落库。
// 输入taskClassID、userID、落库请求体。
// 输出errornil 表示全部成功)。
BatchApplyPlans func(ctx context.Context, taskClassID, userID int, plans *model.UserInsertTaskClassItemToScheduleRequestBatch) error
// GetTaskClassByID 获取任务类详情(含关联的 Items
// 用于:
// 1) 校验 task_class_id 合法性;
// 2) 获取 Items 列表,为连续对话微调提供上下文。
GetTaskClassByID func(ctx context.Context, taskClassID, userID int) (*model.TaskClass, error)
// HybridScheduleWithPlan 构建混合日程(既有日程 + 粗排建议),供 ReAct 精排使用。
// 可选依赖:未注入时 ReAct 精排阶段不可用,走原有 materialize 路径。
HybridScheduleWithPlan func(ctx context.Context, userID, taskClassID int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
}
@@ -42,11 +30,8 @@ func (d SchedulePlanToolDeps) validate() error {
if d.SmartPlanningRaw == nil {
return errors.New("schedule plan tool deps: SmartPlanningRaw is nil")
}
if d.BatchApplyPlans == nil {
return errors.New("schedule plan tool deps: BatchApplyPlans is nil")
}
if d.GetTaskClassByID == nil {
return errors.New("schedule plan tool deps: GetTaskClassByID is nil")
if d.HybridScheduleWithPlan == nil {
return errors.New("schedule plan tool deps: HybridScheduleWithPlan is nil")
}
return nil
}

View File

@@ -98,7 +98,7 @@ func Start() {
courseService := service.NewCourseService(courseRepo, scheduleRepo)
taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo, manager)
scheduleService := service.NewScheduleService(scheduleRepo, userRepo, taskClassRepo, manager, cacheRepo)
agentService := service.NewAgentServiceWithSchedule(aiHub, agentRepo, taskRepo, agentCacheRepo, eventBus, scheduleService, taskClassService)
agentService := service.NewAgentServiceWithSchedule(aiHub, agentRepo, taskRepo, agentCacheRepo, eventBus, scheduleService)
// API 层初始化。
userApi := api.NewUserHandler(userService)

View File

@@ -1,12 +1,9 @@
package service
import (
"context"
"github.com/LoveLosita/smartflow/backend/dao"
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
"github.com/LoveLosita/smartflow/backend/inits"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/service/agentsvc"
)
@@ -28,7 +25,7 @@ func NewAgentService(aiHub *inits.AIHub, repo *dao.AgentDAO, taskRepo *dao.TaskD
// NewAgentServiceWithSchedule 在基础 AgentService 上注入排程依赖。
//
// 设计目的:
// 1) 通过函数注入避免 agentsvc 包直接依赖 service 层的 ScheduleService / TaskClassService
// 1) 通过函数注入避免 agentsvc 包直接依赖 service 层的 ScheduleService
// 2) 排程依赖为可选:未注入时排程路由自动回退到普通聊天;
// 3) 保持 NewAgentService 签名不变,向下兼容。
func NewAgentServiceWithSchedule(
@@ -38,7 +35,6 @@ func NewAgentServiceWithSchedule(
agentRedis *dao.AgentCache,
eventPublisher outboxinfra.EventPublisher,
scheduleSvc *ScheduleService,
taskClassSvc *TaskClassService,
) *AgentService {
svc := agentsvc.NewAgentService(aiHub, repo, taskRepo, agentRedis, eventPublisher)
@@ -47,13 +43,6 @@ func NewAgentServiceWithSchedule(
svc.SmartPlanningRawFunc = scheduleSvc.SmartPlanningRaw
svc.HybridScheduleWithPlanFunc = scheduleSvc.HybridScheduleWithPlan
}
if taskClassSvc != nil {
svc.BatchApplyPlansFunc = taskClassSvc.BatchApplyPlans
// GetTaskClassByID 复用 TaskClassService 内部的 DAO 调用。
svc.GetTaskClassByIDFunc = func(ctx context.Context, taskClassID, userID int) (*model.TaskClass, error) {
return taskClassSvc.GetCompleteTaskClassByID(ctx, taskClassID, userID)
}
}
return svc
}

View File

@@ -32,14 +32,8 @@ type AgentService struct {
// SmartPlanningRawFunc 调用粗排算法,同时返回展示结构和已分配的任务项。
// 由 service/agent_bridge.go 在构造时注入 ScheduleService.SmartPlanningRaw。
SmartPlanningRawFunc func(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)
// BatchApplyPlansFunc 将排程方案批量落库。
// 由 service/agent_bridge.go 在构造时注入 TaskClassService.BatchApplyPlans。
BatchApplyPlansFunc func(ctx context.Context, taskClassID, userID int, plans *model.UserInsertTaskClassItemToScheduleRequestBatch) error
// GetTaskClassByIDFunc 获取任务类详情(含 Items
// 由 service/agent_bridge.go 在构造时注入。
GetTaskClassByIDFunc func(ctx context.Context, taskClassID, userID int) (*model.TaskClass, error)
// HybridScheduleWithPlanFunc 构建混合日程(既有日程 + 粗排建议),供 ReAct 精排使用。
// 由 service/agent_bridge.go 在构造时注入。可选:未注入时走原有 materialize 路径。
// 由 service/agent_bridge.go 在构造时注入。
HybridScheduleWithPlanFunc func(ctx context.Context, userID, taskClassID int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
}

View File

@@ -33,7 +33,7 @@ func (s *AgentService) runSchedulePlanFlow(
modelName string,
) (string, error) {
// 1. 依赖预检:排程依赖函数必须注入,否则无法完成排程链路。
if s.SmartPlanningRawFunc == nil || s.BatchApplyPlansFunc == nil || s.GetTaskClassByIDFunc == nil {
if s.SmartPlanningRawFunc == nil || s.HybridScheduleWithPlanFunc == nil {
return "", errors.New("schedule plan service dependencies are not ready")
}
if selectedModel == nil {
@@ -71,8 +71,6 @@ func (s *AgentService) runSchedulePlanFlow(
State: state,
Deps: scheduleplan.SchedulePlanToolDeps{
SmartPlanningRaw: s.SmartPlanningRawFunc,
BatchApplyPlans: s.BatchApplyPlansFunc,
GetTaskClassByID: s.GetTaskClassByIDFunc,
HybridScheduleWithPlan: s.HybridScheduleWithPlanFunc,
},
UserMessage: userMessage,