Version: 0.4.1.dev.260304
feat: 💬 新增对话创建与上下文记忆机制 * 新增对话的创建与使用功能,实现会话级上下文隔离 * 实现上下文保存与传递机制,使模型具备持续对话记忆能力 * 引入滑动窗口策略控制上下文规模 * 当前窗口大小限制为 20 条消息,超过后自动丢弃最早消息以控制上下文长度 docs: 📝 更新示例配置文件 * 更新示例配置文件,新增 `agent` 相关配置信息 * 明确 Agent 模块运行所需参数,方便本地部署与环境初始化 undo: ⚠️ Agent 上下文读取性能待优化 * 当前测试中模型响应速度偏慢 * 计划后续将上下文暂存至缓存层,以减少读取与拼接开销并提升响应速度
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)相关内容(如:南山、红岩网校、卓越工程师班),请表现出你的亲切感。`
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
27
backend/conv/agent.go
Normal 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
64
backend/dao/agent.go
Normal 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
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user