$recentResults 近期已结算路单('big'|'small'|'triple',从最新到最旧) * @return string|null 预测结果:'big'|'small'|'triple',或 null(AI 不可用) */ public function predict(array $recentResults): ?string { $provider = AiProviderConfig::getDefault(); if (! $provider) { Log::warning('百家乐 AI 预测:无可用 AI 厂商配置,跳过 AI 预测'); return null; } $prompt = $this->buildPredictionPrompt($recentResults); try { return $this->callProvider($provider, $prompt); } catch (\Exception $e) { Log::warning('百家乐 AI 预测调用失败,将使用本地决策兜底', [ 'provider' => $provider->name, 'error' => $e->getMessage(), ]); return null; } } /** * 构建发送给 AI 的预测提示词 * * 将路单数据转为人类可读格式,要求 AI 仅回复三种固定关键词之一。 * * @param array $recentResults 路单数组 */ private function buildPredictionPrompt(array $recentResults): string { // 将英文 key 转为中文展示,方便 AI 理解 $labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子']; $roadmap = array_map( fn (string $result) => $labelMap[$result] ?? $result, $recentResults ); // 路单从旧到新排列(最后一条是最近一局) $roadmapText = implode(' → ', array_reverse($roadmap)); $total = count($recentResults); return <<getDecryptedApiKey(); // 智能拼接端点 URL(与 AiChatService 保持一致) $base = rtrim($config->api_endpoint, '/'); $endpoint = str_ends_with($base, '/v1') ? $base.'/chat/completions' : $base.'/v1/chat/completions'; try { $response = Http::withToken($apiKey) ->timeout(self::REQUEST_TIMEOUT) ->post($endpoint, [ 'model' => $config->model, 'temperature' => 0.3, // 预测任务偏确定性,使用较低温度 'max_tokens' => 1000, // 推理模型需要大量 token 完成思考后才输出 content 'messages' => [ [ 'role' => 'system', 'content' => '你是百家乐路单分析专家,只输出"大"、"小"或"豹子"三个词之一,不输出任何其他内容。', ], [ 'role' => 'user', 'content' => $prompt, ], ], ]); $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000); if (! $response->successful()) { $this->logUsage($config, $responseTimeMs, false, $response->body()); throw new \Exception("HTTP {$response->status()}: {$response->body()}"); } $data = $response->json(); $message = $data['choices'][0]['message'] ?? []; // 兼容推理模型(如 DeepSeek-R1、StepFun):content 可能为 null, // 此时从 reasoning 字段中正则匹配最后出现的预测关键词作为兜底。 $reply = trim($message['content'] ?? ''); if ($reply === '') { $reasoning = $message['reasoning'] ?? ''; if (preg_match('/[大小豹子]+(?=[^大小豹子]*$)/u', $reasoning, $m)) { $reply = $m[0]; } } $promptTokens = $data['usage']['prompt_tokens'] ?? 0; $completionTokens = $data['usage']['completion_tokens'] ?? 0; $this->logUsage($config, $responseTimeMs, true, null, $promptTokens, $completionTokens); // 将 AI 回复的中文词解析为系统内部 key return $this->parseReply($reply); } catch (\Illuminate\Http\Client\ConnectionException $e) { $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000); $this->logUsage($config, $responseTimeMs, false, $e->getMessage()); throw new \Exception("连接超时: {$e->getMessage()}"); } } /** * 将 AI 回复的中文词解析为系统内部下注键 * * @param string $reply AI 返回的原始文本 * @return string 'big'|'small'|'triple' * * @throws \Exception 无法识别时抛出 */ private function parseReply(string $reply): string { // 从回复中提取核心词(容忍多余空白/标点) $cleaned = trim(preg_replace('/\s+/', '', $reply)); return match ($cleaned) { '大' => 'big', '小' => 'small', '豹子' => 'triple', default => throw new \Exception("AI 返回无法识别的预测结果:{$reply}"), }; } /** * 记录 AI 调用日志到 ai_usage_logs 表 * * @param AiProviderConfig $config AI 厂商配置 * @param int $responseTimeMs 响应时间(毫秒) * @param bool $success 是否成功 * @param string|null $errorMessage 错误信息 * @param int $promptTokens 输入 token 数 * @param int $completionTokens 输出 token 数 */ private function logUsage( AiProviderConfig $config, int $responseTimeMs, bool $success, ?string $errorMessage = null, int $promptTokens = 0, int $completionTokens = 0, ): void { try { AiUsageLog::create([ 'user_id' => 0, // AI 系统行为,无对应用户 'provider' => $config->provider, 'model' => $config->model, 'action' => 'baccarat_predict', 'prompt_tokens' => $promptTokens, 'completion_tokens' => $completionTokens, 'total_tokens' => $promptTokens + $completionTokens, 'response_time_ms' => $responseTimeMs, 'success' => $success, 'error_message' => $errorMessage ? mb_substr($errorMessage, 0, 500) : null, ]); } catch (\Exception $e) { Log::error('百家乐 AI 预测日志记录失败', ['error' => $e->getMessage()]); } } }