Files
Cyrene/backend/ai-core/internal/context/builder.go
T
AskaEth 26a61cb57c feat: 第四轮大版本更新 — 修复4个严重Bug、2个UI Bug,实现自主思考重构与主-子会话架构
## 🐛 Bug 修复
- 修复前端对话无响应:消除 ChatContainer 中的双重 WebSocket 连接,优化 sendMessage 失败提示
- 修复 Memory-Service 数据库迁移失败:ai-core 和 memory-service 均添加 ALTER TABLE ADD COLUMN IF NOT EXISTS 模式演化
- 修复语音/STT 不可用:添加 MediaRecorder API 降级方案,修复 whisper-cli 输出文件名错误
- 修复仪表盘数据库按钮失效:补充按钮 ID 属性,重写 controlDB() 控制逻辑

## 🎨 UI 修复
- 修正用户消息头像位置:从 flex-row-reverse 改为 justify-end
- 移除空聊天列表的 emoji 占位图标

##  新功能
- devtools 新增 STT 处理日志面板(环形缓冲区 + WebSocket 广播 + 可视化表格)
- 新增 ADMIN_NICKNAME 环境变量,支持自定义管理员昵称

## 🔧 改进
- 注册流程增加昵称必填字段(前后端同步)

## 🏗️ 架构重构
- 重构自主思考逻辑:从定时器轮询改为事件驱动(对话后触发 + 静默检测),优化提示词使其更自然人性化
- 实现主-子会话架构:新增 4 种子会话类型(general/memory/iot/knowledge),意图分析 → 并行分发 → 结果合成流程

## 📄 新增文档
- docs/architecture/main-session-sub-session-design.md — 子会话架构设计文档
2026-05-19 21:09:48 +08:00

345 lines
10 KiB
Go

