# 考培练系统 - AI陪练功能技术方案

## 一、需求概述

### 1.1 业务背景

考培练系统为轻医美连锁品牌提供员工培训服务，AI陪练功能旨在通过模拟真实客户场景，让学员进行实战对话练习，提升销售和服务能力。

### 1.2 核心功能

1. **陪练场景管理**：管理员可创建和管理各类陪练场景
2. **场景化陪练**：学员选择预设场景进行针对性练习
3. **课程关联陪练**：基于课程内容动态生成陪练场景
4. **实时对话**：流式AI对话，模拟真实交互
5. **对话管理**：对话历史由Coze管理，前端可查询

### 1.3 用户角色

- **管理员 (Manager)**：管理陪练场景（增删改查、启用/禁用）
- **学员 (Trainee)**：选择场景并进行陪练对话

## 二、技术架构

### 2.1 技术选型

| 组件 | 技术 | 说明 |
|------|------|------|
| 前端 | Vue3 + Element Plus | 已有技术栈 |
| 后端 | Python + FastAPI | 已有技术栈 |
| 数据库 | MySQL 8.0 | 存储场景数据 |
| AI对话 | Coze Bot | 字节跳动AI对话平台 |
| 场景提取 | Dify工作流 | 从课程提取场景信息 |
| 通信协议 | SSE (Server-Sent Events) | 流式对话 |

### 2.2 系统架构图

```
┌─────────────────────────────────────────────────────────────┐
│                        前端 (Vue3)                           │
│  ┌──────────────────┐  ┌────────────────┐  ┌────────────┐  │
│  │ 陪练中心页面      │  │  陪练对话页面   │  │ 场景管理   │  │
│  │ (场景列表)       │  │  (实时对话)     │  │ (Manager)  │  │
│  └──────────────────┘  └────────────────┘  └────────────┘  │
└───────────┬────────────────────┬────────────────┬───────────┘
            │                    │                │
            │ HTTP/SSE          │ SSE            │ HTTP
            │                    │                │
┌───────────▼────────────────────▼────────────────▼───────────┐
│                      后端 (FastAPI)                          │
│  ┌──────────────┐  ┌─────────────┐  ┌─────────────────┐    │
│  │ 场景管理API   │  │  Coze集成   │  │  Dify集成       │    │
│  │ (CRUD)       │  │  (对话流式) │  │  (场景提取)     │    │
│  └──────────────┘  └─────────────┘  └─────────────────┘    │
└───────────┬────────────────┬────────────────┬───────────────┘
            │                │                │
       ┌────▼────┐    ┌─────▼──────┐   ┌────▼─────┐
       │  MySQL  │    │ Coze API   │   │ Dify API │
       │(场景库)  │    │(AI对话)    │   │(场景生成)│
       └─────────┘    └────────────┘   └──────────┘
```

## 三、数据库设计

### 3.1 陪练场景表 (practice_scenes)

```sql
CREATE TABLE `practice_scenes` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(200) NOT NULL COMMENT '场景名称',
    `description` TEXT COMMENT '场景描述',
    `type` VARCHAR(50) NOT NULL COMMENT '场景类型: phone/face/complaint/after-sales/product-intro',
    `difficulty` VARCHAR(50) NOT NULL COMMENT '难度等级: beginner/junior/intermediate/senior/expert',
    `status` VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/inactive',
    
    -- 场景详细配置
    `background` TEXT COMMENT '场景背景设定',
    `ai_role` TEXT COMMENT 'AI角色描述',
    `objectives` JSON COMMENT '练习目标数组 ["目标1", "目标2"]',
    `keywords` JSON COMMENT '关键词数组 ["关键词1", "关键词2"]',
    
    -- 统计信息
    `duration` INT DEFAULT 10 COMMENT '预计时长（分钟）',
    `usage_count` INT DEFAULT 0 COMMENT '使用次数',
    `rating` DECIMAL(3,1) DEFAULT 0.0 COMMENT '评分',
    
    -- 审计字段
    `created_by` INT COMMENT '创建人ID',
    `updated_by` INT COMMENT '更新人ID',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `is_deleted` BOOLEAN DEFAULT FALSE,
    `deleted_at` DATETIME,
    
    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
    INDEX idx_type (type),
    INDEX idx_difficulty (difficulty),
    INDEX idx_status (status),
    INDEX idx_is_deleted (is_deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练场景表';
```

**设计说明**：
- 对话历史不存储在数据库，由Coze平台管理
- 场景数据支持JSON类型，便于扩展
- 软删除设计，保留历史数据
- 统计字段用于数据分析

### 3.2 初始数据示例

