fix(bot): гостю больше нельзя приписывать его ступень, баланс и заявки
Проверка на боевом 14.07.2026 (сразу после выката фикса цен): гость — человек не вошёл, никаких заявок у него нет — показал скриншот с 50 ₽, и бот ответил: «Вы сейчас на второй ступени (50 ₽), потому что в этом месяце уже получили заявки». Выдумка про человека. Причина: сторож сверяет личные цифры только у ВОШЕДШЕГО (карточка фактов). У гостя карточки нет — и модель фантазировала свободно. Теперь AnswerGuard знает, посчитаны ли личные цифры собеседника. Если нет (гость) — режет фразы, утверждающие его нынешнее состояние: «вы сейчас на… ступени», «у вас на балансе N», «вы уже получили заявки». Общие объяснения не трогаются: «вы платите только за полученные заявки», «когда наберёте объём — перейдёте на следующую ступень». Тесты бота: 242/242 (+2 новых). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,7 +142,13 @@ class AnswerGuard
|
||||
* @param string $context фрагменты статей, которые модель получила: цены, названные
|
||||
* в ответе, обязаны встречаться здесь — иначе они выдуманы
|
||||
*/
|
||||
public function clean(string $text, string $context): string
|
||||
/**
|
||||
* @param bool $personalFactsKnown мы знаем, КТО пишет, и посчитали его цифры (карточка
|
||||
* фактов). У гостя с лендинга их нет — и тогда любое
|
||||
* «вы сейчас на второй ступени» это выдумка про
|
||||
* человека (проверка на боевом 14.07.2026).
|
||||
*/
|
||||
public function clean(string $text, string $context, bool $personalFactsKnown = true): string
|
||||
{
|
||||
$sentences = preg_split('/(?<=[.!?])\s+/u', trim($text)) ?: [];
|
||||
$allowed = $this->numbersIn($context);
|
||||
@@ -158,6 +164,7 @@ class AnswerGuard
|
||||
|| $this->refusesReplacementHarshly($sentence)
|
||||
|| $this->promisesSupportSla($sentence)
|
||||
|| $this->promisesNicheRelevance($sentence)
|
||||
|| (! $personalFactsKnown && $this->claimsPersonalState($sentence))
|
||||
|| $this->hasFactMismatch($sentence, $facts)
|
||||
|| $this->hasInventedNumber($sentence, $allowed)) {
|
||||
$cutSomething = true;
|
||||
@@ -191,6 +198,39 @@ class AnswerGuard
|
||||
* понять…», отговорку сняли — и клиент получил ответ с середины фразы, решил, что
|
||||
* ему хамят: «Ну хамить то не надо!».
|
||||
*/
|
||||
/**
|
||||
* Фраза утверждает ЛИЧНОЕ состояние собеседника: его ступень, баланс, объём заявок.
|
||||
*
|
||||
* Гостю такое говорить нельзя ни при каких обстоятельствах: он не вошёл, никаких
|
||||
* заявок у него нет, и цифры взять неоткуда. Проверка на боевом 14.07.2026: гость
|
||||
* показал скриншот с 50 ₽ — бот ответил «Вы сейчас на второй ступени, потому что в
|
||||
* этом месяце уже получили заявки». Это выдумка про человека.
|
||||
*
|
||||
* Общие объяснения не трогаем: «вы платите только за полученные заявки», «когда
|
||||
* наберёте объём — перейдёте на следующую ступень» — тут нет утверждения о том, что
|
||||
* у человека происходит ПРЯМО СЕЙЧАС.
|
||||
*/
|
||||
private function claimsPersonalState(string $sentence): bool
|
||||
{
|
||||
$aboutHim = preg_match('/\b(вы|вас|вам|ваш\w*)\b/iu', $sentence) === 1;
|
||||
if (! $aboutHim) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subject = preg_match('/(ступен\w*|баланс\w*|тариф\w*|объ[её]м\w*|заяв\w*|списан\w*)/iu', $sentence) === 1;
|
||||
if (! $subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// «сейчас/уже/находитесь/текущая» — маркеры утверждения о нынешнем состоянии.
|
||||
$rightNow = preg_match('/(сейчас|уже|находит\w*|текущ\w*|на данный момент|в этом месяце)/iu', $sentence) === 1;
|
||||
|
||||
// «у вас на балансе 11 025 ₽» — притяжательное + число, даже без «сейчас».
|
||||
$ownedNumber = preg_match('/(у\s+вас|ваш\w*)[^.!?]{0,40}\d/iu', $sentence) === 1;
|
||||
|
||||
return $rightNow || $ownedNumber;
|
||||
}
|
||||
|
||||
public function polishStart(string $text): string
|
||||
{
|
||||
$text = (string) preg_replace(self::DANGLING_START, '', trim($text));
|
||||
|
||||
@@ -325,7 +325,11 @@ class BotAnswerService
|
||||
array_filter($history, fn ($m) => ($m['role'] ?? '') === 'assistant')
|
||||
));
|
||||
|
||||
$text = $this->guard->clean($text, $context."\n\n".$said);
|
||||
// Гостю с лендинга личные цифры приписывать НЕЛЬЗЯ: он не вошёл, заявок и ступени
|
||||
// у него нет. Карточка фактов есть только у вошедшего — по ней и решаем.
|
||||
$personalFactsKnown = $facts !== null && trim($facts) !== '';
|
||||
|
||||
$text = $this->guard->clean($text, $context."\n\n".$said, $personalFactsKnown);
|
||||
if ($text === '') {
|
||||
return new BotAnswer(self::ESCALATE_TEXT, escalate: true);
|
||||
}
|
||||
|
||||
@@ -203,6 +203,47 @@ it('уверенным считается совпадение хотя бы н
|
||||
expect($search->confidentTopicMatch($sbor, 'сколько стоит сбор источников?'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('гостю бот не приписывает его ступень и баланс — у гостя их нет', function () {
|
||||
twoPricingGrids();
|
||||
|
||||
KnowledgeChunk::create([
|
||||
'source_path' => 'help/tarify.md', 'title' => 'Тарифы — сколько стоит заявка', 'tour' => null,
|
||||
'topics' => 'тариф, цена, ступень, сколько стоит заявка', 'chunk_index' => 0,
|
||||
'content' => 'Всего 7 ступеней. Сейчас сетка такая: {{тарифы}}.',
|
||||
]);
|
||||
|
||||
// Проверка на боевом 14.07.2026: гостю (он не вошёл, заявок у него нет) бот заявил
|
||||
// «Вы сейчас на второй ступени (50 ₽), потому что уже получили заявки». Выдумка.
|
||||
llmAnswers('Вы сейчас на второй ступени (50 ₽), потому что в этом месяце уже получили заявки. На второй ступени цена заявки — 50 ₽, дальше она опускается.');
|
||||
askBot('c-guest', 'Сколько стоит заявка на второй ступени?');
|
||||
|
||||
$answer = (string) BotDialog::where('chat_id', 'c-guest')->where('direction', 'out')->value('message');
|
||||
|
||||
// Личное состояние — вырезано.
|
||||
expect($answer)->not->toContain('Вы сейчас на второй ступени')
|
||||
->and($answer)->not->toContain('уже получили заявки');
|
||||
|
||||
// Безличное объяснение про сетку — осталось, человек не остался без ответа.
|
||||
expect($answer)->toContain('50 ₽');
|
||||
});
|
||||
|
||||
it('вошедшему клиенту его личные цифры называть можно — они из карточки фактов', function () {
|
||||
twoPricingGrids();
|
||||
|
||||
KnowledgeChunk::create([
|
||||
'source_path' => 'help/balans.md', 'title' => 'Баланс', 'tour' => null,
|
||||
'topics' => 'баланс, сколько денег', 'chunk_index' => 0,
|
||||
'content' => 'Баланс виден в разделе «Биллинг».',
|
||||
]);
|
||||
|
||||
$guard = app(App\Services\Bot\AnswerGuard::class);
|
||||
$facts = "ФАКТЫ КЛИЕНТА:\nбаланс: 11 025 ₽\nступень: 2";
|
||||
|
||||
$clean = $guard->clean('У вас сейчас на балансе 11 025 ₽.', $facts, personalFactsKnown: true);
|
||||
|
||||
expect($clean)->toContain('11 025 ₽');
|
||||
});
|
||||
|
||||
it('ответ не начинается с середины фразы, когда снята отговорка «в инструкции этого нет»', function () {
|
||||
KnowledgeChunk::create([
|
||||
'source_path' => 'help/o-servise.md', 'title' => 'О сервисе', 'tour' => null,
|
||||
|
||||
Reference in New Issue
Block a user