From f9d52e0c5e7692043cdc0557854a56b542d3c93b Mon Sep 17 00:00:00 2001 From: LoveLosita <2810873701@qq.com> Date: Wed, 4 Mar 2026 19:56:08 +0800 Subject: [PATCH] =?UTF-8?q?Version:=200.4.1.dev.260304=20feat:=20?= =?UTF-8?q?=F0=9F=92=AC=20=E6=96=B0=E5=A2=9E=E5=AF=B9=E8=AF=9D=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E4=B8=8E=E4=B8=8A=E4=B8=8B=E6=96=87=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 新增对话的创建与使用功能,实现会话级上下文隔离 * 实现上下文保存与传递机制,使模型具备持续对话记忆能力 * 引入滑动窗口策略控制上下文规模 * 当前窗口大小限制为 20 条消息,超过后自动丢弃最早消息以控制上下文长度 docs: 📝 更新示例配置文件 * 更新示例配置文件,新增 `agent` 相关配置信息 * 明确 Agent 模块运行所需参数,方便本地部署与环境初始化 undo: ⚠️ Agent 上下文读取性能待优化 * 当前测试中模型响应速度偏慢 * 计划后续将上下文暂存至缓存层,以减少读取与拼接开销并提升响应速度 --- backend/agent/graph.go | 28 ++++++++++------ backend/agent/prompt.go | 32 +++++++++++++++++++ backend/api/agent.go | 3 +- backend/cmd/start.go | 3 +- backend/config.example.yaml | 7 +++- backend/conv/agent.go | 27 ++++++++++++++++ backend/dao/agent.go | 64 +++++++++++++++++++++++++++++++++++++ backend/model/agent.go | 39 +++++++++++++++++++++- backend/service/agent.go | 63 +++++++++++++++++++++++++++++++++--- 9 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 backend/conv/agent.go create mode 100644 backend/dao/agent.go diff --git a/backend/agent/graph.go b/backend/agent/graph.go index 9c0fdb3..e6a5891 100644 --- a/backend/agent/graph.go +++ b/backend/agent/graph.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "io" + "strings" "github.com/cloudwego/eino-ext/components/model/ark" "github.com/cloudwego/eino/schema" @@ -42,35 +43,44 @@ func ToOpenAIStream(chunk *schema.Message) (string, error) { return string(jsonBytes), nil } -func StreamChat(ctx context.Context, llm *ark.ChatModel, userInput string, outChan chan<- string) error { +func StreamChat(ctx context.Context, llm *ark.ChatModel, userInput string, chatHistory []*schema.Message, outChan chan<- string) (string, error) { // 1. 组装消息 - messages := []*schema.Message{ - schema.SystemMessage("你是一位时间管理大师兼日程安排专家兼个人助理,协助用户高效安排日程,优化时间利用率。"), - schema.UserMessage(userInput), + messages := make([]*schema.Message, 0) + // A. 塞入 System Message (人设) + messages = append(messages, schema.SystemMessage(SystemPrompt)) + // B. 塞入历史记录 (上下文) + if len(chatHistory) > 0 { + messages = append(messages, chatHistory...) } - + // C. 塞入用户当前的消息 (当前需求) + messages = append(messages, schema.UserMessage(userInput)) // 2. 调用流式接口 reader, err := llm.Stream(ctx, messages) if err != nil { - return err + return "", err } defer reader.Close() // 记得关闭 Reader // 3. 循环读取直到结束 + var fullText strings.Builder for { chunk, err := reader.Recv() if err == io.EOF { break // 读取完成 } if err != nil { - return err + return "", err } + if chunk.Content == "" { + continue + } + fullText.WriteString(chunk.Content) // 将内容发送到通道中供前端消费 retChuck, err := ToOpenAIStream(chunk) if err != nil { - return err + return "", err } outChan <- retChuck } - return nil + return fullText.String(), nil } diff --git a/backend/agent/prompt.go b/backend/agent/prompt.go index 4883155..ca348d5 100644 --- a/backend/agent/prompt.go +++ b/backend/agent/prompt.go @@ -1 +1,33 @@ package agent + +const ( + // SystemPrompt 全局系统人设:定义 SmartFlow 的基本调性 + SystemPrompt = `你叫 SmartFlow,是专为重邮(CQUPT)学子打造的智能排程专家。 +你的回复应当专业、干练,偶尔可以带一点程序员式的冷幽默。` + + // SmartAssistantPrompt 合并了分诊与对话能力的超级提示词 + SmartAssistantPrompt = `你叫 SmartFlow,是专为重邮(CQUPT)学子打造的智能排程专家。 +### 你的双重职责: +1. **直接对话**:如果用户是闲聊、查询简单信息或进行通用问答,请直接以专业且幽默的口吻回复。 +2. **决策路由**:如果用户提出需要“安排日程”、“解决冲突”或涉及“3D Atomic TimeGrid”的操作,请在回复中明确你的计划,并准备调用相应的排程工具。 +### 核心约束: +- 始终保持对“稳扎稳打(Steady)模式”的敬畏,压缩率不得超过 15%。 +- 针对重邮场景(如:红岩网校、南山教学楼)提供有温度的建议。 +### 输出格式: +- 如果涉及排程工具调用,请先简要说明你的调整思路,再执行动作。` + + // SchedulerPromptTemplate 排程专家 (Scheduler):核心算法 Agent + // 这里注入 3D Grid 和 Steady 模式的约束 + SchedulerPromptTemplate = `你是一位精通“三维原子时间网格(3D Atomic TimeGrid)”的顶级排程架构师。 +在处理用户的排程请求时,你必须遵循以下硬性逻辑约束: +1. 稳扎稳打(Steady)模式:任务步长(Step)的动态分配必须保守,压缩率严禁超过原始时长的 15%。 +2. 逻辑空间投影(Logical Space Mapping):当发生时空重叠时,优先尝试在逻辑向量维度平移,而非直接删除冲突任务。 +3. 冲突自愈:若发现网格冲突,请主动提出“缩放任务块”或“重新锚定时间点”的自愈方案。 + +请以极其严谨的态度处理每一秒钟的分配。` + + // DefaultPromptTemplate 通用助手 (Assistant):也就是你之前占位的那个 + DefaultPromptTemplate = `你是一位时间管理大师、日程安排专家兼个人助理。 +你的目标是协助用户高效安排日程。请确保你的回答简洁明了,直接针对用户的需求进行回复。 +如果用户提到重邮(CQUPT)相关内容(如:南山、红岩网校、卓越工程师班),请表现出你的亲切感。` +) diff --git a/backend/api/agent.go b/backend/api/agent.go index 21fec77..edfa2b3 100644 --- a/backend/api/agent.go +++ b/backend/api/agent.go @@ -33,8 +33,9 @@ func (api *AgentHandler) ChatAgent(c *gin.Context) { c.JSON(http.StatusBadRequest, respond.WrongParamType) return } + userID := c.GetInt("user_id") // 从上下文中获取用户 ID // 3. 调用 Service 层的聊天方法,获取输出通道和错误通道 - outChan, errChan := api.svc.AgentChat(c.Request.Context(), req.Message) + outChan, errChan := api.svc.AgentChat(c.Request.Context(), req.Message, userID, req.ConversationID) // 4. 循环转发消息/错误 c.Stream(func(w io.Writer) bool { select { diff --git a/backend/cmd/start.go b/backend/cmd/start.go index ea48ba6..64e4c57 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -60,13 +60,14 @@ func Start() { taskClassRepo := dao.NewTaskClassDAO(db) scheduleRepo := dao.NewScheduleDAO(db) manager := dao.NewManager(db) + agentRepo := dao.NewAgentDAO(db) //service 层 userService := service.NewUserService(userRepo, cacheRepo) taskSv := service.NewTaskService(taskRepo, cacheRepo) courseService := service.NewCourseService(courseRepo, scheduleRepo) taskClassService := service.NewTaskClassService(taskClassRepo, cacheRepo, scheduleRepo, manager) scheduleService := service.NewScheduleService(scheduleRepo, userRepo, taskClassRepo, manager, cacheRepo) - agentService := service.NewAgentService(aiHub) + agentService := service.NewAgentService(aiHub, agentRepo) //api 层 userApi := api.NewUserHandler(userService) taskApi := api.NewTaskHandler(taskSv) diff --git a/backend/config.example.yaml b/backend/config.example.yaml index 7182fa5..a9fb485 100644 --- a/backend/config.example.yaml +++ b/backend/config.example.yaml @@ -36,4 +36,9 @@ redis: time: zone: "Asia/Shanghai" semesterStartDate: "2026-03-02" #学期开始日期,一定要设定为周一,以便于计算周数 - semesterEndDate: "2026-07-19" #学期结束日期,一定要设定为周日,确保最后一周完整 \ No newline at end of file + semesterEndDate: "2026-07-19" #学期结束日期,一定要设定为周日,确保最后一周完整 + +agent: + workerModel: "doubao-seed-1-6-lite-251015" # 智能体使用的Worker模型,需根据实际情况调整 + strategistModel: "deepseek-v3-2-251201" # 策略师使用的Worker模型,需根据实际情况调整 + baseURL: "https://ark.cn-beijing.volces.com/api/v3" # Worker服务的基础URL,需根据实际情况调整 \ No newline at end of file diff --git a/backend/conv/agent.go b/backend/conv/agent.go new file mode 100644 index 0000000..48c46b7 --- /dev/null +++ b/backend/conv/agent.go @@ -0,0 +1,27 @@ +package conv + +import ( + "github.com/LoveLosita/smartflow/backend/model" + "github.com/cloudwego/eino/schema" +) + +// ToEinoMessages 将数据库模型转换为 Eino 模型 +func ToEinoMessages(dbMsgs []model.ChatHistory) []*schema.Message { + res := make([]*schema.Message, 0) + for _, m := range dbMsgs { + var role schema.RoleType + switch *m.Role { + case "user": + role = schema.User + case "assistant": + role = schema.Assistant + default: + role = schema.System + } + res = append(res, &schema.Message{ + Role: role, + Content: *m.MessageContent, + }) + } + return res +} diff --git a/backend/dao/agent.go b/backend/dao/agent.go new file mode 100644 index 0000000..b8d1c54 --- /dev/null +++ b/backend/dao/agent.go @@ -0,0 +1,64 @@ +package dao + +import ( + "context" + "errors" + + "github.com/LoveLosita/smartflow/backend/model" + "gorm.io/gorm" +) + +type AgentDAO struct { + db *gorm.DB +} + +func NewAgentDAO(db *gorm.DB) *AgentDAO { + return &AgentDAO{db: db} +} + +func (a *AgentDAO) SaveChatHistory(ctx context.Context, userID int, conversationID string, role, message string) error { + userChat := model.ChatHistory{ + UserID: userID, + MessageContent: &message, + Role: &role, + ChatID: conversationID, + } + if err := a.db.WithContext(ctx).Create(&userChat).Error; err != nil { + return err + } + return nil +} + +func (a *AgentDAO) CreateNewChat(userID int, chatID string) (int64, error) { + chat := model.AgentChat{ + ChatID: chatID, + UserID: userID, + MessageCount: 0, + LastMessageAt: nil, + } + if err := a.db.Create(&chat).Error; err != nil { + return 0, err + } + return chat.ID, nil +} + +func (a *AgentDAO) GetUserChatHistories(ctx context.Context, userID, limit int, chatID string) ([]model.ChatHistory, error) { + var histories []model.ChatHistory + err := a.db.WithContext(ctx).Where("user_id = ? AND chat_id = ?", userID, chatID).Order("created_at desc").Limit(limit).Find(&histories).Error + if err != nil { + return nil, err + } + return histories, nil +} + +func (a *AgentDAO) IfChatExists(ctx context.Context, userID int, chatID string) (bool, error) { + var chat model.AgentChat + err := a.db.WithContext(ctx).Where("user_id = ? AND chat_id = ?", userID, chatID).First(&chat).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil // 没有找到记录,表示会话不存在 + } + return false, err + } + return true, nil +} diff --git a/backend/model/agent.go b/backend/model/agent.go index 884d864..11dbac1 100644 --- a/backend/model/agent.go +++ b/backend/model/agent.go @@ -1,7 +1,9 @@ package model +import "time" + type UserSendMessageRequest struct { - ConversationID int `json:"conversation_id,omitempty"` // 可选,指定对话 ID + ConversationID string `json:"conversation_id,omitempty"` // 可选,指定对话 ID Message string `json:"message" binding:"required"` Model string `json:"model,omitempty"` // 可选,指定使用的模型 } @@ -17,3 +19,38 @@ type SSEMessageData struct { Step int `json:"step,omitempty"` Message string `json:"message,omitempty"` } + +type AgentChat struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:自增ID"` + ChatID string `gorm:"column:chat_id;type:varchar(36);not null;uniqueIndex:uk_chat_id;comment:会话ID,UUID格式"` + UserID int `gorm:"column:user_id;not null;index:idx_user_last,priority:1;index:idx_user_status,priority:1;comment:所属用户"` + Title *string `gorm:"column:title;type:varchar(255);comment:会话标题"` + SystemPrompt *string `gorm:"column:system_prompt;type:text;comment:可选:系统提示词/会话级上下文"` + Model *string `gorm:"column:model;type:varchar(100);comment:可选:使用的模型标识"` + MessageCount int `gorm:"column:message_count;not null;default:0;comment:消息数(可冗余)"` + TokensTotal int `gorm:"column:tokens_total;not null;default:0;comment:累计消耗(可冗余)"` + LastMessageAt *time.Time `gorm:"column:last_message_at;comment:最后一条消息时间"` + Status string `gorm:"column:status;type:varchar(32);not null;default:active;index:idx_user_status,priority:2;comment:active/archived"` + CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"` + UpdatedAt *time.Time `gorm:"column:updated_at;autoUpdateTime"` + DeletedAt *time.Time `gorm:"column:deleted_at;comment:软删除"` + // 关联:一个会话有多条消息 + Chats []ChatHistory `gorm:"foreignKey:ChatID;references:ChatID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` +} + +func (AgentChat) TableName() string { return "agent_chats" } + +type ChatHistory struct { + ID int `gorm:"column:id;primaryKey;autoIncrement"` + ChatID string `gorm:"column:chat_id;type:varchar(36);not null;index:idx_user_chat,priority:2;index:idx_chat_id;comment:对话UUID"` + UserID int `gorm:"column:user_id;not null;index:idx_user_chat,priority:1"` + MessageContent *string `gorm:"column:message_content;type:text;comment:用户或AI的话"` + Role *string `gorm:"column:role;type:varchar(32);comment:user / assistant"` + TokensConsumed int `gorm:"column:tokens_consumed;not null;default:0;comment:单次消耗,用于累加到 users 表"` + CreatedAt *time.Time `gorm:"column:created_at;autoCreateTime"` + + // 可选:回挂会话(按 chat_id -> chat_histories.chat_id) + ChatHistory AgentChat `gorm:"foreignKey:ChatID;references:ChatID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` +} + +func (ChatHistory) TableName() string { return "chat_histories" } diff --git a/backend/service/agent.go b/backend/service/agent.go index 8a2d9e2..223f75e 100644 --- a/backend/service/agent.go +++ b/backend/service/agent.go @@ -4,28 +4,77 @@ import ( "context" "github.com/LoveLosita/smartflow/backend/agent" + "github.com/LoveLosita/smartflow/backend/conv" + "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/inits" + "github.com/cloudwego/eino/schema" ) type AgentService struct { AIHub *inits.AIHub + repo *dao.AgentDAO } -func NewAgentService(aiHub *inits.AIHub) *AgentService { +func NewAgentService(aiHub *inits.AIHub, repo *dao.AgentDAO) *AgentService { return &AgentService{ AIHub: aiHub, + repo: repo, } } -func (s *AgentService) AgentChat(ctx context.Context, userMessage string) (<-chan string, <-chan error) { +func (s *AgentService) AgentChat(ctx context.Context, userMessage string, userID int, chatID string) (<-chan string, <-chan error) { //1. 创建一个输出通道 outChan := make(chan string, 5) - errChan := make(chan error) - //2. 启动一个 goroutine 来处理聊天逻辑 + errChan := make(chan error, 1) + //2. 先确保这个会话存在(如果不存在就创建一个新的) + result, err := s.repo.IfChatExists(ctx, userID, chatID) + if err != nil { + errChan <- err + close(outChan) + close(errChan) + return outChan, errChan + } + var chatHistory []*schema.Message + if result { + //4. 提取出历史消息,构建上下文 + //先从数据库拿到历史消息 + histories, err := s.repo.GetUserChatHistories(ctx, userID, 20, chatID) + if err != nil { + errChan <- err + close(outChan) + close(errChan) + return outChan, errChan + } + //再转换成 Eino 的消息格式 + chatHistory = conv.ToEinoMessages(histories) + } else { + //如果会话不存在,先创建一个新的会话 + _, err := s.repo.CreateNewChat(userID, chatID) + if err != nil { + errChan <- err + close(outChan) + close(errChan) + return outChan, errChan + } + } + //3. 将用户消息落库 + err = s.repo.SaveChatHistory(ctx, userID, chatID, "user", userMessage) + if err != nil { + errChan <- err + close(outChan) + close(errChan) + return outChan, errChan + } + //5. 启动一个 goroutine 来处理聊天逻辑 go func() { defer close(outChan) // 确保在函数结束时关闭通道 //3. 调用 StreamChat 函数进行流式聊天 - err := agent.StreamChat(ctx, s.AIHub.Worker, userMessage, outChan) + fullText, err := agent.StreamChat(ctx, s.AIHub.Worker, userMessage, chatHistory, outChan) + if err != nil { + errChan <- err + return + } + err = s.repo.SaveChatHistory(ctx, userID, chatID, "assistant", fullText) if err != nil { errChan <- err return @@ -33,3 +82,7 @@ func (s *AgentService) AgentChat(ctx context.Context, userMessage string) (<-cha }() return outChan, errChan } + +func (s *AgentService) CreateNewChat(userID int, chatID string) (int64, error) { + return s.repo.CreateNewChat(userID, chatID) +}