# 考培练系统 - 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 **维护人**:考培练系统开发团队