Files
chatroom/app/Services/BaccaratPredictionService.php
lkddi 348f4e0fe0 fix(百家乐AI预测): 兼容推理型模型(StepFun/DeepSeek-R1)
- max_tokens 从 10 调整到 1000,避免推理模型因 finish_reason:length 截断
- content 为 null 时从 reasoning 字段正则提取预测关键词作为兜底
- 经 openrouter/stepfun-step-3.5-flash 实测验证通过
2026-03-28 20:53:42 +08:00

233 lines
8.4 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解析其返回的预测结果。
* 若 AI 不可用或解析失败,返回 null 以便调用方回退本地逻辑。
*
* @param array<int, string> $recentResults 近期已结算路单('big'|'small'|'triple',从最新到最旧)
* @return string|null 预测结果:'big'|'small'|'triple',或 nullAI 不可用)
*/
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<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' => 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、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()]);
}
}
}