6e970d7231
Стоп-лист известных для ИИ канала В дедуплицировался только в проверке видели/не видели, а в промпт уходил с повторами (Корунд ×27, КрасЛомбард ×19). Теперь чистим и сам список по ключу имени — ИИ видит каждое имя один раз: короче, дешевле по токенам, точнее. TDD, 137/137 unit зелёные. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
5.3 KiB
PHP
104 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\ChannelB\ChannelBSearch;
|
|
use App\Services\Autopodbor\Agent\ChannelB\ResearcherClient;
|
|
use App\Services\Autopodbor\Agent\ChannelB\ResearcherParser;
|
|
|
|
/**
|
|
* Канал В, ФИНАЛ владельца: ИИ — генератор ИМЁН федералов/онлайн. На вход ИИ даём СПИСОК ИЗ КАНАЛА А
|
|
* (уже найденные в справочниках фирмы) + примеры клиента как «уже известных — не повторять». ОДНА модель
|
|
* × 2 прохода, растущий стоп-лист. Выход — только новые ИМЕНА (+ тип). Якоря добывает потом Firecrawl.
|
|
* Чистая логика — модель за границей ResearcherClient.
|
|
*/
|
|
final class FakeResearcher implements ResearcherClient
|
|
{
|
|
/** @var list<string> */
|
|
public array $userPrompts = [];
|
|
|
|
/** @param list<string> $answers */
|
|
public function __construct(private array $answers) {}
|
|
|
|
public function research(string $system, string $user): string
|
|
{
|
|
$this->userPrompts[] = $user;
|
|
|
|
return $this->answers[count($this->userPrompts) - 1] ?? '[]';
|
|
}
|
|
}
|
|
|
|
it('2 прохода: копит новые имена без дублей, наращивает стоп-лист, исключает список из А', function () {
|
|
$pass1 = '[{"name":"CarMoney","type":"федеральная"},{"name":"Финансовый дом","type":"федеральная"}]';
|
|
$pass2 = '[{"name":"Webbankir","type":"федеральная"},{"name":"CarMoney","type":"федеральная"}]'; // дубль
|
|
|
|
$fake = new FakeResearcher([$pass1, $pass2]);
|
|
$search = new ChannelBSearch($fake, new ResearcherParser);
|
|
|
|
// knownFromA = имена, уже найденные каналом А (справочники)
|
|
$out = $search->harvest(
|
|
profile: 'займы под залог авто',
|
|
region: 'Красноярский край',
|
|
clientSite: 'lkomega.ru',
|
|
known: ['КрасЛомбард', 'Голд Авто Инвест', 'Ваш инвестор'],
|
|
passes: 2,
|
|
);
|
|
|
|
// 3 новых уникальных (CarMoney из прохода 2 — дубль, не добавлен)
|
|
expect($out)->toHaveCount(3)
|
|
->and(array_column($out, 'name'))->toContain('CarMoney', 'Финансовый дом', 'Webbankir');
|
|
|
|
expect($fake->userPrompts)->toHaveCount(2);
|
|
|
|
// Проход 1: стоп-лист несёт список из А
|
|
expect($fake->userPrompts[0])->toContain('КрасЛомбард')
|
|
->and($fake->userPrompts[0])->toContain('Голд Авто Инвест');
|
|
|
|
// Проход 2: стоп-лист вырос — несёт найденное в проходе 1
|
|
expect($fake->userPrompts[1])->toContain('CarMoney')
|
|
->and($fake->userPrompts[1])->toContain('Финансовый дом');
|
|
});
|
|
|
|
it('ИИ не должен повторять имена из канала А (дедуп с known)', function () {
|
|
// ИИ вернул фирму, уже найденную каналом А → не добавляем
|
|
$fake = new FakeResearcher(['[{"name":"КрасЛомбард","type":"региональная"},{"name":"Cashmotor","type":"федеральная"}]']);
|
|
$search = new ChannelBSearch($fake, new ResearcherParser);
|
|
|
|
$out = $search->harvest('займы', 'Красноярский край', 'lkomega.ru', ['КрасЛомбард'], 1);
|
|
|
|
expect($out)->toHaveCount(1)->and($out[0]['name'])->toBe('Cashmotor');
|
|
});
|
|
|
|
it('пустой ответ прохода не падает и не добавляет', function () {
|
|
$fake = new FakeResearcher(['[{"name":"Cashmotor","type":"федеральная"}]', '[]']);
|
|
$search = new ChannelBSearch($fake, new ResearcherParser);
|
|
|
|
$out = $search->harvest('займы', 'Красноярский край', 'lkomega.ru', [], 2);
|
|
|
|
expect($out)->toHaveCount(1)->and($out[0]['name'])->toBe('Cashmotor');
|
|
});
|
|
|
|
it('чистый стоп-лист для ИИ: дубли-филиалы схлопнуты — каждое имя один раз', function () {
|
|
// Канал А отдал филиалы: «Корунд» ×3 (в т.ч. другой регистр), «КрасЛомбард» ×2.
|
|
// В промпт ИИ должно уйти по одному — иначе «- Корунд» повторится 27 раз (реальный случай).
|
|
$fake = new FakeResearcher(['[]']);
|
|
$search = new ChannelBSearch($fake, new ResearcherParser);
|
|
|
|
$search->harvest(
|
|
profile: 'займы под залог авто',
|
|
region: 'Красноярский край',
|
|
clientSite: 'lkomega.ru',
|
|
known: ['Корунд', 'Корунд', 'корунд', 'КрасЛомбард', 'КрасЛомбард'],
|
|
passes: 1,
|
|
);
|
|
|
|
$prompt = $fake->userPrompts[0];
|
|
expect(substr_count($prompt, '- Корунд'))->toBe(1)
|
|
->and(substr_count($prompt, '- КрасЛомбард'))->toBe(1);
|
|
});
|
|
|
|
it('системный промт просит ТОЛЬКО названия (§11.3)', function () {
|
|
expect(ChannelBSearch::SYSTEM_PROMPT)->toContain('только НАЗВАНИЯ')
|
|
->and(ChannelBSearch::SYSTEM_PROMPT)->toContain('строго JSON');
|
|
});
|