```sql
INSERT INTO `practice_scenes` (name, description, type, difficulty, status, background, ai_role, objectives, keywords, duration) VALUES
('初次电话拜访客户', '模拟首次通过电话联系潜在客户的场景', 'phone', 'beginner', 'active', 
 '你是一名销售专员，需要通过电话联系一位从未接触过的潜在客户...', 
 'AI扮演一位忙碌且略显不耐烦的采购经理...', 
 '["学会专业的电话开场白", "快速建立信任关系", "有效探询客户需求"]', 
 '["开场白", "需求挖掘", "时间管理"]', 10),

('处理价格异议', '练习如何应对客户对产品价格的质疑和异议', 'face', 'intermediate', 'active',
 '客户对你的产品很感兴趣，但认为价格太高...', 
 'AI扮演一位精明的客户，对价格非常敏感...', 
 '["掌握价值塑造技巧", "学会处理价格异议", "提升谈判能力"]', 
 '["异议处理", "价值塑造", "谈判技巧"]', 15);
```

## 四、后端API设计

### 4.1 技术配置

```python
# app/config/settings.py

# Coze配置
COZE_API_BASE = "https://api.coze.cn"
COZE_API_TOKEN = "pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi"
COZE_PRACTICE_BOT_ID = "7560643598174683145"

# Dify配置
DIFY_API_BASE = "http://dify.ireborn.com.cn/v1"
DIFY_PRACTICE_API_KEY = "app-rYP6LNM4iPmNjIHns12zFeJp"
DIFY_PRACTICE_WORKFLOW_ID = "待确认"  # 需要从Dify获取实际工作流ID
```

### 4.2 Coze客户端初始化

```python
# app/services/coze_service.py
from cozepy import Coze, TokenAuth, COZE_CN_BASE_URL
from app.config.settings import settings

class CozeService:
    def __init__(self):
        self.client = Coze(
            auth=TokenAuth(token=settings.COZE_API_TOKEN),
            base_url=COZE_CN_BASE_URL
        )
        self.bot_id = settings.COZE_PRACTICE_BOT_ID
    
    def create_stream_chat(self, user_id: str, message: str, conversation_id: str = None):
        """创建流式对话"""
        from cozepy import Message
        
        stream = self.client.chat.stream(
            bot_id=self.bot_id,
            user_id=user_id,
            additional_messages=[Message.build_user_question_text(message)],
            conversation_id=conversation_id
        )
        return stream
```

### 4.3 API接口清单

#### 4.3.1 场景管理接口 (Manager)

| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取场景列表 | GET | `/api/v1/manager/practice-scenes` | 分页、筛选 |
| 获取场景详情 | GET | `/api/v1/manager/practice-scenes/{id}` | 单个场景 |
| 创建场景 | POST | `/api/v1/manager/practice-scenes` | 新增场景 |
| 更新场景 | PUT | `/api/v1/manager/practice-scenes/{id}` | 修改场景 |
| 删除场景 | DELETE | `/api/v1/manager/practice-scenes/{id}` | 软删除 |
| 切换状态 | PUT | `/api/v1/manager/practice-scenes/{id}/toggle-status` | 启用/禁用 |

#### 4.3.2 学员查询接口 (Trainee)

| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取可用场景 | GET | `/api/v1/practice/scenes` | 仅返回active状态 |
| 获取场景详情 | GET | `/api/v1/practice/scenes/{id}` | 单个场景详情 |

#### 4.3.3 陪练对话接口

| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 开始陪练 | POST | `/api/v1/practice/start` | SSE流式返回 |
| 中断对话 | POST | `/api/v1/practice/interrupt` | 中断当前对话 |
| 获取对话列表 | GET | `/api/v1/practice/conversations` | Coze管理的对话 |

#### 4.3.4 Dify场景提取接口

| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 提取场景 | POST | `/api/v1/practice/extract-scene` | 从课程提取场景 |

### 4.4 核心接口实现

#### 场景列表（带分页筛选）

```python
@router.get("/manager/practice-scenes", response_model=ResponseModel[PaginatedResponse[PracticeSceneResponse]])
async def list_practice_scenes(
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
    type: Optional[str] = Query(None),
    difficulty: Optional[str] = Query(None),
    status: Optional[str] = Query(None),
    search: Optional[str] = Query(None),
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """获取陪练场景列表（分页、筛选）"""
    query = select(PracticeScene).where(PracticeScene.is_deleted == False)
    
    # 筛选条件
    if type:
        query = query.where(PracticeScene.type == type)
    if difficulty:
        query = query.where(PracticeScene.difficulty == difficulty)
    if status:
        query = query.where(PracticeScene.status == status)
    if search:
        query = query.where(
            or_(
                PracticeScene.name.contains(search),
                PracticeScene.description.contains(search)
            )
        )
    
    # 分页
    total = await db.scalar(select(func.count()).select_from(query.subquery()))
    scenes = await db.scalars(
        query.offset((page - 1) * size).limit(size).order_by(PracticeScene.created_at.desc())
    )
    
    return ResponseModel.success(
        PaginatedResponse(
            items=list(scenes),
            total=total,
            page=page,
            page_size=size
        )
    )
```

