set('services.yandexgpt', [ 'api_key' => 'k', 'folder_id' => 'f', 'model' => 'yandexgpt-lite/latest', 'endpoint' => 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion', 'timeout_seconds' => 8, ]); Http::fake([ 'llm.api.cloud.yandex.net/*' => Http::response([ 'result' => ['alternatives' => [['message' => ['role' => 'assistant', 'text' => 'Ответ.']]]], ]), ]); for ($i = 0; $i < 20; $i++) { KnowledgeChunk::create([ 'source_path' => "help/a{$i}.md", 'title' => "Статья {$i}", 'tour' => null, 'topics' => 'проект, баланс, тариф', 'chunk_index' => 0, 'content' => str_repeat("Текст про проект и баланс номер {$i}. ", 30), ]); } $latencies = []; for ($i = 0; $i < 10; $i++) { $chat = "chat-{$i}"; $row = BotDialog::create([ 'chat_id' => $chat, 'direction' => 'in', 'message' => 'что такое проект?', 'created_at' => now(), ]); (new ProcessChatMessageJob($chat, (int) $row->id, 'что такое проект?'))->handle(); $latencies[] = (int) BotDialog::where('chat_id', $chat) ->where('direction', 'out')->value('latency_ms'); } sort($latencies); $p95 = $latencies[(int) floor(count($latencies) * 0.95) - 1] ?? end($latencies); // Бюджет спеки §6: поиск ≤300мс + сборка/журнал ≤200мс. LLM (до 3с) и сеть // Jivo (до 0.5с) — вне нашего кода, замоканы; живой p95 — на приёмке. expect($p95)->toBeLessThan(500); });