- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
24 KiB
24 KiB
考培练系统 - AI陪练功能技术方案
一、需求概述
1.1 业务背景
考培练系统为轻医美连锁品牌提供员工培训服务,AI陪练功能旨在通过模拟真实客户场景,让学员进行实战对话练习,提升销售和服务能力。
1.2 核心功能
- 陪练场景管理:管理员可创建和管理各类陪练场景
- 场景化陪练:学员选择预设场景进行针对性练习
- 课程关联陪练:基于课程内容动态生成陪练场景
- 实时对话:流式AI对话,模拟真实交互
- 对话管理:对话历史由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
核心功能:
- 场景筛选(类型、难度、关键词)
- 场景卡片展示
- 场景详情查看
- 开始陪练按钮
关键代码:
// 获取场景列表
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
核心功能:
- SSE流式接收AI响应
- 消息列表展示(用户/AI)
- 输入框和发送按钮
- 中断对话功能
- 场景信息首次发送(关键功能)
⚠️ 与参考代码的关键差异:
- 参考代码:场景信息不传递,直接开始对话
- 考培练系统:场景信息必须作为第一条消息发送给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
修改点:
- 课程卡片增加"陪练"按钮
- 点击按钮调用Dify提取场景
- 提取成功后跳转到对话页面
// 点击陪练按钮
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 日志记录
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 维护人:考培练系统开发团队