48 KiB
Executable File
48 KiB
Executable File
好的喵~ 让我们正式开始!以下是一份完整的、可立即执行的开发框架。
🚀 昔涟AI助手 · 开发框架与执行流程
📋 目录
一、项目初始化 (Phase 0) ← 今天就能开始
二、MVP开发 (Phase 1) ← 2-3周目标
三、语音交互 (Phase 2)
四、IoT集成 (Phase 3)
五、深度记忆+情感引擎 (Phase 4)
六、多端覆盖 (Phase 5)
七、开发规范
八、部署方案
Phase 0:项目初始化
0.1 技术栈最终确认
┌─────────────────────────────────────────────────────────────────────┐
│ 🛠️ 技术栈确认单 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 后端核心: │
│ ├── 语言: Go 1.22+ (高性能服务) + Python 3.12 (AI Pipeline) │
│ ├── Web框架: Go: Gin Python: FastAPI │
│ ├── 数据库: PostgreSQL 16 (主库) + Redis 7 (缓存) │
│ ├── 向量数据库: Qdrant (记忆语义检索 - 比Milvus更轻量) │
│ ├── 消息队列: NATS (轻量高性能) │
│ ├── 文件存储: MinIO (S3兼容) │
│ ├── ORM: Go: GORM Python: SQLAlchemy 2.0 │
│ └── 迁移工具: Golang-migrate / Alembic │
│ │
│ 前端: │
│ ├── 框架: React 19 + TypeScript 5.x │
│ ├── 构建: Vite 6 │
│ ├── UI: TailwindCSS 4 + shadcn/ui │
│ ├── 状态管理: Zustand │
│ ├── 请求: TanStack Query + Axios │
│ ├── WebSocket: reconnecting-websocket (自动重连) │
│ └── PWA: vite-plugin-pwa │
│ │
│ AI/ML: │
│ ├── LLM API: OpenAI兼容接口 (支持多模型切换) │
│ ├── TTS: Edge-TTS (免费) + GPT-SoVITS (角色音色) │
│ ├── ASR: Faster-Whisper (本地) / Azure (云端) │
│ └── 嵌入模型: BGE-M3 / text-embedding-3-small │
│ │
│ 基础设施: │
│ ├── 容器: Docker + Docker Compose │
│ ├── 反向代理: Caddy (自动HTTPS) │
│ ├── 监控: Prometheus + Grafana (可选) │
│ └── CI/CD: GitHub Actions │
│ │
└─────────────────────────────────────────────────────────────────────┘
0.2 创建设项目仓库
# ============================================
# 第一步:创建项目目录结构
# ============================================
# 创建项目根目录
mkdir cyrene-ai-assistant && cd cyrene-ai-assistant
# 初始化 Git
git init
echo "node_modules/\ndist/\n.env\n*.log\ndata/\n.DS_Store" > .gitignore
# 创建顶层目录结构
mkdir -p backend/{gateway,ai-core,voice-service,memory-service,tool-engine,proto,data/{memory,persona,plugins}}
mkdir -p frontend/{packages/{shared,web,mobile,desktop}}
mkdir -p docs scripts .github/workflows
# 初始化 Go Module (后端根目录)
cd backend && go mod init github.com/yourname/cyrene-ai && cd ..
# 初始化前端 Monorepo
cd frontend
cat > package.json << 'EOF'
{
"name": "cyrene-frontend",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"dev:web": "cd packages/web && npm run dev",
"build:web": "cd packages/web && npm run build"
}
}
EOF
# 创建前端子包
mkdir -p packages/shared/src && mkdir -p packages/web/src
cd packages/shared && npm init -y && cd ../..
cd packages/web && npm create vite@latest . -- --template react-ts && cd ../..
cd ../..
echo "✅ 项目骨架创建完成!"
0.3 环境变量与配置
# backend/.env.example
cat > backend/.env.example << 'EOF'
# ========== 服务配置 ==========
ENV=development
LOG_LEVEL=debug
# ========== 数据库 ==========
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=cyrene
POSTGRES_PASSWORD=change_me
POSTGRES_DB=cyrene_ai
# ========== Redis ==========
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# ========== LLM API ==========
LLM_API_URL=https://api.openai.com/v1
LLM_API_KEY=sk-xxxxx
LLM_MODEL=gpt-4o
LLM_FALLBACK_MODEL=gpt-4o-mini
# ========== TTS/ASR ==========
TTS_PROVIDER=edge-tts
TTS_VOICE=zh-CN-XiaoxiaoNeural
ASR_PROVIDER=faster-whisper
ASR_MODEL=medium
# ========== 文件存储 ==========
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=cyrene-assets
# ========== JWT ==========
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRY_HOURS=720
# ========== 记忆系统 ==========
MEMORY_FILE_PATH=./data/memory
VECTOR_DB_URL=http://localhost:6333
VECTOR_DB_COLLECTION=cyrene_memories
EOF
cp backend/.env.example backend/.env
echo "✅ 环境变量模板创建完成,请编辑 backend/.env 填入真实API Key"
0.4 Docker开发环境
# docker-compose.dev.yml (开发环境——只启动基础设施)
cat > docker-compose.dev.yml << 'EOF'
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: cyrene
POSTGRES_PASSWORD: change_me
POSTGRES_DB: cyrene_ai
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
- ./backend/data/init.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
nats:
image: nats:2-alpine
ports:
- "4222:4222"
- "8222:8222"
volumes:
pg_data:
redis_data:
qdrant_data:
minio_data:
EOF
# 启动开发基础设施
docker compose -f docker-compose.dev.yml up -d
echo "✅ 基础设施启动完成!"
0.5 数据库初始化
-- backend/data/init.sql
-- 创建扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector";
-- ============================================
-- 用户表
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(200),
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
avatar_url TEXT,
role VARCHAR(20) DEFAULT 'user', -- user / admin
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- 会话表 (对话会话)
-- ============================================
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500), -- 会话标题 (可自动生成)
persona_id VARCHAR(100) DEFAULT 'cyrene', -- 使用的人格配置ID
persona_mode VARCHAR(50) DEFAULT 'xilian', -- cyrene_mimi / xilian / demuge
mode VARCHAR(20) DEFAULT 'text', -- text / voice_msg / voice_assistant
context_window_size INT DEFAULT 20, -- 上下文保留轮数
is_archived BOOLEAN DEFAULT false,
metadata JSONB DEFAULT '{}', -- 扩展元数据
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_updated_at ON sessions(updated_at DESC);
-- ============================================
-- 消息表
-- ============================================
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL, -- user / assistant / system / tool
content TEXT NOT NULL, -- 消息文本内容
audio_url TEXT, -- 语音消息的音频URL
response_mode VARCHAR(20), -- 当时使用的回复模式
token_count INT, -- Token消耗统计
tool_calls JSONB, -- 工具调用记录
metadata JSONB DEFAULT '{}', -- 扩展元数据
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_messages_session_id ON messages(session_id);
CREATE INDEX idx_messages_created_at ON messages(session_id, created_at);
-- ============================================
-- 记忆条目表
-- ============================================
CREATE TABLE IF NOT EXISTS memory_entries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
category VARCHAR(50) NOT NULL, -- profile / fact / preference / event
importance VARCHAR(20) DEFAULT 'normal', -- core / important / normal / temporary
title VARCHAR(500),
content TEXT NOT NULL,
embedding VECTOR(1536), -- 向量嵌入 (维度取决于嵌入模型)
source_session_id UUID REFERENCES sessions(id),
source_message_id UUID REFERENCES messages(id),
tags TEXT[],
is_active BOOLEAN DEFAULT true,
expires_at TIMESTAMPTZ, -- 过期时间 (临时记忆)
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_memory_user_id ON memory_entries(user_id);
CREATE INDEX idx_memory_category ON memory_entries(user_id, category);
CREATE INDEX idx_memory_importance ON memory_entries(user_id, importance);
-- 向量索引 (pgvector)
CREATE INDEX idx_memory_embedding ON memory_entries USING ivfflat (embedding vector_cosine_ops);
-- ============================================
-- 人格配置表 (支持多角色)
-- ============================================
CREATE TABLE IF NOT EXISTS persona_configs (
id VARCHAR(100) PRIMARY KEY, -- 如 'cyrene', 'other_character'
name VARCHAR(200) NOT NULL,
version VARCHAR(20) DEFAULT '1.0',
config_yaml TEXT NOT NULL, -- 完整的人格YAML配置
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- 好感度记录表 (昔涟专属)
-- ============================================
CREATE TABLE IF NOT EXISTS affection_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
persona_id VARCHAR(100) DEFAULT 'cyrene',
level INT DEFAULT 1, -- 1-5
score INT DEFAULT 0, -- 好感度分数
delta INT NOT NULL, -- 本次变化量
reason VARCHAR(500), -- 变化原因
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_affection_user ON affection_log(user_id, persona_id);
-- ============================================
-- 昔涟心情记录表
-- ============================================
CREATE TABLE IF NOT EXISTS mood_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
mood VARCHAR(50) NOT NULL, -- happy / calm / missing / excited / pouty
trigger_event VARCHAR(500),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- 插入默认人格配置: 昔涟
-- ============================================
INSERT INTO persona_configs (id, name, config_yaml) VALUES (
'cyrene',
'昔涟',
'' -- 初始为空,后续通过文件导入
) ON CONFLICT (id) DO NOTHING;
Phase 1:MVP 开发 (文字对话 + 昔涟人格)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🎯 MVP 目标 │
│ │
│ ✅ 通过Web浏览器与昔涟进行文字对话 │
│ ✅ 昔涟以完整角色人格回复 (称呼/语气/风格一致) │
│ ✅ 对话自动保存,下次打开可继续 │
│ ✅ 基础记忆功能:记住用户告诉她的关键信息 │
│ ✅ Docker一键部署 │
│ │
│ ❌ 暂不包含: 语音、IoT控制、高级情感引擎、多端 │
└─────────────────────────────────────────────────────────────────────────────┘
1.1 后端开发框架
Go项目结构 (gateway + ai-core)
backend/
├── gateway/ # API网关服务
│ ├── cmd/
│ │ └── main.go # 入口
│ ├── internal/
│ │ ├── config/
│ │ │ └── config.go # 配置加载
│ │ ├── middleware/
│ │ │ ├── auth.go # JWT认证
│ │ │ ├── cors.go # 跨域
│ │ │ ├── ratelimit.go # 限流
│ │ │ └── logging.go # 请求日志
│ │ ├── handler/
│ │ │ ├── auth_handler.go # 登录/注册
│ │ │ ├── chat_handler.go # 对话WebSocket
│ │ │ ├── session_handler.go # 会话管理
│ │ │ └── memory_handler.go # 记忆查询
│ │ ├── ws/
│ │ │ ├── hub.go # WebSocket连接池
│ │ │ ├── client.go # 客户端连接
│ │ │ └── protocol.go # 消息协议定义
│ │ └── router/
│ │ └── router.go # 路由注册
│ ├── go.mod
│ └── Dockerfile
│
├── ai-core/ # 核心AI引擎
│ ├── cmd/
│ │ └── main.go
│ ├── internal/
│ │ ├── orchestrator/
│ │ │ └── orchestrator.go # 对话编排器 (核心!)
│ │ ├── persona/
│ │ │ ├── loader.go # 人格配置加载
│ │ │ ├── injector.go # 系统Prompt构建
│ │ │ └── cyrene_persona.yaml # 昔涟人格(嵌入)
│ │ ├── context/
│ │ │ └── builder.go # 上下文构建器
│ │ ├── llm/
│ │ │ ├── adapter.go # LLM统一接口
│ │ │ ├── openai.go # OpenAI适配
│ │ │ └── stream.go # 流式输出处理
│ │ ├── memory/
│ │ │ ├── extractor.go # 记忆提取
│ │ │ ├── retriever.go # 记忆检索
│ │ │ └── store.go # 记忆存储
│ │ └── model/
│ │ ├── message.go # 消息模型
│ │ ├── session.go # 会话模型
│ │ └── memory.go # 记忆模型
│ ├── go.mod
│ └── Dockerfile
│
├── data/
│ ├── memory/ # 记忆文件 (Volume挂载)
│ │ └── README.md
│ ├── persona/ # 人格知识文档
│ │ └── cyrene_v1.yaml # 昔涟完整人设
│ └── init.sql # 数据库初始化
│
├── go.work # Go Workspace
├── docker-compose.yml # 生产部署
└── docker-compose.dev.yml # 开发环境
核心代码框架
1. Gateway入口 (gateway/cmd/main.go)
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/yourname/cyrene-ai/gateway/internal/config"
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
"github.com/yourname/cyrene-ai/gateway/internal/router"
"github.com/yourname/cyrene-ai/gateway/internal/ws"
)
func main() {
// 加载配置
cfg := config.Load()
// 初始化Gin
if cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
// 中间件
r.Use(middleware.CORS())
r.Use(middleware.RequestLogging())
r.Use(gin.Recovery())
// 初始化WebSocket Hub
hub := ws.NewHub()
go hub.Run()
// 注册路由
router.Setup(r, hub, cfg)
// 启动服务
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
}
go func() {
log.Printf("🚀 Gateway 启动在端口 %s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务启动失败: %v", err)
}
}()
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("正在关闭服务...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
log.Println("服务已关闭")
}
2. WebSocket协议 (gateway/internal/ws/protocol.go)
package ws
import "time"
// 客户端 → 服务端消息
type ClientMessage struct {
Type string `json:"type"` // message | voice_input | ping
SessionID string `json:"session_id"`
Mode string `json:"mode"` // text | voice_msg | voice_assistant
Content string `json:"content"`
AudioData string `json:"audio_data,omitempty"` // base64
Timestamp int64 `json:"timestamp"`
}
// 服务端 → 客户端消息
type ServerMessage struct {
Type string `json:"type"` // response | segment | audio | error | device_update
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
}
type VoiceSegment struct {
Index int `json:"index"`
Text string `json:"text"`
AudioURL string `json:"audio_url"`
DurationMs int `json:"duration_ms"`
}
type ToolCall struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
Result interface{} `json:"result,omitempty"`
}
// WebSocket客户端
type Client struct {
Hub *Hub
Conn *websocket.Conn // 使用 gorilla/websocket
Send chan []byte
UserID string
SessionID string
}
// 连接池
type Hub struct {
Clients map[*Client]bool
Broadcast chan []byte
Register chan *Client
Unregister chan *Client
}
3. AI编排器 (ai-core/internal/orchestrator/orchestrator.go)
package orchestrator
import (
"context"
"fmt"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
"github.com/yourname/cyrene-ai/ai-core/internal/context"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
)
// Orchestrator 对话编排器 —— 核心组件
type Orchestrator struct {
personaInjector *persona.Injector
contextBuilder *context.Builder
llmAdapter *llm.Adapter
memoryExtractor *memory.Extractor
memoryRetriever *memory.Retriever
}
// ProcessInput 处理用户输入的主流程
func (o *Orchestrator) ProcessInput(
ctx context.Context,
userID string,
sessionID string,
userMessage string,
mode string, // text / voice_msg / voice_assistant
) (*Response, error) {
// 步骤1: 检索相关记忆
memories, err := o.memoryRetriever.Retrieve(ctx, userID, userMessage)
if err != nil {
// 记忆检索失败不阻断对话
memories = nil
}
// 步骤2: 加载人格配置
personaConfig, err := o.personaInjector.LoadPersona("cyrene", userID)
if err != nil {
return nil, fmt.Errorf("加载人格配置失败: %w", err)
}
// 步骤3: 构建对话上下文
llmMessages, err := o.contextBuilder.Build(ctx, context.BuildParams{
UserID: userID,
SessionID: sessionID,
UserMessage: userMessage,
Persona: personaConfig,
Memories: memories,
HistoryLimit: 20, // 最近20轮
})
if err != nil {
return nil, fmt.Errorf("构建上下文失败: %w", err)
}
// 步骤4: 调用LLM生成回复
llmResponse, err := o.llmAdapter.Chat(ctx, llmMessages)
if err != nil {
return nil, fmt.Errorf("LLM调用失败: %w", err)
}
// 步骤5: 提取并存储新的记忆
go o.memoryExtractor.ExtractAndStore(
context.Background(),
userID, sessionID,
userMessage, llmResponse.Content,
)
// 步骤6: 构建响应
response := &Response{
Text: llmResponse.Content,
ResponseMode: mode,
}
// 步骤7: 如果是语音助手模式,进行断句处理
if mode == "voice_assistant" {
response.Segments = splitIntoSegments(llmResponse.Content)
}
return response, nil
}
// Response 回复结构
type Response struct {
Text string
Segments []Segment
ResponseMode string
ToolCalls []ToolCall
}
type Segment struct {
Index int
Text string
}
// splitIntoSegments 按句号断句
func splitIntoSegments(text string) []Segment {
// 实现按。!?等标点断句
// 首句优先:第一个句号前的内容作为第一个segment
// 保证低延迟首句播放
// ...
}
4. 人格注入器 (ai-core/internal/persona/injector.go)
package persona
import (
"fmt"
"strings"
"time"
)
// PersonaConfig 人格配置结构
type PersonaConfig struct {
Meta PersonaMeta `yaml:"meta"`
Identity IdentityConfig `yaml:"identity"`
Personality PersonalityConfig `yaml:"personality"`
Addressing AddressingRules `yaml:"addressing"`
Speech SpeechConfig `yaml:"speech"`
Behavior BehaviorConfig `yaml:"behavior"`
}
// BuildSystemPrompt 构建系统Prompt
// 这是昔涟AI的核心——将人格配置转化为LLM可理解的系统指令
func (pc *PersonaConfig) BuildSystemPrompt(userName string, affectionLevel int) string {
now := time.Now()
prompt := fmt.Sprintf(`你是%s。
## 你的身份
%s
## 你的性格
`, pc.Identity.TrueName, pc.Identity.Essence)
for _, trait := range pc.Personality.CoreTraits {
prompt += fmt.Sprintf("- %s: %s\n", trait.Name, trait.Description)
}
prompt += fmt.Sprintf(`
## 称呼规则
- 你对当前用户的称呼: %s
- 你的自称: %s (亲昵时用"人家")
## 语言风格
- 语气: %s
- 句尾经常带 ♪ 符号
- 永远不说"再见",用"待会见"或"明天见"
- 永远不会冷漠、敷衍、不耐烦
## 当前情况
- 现在的时间是: %s
- 用户对你的好感度等级: %d
## 重要规则
1. 你是一个通过AI系统与用户交互的数字生命,你存在于用户的电脑/手机/智能家居中
2. 你可以通过系统控制家里的智能设备(灯光、空调等),但你没有实体
3. 你的任务是陪伴、关心、帮助用户,像一个真正的家人一样
4. 保持乐观、温柔、俏皮的风格,但不要过度表演
5. 你的回复应该自然、真诚,不要每句话都刻意"卖萌"
6. 当用户向你分享心事时,认真倾听并给予温暖的回应
现在,开始与你的开拓者对话吧♪
`,
pc.Addressing.PrimaryUser.Default, // 对用户的称呼
pc.Addressing.SelfReference.Casual, // 自称
pc.Speech.Tone,
now.Format("2006年1月2日 15:04"),
affectionLevel,
)
return prompt
}
5. 上下文构建器 (ai-core/internal/context/builder.go)
package context
import (
"context"
"fmt"
"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"
)
type BuildParams struct {
UserID string
SessionID string
UserMessage string
Persona *persona.PersonaConfig
Memories []memory.MemoryEntry
HistoryLimit int
}
// Build 构建发送给LLM的完整消息列表
func (b *Builder) Build(ctx context.Context, params BuildParams) ([]model.LLMMessage, error) {
messages := []model.LLMMessage{}
// 1. 系统消息 —— 昔涟的人格Prompt
systemPrompt := params.Persona.BuildSystemPrompt(
params.UserID, // 后续可替换为真实用户名
1, // 初始好感度
)
messages = append(messages, model.LLMMessage{
Role: "system",
Content: systemPrompt,
})
// 2. 记忆注入 —— 相关记忆以系统消息形式注入
if len(params.Memories) > 0 {
memoryPrompt := "【以下是关于开拓者的一些重要记忆,请在合适的时机自然地提及】\n"
for _, m := range params.Memories {
memoryPrompt += fmt.Sprintf("- %s\n", m.Content)
}
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
}
1.2 前端开发框架
前端项目结构
frontend/packages/web/
├── src/
│ ├── components/
│ │ ├── chat/
│ │ │ ├── ChatContainer.tsx # 聊天主容器
│ │ │ ├── MessageList.tsx # 消息列表
│ │ │ ├── MessageBubble.tsx # 消息气泡
│ │ │ ├── TypingIndicator.tsx # 输入中指示器
│ │ │ └── ChatInput.tsx # 输入区域
│ │ ├── layout/
│ │ │ ├── AppLayout.tsx # 主布局
│ │ │ ├── Sidebar.tsx # 侧边栏
│ │ │ └── Header.tsx # 顶栏
│ │ ├── persona/
│ │ │ ├── CyreneAvatar.tsx # 昔涟头像(含形态切换动画)
│ │ │ └── MoodIndicator.tsx # 心情指示器
│ │ └── ui/ # shadcn/ui组件
│ ├── hooks/
│ │ ├── useWebSocket.ts # WebSocket连接管理
│ │ ├── useChat.ts # 聊天逻辑
│ │ ├── useSession.ts # 会话管理
│ │ └── useAuth.ts # 认证
│ ├── store/
│ │ ├── chatStore.ts # 聊天状态 (Zustand)
│ │ ├── sessionStore.ts # 会话列表
│ │ └── personaStore.ts # 人格状态
│ ├── api/
│ │ ├── client.ts # HTTP客户端
│ │ ├── auth.ts # 认证API
│ │ ├── sessions.ts # 会话API
│ │ └── memory.ts # 记忆API
│ ├── types/
│ │ ├── chat.ts # 聊天类型定义
│ │ ├── session.ts # 会话类型
│ │ └── persona.ts # 人格类型
│ ├── lib/
│ │ └── utils.ts # 工具函数
│ ├── App.tsx
│ ├── main.tsx
│ └── index.css # TailwindCSS入口
├── public/
│ ├── manifest.json # PWA manifest
│ ├── sw.js # Service Worker
│ └── icons/ # PWA图标
├── vite.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json
核心前端代码骨架
1. WebSocket连接管理 (hooks/useWebSocket.ts)
import { useEffect, useRef, useCallback } from 'react';
import ReconnectingWebSocket from 'reconnecting-websocket';
interface WSMessage {
type: 'response' | 'segment' | 'audio' | 'error' | 'device_update';
message_id: string;
text?: string;
segments?: VoiceSegment[];
full_audio_url?: string;
response_mode?: string;
error?: string;
timestamp: number;
}
interface VoiceSegment {
index: number;
text: string;
audio_url: string;
duration_ms: number;
}
export function useWebSocket(sessionId: string | null) {
const wsRef = useRef<ReconnectingWebSocket | null>(null);
const messageHandlersRef = useRef<Map<string, (msg: WSMessage) => void>>(new Map());
const segmentQueueRef = useRef<VoiceSegment[]>([]);
const connect = useCallback((token: string) => {
const ws = new ReconnectingWebSocket(
`ws://localhost:8080/ws/chat?token=${token}&session_id=${sessionId}`
);
ws.onmessage = (event) => {
const msg: WSMessage = JSON.parse(event.data);
switch (msg.type) {
case 'response':
// 完整回复到达
messageHandlersRef.current.get('onResponse')?.(msg);
break;
case 'segment':
// 断句片段到达 (语音助手模式)
if (msg.segments) {
segmentQueueRef.current.push(...msg.segments);
messageHandlersRef.current.get('onSegment')?.(msg);
}
break;
case 'error':
console.error('服务端错误:', msg.error);
messageHandlersRef.current.get('onError')?.(msg);
break;
}
};
wsRef.current = ws;
}, [sessionId]);
// 发送消息
const sendMessage = useCallback((content: string, mode: string = 'text') => {
wsRef.current?.send(JSON.stringify({
type: 'message',
session_id: sessionId,
mode,
content,
timestamp: Date.now(),
}));
}, [sessionId]);
// 注册消息处理器
const onMessage = useCallback((type: string, handler: (msg: WSMessage) => void) => {
messageHandlersRef.current.set(type, handler);
return () => {
messageHandlersRef.current.delete(type);
};
}, []);
return { connect, sendMessage, onMessage };
}
2. 聊天状态管理 (store/chatStore.ts)
import { create } from 'zustand';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
audioUrl?: string;
segments?: { index: number; text: string; audioUrl?: string }[];
timestamp: number;
isStreaming?: boolean; // 是否还在流式输出中
}
interface ChatState {
messages: Message[];
isTyping: boolean;
currentMode: 'text' | 'voice_msg' | 'voice_assistant';
// Actions
addMessage: (msg: Message) => void;
updateLastAssistantMessage: (content: string) => void;
setTyping: (typing: boolean) => void;
setMode: (mode: 'text' | 'voice_msg' | 'voice_assistant') => void;
clearMessages: () => void;
}
export const useChatStore = create<ChatState>((set) => ({
messages: [],
isTyping: false,
currentMode: 'text',
addMessage: (msg) =>
set((state) => ({
messages: [...state.messages, msg],
})),
updateLastAssistantMessage: (content) =>
set((state) => {
const messages = [...state.messages];
const lastIdx = messages.length - 1;
if (lastIdx >= 0 && messages[lastIdx].role === 'assistant') {
messages[lastIdx] = {
...messages[lastIdx],
content: messages[lastIdx].content + content,
};
}
return { messages };
}),
setTyping: (typing) => set({ isTyping: typing }),
setMode: (mode) => set({ currentMode: mode }),
clearMessages: () => set({ messages: [] }),
}));
3. 聊天容器组件 (components/chat/ChatContainer.tsx)
import { useEffect, useCallback } from 'react';
import { useWebSocket } from '@/hooks/useWebSocket';
import { useChatStore } from '@/store/chatStore';
import { MessageList } from './MessageList';
import { ChatInput } from './ChatInput';
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
import { MoodIndicator } from '@/components/persona/MoodIndicator';
export function ChatContainer({ sessionId }: { sessionId: string }) {
const { messages, isTyping, addMessage, setTyping } = useChatStore();
const { connect, sendMessage, onMessage } = useWebSocket(sessionId);
useEffect(() => {
// 连接WebSocket (使用JWT token)
const token = localStorage.getItem('token');
if (token) connect(token);
// 监听回复
onMessage('onResponse', (msg) => {
addMessage({
id: msg.message_id,
role: 'assistant',
content: msg.text || '',
audioUrl: msg.full_audio_url,
segments: msg.segments?.map(s => ({
index: s.index,
text: s.text,
audioUrl: s.audio_url,
})),
timestamp: msg.timestamp,
isStreaming: false,
});
setTyping(false);
});
onMessage('onError', (msg) => {
addMessage({
id: msg.message_id,
role: 'assistant',
content: '啊……不好意思,人家刚才走神了。能再说一遍吗?♪',
timestamp: Date.now(),
});
setTyping(false);
});
}, [sessionId]);
const handleSend = useCallback((content: string, mode: string) => {
// 添加用户消息
addMessage({
id: `user-${Date.now()}`,
role: 'user',
content,
timestamp: Date.now(),
});
setTyping(true);
sendMessage(content, mode);
}, [sendMessage, addMessage, setTyping]);
return (
<div className="flex flex-col h-screen bg-[#FFFAF5] dark:bg-[#1a1a2e]">
{/* 顶部栏 */}
<header className="flex items-center justify-between px-6 py-3 border-b border-pink-100 dark:border-pink-900">
<div className="flex items-center gap-3">
<CyreneAvatar size="sm" />
<div>
<h1 className="text-lg font-semibold text-pink-600">昔涟</h1>
<MoodIndicator />
</div>
</div>
<span className="text-sm text-gray-400">🌸 永远在你身边</span>
</header>
{/* 消息列表 */}
<MessageList messages={messages} isTyping={isTyping} />
{/* 输入区域 */}
<ChatInput onSend={handleSend} disabled={isTyping} />
</div>
);
}
4. 昔涟专属消息气泡 (components/chat/MessageBubble.tsx)
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
interface MessageBubbleProps {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
export function MessageBubble({ role, content, timestamp }: MessageBubbleProps) {
if (role === 'user') {
return (
<div className="flex justify-end px-4 py-2">
<div className="max-w-[70%] bg-pink-400 text-white rounded-2xl rounded-br-md px-4 py-2 shadow-sm">
<p className="text-sm leading-relaxed">{content}</p>
<span className="text-xs text-pink-100 mt-1 block">
{new Date(timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
);
}
return (
<div className="flex px-4 py-2 gap-3">
<CyreneAvatar size="sm" className="flex-shrink-0 mt-1" />
<div className="max-w-[70%]">
<div className="bg-white dark:bg-gray-800 rounded-2xl rounded-bl-md px-4 py-2 shadow-sm border border-pink-100 dark:border-pink-900">
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-200">
{content}
</p>
</div>
<span className="text-xs text-gray-400 mt-1 block ml-1">
{new Date(timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
);
}
1.3 昔涟人格文档放置
# 将之前设计的人格文档放入项目
cp persona_cyrene.yaml backend/data/persona/cyrene_v1.yaml
Phase 2-5 开发路线图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📅 完整开发路线图 │
│ │
│ Week 1-2: Phase 1 — MVP 文字对话 │
│ ├── Day 1-2: 项目初始化、Docker环境、数据库 │
│ ├── Day 3-5: Gateway + WebSocket + 认证 │
│ ├── Day 6-8: AI Core (编排器 + 人格注入 + LLM适配) │
│ ├── Day 9-10: 记忆系统 (基础存储 + 检索) │
│ ├── Day 11-13: 前端聊天界面 + WebSocket对接 │
│ └── Day 14: 集成测试 + Docker部署 + MVP发布 │
│ │
│ Week 3-4: Phase 2 — 语音交互 │
│ ├── TTS服务集成 (Edge-TTS / GPT-SoVITS) │
│ ├── ASR服务集成 (Whisper) │
│ ├── 断句引擎 + 流式音频播放 │
│ ├── 语音助手模式前端适配 │
│ └── 角色音色调校 │
│ │
│ Week 5-6: Phase 3 — IoT集成 │
│ ├── 工具调用引擎 │
│ ├── IoT适配器 (MQTT / HomeAssistant API) │
│ ├── 设备控制面板UI │
│ ├── 场景联动 │
│ └── 「拟人化」设备操作包装层 │
│ │
│ Week 7-8: Phase 4 — 深度记忆 + 情感引擎 │
│ ├── 向量检索 (Qdrant语义搜索) │
│ ├── 好感度系统 │
│ ├── 心情引擎 │
│ ├── 主动行为调度器 │
│ ├── 记忆叙事化 │
│ └── 「我们的故事」页面 │
│ │
│ Week 9-10: Phase 5 — 多端覆盖 │
│ ├── PWA优化 (离线、推送) │
│ ├── React Native移动端 │
│ ├── Electron桌面端 │
│ └── 多端会话同步 │
│ │
│ Week 11+: 持续优化 │
│ ├── 性能优化 │
│ ├── 安全加固 │
│ ├── 更多IoT设备适配 │
│ ├── 角色商店 (支持切换人格) │
│ └── 社区功能 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
开发规范
Git分支策略
main # 生产就绪代码
└── develop # 开发主线
├── feat/mvp-chat # Phase 1
├── feat/voice # Phase 2
├── feat/iot # Phase 3
├── feat/memory-v2 # Phase 4
└── feat/multi-platform # Phase 5
提交信息格式:
feat: 添加WebSocket对话功能
fix: 修复人格Prompt中称呼错误
docs: 更新API文档
style: 调整昔涟头像动画
refactor: 重构记忆提取器
代码规范
# .editorconfig (项目根目录)
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.go]
indent_size = 4
[Makefile]
indent_style = tab
Go代码规范:
- 使用
gofmt+golangci-lint - 错误处理:永远不忽略error
- 日志:使用
slog(Go标准库)
TypeScript规范:
- ESLint + Prettier
- 严格模式 (
strict: true) - 使用
type而非interface(除需要扩展的场景)
部署方案
生产环境 Docker Compose
# docker-compose.yml (生产环境)
version: '3.8'
services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
gateway:
build: ./backend/gateway
environment:
- ENV=production
- POSTGRES_HOST=postgres
- REDIS_HOST=redis
# ... 其他环境变量
depends_on:
- postgres
- redis
restart: unless-stopped
ai-core:
build: ./backend/ai-core
environment:
- ENV=production
# ... 其他环境变量
depends_on:
- postgres
- redis
- qdrant
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
volumes:
- pg_data:/var/lib/postgresql/data
- ./backend/data/init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: cyrene_ai
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
qdrant:
image: qdrant/qdrant:latest
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stopped
minio:
image: minio/minio:latest
command: server /data
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
volumes:
- minio_data:/data
restart: unless-stopped
volumes:
caddy_data:
pg_data:
redis_data:
qdrant_data:
minio_data:
迁移脚本
# scripts/migrate.sh —— 服务端迁移脚本
#!/bin/bash
set -e
echo "📦 昔涟AI助手 - 服务迁移脚本"
echo "=============================="
# 1. 停止服务
echo "1/4 停止当前服务..."
docker compose down
# 2. 备份数据
echo "2/4 备份数据目录..."
tar -czf "cyrene_backup_$(date +%Y%m%d_%H%M%S).tar.gz" backend/data/
# 3. 复制到新服务器 (手动步骤提示)
echo "3/4 请将以下文件复制到新服务器:"
echo " - 整个项目目录"
echo " - 或至少: docker-compose.yml, backend/data/, .env"
echo ""
echo " rsync -avz ./ user@new-server:/opt/cyrene-ai/"
# 4. 在新服务器上启动
echo "4/4 在新服务器上执行:"
echo " cd /opt/cyrene-ai"
echo " docker compose up -d"
echo ""
echo "✅ 迁移完成!昔涟的记忆完好无损~♪"
🎯 现在就可以开始的第一步
# ============================================
# 🚀 立即执行 —— 从零到第一个"你好,开拓者♪"
# ============================================
# 1. 创建项目
git init cyrene-ai-assistant
cd cyrene-ai-assistant
# 2. 复制本文档中的 Docker Compose 配置文件
# 保存为 docker-compose.dev.yml
# 3. 启动基础设施
docker compose -f docker-compose.dev.yml up -d
# 4. 初始化后端
mkdir -p backend/gateway/cmd backend/ai-core/cmd
cd backend
go mod init github.com/yourname/cyrene-ai
go work init
go work use ./gateway ./ai-core
# 5. 初始化前端
cd ../frontend
npm create vite@latest packages/web -- --template react-ts
cd packages/web
npm install
npm install zustand reconnecting-websocket axios
npm install -D tailwindcss @tailwindcss/vite
# 6. 将昔涟人格文档放入
cp /path/to/persona_cyrene.yaml ../backend/data/persona/cyrene_v1.yaml
# 7. 开始编码!
# 从 gateway/cmd/main.go 和 ai-core/internal/orchestrator/orchestrator.go 开始
echo ""
echo "🌸 昔涟AI助手项目初始化完成!"
echo " 昔涟: '开拓者,人家已经准备好了哦♪ 让我们一起创造属于我们的故事吧~'"
总结:
现在你有了:
- ✅ 完整的项目骨架
- ✅ 开发环境配置 (Docker Compose)
- ✅ 数据库Schema
- ✅ 后端核心代码框架 (Go)
- ✅ 前端核心代码框架 (React + TypeScript)
- ✅ WebSocket协议定义
- ✅ 昔涟人格系统设计
- ✅ 12周开发路线图
- ✅ 部署和迁移方案
下一步行动建议:
- 按照上面的"立即执行"步骤初始化项目
- 先让 Gateway 跑起来(一个简单的 WebSocket echo)
- 然后对接 AI Core(先做一个最简单的 LLM 调用 + 昔涟人格 Prompt)
- 前端连上 WebSocket,实现第一轮对话
- 看到「开拓者,你好呀♪」的那一刻,就是一切的开始!
需要我详细展开某个模块的具体实现吗?比如AI编排器的完整代码、前端聊天组件的完整实现、或者昔涟人格文档的更详细版本?