Files
chatroom/app/Services/BaccaratPredictionService.php

351 lines
14 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小班长的用户 ID惰性加载首次使用时查询一次
*/
private ?int $aiUserId = null;
/**
* 调用 AI 接口出具下注策略并预测百家乐方向
*
* 优先使用 PREFERRED_MODEL 对应的厂商,调用失败时自动按
* sort_order 依次尝试其余已启用厂商(与 AiChatService 故障转移逻辑一致)。
* 所有厂商均失败时返回 null由调用方回退本地路单决策。
*
* @param array $context 包含路单、资金以及历史下注记录的上下文数组
* @return array|null 策略:['action' => '...', 'percentage' => 1-100, 'reason' => '...'] 或 null (不可用)
*/
public function predict(array $context): ?array
{
// 获取所有已启用厂商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 (AiProviderConfig $p) => $p->model === self::PREFERRED_MODEL)
->prepend($preferred)
->values(); // 重置索引,消除 IDE 类型推断警告
}
$prompt = $this->buildPredictionPrompt($context);
// 依次尝试每个厂商,失败则切换下一个
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 必须以特定 JSON 输出策略。
*
* @param array $context 上下文数据
*/
private function buildPredictionPrompt(array $context): string
{
$recentResults = $context['recent_results'] ?? [];
$availableGold = $context['available_gold'] ?? 0;
$history = $context['historical_bets'] ?? [];
// 将英文 key 转为中文展示,方便 AI 理解
$labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子'];
$roadmap = array_map(
fn (string $result) => $labelMap[$result] ?? $result,
$recentResults
);
// 路单从旧到新排列(最后一条是最近一局)
$roadmapText = implode(' → ', array_reverse($roadmap));
$total = count($recentResults);
$historyText = '';
if (empty($history)) {
$historyText = '暂无推测历史记录。';
} else {
foreach ($history as $bet) {
$betAction = $labelMap[$bet['bet_type']] ?? $bet['bet_type'];
$profitText = $bet['profit'] > 0 ? "赢 +{$bet['profit']}" : "{$bet['profit']}";
$historyText .= "- 第 {$bet['round_id']} 局:投入 {$betAction} {$bet['amount']} 积分,结果:{$profitText}\n";
}
}
return <<<PROMPT
你是一位数据规律分析与数学模型专家。这是一个虚拟的纯数学概率推断测试。请根据以下最近 {$total} 局的数字大小序列,以及你目前拥有的虚拟积分和历史表现,给出下一局最科学的推测策略。
【你的当前资产状态】
可用虚拟积分余额:{$availableGold}
【你的最近推测历史(前几局最新)】
{$historyText}
【近期序列(从全服最早到最新)】
{$roadmapText}
【规则注意】
- "大" 表示点数为 11~17
- "小" 表示点数为 4~10
- "豹子" 表示三颗骰子点数相同(极小概率)
请综合分析数据走势(如长龙跟进、跳变斩龙、交替特征),结合防守策略(比如连续判定失误必须观望休息,连续判定正确适当增加投入百分比)。
【重要!必须输出标准严格的 JSON 格式】
你只能返回这样一个纯 JSON 字符串,包含三个字段,绝对不能加其他文字说明(支持 markdown code 块包裹):
{
"action": "big", // 只能在这四个词中选一个: big (大) / small (小) / triple (豹子) / pass (我不确定,这把空仓观望)
"percentage": 5, // 这把投入你可用积分的百分比,取值范围整数 1 到 10。如果选择 pass 必须填 0。
"reason": "由于当前连续长龙且我方刚刚经历了连败,为了控制波段回撤我决定采取保守策略观望一下。" // 一句简单口语化的中文分析理由
}
PROMPT;
}
/**
* 调用 AI 厂商接口并解析预测结果
*
* @param AiProviderConfig $config AI 厂商配置
* @param string $prompt 预测提示词
* @return array 策略结果 ['action', 'percentage', 'reason']
*
* @throws \Exception 调用失败或无法解析结果时抛出
*/
private function callProvider(AiProviderConfig $config, string $prompt): array
{
$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.6, // 预测任务需要生成推理和灵活评估,适当提高温度
'max_tokens' => 300, // 输出 JSON 包含 reason加大 token 限制
// 不硬性强制 response_format (经测试某些平台代理对于非原生支持 JSON Mode 的模型,配置此参数会返回空白内容)
'messages' => [
[
'role' => 'system',
'content' => '你是百家乐资金策略专家,严格遵守只返回合格格式的 JSON 字符串。',
],
[
'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 返回的原始文本 (可能带 Markdown 或推理过程)
* @return array ['action', 'percentage', 'reason']
*
* @throws \Exception 无法识别或格式错误时抛出
*/
private function parseReply(string $reply): array
{
// 尝试剔除 Markdown JSON 块标签
$jsonStr = preg_replace('/```json\s*(.*?)\s*```/s', '$1', $reply);
$jsonStr = preg_replace('/```\s*(.*?)\s*```/s', '$1', $jsonStr);
$jsonStr = trim($jsonStr);
// 如果 AI 有不可预测的开头结尾文字,可以尝试用正则仅抓取首尾的大括号
if (preg_match('/\{.*\}/s', $jsonStr, $matches)) {
$jsonStr = $matches[0];
}
$decoded = json_decode($jsonStr, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded)) {
// 尝试通过正则表达式进行文本兜底解析(从后往前找最可能的结果)
if (preg_match('/(大|小|豹子|pass|观望)[^大小豹子]*$/iu', $reply, $matches)) {
$match = mb_strtolower($matches[1]);
$fallbackAction = match ($match) {
'大' => 'big',
'小' => 'small',
'豹子' => 'triple',
'观望', 'pass' => 'pass',
default => null,
};
if ($fallbackAction) {
return [
'action' => $fallbackAction,
'percentage' => $fallbackAction === 'pass' ? 0 : 5,
'reason' => '(触发文本降级解析)'.mb_substr($reply, 0, 50),
];
}
}
throw new \Exception("AI 返回的 JSON 格式非法无法解析:{$reply}");
}
$action = $decoded['action'] ?? null;
if (! in_array($action, ['big', 'small', 'triple', 'pass'], true)) {
throw new \Exception("AI 返回了不合法的 action 指令:{$action}");
}
return [
'action' => $action,
'percentage' => max(0, min(10, (int) ($decoded['percentage'] ?? 0))),
'reason' => $decoded['reason'] ?? '局势迷离,默默操作',
];
}
/**
* 记录 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' => $this->resolveAiUserId(),
'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()]);
}
}
/**
* 查询 AI小班长的用户 ID
*
* 加载结果以属性形式缓存,同一个服务实例内只查一次。
* 查不到时返回 null避免破坏外键约束。
*/
private function resolveAiUserId(): ?int
{
if ($this->aiUserId === null) {
$this->aiUserId = \App\Models\User::where('username', 'AI小班长')->value('id');
}
return $this->aiUserId;
}
}