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(总分流图 + 智能排程流转图)并补充决策文档 - ⚠️ 新增“连续微调复用”链路我尚未完成测试,且文档状态目前较为混乱,待连续对话微调功能真正测试完成后再统一更新
143 lines
6.0 KiB
Go
143 lines
6.0 KiB
Go
package agentsvc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"strings"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/agent/scheduleplan"
|
||
"github.com/LoveLosita/smartflow/backend/conv"
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"github.com/LoveLosita/smartflow/backend/pkg"
|
||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||
"github.com/cloudwego/eino/schema"
|
||
"github.com/spf13/viper"
|
||
)
|
||
|
||
// runSchedulePlanFlow 执行“智能排程”分支。
|
||
//
|
||
// 职责边界:
|
||
// 1. 负责把本次请求接入 scheduleplan graph,并注入运行依赖。
|
||
// 2. 负责读取对话历史(优先 Redis,未命中再回源 DB)用于连续对话微调。
|
||
// 3. 负责把排程预览快照写入 Redis(供查询接口拉取 JSON)。
|
||
// 4. 负责返回给上层“可直接发给用户的最终文本回复”。
|
||
// 5. 不负责聊天持久化(由 AgentChat 主链路统一处理)。
|
||
func (s *AgentService) runSchedulePlanFlow(
|
||
ctx context.Context,
|
||
selectedModel *ark.ChatModel,
|
||
userMessage string,
|
||
userID int,
|
||
chatID string,
|
||
traceID string,
|
||
extra map[string]any,
|
||
emitStage func(stage, detail string),
|
||
outChan chan<- string,
|
||
modelName string,
|
||
) (string, error) {
|
||
// 1. 依赖预检:缺硬依赖时直接失败,避免进入 graph 后才出现空指针或半途失败。
|
||
// 1.1 SmartPlanningMultiRaw / HybridScheduleWithPlanMulti / ResolvePlanningWindow 任一缺失都无法继续。
|
||
// 1.2 selectedModel 为空时无法执行 LLM 节点,直接返回错误由上层处理。
|
||
if s.SmartPlanningMultiRawFunc == nil || s.HybridScheduleWithPlanMultiFunc == nil || s.ResolvePlanningWindowFunc == nil {
|
||
return "", errors.New("schedule plan service dependencies are not ready")
|
||
}
|
||
if selectedModel == nil {
|
||
return "", errors.New("schedule plan model is nil")
|
||
}
|
||
|
||
// 2. 连续对话微调前置处理:先尝试读取“上一版预览快照”,再清理旧 key。
|
||
// 2.1 先读后删的原因:
|
||
// 2.1.1 若先删再读,会丢失“连续微调起点”;
|
||
// 2.1.2 先读可让本轮在内存中复用上轮 HybridEntries。
|
||
// 2.2 清理旧 key 仍然保留,避免前端在本轮进行中误读到旧结果。
|
||
var previousPreview *model.SchedulePlanPreviewCache
|
||
if s.agentCache != nil {
|
||
preview, getErr := s.agentCache.GetSchedulePlanPreview(ctx, userID, chatID)
|
||
if getErr != nil {
|
||
log.Printf("读取上一版排程预览失败 chat_id=%s: %v", chatID, getErr)
|
||
} else {
|
||
previousPreview = preview
|
||
}
|
||
if delErr := s.agentCache.DeleteSchedulePlanPreview(ctx, userID, chatID); delErr != nil {
|
||
log.Printf("清理旧排程预览失败 chat_id=%s: %v", chatID, delErr)
|
||
}
|
||
}
|
||
|
||
// 3. 读取对话历史:先快后稳。
|
||
// 3.1 先查 Redis,命中则避免回源 DB,降低请求时延。
|
||
// 3.2 Redis 异常仅记录日志,不中断主流程(回源 DB 兜底)。
|
||
var chatHistory []*schema.Message
|
||
if s.agentCache != nil {
|
||
history, err := s.agentCache.GetHistory(ctx, chatID)
|
||
if err != nil {
|
||
log.Printf("获取排程对话历史失败 chat_id=%s: %v", chatID, err)
|
||
} else if history != nil {
|
||
chatHistory = history
|
||
}
|
||
}
|
||
|
||
// 3.3 Redis 未命中时回源 DB,保证链路在缓存波动时仍可用。
|
||
// 3.4 DB 回源失败同样只记日志并继续,让 graph 按“无历史”降级运行。
|
||
if chatHistory == nil && s.repo != nil {
|
||
histories, hisErr := s.repo.GetUserChatHistories(ctx, userID, pkg.HistoryFetchLimitByModel("worker"), chatID)
|
||
if hisErr != nil {
|
||
log.Printf("回源 DB 获取排程对话历史失败 chat_id=%s: %v", chatID, hisErr)
|
||
} else {
|
||
chatHistory = conv.ToEinoMessages(histories)
|
||
}
|
||
}
|
||
|
||
// 4. 执行 graph 主流程。
|
||
// 4.1 这里只负责参数拼装与调用,不在 service 层重复实现 graph 节点逻辑。
|
||
// 4.2 并发度/预算从配置注入,避免把调优参数写死在代码中。
|
||
state := scheduleplan.NewSchedulePlanState(traceID, userID, chatID)
|
||
// 4.3 连续对话微调注入:
|
||
// 4.3.1 若命中上轮预览,则把任务类/混合条目/分配结果注入 state;
|
||
// 4.3.2 这样 rough_build 可按需复用旧底板,避免每轮都重新粗排。
|
||
if previousPreview != nil {
|
||
state.HasPreviousPreview = true
|
||
state.PreviousTaskClassIDs = append([]int(nil), previousPreview.TaskClassIDs...)
|
||
state.PreviousHybridEntries = cloneHybridEntries(previousPreview.HybridEntries)
|
||
state.PreviousAllocatedItems = cloneTaskClassItems(previousPreview.AllocatedItems)
|
||
}
|
||
finalState, runErr := scheduleplan.RunSchedulePlanGraph(ctx, scheduleplan.SchedulePlanGraphRunInput{
|
||
Model: selectedModel,
|
||
State: state,
|
||
Deps: scheduleplan.SchedulePlanToolDeps{
|
||
SmartPlanningMultiRaw: s.SmartPlanningMultiRawFunc,
|
||
HybridScheduleWithPlanMulti: s.HybridScheduleWithPlanMultiFunc,
|
||
ResolvePlanningWindow: s.ResolvePlanningWindowFunc,
|
||
},
|
||
UserMessage: userMessage,
|
||
Extra: extra,
|
||
ChatHistory: chatHistory,
|
||
EmitStage: emitStage,
|
||
OutChan: outChan,
|
||
ModelName: modelName,
|
||
DailyRefineConcurrency: viper.GetInt("agent.dailyRefineConcurrency"),
|
||
WeeklyAdjustBudget: viper.GetInt("agent.weeklyAdjustBudget"),
|
||
})
|
||
if runErr != nil {
|
||
// 4.3 graph 失败直接上抛,由上层决定回落或报错。
|
||
return "", runErr
|
||
}
|
||
|
||
// 5. 组装最终回复文本。
|
||
// 5.1 明确移除“把排程结果序列化成 JSON 文本直接回传”的抽象,
|
||
// 避免在 SSE 聊天链路里吐出原始 JSON,影响前端展示与用户体验。
|
||
// 5.2 当 finalState 为空或 summary 为空时,返回统一兜底文案,保证接口有稳定输出。
|
||
if finalState == nil {
|
||
return "排程流程异常,请稍后重试。", nil
|
||
}
|
||
reply := strings.TrimSpace(finalState.FinalSummary)
|
||
if reply == "" {
|
||
reply = "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
|
||
// 6. 旁路写入排程预览缓存(结构化 JSON),给查询接口拉取。
|
||
// 6.1 失败只记日志,不影响本次对话回复;
|
||
// 6.2 成功后前端可通过 conversation_id 获取 candidate_plans。
|
||
s.saveSchedulePlanPreview(ctx, userID, chatID, finalState)
|
||
return reply, nil
|
||
}
|