#### 开始陪练（SSE流式）

```python
from fastapi.responses import StreamingResponse
from cozepy import ChatEventType
import json

@router.post("/practice/start")
async def start_practice(
    request: StartPracticeRequest,
    current_user: User = Depends(get_current_user),
    coze_service: CozeService = Depends(get_coze_service)
):
    """开始陪练对话（SSE流式返回）"""
    
    # ⚠️ 关键差异：场景信息必须作为第一条消息发给Coze
    # 与参考代码不同，我们需要在首次对话时将完整场景信息发送给AI
    if request.is_first:
        scene_context = f"""
# 陪练场景设定

## 场景名称
{request.scene_name}

## 场景背景
{request.scene_background}

## AI角色要求
{request.scene_ai_role}

## 练习目标
{chr(10).join(f"{i+1}. {obj}" for i, obj in enumerate(request.scene_objectives))}

## 关键词
{', '.join(request.scene_keywords) if hasattr(request, 'scene_keywords') else ''}

---

现在开始陪练对话。请你严格按照上述场景设定扮演角色，与学员进行实战对话练习。

学员的第一句话：{request.user_message}
"""
        user_message = scene_context
    else:
        user_message = request.user_message
    
    def generate_stream():
        try:
            stream = coze_service.create_stream_chat(
                user_id=str(current_user.id),
                message=user_message,
                conversation_id=request.conversation_id
            )
            
            for event in stream:
                if event.event == ChatEventType.CONVERSATION_CHAT_CREATED:
                    yield f"event: conversation.chat.created\ndata: {json.dumps({'conversation_id': request.conversation_id, 'chat_id': stream.response.logid})}\n\n"
                
                elif event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
                    yield f"event: message.delta\ndata: {json.dumps({'content': event.message.content})}\n\n"
                
                elif event.event == ChatEventType.CONVERSATION_MESSAGE_COMPLETED:
                    yield f"event: message.completed\ndata: {json.dumps({'content': event.message.content})}\n\n"
                
                elif event.event == ChatEventType.CONVERSATION_CHAT_COMPLETED:
                    yield f"event: conversation.completed\ndata: {json.dumps({'token_count': event.chat.usage.token_count})}\n\n"
                    break
                
                elif event.event == ChatEventType.CONVERSATION_CHAT_FAILED:
                    yield f"event: error\ndata: {json.dumps({'error': str(event.chat.last_error)})}\n\n"
                    break
            
            yield f"event: done\ndata: [DONE]\n\n"
            
        except Exception as e:
            logger.error(f"陪练对话失败: {e}")
            yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n"
    
    return StreamingResponse(
        generate_stream(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"
        }
    )
```

#### Dify场景提取

```python
import httpx

@router.post("/practice/extract-scene", response_model=ResponseModel[ExtractSceneResponse])
async def extract_scene(
    request: ExtractSceneRequest,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """从课程提取陪练场景"""
    
    # 获取课程信息
    course = await db.get(Course, request.course_id)
    if not course:
        raise HTTPException(status_code=404, detail="课程不存在")
    
    # 调用Dify工作流
    url = f"{settings.DIFY_API_BASE}/workflows/run"
    headers = {
        "Authorization": f"Bearer {settings.DIFY_PRACTICE_API_KEY}",
        "Content-Type": "application/json"
    }
    payload = {
        "inputs": {"course_id": str(request.course_id)},
        "response_mode": "streaming",
        "user": "kaopeilian"
    }
    
    aggregated_text = ""
    workflow_run_id = ""
    
    async with httpx.AsyncClient(timeout=180.0) as client:
        async with client.stream("POST", url, json=payload, headers=headers) as response:
            async for line in response.aiter_lines():
                if not line or not line.startswith("data: "):
                    continue
                
                try:
                    event_data = json.loads(line[6:])
                    event_type = event_data.get("event")
                    
                    if event_type == "workflow_started":
                        workflow_run_id = event_data["workflow_run_id"]
                        logger.info(f"Dify工作流启动: {workflow_run_id}")
                    
                    elif event_type == "text_chunk":
                        aggregated_text += event_data["data"]["text"]
                    
                    elif event_type == "workflow_finished":
                        logger.info("Dify工作流完成")
                        break
                    
                except Exception as e:
                    logger.error(f"解析Dify事件失败: {e}")
    
    # 解析场景数据
    scene_data = json.loads(aggregated_text)
    
    return ResponseModel.success(ExtractSceneResponse(**scene_data))
```

