Files
chatroom/app/Services/BaccaratPredictionService.php
lkddi d16626d121 feat(百家乐AI预测): 实现多厂商自动故障转移
- predict() 改为遍历所有已启用厂商,与 AiChatService 保持一致
- 首选 glm-5.1-free,失败后自动按 sort_order 切换下一个厂商
- 所有厂商均失败才返回 null 回退本地路单决策
- 每次调用成功/失败均写入日志,便于追踪
2026-03-28 21:15:49 +08:00

266 lines
9.7 KiB
PHP
Raw 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.
<?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()]);
}
}
}