#feature: AI analysis update
This commit is contained in:
@@ -104,9 +104,10 @@ class AiClient
|
||||
*
|
||||
* @param array $messages 消息数组
|
||||
* @param string|null $systemPrompt 系统提示词
|
||||
* @param int $maxRetries 最大重试次数
|
||||
* @return string AI 响应内容
|
||||
*/
|
||||
public function chat(array $messages, ?string $systemPrompt = null): string
|
||||
public function chat(array $messages, ?string $systemPrompt = null, int $maxRetries = 3): string
|
||||
{
|
||||
$provider = $this->getActiveProvider();
|
||||
if (!$provider) {
|
||||
@@ -124,24 +125,45 @@ class AiClient
|
||||
|
||||
$endpoint = rtrim($provider['endpoint'], '/') . '/chat/completions';
|
||||
|
||||
$response = Http::timeout($provider['timeout'] ?? 120)
|
||||
->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $provider['api_key'],
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post($endpoint, [
|
||||
'model' => $provider['model'],
|
||||
'messages' => $allMessages,
|
||||
'temperature' => $provider['temperature'] ?? 0.3,
|
||||
'max_tokens' => $provider['max_tokens'] ?? 4096,
|
||||
]);
|
||||
$lastException = null;
|
||||
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||
$response = Http::timeout($provider['timeout'] ?? 120)
|
||||
->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $provider['api_key'],
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post($endpoint, [
|
||||
'model' => $provider['model'],
|
||||
'messages' => $allMessages,
|
||||
'temperature' => $provider['temperature'] ?? 0.3,
|
||||
'max_tokens' => $provider['max_tokens'] ?? 4096,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json('choices.0.message.content', '');
|
||||
}
|
||||
|
||||
// 处理 429 Too Many Requests 错误
|
||||
if ($response->status() === 429) {
|
||||
$retryAfter = $response->header('Retry-After');
|
||||
$waitSeconds = $retryAfter ? (int) $retryAfter : min(pow(2, $attempt) * 5, 60);
|
||||
|
||||
if ($attempt < $maxRetries) {
|
||||
sleep($waitSeconds);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$response->successful()) {
|
||||
$error = $response->json('error.message') ?? $response->body();
|
||||
throw new RuntimeException("AI 请求失败: {$error}");
|
||||
$lastException = new RuntimeException("AI 请求失败: {$error}");
|
||||
|
||||
// 对于非 429 错误,不重试
|
||||
if ($response->status() !== 429) {
|
||||
throw $lastException;
|
||||
}
|
||||
}
|
||||
|
||||
return $response->json('choices.0.message.content', '');
|
||||
throw $lastException ?? new RuntimeException('AI 请求失败: 未知错误');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,7 +222,7 @@ class AiClient
|
||||
return <<<'PROMPT'
|
||||
你是一个专业的日志分析专家。你的任务是分析提供的日志内容,识别异常和问题,并给出分类和建议。
|
||||
|
||||
请按照以下 JSON 格式返回分析结果:
|
||||
请严格按照以下 JSON 格式返回分析结果,不要添加任何额外的文字说明或代码块标记:
|
||||
{
|
||||
"core_anomalies": [
|
||||
{
|
||||
@@ -222,7 +244,7 @@ class AiClient
|
||||
- classification: database(数据库), network(网络), application(应用逻辑), configuration(配置), resource(资源), other(其他)
|
||||
- impact: high(高影响), medium(中等影响), low(低影响)
|
||||
|
||||
请确保返回有效的 JSON 格式。
|
||||
重要:只返回纯 JSON,不要使用 markdown 代码块(```json)包裹。
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
@@ -239,16 +261,47 @@ PROMPT;
|
||||
|
||||
private function parseAnalysisResponse(string $response): array
|
||||
{
|
||||
// 尝试提取 JSON
|
||||
if (preg_match('/\{[\s\S]*}/', $response, $matches)) {
|
||||
$json = $matches[0];
|
||||
$decoded = json_decode($json, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$jsonContent = $response;
|
||||
|
||||
// 1. 首先尝试从 markdown 代码块中提取 JSON
|
||||
// 匹配 ```json ... ``` 或 ``` ... ``` 格式(使用贪婪匹配获取最后一个代码块)
|
||||
if (preg_match_all('/```(?:json)?\s*([\s\S]*?)```/', $response, $matches)) {
|
||||
// 尝试每个匹配的代码块,找到有效的 JSON
|
||||
foreach ($matches[1] as $match) {
|
||||
$trimmed = trim($match);
|
||||
$decoded = json_decode($trimmed, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded) && isset($decoded['core_anomalies'])) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
// 如果没有找到有效的 JSON,使用第一个匹配
|
||||
$jsonContent = trim($matches[1][0]);
|
||||
}
|
||||
|
||||
// 2. 尝试直接解析(可能已经是纯 JSON)
|
||||
$decoded = json_decode($jsonContent, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
// 3. 尝试从内容中提取 JSON 对象(处理前后有其他文本的情况)
|
||||
// 使用更精确的匹配:找到包含 core_anomalies 的 JSON 对象
|
||||
if (preg_match('/\{[^{}]*"core_anomalies"[\s\S]*}/', $response, $matches)) {
|
||||
$decoded = json_decode($matches[0], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法解析 JSON,返回原始响应
|
||||
// 4. 最后尝试匹配任意 JSON 对象
|
||||
if (preg_match('/\{[\s\S]*}/', $jsonContent, $matches)) {
|
||||
$decoded = json_decode($matches[0], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 如果无法解析 JSON,返回原始响应
|
||||
return [
|
||||
'core_anomalies' => [],
|
||||
'summary' => $response,
|
||||
|
||||
Reference in New Issue
Block a user