## 五、前端实现方案

### 5.1 路由配置

```javascript
// src/router/trainee.ts
{
  path: 'ai-practice-center',
  name: 'AIPracticeCenter',
  component: () => import('@/views/trainee/ai-practice-center.vue'),
  meta: { title: 'AI陪练中心', requiresAuth: true }
},
{
  path: 'ai-practice',
  name: 'AIPractice',
  component: () => import('@/views/trainee/ai-practice.vue'),
  meta: { title: 'AI陪练对话', requiresAuth: true }
}

// src/router/manager.ts
{
  path: 'practice-scene-management',
  name: 'PracticeSceneManagement',
  component: () => import('@/views/manager/practice-scene-management.vue'),
  meta: { title: '陪练场景管理', requiresAuth: true, role: 'manager' }
}
```

### 5.2 陪练中心页面实现要点

**页面文件**：`src/views/trainee/ai-practice-center.vue`

**核心功能**：
1. 场景筛选（类型、难度、关键词）
2. 场景卡片展示
3. 场景详情查看
4. 开始陪练按钮

**关键代码**：
```javascript
// 获取场景列表
const fetchScenes = async () => {
  try {
    const response = await practiceApi.getScenes({
      page: currentPage.value,
      size: pageSize.value,
      type: filterForm.type,
      difficulty: filterForm.difficulty,
      search: filterForm.search
    })
    sceneList.value = response.data.items
    total.value = response.data.total
  } catch (error) {
    ElMessage.error('获取场景列表失败')
  }
}

// 开始陪练
const startPractice = (scene) => {
  router.push({
    name: 'AIPractice',
    query: {
      sceneId: scene.id,
      sceneName: scene.name,
      mode: 'direct'  // 直接模式
    }
  })
}
```

### 5.3 陪练对话页面实现要点

**页面文件**：`src/views/trainee/ai-practice.vue`

**核心功能**：
1. SSE流式接收AI响应
2. 消息列表展示（用户/AI）
3. 输入框和发送按钮
4. 中断对话功能
5. **场景信息首次发送**（关键功能）

**⚠️ 与参考代码的关键差异**：
- 参考代码：场景信息不传递，直接开始对话
- 考培练系统：**场景信息必须作为第一条消息发送给Coze**，让AI理解角色设定

