LocalAIAnalysisService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Http;
  4. use Illuminate\Support\Facades\Log;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Str;
  7. /**
  8. * 本地AI分析服务
  9. * 直接调用QuestionBankService的AI分析API,不依赖LearningAnalytics项目
  10. */
  11. class LocalAIAnalysisService
  12. {
  13. protected string $questionBankApiUrl;
  14. protected int $timeout = 60;
  15. public function __construct(
  16. private readonly QuestionBankService $questionBankService
  17. ) {
  18. $this->questionBankApiUrl = config('services.question_bank.url', env('QUESTION_BANK_API_URL', 'http://localhost:5015'));
  19. }
  20. /**
  21. * 分析学生答案
  22. *
  23. * @param array $answerData 包含 question_text, student_answer, score, max_score, kp_code 等
  24. * @return array 分析结果
  25. */
  26. public function analyzeAnswer(array $answerData): array
  27. {
  28. Log::info('LocalAIAnalysisService: 开始分析答案', [
  29. 'question_id' => $answerData['question_id'] ?? 'unknown',
  30. 'kp_code' => $answerData['kp_code'] ?? 'unknown',
  31. ]);
  32. // 直接使用规则分析(不再调用外部服务,避免超时)
  33. return [
  34. 'success' => true,
  35. 'data' => $this->ruleBasedAnalysis($answerData),
  36. 'model_used' => 'rule-based-analysis',
  37. 'fallback' => false,
  38. ];
  39. }
  40. /**
  41. * 批量分析学生答案
  42. *
  43. * @param array $answers 答案数组
  44. * @return array 分析结果数组
  45. */
  46. public function analyzeBatchAnswers(array $answers): array
  47. {
  48. $results = [];
  49. foreach ($answers as $answer) {
  50. $results[] = $this->analyzeAnswer($answer);
  51. }
  52. return $results;
  53. }
  54. /**
  55. * 基于规则的答案分析(备用方案)
  56. */
  57. private function ruleBasedAnalysis(array $answerData): array
  58. {
  59. $studentAnswer = $answerData['student_answer'] ?? '';
  60. $score = (float) ($answerData['score'] ?? 0);
  61. $maxScore = (float) ($answerData['max_score'] ?? 10);
  62. // 无答案情况
  63. if (empty(trim($studentAnswer))) {
  64. return [
  65. 'correct' => false,
  66. 'score' => 0,
  67. 'full_score' => $maxScore,
  68. 'partial_score_ratio' => 0.0,
  69. 'mistake_type' => '未作答',
  70. 'mistake_category' => '态度/习惯',
  71. 'reason' => '学生未作答',
  72. 'correct_solution' => '请参考标准答案',
  73. 'suggestions' => '建议鼓励学生尝试作答,不要留白',
  74. 'next_steps' => ['复习相关知识点', '尝试从已知条件入手'],
  75. 'analysis_confidence' => 1.0,
  76. 'analysis_tokens' => 0,
  77. ];
  78. }
  79. $scoreRatio = $maxScore > 0 ? $score / $maxScore : 0;
  80. // 满分
  81. if ($scoreRatio == 1.0) {
  82. return [
  83. 'correct' => true,
  84. 'score' => $score,
  85. 'full_score' => $maxScore,
  86. 'partial_score_ratio' => 1.0,
  87. 'mistake_type' => '无',
  88. 'mistake_category' => '无',
  89. 'reason' => '回答正确',
  90. 'correct_solution' => '回答正确',
  91. 'suggestions' => '继续保持',
  92. 'next_steps' => ['尝试更高难度的题目'],
  93. 'analysis_confidence' => 1.0,
  94. 'analysis_tokens' => 0,
  95. ];
  96. }
  97. // 零分或低分
  98. return [
  99. 'correct' => false,
  100. 'score' => $score,
  101. 'full_score' => $maxScore,
  102. 'partial_score_ratio' => $scoreRatio,
  103. 'mistake_type' => $scoreRatio < 0.3 ? '概念错误' : '计算错误/步骤缺失',
  104. 'mistake_category' => $scoreRatio < 0.3 ? '知识掌握' : '解题技巧',
  105. 'reason' => $scoreRatio < 0.3
  106. ? '学生对题目理解存在偏差或知识掌握不牢固,需要系统复习'
  107. : '解题方向有误,需要学习正确的解题方法',
  108. 'correct_solution' => '需要从基础概念开始,系统学习相关知识',
  109. 'suggestions' => '建议从基础概念开始,系统复习相关知识,多做基础练习',
  110. 'next_steps' => ['学习基础概念', '理解公式原理', '从简单题开始练习', '逐步提升难度'],
  111. 'analysis_confidence' => 0.7,
  112. 'analysis_tokens' => 100,
  113. ];
  114. }
  115. /**
  116. * 更新学生掌握度
  117. *
  118. * @param string $studentId 学生ID
  119. * @param string $kpCode 知识点编码
  120. * @param float $currentMastery 当前掌握度
  121. * @param bool $isCorrect 是否正确
  122. * @param float $difficulty 题目难度
  123. * @return array 更新后的掌握度
  124. */
  125. public function updateMastery(string $studentId, string $kpCode, float $currentMastery, bool $isCorrect, float $difficulty = 0.5): array
  126. {
  127. try {
  128. // 简化的掌握度更新算法
  129. // 基于BKT(贝叶斯知识追踪)模型的简化版本
  130. $learningRate = 0.1; // 学习速率
  131. $forgettingRate = 0.05; // 遗忘速率
  132. // 根据正确性和难度调整学习速率
  133. $adjustedLearningRate = $learningRate * (1 + (1 - $difficulty));
  134. $adjustedForgettingRate = $forgettingRate * (1 - (1 - $difficulty));
  135. $newMastery = $currentMastery;
  136. if ($isCorrect) {
  137. // 答对了,增加掌握度
  138. $newMastery = $currentMastery + ($adjustedLearningRate * (1 - $currentMastery));
  139. } else {
  140. // 答错了,遗忘一些
  141. $newMastery = $currentMastery * (1 - $adjustedForgettingRate);
  142. }
  143. // 确保在[0,1]范围内
  144. $newMastery = max(0, min(1, $newMastery));
  145. // 保存到数据库
  146. $this->saveMasteryToDatabase($studentId, $kpCode, $newMastery);
  147. return [
  148. 'kp_code' => $kpCode,
  149. 'old_mastery' => $currentMastery,
  150. 'new_mastery' => $newMastery,
  151. 'change' => $newMastery - $currentMastery,
  152. 'is_correct' => $isCorrect,
  153. ];
  154. } catch (\Exception $e) {
  155. Log::error('LocalAIAnalysisService: 更新掌握度失败', [
  156. 'student_id' => $studentId,
  157. 'kp_code' => $kpCode,
  158. 'error' => $e->getMessage(),
  159. ]);
  160. return [
  161. 'kp_code' => $kpCode,
  162. 'old_mastery' => $currentMastery,
  163. 'new_mastery' => $currentMastery,
  164. 'change' => 0,
  165. 'is_correct' => $isCorrect,
  166. 'error' => $e->getMessage(),
  167. ];
  168. }
  169. }
  170. /**
  171. * 保存掌握度到数据库
  172. */
  173. private function saveMasteryToDatabase(string $studentId, string $kpCode, float $mastery): void
  174. {
  175. try {
  176. // 尝试更新现有记录
  177. $updated = DB::table('student_knowledge_mastery')
  178. ->where('student_id', $studentId)
  179. ->where('kp_code', $kpCode)
  180. ->update([
  181. 'mastery_level' => $mastery,
  182. 'updated_at' => now(),
  183. ]);
  184. // 如果没有更新任何记录,插入新记录
  185. if ($updated === 0) {
  186. DB::table('student_knowledge_mastery')
  187. ->insert([
  188. 'student_id' => $studentId,
  189. 'kp_code' => $kpCode,
  190. 'mastery_level' => $mastery,
  191. 'confidence_level' => 0.5, // 默认置信度
  192. 'created_at' => now(),
  193. 'updated_at' => now(),
  194. ]);
  195. }
  196. Log::debug('LocalAIAnalysisService: 掌握度已保存', [
  197. 'student_id' => $studentId,
  198. 'kp_code' => $kpCode,
  199. 'mastery' => $mastery,
  200. ]);
  201. } catch (\Exception $e) {
  202. // 表不存在时静默跳过
  203. Log::debug('LocalAIAnalysisService: 掌握度表不存在,跳过保存', [
  204. 'student_id' => $studentId,
  205. 'kp_code' => $kpCode,
  206. 'error' => $e->getMessage(),
  207. ]);
  208. }
  209. }
  210. /**
  211. * 获取学生掌握度
  212. *
  213. * @param string $studentId 学生ID
  214. * @param string|null $kpCode 知识点编码(可选)
  215. * @return array 掌握度数据
  216. */
  217. public function getStudentMastery(string $studentId, ?string $kpCode = null): array
  218. {
  219. try {
  220. $query = DB::table('student_knowledge_mastery')
  221. ->where('student_id', $studentId);
  222. if ($kpCode) {
  223. $query->where('kp_code', $kpCode);
  224. }
  225. $masteries = $query->get()->toArray();
  226. return [
  227. 'success' => true,
  228. 'data' => $masteries,
  229. ];
  230. } catch (\Exception $e) {
  231. // 表不存在时返回空数据
  232. Log::debug('LocalAIAnalysisService: 掌握度表不存在,返回空数据', [
  233. 'student_id' => $studentId,
  234. 'kp_code' => $kpCode,
  235. 'error' => $e->getMessage(),
  236. ]);
  237. return [
  238. 'success' => false,
  239. 'message' => '表不存在',
  240. 'data' => [],
  241. ];
  242. }
  243. }
  244. /**
  245. * 创建掌握度快照
  246. *
  247. * @param string $studentId 学生ID
  248. * @param string|null $paperId 关联试卷ID(可选)
  249. * @param string|null $answerRecordId 关联作答记录ID(可选)
  250. * @return array|null 快照数据
  251. */
  252. public function createMasterySnapshot(string $studentId, ?string $paperId = null, ?string $answerRecordId = null): ?array
  253. {
  254. try {
  255. // 获取最新掌握度数据
  256. $masteryData = $this->getStudentMastery($studentId);
  257. if (empty($masteryData['data'])) {
  258. Log::info('LocalAIAnalysisService: 没有掌握度数据,返回空快照', [
  259. 'student_id' => $studentId,
  260. ]);
  261. return [
  262. 'snapshot_id' => 'snap_' . Str::uuid()->toString(),
  263. 'student_id' => $studentId,
  264. 'paper_id' => $paperId,
  265. 'answer_record_id' => $answerRecordId,
  266. 'overall_mastery' => 0,
  267. 'weak_count' => 0,
  268. 'strong_count' => 0,
  269. 'mastery_changes' => [],
  270. ];
  271. }
  272. $snapshotId = 'snap_' . Str::uuid()->toString();
  273. $masteryItems = $masteryData['data'];
  274. $overallMastery = 0;
  275. $weakCount = 0;
  276. $strongCount = 0;
  277. foreach ($masteryItems as $item) {
  278. $level = (float) ($item->mastery_level ?? 0);
  279. $overallMastery += $level;
  280. if ($level < 0.6) {
  281. $weakCount++;
  282. } elseif ($level > 0.8) {
  283. $strongCount++;
  284. }
  285. }
  286. $overallMastery = count($masteryItems) > 0
  287. ? round($overallMastery / count($masteryItems), 4)
  288. : 0;
  289. return [
  290. 'snapshot_id' => $snapshotId,
  291. 'student_id' => $studentId,
  292. 'paper_id' => $paperId,
  293. 'answer_record_id' => $answerRecordId,
  294. 'overall_mastery' => $overallMastery,
  295. 'weak_count' => $weakCount,
  296. 'strong_count' => $strongCount,
  297. 'mastery_changes' => [],
  298. ];
  299. } catch (\Exception $e) {
  300. Log::error('LocalAIAnalysisService: 创建掌握度快照失败', [
  301. 'student_id' => $studentId,
  302. 'paper_id' => $paperId,
  303. 'error' => $e->getMessage(),
  304. ]);
  305. return null;
  306. }
  307. }
  308. }