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:
@@ -26,6 +26,21 @@ type AgentService struct {
|
||||
taskRepo *dao.TaskDAO
|
||||
agentCache *dao.AgentCache
|
||||
eventPublisher outboxinfra.EventPublisher
|
||||
|
||||
// ── 排程计划依赖(函数注入,避免 service 包循环依赖)──
|
||||
|
||||
// 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 路径。
|
||||
HybridScheduleWithPlanFunc func(ctx context.Context, userID, taskClassID int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)
|
||||
}
|
||||
|
||||
// NewAgentService 构造 AgentService。
|
||||
@@ -233,7 +248,7 @@ func (s *AgentService) runNormalChatFlow(
|
||||
s.ensureConversationTitleAsync(userID, chatID)
|
||||
}
|
||||
|
||||
func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThinking bool, modelName string, userID int, chatID string) (<-chan string, <-chan error) {
|
||||
func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThinking bool, modelName string, userID int, chatID string, extra map[string]any) (<-chan string, <-chan error) {
|
||||
requestStart := time.Now()
|
||||
traceID := uuid.NewString()
|
||||
|
||||
@@ -366,7 +381,27 @@ func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThin
|
||||
return
|
||||
}
|
||||
|
||||
// 3.6 未知 action 兜底:走普通聊天,保证可用性。
|
||||
// 3.6 schedule_plan:执行智能排程 graph。
|
||||
if routing.Action == route.ActionSchedulePlan {
|
||||
reply, planErr := s.runSchedulePlanFlow(requestCtx, selectedModel, userMessage, userID, chatID, traceID, extra, progress.Emit, outChan, resolvedModelName)
|
||||
if planErr != nil {
|
||||
log.Printf("智能排程 graph 执行失败,回退普通聊天 trace_id=%s chat_id=%s err=%v", traceID, chatID, planErr)
|
||||
progress.Emit("schedule_plan.fallback", "智能排程暂不可用,先切回普通对话。")
|
||||
s.runNormalChatFlow(requestCtx, selectedModel, resolvedModelName, userMessage, ifThinking, userID, chatID, traceID, requestStart, outChan, errChan)
|
||||
return
|
||||
}
|
||||
|
||||
if emitErr := emitSingleAssistantCompletion(outChan, resolvedModelName, reply); emitErr != nil {
|
||||
pushErrNonBlocking(errChan, emitErr)
|
||||
return
|
||||
}
|
||||
requestTotalTokens := snapshotRequestTokenMeter(requestCtx).TotalTokens
|
||||
s.persistChatAfterReply(requestCtx, userID, chatID, userMessage, reply, 0, requestTotalTokens, errChan)
|
||||
s.ensureConversationTitleAsync(userID, chatID)
|
||||
return
|
||||
}
|
||||
|
||||
// 3.7 未知 action 兜底:走普通聊天,保证可用性。
|
||||
s.runNormalChatFlow(requestCtx, selectedModel, resolvedModelName, userMessage, ifThinking, userID, chatID, traceID, requestStart, outChan, errChan)
|
||||
}()
|
||||
|
||||
|
||||
101
backend/service/agentsvc/agent_schedule_plan.go
Normal file
101
backend/service/agentsvc/agent_schedule_plan.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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/pkg"
|
||||
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// runSchedulePlanFlow 执行"智能排程"分支。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把本次请求接入 scheduleplan 执行器;
|
||||
// 2. 负责注入排程依赖(SmartPlanning / BatchApplyPlans / GetTaskClassByID);
|
||||
// 3. 负责对话历史获取,支持连续对话微调;
|
||||
// 4. 不负责聊天持久化(由 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. 依赖预检:排程依赖函数必须注入,否则无法完成排程链路。
|
||||
if s.SmartPlanningRawFunc == nil || s.BatchApplyPlansFunc == nil || s.GetTaskClassByIDFunc == nil {
|
||||
return "", errors.New("schedule plan service dependencies are not ready")
|
||||
}
|
||||
if selectedModel == nil {
|
||||
return "", errors.New("schedule plan model is nil")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 缓存未命中时回源 DB。
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 初始化排程状态对象。
|
||||
state := scheduleplan.NewSchedulePlanState(traceID, userID, chatID)
|
||||
|
||||
// 4. 构建依赖注入并执行 graph。
|
||||
finalState, runErr := scheduleplan.RunSchedulePlanGraph(ctx, scheduleplan.SchedulePlanGraphRunInput{
|
||||
Model: selectedModel,
|
||||
State: state,
|
||||
Deps: scheduleplan.SchedulePlanToolDeps{
|
||||
SmartPlanningRaw: s.SmartPlanningRawFunc,
|
||||
BatchApplyPlans: s.BatchApplyPlansFunc,
|
||||
GetTaskClassByID: s.GetTaskClassByIDFunc,
|
||||
HybridScheduleWithPlan: s.HybridScheduleWithPlanFunc,
|
||||
},
|
||||
UserMessage: userMessage,
|
||||
Extra: extra,
|
||||
ChatHistory: chatHistory,
|
||||
EmitStage: emitStage,
|
||||
OutChan: outChan,
|
||||
ModelName: modelName,
|
||||
})
|
||||
|
||||
if runErr != nil {
|
||||
return "", runErr
|
||||
}
|
||||
|
||||
// 5. 提取最终回复。
|
||||
if finalState == nil {
|
||||
return "排程流程异常,请稍后重试。", nil
|
||||
}
|
||||
|
||||
reply := strings.TrimSpace(finalState.FinalSummary)
|
||||
if reply == "" {
|
||||
reply = "排程流程已完成,但未生成结果摘要。"
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
Reference in New Issue
Block a user