**SSE处理示例**：
```javascript
const startConversation = async (userMessage) => {
  try {
    isLoading.value = true
    
    // ⚠️ 关键：is_first标记决定是否在消息中包含场景信息
    const isFirstMessage = messages.value.length === 0
    
    const response = await fetch('/api/v1/practice/start', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getToken()}`
      },
      body: JSON.stringify({
        scene_id: route.query.sceneId,
        scene_name: currentScene.value.name,
        scene_description: currentScene.value.description,
        scene_background: currentScene.value.background,
        scene_ai_role: currentScene.value.ai_role,
        scene_objectives: currentScene.value.objectives,
        scene_keywords: currentScene.value.keywords,
        user_message: userMessage,
        conversation_id: conversationId.value,
        is_first: isFirstMessage  // 首次消息会将场景信息拼接到user_message
      })
    })
    
    const reader = response.body.getReader()
    const decoder = new TextDecoder()
    
    let aiMessage = {
      role: 'assistant',
      content: '',
      timestamp: Date.now()
    }
    messages.value.push(aiMessage)
    
    while (true) {
      const { value, done } = await reader.read()
      if (done) break
      
      const chunk = decoder.decode(value)
      const lines = chunk.split('\n\n')
      
      for (const line of lines) {
        if (!line.trim() || !line.startsWith('event: ')) continue
        
        const [eventLine, dataLine] = line.split('\n')
        const event = eventLine.replace('event: ', '')
        const data = JSON.parse(dataLine.replace('data: ', ''))
        
        if (event === 'message.delta') {
          aiMessage.content += data.content
        } else if (event === 'conversation.completed') {
          console.log('对话完成，Token用量:', data.token_count)
        } else if (event === 'error') {
          ElMessage.error(data.error)
        }
      }
    }
    
  } catch (error) {
    ElMessage.error('发送消息失败')
  } finally {
    isLoading.value = false
  }
}
```

### 5.4 课程中心集成

**文件**：`src/views/trainee/course-center.vue`

**修改点**：
1. 课程卡片增加"陪练"按钮
2. 点击按钮调用Dify提取场景
3. 提取成功后跳转到对话页面

```javascript
// 点击陪练按钮
const handlePractice = async (course) => {
  try {
    ElMessage.info('正在提取陪练场景...')
    
    // 调用Dify提取场景
    const response = await practiceApi.extractScene({
      course_id: course.id
    })
    
    // 跳转到对话页面
    router.push({
      name: 'AIPractice',
      query: {
        mode: 'course',  // 课程模式
        courseId: course.id,
        sceneData: JSON.stringify(response.data)
      }
    })
  } catch (error) {
    ElMessage.error('提取场景失败')
  }
}
```

## 六、两种陪练入口对比

| 对比项 | 陪练中心入口 | 课程中心入口 |
|--------|------------|------------|
| **场景来源** | 数据库预设场景 | Dify动态生成 |
| **适用场景** | 通用陪练练习 | 课程相关练习 |
| **流程** | 选择场景 → 对话 | 提取场景 → 对话 |
| **场景质量** | 人工设计，质量稳定 | AI生成，依赖课程质量 |
| **灵活性** | 固定场景 | 动态适配课程内容 |
| **实现复杂度** | 简单 | 中等（需要Dify集成） |

## 七、开发排期建议

### 第一阶段：数据库与基础API（2天）
- 创建practice_scenes表
- 插入5-10条初始场景数据
- 实现场景管理API（CRUD）
- 实现学员查询API

### 第二阶段：前端管理页面（2天）
- 场景管理页面开发
- 表单验证与提交
- 列表筛选与分页

### 第三阶段：Coze集成（3天）
- Coze SDK集成
- 流式对话API实现
- 对话管理接口
- 陪练中心页面开发
- 陪练对话页面开发

### 第四阶段：Dify集成（2天）
- Dify场景提取API
- 课程中心集成
- 场景数据转换

### 第五阶段：联调与优化（2天）
- 端到端测试
- 错误处理完善
- 性能优化
- 用户体验优化

**总计：约11个工作日**

## 八、风险与应对

### 8.1 技术风险

| 风险 | 影响 | 应对措施 |
|------|------|---------|
| Coze API不稳定 | 对话中断 | 实现重试机制，超时提示 |
| Dify场景质量差 | 练习效果差 | 人工审核机制，场景优化 |
| SSE连接中断 | 用户体验差 | 断线重连，状态恢复 |
| Token用量超限 | 成本问题 | 监控Token使用，设置限额 |

### 8.2 业务风险

| 风险 | 影响 | 应对措施 |
|------|------|---------|
| 场景设计不合理 | 练习无效 | 场景评分反馈，持续优化 |
| AI回复不够真实 | 体验差 | 优化Bot Prompt，多轮测试 |
| 用户使用频率低 | ROI低 | 增加趣味性，积分激励 |

## 九、监控与维护

### 9.1 关键指标

- API响应时间
- SSE连接成功率
- 对话完成率
- Token使用量
- 场景使用次数
- 用户活跃度

### 9.2 日志记录

```python
logger.info(f"用户{user_id}开始陪练，场景{scene_id}")
logger.info(f"Coze对话创建成功，logid={stream.response.logid}")
logger.info(f"对话完成，Token用量={token_count}")
logger.error(f"陪练对话失败: {error}")
```

### 9.3 异常告警

- Coze API调用失败超过阈值
- Dify工作流超时频繁
- SSE连接异常率超标

## 十、附录

### 10.1 参考文档

- [Coze API文档](./Coze-API文档.md)
- [陪练功能API接口规范](./陪练功能API接口规范.md)
- [陪练功能数据流程图](./陪练功能数据流程图.md)

### 10.2 示例代码位置

- Coze Python SDK：`参考代码/coze-py-main/`
- 后端参考实现：`参考代码/coze-chat-backend/main.py`
- 前端陪练页面：`kaopeilian-frontend/src/views/trainee/ai-practice-center.vue`

### 10.3 配置信息

```python
# 陪练系统专用配置
COZE_API_BASE = "https://api.coze.cn"
COZE_API_TOKEN = "pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi"
COZE_PRACTICE_BOT_ID = "7560643598174683145"

DIFY_API_BASE = "http://dify.ireborn.com.cn/v1"
DIFY_PRACTICE_API_KEY = "app-rYP6LNM4iPmNjIHns12zFeJp"
DIFY_PRACTICE_WORKFLOW_ID = "待确认"
```

---

**文档版本**：v1.0
**最后更新**：2025-10-13
**维护人**：考培练系统开发团队

