Version: 0.7.1.dev.260320
🧠 agent智能编排:删除了落库相关逻辑。再次重申:agent智能编排旨在为用户预览排程结果,实际的落库由用户决定,并通过按钮触发常规接口进行落库。目前仅保留 ReAct 精排循环链路(待改进)。 📄 修改了 ReAct 智能精排决策文档相关内容。 🔄 undo:当前 agent 智能排程逻辑待改进。
This commit is contained in:
@@ -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 从对话历史中提取上版排程方案。
|
||||
//
|
||||
// 策略:
|
||||
|
||||
Reference in New Issue
Block a user