Files
012-kaopeilian/docs/规划/全链路联调/Ai工作流/coze/陪练功能技术方案.md
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

24 KiB
Raw Blame History

考培练系统 - 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)

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 初始数据示例

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 技术配置

# 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客户端初始化

# 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 核心接口实现

场景列表(带分页筛选)

@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流式

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场景提取

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 路由配置

// 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. 开始陪练按钮

关键代码

// 获取场景列表
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处理示例

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. 提取成功后跳转到对话页面
// 点击陪练按钮
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集成

七、开发排期建议

第一阶段数据库与基础API2天

  • 创建practice_scenes表
  • 插入5-10条初始场景数据
  • 实现场景管理APICRUD
  • 实现学员查询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 日志记录

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 参考文档

10.2 示例代码位置

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

10.3 配置信息

# 陪练系统专用配置
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 维护人:考培练系统开发团队