Files
chatroom/app/Services/BaccaratPredictionService.php

266 lines
9.7 KiB
PHP
Raw Normal View History

<?php
/**
* 文件功能:百家乐 AI 预测服务
*
* 通过调用已配置的 AI 厂商接口OpenAI 兼容协议),
* 根据近期路单数据分析走势,预测下一局应押注""""还是"豹子"
*
* 复用 AiChatService 中相同的厂商配置与 HTTP 调用模式。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Services;
use App\Models\AiProviderConfig;
use App\Models\AiUsageLog;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class BaccaratPredictionService
{
/**
* AI 请求超时时间(秒)
*
* 百家乐预测对延迟敏感,超时则回退本地决策。
*/
private const REQUEST_TIMEOUT = 30;
/**
* 百家乐预测优先使用的模型名称
*
* 在后台 AI 配置中留存此模型时自动选用;
* 若该模型未配置或已禁用,则回退到默认 AI 厂商。
*/
private const PREFERRED_MODEL = 'glm-5.1-free';
/**
* 调用 AI 接口预测百家乐下注方向
*
* 优先使用 PREFERRED_MODEL 对应的厂商,调用失败时自动按
* sort_order 依次尝试其余已启用厂商(与 AiChatService 故障转移逻辑一致)。
* 所有厂商均失败时返回 null,由调用方回退本地路单决策。
*
* @param array<int, string> $recentResults 近期已结算路单('big'|'small'|'triple',从最新到最旧)
* @return string|null 预测结果:'big'|'small'|'triple',或 nullAI 全部不可用)
*/
public function predict(array $recentResults): ?string
{
// 获取所有已启用厂商PREFERRED_MODEL 对应的排在最前面
$providers = AiProviderConfig::getEnabledProviders();
if ($providers->isEmpty()) {
Log::warning('百家乐 AI 预测:无任何已启用的 AI 厂商配置');
return null;
}
// 将首选模型提到队列最前(若存在)
$preferred = $providers->firstWhere('model', self::PREFERRED_MODEL);
if ($preferred) {
$providers = $providers->reject(fn ($p) => $p->model === self::PREFERRED_MODEL)
->prepend($preferred);
}
$prompt = $this->buildPredictionPrompt($recentResults);
// 依次尝试每个厂商,失败则切换下一个
foreach ($providers as $provider) {
try {
$result = $this->callProvider($provider, $prompt);
Log::info('百家乐 AI 预测成功', [
'provider' => $provider->name,
'model' => $provider->model,
'result' => $result,
]);
return $result;
} catch (\Exception $e) {
Log::warning('百家乐 AI 预测厂商调用失败,尝试下一个', [
'provider' => $provider->name,
'model' => $provider->model,
'error' => $e->getMessage(),
]);
continue;
}
}
// 所有厂商均失败
Log::error('百家乐 AI 预测:所有 AI 厂商均不可用,将使用本地决策兜底');
return null;
}
/**
* 构建发送给 AI 的预测提示词
*
* 将路单数据转为人类可读格式,要求 AI 仅回复三种固定关键词之一。
*
* @param array<int, string> $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 <<<PROMPT
你是一位百家乐路单分析专家。请根据以下最近 {$total} 局的开奖路单,预测下一局最可能的结果。
路单(从最早到最新):{$roadmapText}
注意:
- "" 表示骰子总点数为 11~17
- "" 表示骰子总点数为 4~10
- "豹子" 表示三颗骰子点数相同(极小概率)
请综合分析路单走势(如连庄、跳变、交替等特征),仅输出以下三个词之一:
/ / 豹子
【重要】只输出单个词,不要包含任何其他文字、标点、换行或解释。
PROMPT;
}
/**
* 调用 AI 厂商接口并解析预测结果
*
* @param AiProviderConfig $config AI 厂商配置
* @param string $prompt 预测提示词
* @return string 预测结果:'big'|'small'|'triple'
*
* @throws \Exception 调用失败或无法解析结果时抛出
*/
private function callProvider(AiProviderConfig $config, string $prompt): string
{
$startTime = microtime(true);
$apiKey = $config->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' => 50, // 非推理模型只需输出单词50 足够;推理模型请调高此值
'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、StepFuncontent 可能为 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()]);
}
}
}