package context
import (
"context"
"fmt"
"log"
"strings"
"sync"
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
)
// IoTDeviceSummary IoT设备摘要接口(避免循环依赖)
type IoTDeviceSummary interface {
GetName() string
GetType() string
GetStatus() string
}
// ConversationStore 会话历史存储接口
type ConversationStore struct {
mu sync.RWMutex
messages map[string][]model.LLMMessage // key = sessionID
maxHistory int
}
// NewConversationStore 创建会话历史存储
func NewConversationStore(maxHistory int) *ConversationStore {
return &ConversationStore{
messages: make(map[string][]model.LLMMessage),
maxHistory: maxHistory,
}
}
// AddMessage 添加消息到会话历史
func (cs *ConversationStore) AddMessage(sessionID string, msg model.LLMMessage) {
cs.mu.Lock()
defer cs.mu.Unlock()
msgs := cs.messages[sessionID]
msgs = append(msgs, msg)
// 限制历史长度
if len(msgs) > cs.maxHistory {
// 保留 system 消息在开头,只裁剪 user/assistant 消息
cutoff := len(msgs) - cs.maxHistory
for cutoff < len(msgs) && msgs[cutoff].Role == model.RoleSystem {
cutoff++
}
if cutoff > 0 {
msgs = msgs[cutoff:]
}
}
cs.messages[sessionID] = msgs
}
// GetHistory 获取会话历史
func (cs *ConversationStore) GetHistory(sessionID string, limit int) []model.LLMMessage {
cs.mu.RLock()
defer cs.mu.RUnlock()
msgs := cs.messages[sessionID]
if len(msgs) == 0 {
return nil
}
start := 0
if limit > 0 && len(msgs) > limit {
start = len(msgs) - limit
}
result := make([]model.LLMMessage, len(msgs[start:]))
copy(result, msgs[start:])
return result
}
// Builder 对话上下文构建器
type Builder struct {
convStore *ConversationStore
}
// NewBuilder 创建上下文构建器
func NewBuilder(convStore *ConversationStore) *Builder {
return &Builder{convStore: convStore}
}
type BuildParams struct {
UserID string
SessionID string
UserMessage string
Persona *persona.PersonaConfig
Memories []memory.MemoryEntry
HistoryLimit int
DeviceContext string // 注入的设备状态文本
PendingThoughts []string // 待注入的后台思考
Nickname string // 用户昵称 (昔涟对用户的称呼)
}
// Build 构建发送给LLM的完整消息列表
func (b *Builder) Build(ctx context.Context, params BuildParams) ([]model.LLMMessage, error) {
messages := []model.LLMMessage{}
// 1. 系统消息 —— 昔涟的人格Prompt
// 使用传入的昵称,如果为空则回退到 userID
userName := params.Nickname
if userName == "" {
userName = params.UserID
}
systemPrompt := params.Persona.BuildSystemPrompt(
userName,
1,
)
// 1.1 注入设备上下文到系统消息
if params.DeviceContext != "" {
systemPrompt += "\n\n" + params.DeviceContext
}
// 1.2 注入后台思考到系统消息(不打扰地)
if len(params.PendingThoughts) > 0 {
systemPrompt += "\n\n【昔涟的内心思考(仅供你参考,不要直接复述,请自然地融入对话)】\n"
for _, thought := range params.PendingThoughts {
systemPrompt += fmt.Sprintf("- %s\n", thought)
}
}
messages = append(messages, model.LLMMessage{
Role: "system",
Content: systemPrompt,
})
// 2. 记忆注入 —— 相关记忆以系统消息形式注入,按重要性排序并分类标注
if len(params.Memories) > 0 {
// 按 Importance 排序
sortedMems := make([]memory.MemoryEntry, len(params.Memories))
copy(sortedMems, params.Memories)
sortMemoriesByImportance(sortedMems)
// 分离核心记忆和最近记忆
var coreMems, recentMems, otherMems []memory.MemoryEntry
for _, m := range sortedMems {
if m.Importance >= 8 {
coreMems = append(coreMems, m)
} else if m.Importance >= 5 {
recentMems = append(recentMems, m)
} else {
otherMems = append(otherMems, m)
}
}
// 限制每类记忆数量
if len(coreMems) > 5 {
coreMems = coreMems[:5]
}
if len(recentMems) > 8 {
recentMems = recentMems[:8]
}
if len(otherMems) > 3 {
otherMems = otherMems[:3]
}
var memoryPrompt string
memoryPrompt += "【以下是关于开拓者的重要记忆,请在合适的时机自然地提及】\n\n"
if len(coreMems) > 0 {
memoryPrompt += "★ 核心记忆(非常重要,务必优先参考):\n"
for _, m := range coreMems {
memoryPrompt += formatMemoryLine(m)
}
memoryPrompt += "\n"
}
if len(recentMems) > 0 {
memoryPrompt += "● 常用记忆:\n"
for _, m := range recentMems {
memoryPrompt += formatMemoryLine(m)
}
memoryPrompt += "\n"
}
if len(otherMems) > 0 {
memoryPrompt += "○ 其他记忆:\n"
for _, m := range otherMems {
memoryPrompt += formatMemoryLine(m)
}
memoryPrompt += "\n"
}
messages = append(messages, model.LLMMessage{
Role: "system",
Content: memoryPrompt,
})
}
// 3. 历史对话
history, err := b.loadHistory(ctx, params.SessionID, params.HistoryLimit)
if err == nil {
messages = append(messages, history...)
}
// 4. 当前用户消息
messages = append(messages, model.LLMMessage{
Role: "user",
Content: params.UserMessage,
})
return messages, nil
}
// loadHistory 从 ConversationStore 加载会话历史
func (b *Builder) loadHistory(_ context.Context, sessionID string, limit int) ([]model.LLMMessage, error) {
if b.convStore == nil {
log.Printf("[context] 会话历史存储未初始化,跳过加载")
return nil, nil
}
history := b.convStore.GetHistory(sessionID, limit)
if len(history) == 0 {
log.Printf("[context] 会话 %s 无历史记录", sessionID)
return nil, nil
}
log.Printf("[context] 加载会话 %s 历史 %d 条", sessionID, len(history))
return history, nil
}
// CacheMessage 缓存消息到会话历史(供chat handler在回复后调用)
func (b *Builder) CacheMessage(sessionID string, role model.Role, content string) {
if b.convStore == nil {
return
}
b.convStore.AddMessage(sessionID, model.LLMMessage{
Role: role,
Content: content,
})
}
// GetHistory 获取会话历史(供 Orchestrator 使用)
func (b *Builder) GetHistory(sessionID string, limit int) []model.LLMMessage {
if b.convStore == nil {
return nil
}
return b.convStore.GetHistory(sessionID, limit)
}
// InjectDeviceContext 将设备状态格式化为简洁的文本注入系统上下文
func InjectDeviceContext(devices []DeviceInfo) string {
if len(devices) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("[当前IoT设备状态 — 你已知晓这些设备的状态,无需调用工具查询,直接引用即可]\n")
for _, d := range devices {
switch d.Type {
case "light":
if d.Status == "on" {
sb.WriteString(fmt.Sprintf("- %s: 开启 (亮度%d%%, %s)\n", d.Name, d.Brightness, d.Color))
} else {
sb.WriteString(fmt.Sprintf("- %s: 关闭\n", d.Name))
}
case "ac":
if d.Status == "on" {
modeLabel := acModeLabel(d.Mode)
sb.WriteString(fmt.Sprintf("- %s: 运行中 (%s%.0f°C)\n", d.Name, modeLabel, d.Temperature))
} else {
sb.WriteString(fmt.Sprintf("- %s: 关闭\n", d.Name))
}
case "curtain":
statusLabel := "已关闭"
if d.Status == "open" {
statusLabel = "已打开"
}
sb.WriteString(fmt.Sprintf("- %s: %s\n", d.Name, statusLabel))
case "sensor":
sb.WriteString(fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit))
case "lock":
statusLabel := "已锁定"
if d.Status == "unlocked" {
statusLabel = "已解锁"
}
sb.WriteString(fmt.Sprintf("- %s: %s (电量%d%%)\n", d.Name, statusLabel, d.Battery))
}
}
return sb.String()
}
// DeviceInfo 设备信息(避免循环依赖的简化结构体)
type DeviceInfo struct {
Name string
Type string
Status string
Brightness int
Color string
Temperature float64
Mode string
Value float64
Unit string
Battery int
}
func acModeLabel(mode string) string {
switch mode {
case "cool":
return "制冷"
case "heat":
return "制热"
case "auto":
return "自动"
default:
return mode
}
}
// sortMemoriesByImportance 按 Importance 降序排列记忆
func sortMemoriesByImportance(mems []memory.MemoryEntry) {
for i := 0; i < len(mems); i++ {
for j := i + 1; j < len(mems); j++ {
if mems[j].Importance > mems[i].Importance ||
(mems[j].Importance == mems[i].Importance && mems[j].Priority > mems[i].Priority) {
mems[i], mems[j] = mems[j], mems[i]
}
}
}
}
// formatMemoryLine 格式化单条记忆为展示行
func formatMemoryLine(m model.MemoryEntry) string {
content := m.Content
runes := []rune(content)
if len(runes) > 80 {
content = string(runes[:80]) + "…"
}
stars := ""
for i := 0; i < m.Importance/2; i++ {
stars += "★"
}
if m.Importance%2 != 0 {
stars += "☆"
}
return fmt.Sprintf("- [%s%s] %s\n", m.Category.DisplayName(), stars, content)
}