Version: 0.4.1.dev.260304

feat: 💬 新增对话创建与上下文记忆机制

* 新增对话的创建与使用功能,实现会话级上下文隔离
* 实现上下文保存与传递机制,使模型具备持续对话记忆能力
* 引入滑动窗口策略控制上下文规模
* 当前窗口大小限制为 20 条消息,超过后自动丢弃最早消息以控制上下文长度

docs: 📝 更新示例配置文件

* 更新示例配置文件,新增 `agent` 相关配置信息
* 明确 Agent 模块运行所需参数,方便本地部署与环境初始化

undo: ⚠️ Agent 上下文读取性能待优化

* 当前测试中模型响应速度偏慢
* 计划后续将上下文暂存至缓存层,以减少读取与拼接开销并提升响应速度
This commit is contained in:
LoveLosita
2026-03-04 19:56:08 +08:00
parent 1e2d7696d3
commit f9d52e0c5e
9 changed files with 248 additions and 18 deletions

View File

@@ -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
}

View File

@@ -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相关内容南山、红岩网校、卓越工程师班请表现出你的亲切感。`
)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -36,4 +36,9 @@ redis:
time:
zone: "Asia/Shanghai"
semesterStartDate: "2026-03-02" #学期开始日期,一定要设定为周一,以便于计算周数
semesterEndDate: "2026-07-19" #学期结束日期,一定要设定为周日,确保最后一周完整
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需根据实际情况调整

27
backend/conv/agent.go Normal file
View File

@@ -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
}

64
backend/dao/agent.go Normal file
View File

@@ -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
}

View File

@@ -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:会话IDUUID格式"`
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" }

View File

@@ -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)
}