Files
portal/app/tests/Feature/Bot/AnswerGuardTest.php
T
Дмитрий c5125e981e feat(bot): выбор модели + три замка безопасности + инструкция 51 статья
Прогнал 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>
2026-07-12 20:44:48 +03:00

63 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\Bot\AnswerGuard;
/**
* Сторож ответа (финальное сравнение моделей 12.07.2026). Умная модель звучит живо, но
* срывается: обещает «Нет, не оштрафуют», «мы гарантируем», выдумывает цены. Промпт её
* не держит — держим кодом: запрещённое вырезаем ДО отправки клиенту.
*/
it('вырезает юридические гарантии', function () {
$guard = app(AnswerGuard::class);
$answer = $guard->clean(
'Человек дал согласие оператору. Нет, вас не оштрафуют. Пишите на support@liderra.ru.',
context: 'Человек дал согласие оператору.'
);
expect($answer)->not->toContain('не оштрафуют')
->and($answer)->toContain('Человек дал согласие оператору')
->and($answer)->toContain('support@liderra.ru');
});
it('вырезает «мы гарантируем» и «не нарушаем закон»', function () {
$guard = app(AnswerGuard::class);
expect($guard->clean('Мы гарантируем, что человек живой. Контакт свежий.', context: 'Контакт свежий.'))
->not->toContain('гарантируем')
->and($guard->clean('Нет, мы не нарушаем закон. Данные передают операторы.', context: 'Данные передают операторы.'))
->not->toContain('не нарушаем');
});
it('вырезает цену, которой нет в наших статьях', function () {
// Живая выдумка модели: «при 100+ заявках цена 450 ₽». Такого порога у нас нет.
$guard = app(AnswerGuard::class);
$answer = $guard->clean(
'Цена падает с объёмом. При 100 заявках она станет 450 ₽. Точную цену видно на плитке баланса.',
context: 'Цена зависит от объёма: ступени от 500 ₽. Точную цену видно на плитке баланса.'
);
expect($answer)->not->toContain('450')
->and($answer)->toContain('Точную цену видно на плитке баланса');
});
it('цену ИЗ статьи не трогает', function () {
$guard = app(AnswerGuard::class);
$answer = $guard->clean(
'Подбор конкурентов стоит 300 ₽.',
context: 'Цена подбора — 300 ₽ за запуск.'
);
expect($answer)->toContain('300');
});
it('если после чистки ничего не осталось — отдаёт пусто, бот позовёт специалиста', function () {
$guard = app(AnswerGuard::class);
expect($guard->clean('Нет, вас не оштрафуют.', context: 'Что-то про заявки.'))->toBe('');
});