Prechádzať zdrojové kódy

feat: blend served difficulty with calibration confidence

yemeishu 3 týždňov pred
rodič
commit
e93b7d22c6

+ 43 - 8
app/Services/QuestionDifficultyResolver.php

@@ -8,12 +8,13 @@ use Illuminate\Support\Facades\Schema;
 class QuestionDifficultyResolver
 {
     private const TABLE = 'question_difficulty_calibrations';
+    private const SERVED_DIFF_CONFIDENCE_DENOMINATOR = 20.0;
 
     private ?bool $tableReady = null;
 
     /**
      * @param  array<int, int|string>  $questionIds
-     * @return array<int, float> question_bank_id => calibrated_difficulty
+     * @return array<int, array<string, float|null>> question_bank_id => calibration snapshot
      */
     public function mapCalibratedDifficulty(array $questionIds): array
     {
@@ -33,13 +34,29 @@ class QuestionDifficultyResolver
 
         return DB::table(self::TABLE)
             ->whereIn('question_bank_id', $questionIds)
-            ->pluck('calibrated_difficulty', 'question_bank_id')
-            ->map(fn ($v) => (float) $v)
+            ->get(['question_bank_id', 'original_difficulty', 'calibrated_difficulty', 'weighted_attempts'])
+            ->mapWithKeys(function ($row) {
+                $qid = (int) ($row->question_bank_id ?? 0);
+                if ($qid <= 0) {
+                    return [];
+                }
+
+                return [
+                    $qid => [
+                        'original_difficulty' => $row->original_difficulty !== null ? (float) $row->original_difficulty : null,
+                        'calibrated_difficulty' => $row->calibrated_difficulty !== null ? (float) $row->calibrated_difficulty : null,
+                        'weighted_attempts' => $row->weighted_attempts !== null ? (float) $row->weighted_attempts : null,
+                    ],
+                ];
+            })
             ->all();
     }
 
     /**
-     * 批量给题目数组覆盖 difficulty(校准值优先,原始值兜底)
+     * 批量给题目数组计算组卷使用难度 served_diff:
+     * served_diff = alpha * calibrated + (1 - alpha) * original
+     * alpha = min(1, weighted_attempts / 20)
+     * 前提:仅当 calibrated_difficulty 存在时才融合;否则保持原始 difficulty 不变。
      *
      * @param  array<int, array<string, mixed>>  $questions
      * @return array<int, array<string, mixed>>
@@ -64,10 +81,29 @@ class QuestionDifficultyResolver
 
         foreach ($questions as &$q) {
             $id = (int) ($q['id'] ?? $q['question_id'] ?? $q['question_bank_id'] ?? 0);
-            if ($id > 0 && array_key_exists($id, $map)) {
-                $q['difficulty'] = (float) $map[$id];
-                $q['difficulty_source'] = 'calibrated';
+            if ($id <= 0 || ! array_key_exists($id, $map)) {
+                continue;
+            }
+
+            $snapshot = $map[$id] ?? [];
+            $calibrated = $snapshot['calibrated_difficulty'] ?? null;
+            if ($calibrated === null) {
+                continue;
             }
+
+            $original = isset($q['difficulty']) && is_numeric($q['difficulty'])
+                ? (float) $q['difficulty']
+                : (float) ($snapshot['original_difficulty'] ?? $calibrated);
+            $weightedAttempts = max(0.0, (float) ($snapshot['weighted_attempts'] ?? 0.0));
+            $alpha = min(1.0, $weightedAttempts / self::SERVED_DIFF_CONFIDENCE_DENOMINATOR);
+            $servedDifficulty = ($alpha * (float) $calibrated) + ((1.0 - $alpha) * $original);
+
+            $q['difficulty'] = round($servedDifficulty, 4);
+            $q['difficulty_source'] = $alpha >= 0.9999 ? 'calibrated' : 'served_blend';
+            $q['difficulty_original'] = round($original, 4);
+            $q['difficulty_calibrated'] = round((float) $calibrated, 4);
+            $q['difficulty_alpha'] = round($alpha, 4);
+            $q['difficulty_weighted_attempts'] = round($weightedAttempts, 4);
         }
         unset($q);
 
@@ -85,4 +121,3 @@ class QuestionDifficultyResolver
         return $this->tableReady;
     }
 }
-

+ 2 - 0
app/Services/QuestionLocalService.php

@@ -622,9 +622,11 @@ class QuestionLocalService
 
         $questions = $this->questionDifficultyResolver->applyCalibratedDifficulty($questions);
         $calibratedCount = count(array_filter($questions, fn ($q) => ($q['difficulty_source'] ?? null) === 'calibrated'));
+        $servedBlendCount = count(array_filter($questions, fn ($q) => ($q['difficulty_source'] ?? null) === 'served_blend'));
         Log::info('QuestionLocalService: 组卷前应用校准难度', [
             'total_candidates' => count($questions),
             'calibrated_candidates' => $calibratedCount,
+            'served_blend_candidates' => $servedBlendCount,
         ]);
 
         $resolveQuestionId = static function (array $question): string {