Files
012-kaopeilian/backend/app/services/ai/practice_scene_service.py
111 442ac78b56
Some checks failed
continuous-integration/drone/push Build is failing
sync: 同步服务器最新代码 (2026-01-27)
更新内容:
- 后端 AI 服务优化(能力分析、知识点解析等)
- 前端考试和陪练界面更新
- 修复多个 prompt 和 JSON 解析问题
- 更新 Coze 语音客户端
2026-01-27 10:03:28 +08:00

385 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
陪练场景准备服务 - Python 原生实现
功能:
- 根据课程ID获取知识点
- 调用 AI 生成陪练场景配置
- 解析并返回结构化场景数据
提供稳定可靠的陪练场景提取能力。
"""
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import ExternalServiceError
from .ai_service import AIService, AIResponse
from .llm_json_parser import parse_with_fallback, clean_llm_output
from .prompts.practice_scene_prompts import (
SYSTEM_PROMPT,
USER_PROMPT,
PRACTICE_SCENE_SCHEMA,
DEFAULT_SCENE_TYPE,
DEFAULT_DIFFICULTY,
)
logger = logging.getLogger(__name__)
# ==================== 数据结构 ====================
@dataclass
class PracticeScene:
"""陪练场景数据结构"""
name: str
description: str
background: str
ai_role: str
objectives: List[str]
keywords: List[str]
type: str = DEFAULT_SCENE_TYPE
difficulty: str = DEFAULT_DIFFICULTY
@dataclass
class PracticeSceneResult:
"""陪练场景生成结果"""
success: bool
scene: Optional[PracticeScene] = None
raw_response: Dict[str, Any] = field(default_factory=dict)
ai_provider: str = ""
ai_model: str = ""
ai_tokens: int = 0
ai_latency_ms: int = 0
knowledge_points_count: int = 0
error: str = ""
# ==================== 服务类 ====================
class PracticeSceneService:
"""
陪练场景准备服务
使用 Python 原生实现。
使用示例:
```python
service = PracticeSceneService()
result = await service.prepare_practice_knowledge(
db=db_session,
course_id=1
)
if result.success:
print(result.scene.name)
print(result.scene.objectives)
```
"""
def __init__(self):
"""初始化服务"""
self.ai_service = AIService(module_code="practice_scene")
async def prepare_practice_knowledge(
self,
db: AsyncSession,
course_id: int
) -> PracticeSceneResult:
"""
准备陪练所需的知识内容并生成场景
陪练知识准备的 Python 实现。
Args:
db: 数据库会话(支持多租户,由调用方传入对应租户的数据库连接)
course_id: 课程ID
Returns:
PracticeSceneResult: 包含场景配置和元信息的结果对象
"""
try:
logger.info(f"开始陪练知识准备 - course_id: {course_id}")
# 1. 查询知识点
knowledge_points = await self._fetch_knowledge_points(db, course_id)
if not knowledge_points:
logger.warning(f"课程没有知识点 - course_id: {course_id}")
return PracticeSceneResult(
success=False,
error=f"课程 {course_id} 没有可用的知识点"
)
logger.info(f"获取到 {len(knowledge_points)} 个知识点 - course_id: {course_id}")
# 2. 格式化知识点为文本
knowledge_text = self._format_knowledge_points(knowledge_points)
# 3. 调用 AI 生成场景
ai_response = await self._call_ai_generation(knowledge_text)
logger.info(
f"AI 生成完成 - provider: {ai_response.provider}, "
f"tokens: {ai_response.total_tokens}, latency: {ai_response.latency_ms}ms"
)
# 4. 解析 JSON 结果
scene_data = self._parse_scene_response(ai_response.content)
if not scene_data:
logger.error(f"场景解析失败 - course_id: {course_id}")
return PracticeSceneResult(
success=False,
raw_response={"ai_output": ai_response.content},
ai_provider=ai_response.provider,
ai_model=ai_response.model,
ai_tokens=ai_response.total_tokens,
ai_latency_ms=ai_response.latency_ms,
knowledge_points_count=len(knowledge_points),
error="AI 输出解析失败"
)
# 5. 构建场景对象
scene = self._build_scene_object(scene_data)
logger.info(
f"陪练场景生成成功 - course_id: {course_id}, "
f"scene_name: {scene.name}, type: {scene.type}"
)
return PracticeSceneResult(
success=True,
scene=scene,
raw_response=scene_data,
ai_provider=ai_response.provider,
ai_model=ai_response.model,
ai_tokens=ai_response.total_tokens,
ai_latency_ms=ai_response.latency_ms,
knowledge_points_count=len(knowledge_points)
)
except Exception as e:
logger.error(
f"陪练知识准备失败 - course_id: {course_id}, error: {e}",
exc_info=True
)
return PracticeSceneResult(
success=False,
error=str(e)
)
async def _fetch_knowledge_points(
self,
db: AsyncSession,
course_id: int
) -> List[Dict[str, Any]]:
"""
从数据库获取课程知识点
获取课程知识点
"""
# 知识点查询 SQL
# SELECT kp.name, kp.description
# FROM knowledge_points kp
# INNER JOIN course_materials cm ON kp.material_id = cm.id
# WHERE kp.course_id = {course_id}
# AND kp.is_deleted = 0
# AND cm.is_deleted = 0
# ORDER BY kp.id;
sql = text("""
SELECT kp.name, kp.description
FROM knowledge_points kp
INNER JOIN course_materials cm ON kp.material_id = cm.id
WHERE kp.course_id = :course_id
AND kp.is_deleted = 0
AND cm.is_deleted = 0
ORDER BY kp.id
""")
try:
result = await db.execute(sql, {"course_id": course_id})
rows = result.fetchall()
knowledge_points = []
for row in rows:
knowledge_points.append({
"name": row[0],
"description": row[1] or ""
})
return knowledge_points
except Exception as e:
logger.error(f"查询知识点失败: {e}")
raise ExternalServiceError(f"数据库查询失败: {e}")
def _format_knowledge_points(self, knowledge_points: List[Dict[str, Any]]) -> str:
"""
将知识点列表格式化为文本
Args:
knowledge_points: 知识点列表
Returns:
格式化后的文本
"""
lines = []
for i, kp in enumerate(knowledge_points, 1):
name = kp.get("name", "")
description = kp.get("description", "")
if description:
lines.append(f"{i}. {name}\n {description}")
else:
lines.append(f"{i}. {name}")
return "\n\n".join(lines)
async def _call_ai_generation(self, knowledge_text: str) -> AIResponse:
"""
调用 AI 生成陪练场景
Args:
knowledge_text: 格式化后的知识点文本
Returns:
AI 响应对象
"""
# 构建用户消息
user_message = USER_PROMPT.format(knowledge_points=knowledge_text)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message}
]
# 调用 AI自动降级4sapi.com → OpenRouter
response = await self.ai_service.chat(
messages=messages,
temperature=0.7, # 适中的创意性
prompt_name="practice_scene_generation"
)
return response
def _parse_scene_response(self, ai_output: str) -> Optional[Dict[str, Any]]:
"""
解析 AI 输出的场景 JSON
使用 LLM JSON Parser 进行多层兜底解析
Args:
ai_output: AI 原始输出
Returns:
解析后的字典,失败返回 None
"""
# 先清洗输出
cleaned_output, rules = clean_llm_output(ai_output)
if rules:
logger.debug(f"AI 输出已清洗: {rules}")
# 使用带 Schema 校验的解析
result = parse_with_fallback(
cleaned_output,
schema=PRACTICE_SCENE_SCHEMA,
default=None,
validate_schema=True,
on_error="none"
)
return result
def _build_scene_object(self, scene_data: Dict[str, Any]) -> PracticeScene:
"""
从解析的字典构建场景对象
Args:
scene_data: 解析后的场景数据
Returns:
PracticeScene 对象
"""
# 提取 scene 字段JSON 格式为 {"scene": {...}}
scene = scene_data.get("scene", scene_data)
return PracticeScene(
name=scene.get("name", "陪练场景"),
description=scene.get("description", ""),
background=scene.get("background", ""),
ai_role=scene.get("ai_role", "AI扮演客户"),
objectives=scene.get("objectives", []),
keywords=scene.get("keywords", []),
type=scene.get("type", DEFAULT_SCENE_TYPE),
difficulty=scene.get("difficulty", DEFAULT_DIFFICULTY)
)
def scene_to_dict(self, scene: PracticeScene) -> Dict[str, Any]:
"""
将场景对象转换为字典
便于 API 响应序列化
Args:
scene: PracticeScene 对象
Returns:
字典格式的场景数据
"""
return {
"scene": {
"name": scene.name,
"description": scene.description,
"background": scene.background,
"ai_role": scene.ai_role,
"objectives": scene.objectives,
"keywords": scene.keywords,
"type": scene.type,
"difficulty": scene.difficulty
}
}
# ==================== 全局实例 ====================
practice_scene_service = PracticeSceneService()
# ==================== 便捷函数 ====================
async def prepare_practice_knowledge(
db: AsyncSession,
course_id: int
) -> PracticeSceneResult:
"""
准备陪练所需的知识内容(便捷函数)
Args:
db: 数据库会话
course_id: 课程ID
Returns:
PracticeSceneResult 结果对象
"""
return await practice_scene_service.prepare_practice_knowledge(db, course_id)