c5125e981e
Прогнал 250 вопросов через настоящий вебхук шесть раз и сравнил четыре модели вслепую силами судей-агентов. Итоги — в docs findings. Модель. Появился переключатель services.jivo_bot.llm: yandex (данные не покидают РФ) либо aitunnel (зарубежные модели через российский шлюз). По слепому сравнению Claude Haiku 4.5 выиграл финал у Gemini 157:24 и у обеих моделей Яндекса: он полнее, точнее по кнопкам и цифрам и говорит как человек, а не справочник. Три замка, потому что живая модель = самоуверенная модель: - PersonalDataMask — телефоны и почта вырезаются ДО отправки в любую модель (клиент оставляет номер в чате; за границу ПДн уходить не должны). - ChatTextCleaner — markdown и смайлики (Jivo их не рисует): было 191 ответ с разметкой, стало 0. - AnswerGuard — режет юридические гарантии («вас не оштрафуют», «мы гарантируем», «не нарушаем закон») и ЛЮБУЮ цену, которой нет в выданных статьях. Пусто после чистки → зовём живого специалиста. Поиск по инструкции переписан: выбрасываются вопросительные слова (из-за них «какие у вас тарифы» находило выгрузку в Excel), ранжирование сперва по заголовку и синонимам, выдача — три РАЗНЫЕ статьи и каждая целиком, плюс починка опечаток («каг сазадь праэкт») и учёт прошлой реплики в коротких «а это платно?». Инструкция: 40 → 51 статья. Новые — по фактам владельца: бесплатный тест и подарок 1000 ₽, «мы канал, а не реклама», СМС → через час звонок, средние показатели (8–12 из 100 разговаривают, ещё 5–7 по СМС), номер не вернётся 30 дней, замена брака, вывод остатка, поддержка 24/7, удаление данных, ниши. Вычищено враньё: минус баланса, «списали без заявки», пауза вечером, выходные, «Вся РФ», статус «Отказ» (его нет — есть «Не реализовано»), amoCRM, платный повторный сбор, нарастающий итог тарифных ступеней. Тесты: 106 из 106. Каждая найденная тупость закреплена тестом. НЕ НА ПРОДЕ. Выкат — только по прямой команде владельца. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.9 KiB
PHP
78 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\KnowledgeChunk;
|
|
use App\Services\Bot\BotAnswerService;
|
|
use App\Services\Bot\ChatTextCleaner;
|
|
use App\Services\Bot\KnowledgeSearch;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* Слепое сравнение моделей 12.07.2026: Gemini выиграл у Яндекса 62:36, но судьи нашли
|
|
* пять болезней — враньё про платный повтор сбора, цены по памяти, «приглашу специалиста»,
|
|
* гарантии и markdown-разметка в чате.
|
|
*/
|
|
beforeEach(function () {
|
|
config()->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,
|
|
]);
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
});
|
|
|
|
it('в чат не уходит markdown-разметка — Jivo её не рисует', function () {
|
|
$cleaner = app(ChatTextCleaner::class);
|
|
|
|
expect($cleaner->clean('**Важно:** пишите на `support@liderra.ru`'))
|
|
->toBe('Важно: пишите на support@liderra.ru')
|
|
->and($cleaner->clean("Причины:\n* первая\n* вторая"))
|
|
->toBe("Причины:\n— первая\n— вторая")
|
|
->and($cleaner->clean('### Заголовок'))->toBe('Заголовок');
|
|
});
|
|
|
|
it('ответ бота приходит клиенту без звёздочек и кавычек', function () {
|
|
Http::fake(['*' => Http::response([
|
|
'result' => ['alternatives' => [['message' => ['text' => '**Списания** видно в разделе `Биллинг`.']]]],
|
|
], 200)]);
|
|
|
|
$answer = app(BotAnswerService::class)->answer('где посмотреть списания');
|
|
|
|
expect($answer->text)->not->toContain('**')
|
|
->and($answer->text)->not->toContain('`')
|
|
->and($answer->text)->toContain('Списания');
|
|
});
|
|
|
|
it('статья честно предупреждает: повторный сбор платный даже без результата', function () {
|
|
// Судья 12.07.2026: обе модели соврали на «источников не нашлось — деньги спишутся?».
|
|
// Первый сбор без результата бесплатен, ПОВТОРНЫЙ списывается всегда.
|
|
$hits = app(KnowledgeSearch::class)->search('источников не нашлось, деньги спишутся?', 3);
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
|
|
|
expect($text)->toContain('Повторный сбор списывается всегда')
|
|
->and($text)->toContain('даже если ничего не нашлось');
|
|
});
|
|
|
|
it('правила для модели запрещают гарантии, цены по памяти и «приглашу специалиста»', function () {
|
|
// Правила живут в системном промпте — проверяем, что они там есть и доезжают до модели.
|
|
Http::fake(['*' => Http::response([
|
|
'result' => ['alternatives' => [['message' => ['text' => 'Ответ.']]]],
|
|
], 200)]);
|
|
|
|
app(BotAnswerService::class)->answer('сколько стоит заявка');
|
|
|
|
Http::assertSent(function ($request) {
|
|
$system = (string) ($request->data()['messages'][0]['text'] ?? '');
|
|
|
|
return str_contains($system, 'Никогда не называй цены по памяти')
|
|
&& str_contains($system, 'Не обещай «приглашу специалиста»')
|
|
&& str_contains($system, 'Не давай гарантий')
|
|
&& str_contains($system, 'Без markdown');
|
|
});
|
|
});
|