Перейти к содержимому

Согласованный термин тоже может быть ошибкой: как AI проверяет доменную модель по поведению

· 13 мин
Содержание

TL;DR: Согласие делает термин общим, но не делает модель верной. AI помог нам опровергнуть модель привычного "контрагента" по её жизненному циклу. После проверки командой новое различие закрепилось в контрактах, коде, тестах и хранении результатов. Самый полезный режим AI здесь – построить конкурирующую модель и попытаться сломать обе, а не придумать красивый термин.


У нас не было проблемы с общим языком.

Вся проектная команда одинаково называла передаваемый объект "контрагентом" или просто "КА". Термин жил в обсуждениях и проектных артефактах достаточно долго, чтобы перестать вызывать вопросы. Когда все употребляют одно слово одинаково, обычно кажется, что с моделью всё в порядке.

Но наш "контрагент" создавался ради звонка, попадал в кампанию, выбирал одного из нескольких адресатов, обрабатывался, получал результат и завершал работу. Иногда звонить нужно было вообще не самому контрагенту, а поручителю.

Я пришёл в проект позже остальных и ещё не успел привыкнуть к этому слову. Поэтому довольно быстро задал вопрос, который звучал примерно так:

Почему контрагент создаётся для звонка, меняет маршрут и получает результат обработки? Сам контрагент ведь существует независимо от нашего обзвона.

Сначала это выглядело как спор о более удачном названии. В итоге мы поменяли контракты, модель хранения, тесты и жизненный цикл. Ошибка оказалась не между словами двух людей. Она находилась внутри одного существительного, с которым все уже согласились.

Здесь системы обезличены, а точные статусы, алгоритмы и детали процесса заменены отраслевыми аналогами; причинная цепочка и границы изменений сохранены.

Все говорили одинаково

В проекте взаимодействовали две большие системы. Внутренняя система взыскания хранила контрагента, задолженность, стадию просрочки, контакты связанных лиц, предыдущие разговоры, обещания об оплате и дату следующего взаимодействия. Она определяла, с кем, почему и когда нужно связаться.

Внешняя платформа контакт-центра исполняла это намерение: управляла кампаниями и дозвоном, предоставляла оператору интерфейс для работы, сохраняла попытки и записи разговоров, а затем возвращала результат.

Между ними находился интеграционный слой. Я разрабатывал сервис-адаптер внутри него. Он передавал во внешнюю платформу намерение совершить контакт вместе с достаточным бизнес-контекстом, а обратно получал факт конкретной попытки и её результат.

Слово "контрагент" выглядело естественно, потому что большая часть полей действительно описывала контрагента:

  • кто он;
  • по какому обязательству возник долг;
  • сколько длится просрочка;
  • какие контакты доступны;
  • с кем уже разговаривали;
  • что обещали оплатить.

Поля подтверждали привычное имя. Противоречие проявилось только тогда, когда объект пришлось провести через границу системы и заставить что-то делать.

В первом цикле я уже разбирал противоположную проблему: разные люди вкладывают в одно слово разные смыслы, и предположения просачиваются в код. Здесь люди не расходились в определениях. Они последовательно использовали одну и ту же неверную категорию.

Существительное становится проверяемой гипотезой

Название объекта не обязано быть делом вкуса. У существительного есть проверяемые последствия.

Если перед нами контрагент, у объекта должна быть независимая идентичность стороны договорных отношений. Удаление рабочей записи из кампании не может удалить эту сторону. Неудачный звонок не создаёт нового контрагента. Смена номера или ответственного сотрудника не меняет его идентичность.

Передаваемая единица вела себя иначе:

  • создавалась ради будущего контакта;
  • содержала несколько кандидатов для связи;
  • выбирала адресата и способ дозвона;
  • попадала в обработку;
  • получала попытки и результаты;
  • после разговора планировала следующее действие;
  • меняла маршрут после переоценки задолженности;
  • завершалась, хотя контрагент продолжал существовать.

Так появились четыре понятия с разным временем жизни:

ПонятиеКогда существуетСвязь и кардинальность
КонтрагентНезависимо от конкретного контактаОдин контрагент может породить много заявок
Заявка на контактПока выполняется определённая работаОдна заявка содержит много кандидатов и попыток
Попытка контактаВ рамках одного конкретного вызоваКаждая попытка относится к одной заявке и выбранному адресату
Результат контактаКак факт состоявшейся или несостоявшейся попыткиКаждый результат относится к породившей его попытке

Поля показывают, какими данными процесс пользуется. Жизненный цикл показывает, чем объект является.

Подробные данные контрагента внутри передаваемой структуры не превращают всю структуру в контрагента. Они могут быть срезом контекста, который временная рабочая единица использует для своей цели.

Разные жизненные циклы ещё не доказывают точные границы агрегатов и классов. Зато они дают фальсифицируемые утверждения: одна заявка может завершиться без исчезновения контрагента; у одного контрагента могут быть разные заявки; одна заявка допускает несколько попыток; результат относится к породившей его попытке. Реальный сценарий, нарушающий эти утверждения, заставил бы пересмотреть разделение.

В исследовании Bragilovski и коллег людей, подходы на основе правил, классическое машинное обучение и языковые модели сравнивали на девяти наборах пользовательских историй. Языковые модели хорошо выделяли классы, но ни один автоматический подход не совпал с человеческим результатом для модели целиком. Авторы отдельно отмечают, что в пользовательских историях обычно нет кардинальностей. Убедительный список существительных ещё не выдерживает реальные сценарии.

AI не выбрал синоним, а собрал конкурирующую модель

Я заметил это противоречие, а затем вместе с текущим именем и списком полей передал AI сценарии:

  • откуда появляется объект;
  • зачем адаптер его создаёт и обогащает;
  • как выбирается контакт;
  • почему меняется маршрут;
  • что происходит после разговора;
  • как планируется следующее действие.

Из одного набора фактов получались две конкурирующие модели:

ГипотезаЧто объясняетГде ломается
Расширенный контрагентПочему внутри столько данных о долге и контактахНе объясняет отдельное создание, завершение, попытки и удаление из кампании
Заявка на контактОбъясняет цель, маршрут, попытки и результатТребует подтвердить границы, кардинальности и язык у владельцев процесса

AI собрал контраргументы к первой гипотезе и предложил рабочее название: "заявка на контакт с должником". В статье я использую условные названия DebtorContactRequest, ContactAttempt и ContactResult; они показывают смысл границ, а не раскрывают реальные имена классов.

Ценность ответа была в конкурирующей версии устройства системы: её можно было принести людям и проверить по процессу. Само существительное оставалось кандидатом.

У свежего взгляда короткий срок годности. Я заметил противоречие, потому что ещё не научился автоматически переводить "КА" в набор привычных оговорок. AI тоже не участвовал в локальной истории, которая сделала это имя нормой: он получил сценарии, но не привычку считать термин правильным. Ни то ни другое не давало нам бизнес-истины – зато возвращало исчезнувшее смысловое трение.

Исследование OpenAI с CriticGPT было посвящено другому: ошибкам в сгенерированном коде. Но разделение ролей получилось полезным и для нашего случая. Люди с AI-критиком давали более полные замечания, чем люди без него, а совместная работа галлюцинировала меньше проблем, чем критик в одиночку. Параллель ограниченная, но практичная: AI предлагает больше точек для проверки, а человек выясняет, существуют ли найденные проблемы в реальности.

Когда гипотеза стала решением

Я показал разбор руководителю проекта и своей команде. Мы согласились, что заявка лучше отражает назначение передаваемой единицы.

Затем мы предложили этот термин соседней команде, которая развивала внутреннюю систему. Как минимум на границе наших контекстов новое имя закрепилось. Оно появилось в документации, сервисных контрактах, коде адаптера, тестах и последующих задачах. Старые задачи никто задним числом не переписывал, поэтому в истории проекта остался прежний "КА".

Я не проверял, меняла ли соседняя команда устоявшийся язык внутри своей системы. Для результата это не требовалось. Внутри одного контекста она могла продолжать работать с контрагентом, а контракт с адаптером начал явно описывать передаваемую работу. Граница перестала притворяться репликацией карточки.

AI не утвердил модель. Предложенное различие стало решением на этой межсервисной границе после обсуждения с людьми, которые отвечали за процесс, и проверки последствий в системе.

Где новая модель оставила следы

Если после обсуждения изменилось только имя класса, возможно, команда просто выбрала более удачное название. У нас принятое различие со временем проявилось в нескольких слоях системы:

АртефактСтарая модельРазделённая модель
КонтрактПередаёт расширенного "контрагента"Передаёт заявку на будущую работу
ИдентичностьДанные человека, долг и обработка склееныКонтрагент существует независимо от заявки
ПоведениеФильтрация, ранжирование и маршрут теряются в коде преобразованияПравила работают с заявкой и её кандидатами
ХранениеПоследний результат затирает предыдущийПопытки и результаты образуют историю
ТестыПроверяют перенос и обновление полейПроверяют выбор контакта и переходы жизненного цикла
Задачи"Обновить контрагента после звонка""Сохранить попытку, результат и переоценить заявку"

Переименование само по себе ничего этого не реализовало. Принятое различие дало команде границу, вокруг которой изменения перестали выглядеть случайным рефакторингом.

Различие становится архитектурным, когда меняет ответы на три вопроса: что имеет собственную идентичность, что может повторяться и где живёт история.

Кому принадлежат правила выбора контакта

У нас существовало несколько алгоритмов фильтрации и ранжирования контактов. При создании заявки адаптер собирал доступные номера, отбрасывал неактуальные и повторяющиеся, а оставшиеся ранжировал по роли контактного лица, приоритету и предыдущим попыткам. В зависимости от сценария первой целью мог быть сам должник, а следующей – поручитель или другое связанное лицо.

Пока объект назывался контрагентом, эти правила выглядели как ещё один этап преобразования его карточки. После разделения стало понятно, над чем они работают: заявка определяет следующего кандидата и маршрут. Контрагент от выбора номера не меняется.

Складывать всю эту логику внутрь одного класса DebtorContactRequest необязательно. Отдельные правила могут оставаться самостоятельными. Важно другое: их входом и результатом становится путь заявки, а не новая версия идентичности контрагента.

Со временем мы пересмотрели и жизненный цикл. При создании заявка получала кандидатов и маршрут. Каждый звонок становился отдельной попыткой. Недозвон или неактуальный номер могли переключить работу на следующий контакт, просьба перезвонить или обещание оплаты переносили следующее действие, а изменение состояния задолженности заставляло переоценить заявку: продолжить, сменить маршрут или завершить.

Тесты начали описывать это поведение вместо перестановки полей: неактуальный контакт исключается, неудачная попытка переводит работу к следующему кандидату, результат разговора влияет на дату следующего действия.

Принятое различие делает границу видимой ещё до выбора паттернов и помогает ответить, кому принадлежит поведение. Если смоделировать процесс как сущность контрагента, его правила могут начать прилипать к карточке, преобразованию или сервису без ясной ответственности.

"Последний результат" – запрос к истории, замаскированный под поле

Самый наглядный инженерный след новой границы появился не в словаре и даже не в названии класса. Он появился в базе данных.

До разделения модели мы сохраняли в сущности "КА" только последний результат контакта. Новая попытка затирала предыдущую. Система знала текущее значение, но теряла последовательность, которая к нему привела.

Слово "последний" описывает не природу результата, а способ чтения: выбрать самый поздний результат среди попыток. Для такого запроса сначала должна существовать история.

После принятия разделённой модели результаты вынесли из таблицы контрагентов в отдельную структуру и связали одновременно с заявкой и конкретной попыткой. Один неудачный звонок, разговор с поручителем и последующий контакт с самим должником перестали схлопываться в одно поле. Они стали разными фактами одной работы.

Текущий результат при необходимости всё ещё можно вычислить или сохранить как производную модель для чтения. Для этого не нужен Event Sourcing. Обычных связанных записей попыток и результатов достаточно. Существенно другое: производное значение больше не уничтожает исходные факты.

Текущее состояние отвечает на вопрос "что верно сейчас?". История объясняет "как и почему мы сюда пришли?". Старое имя склеивало эти вопросы внутри долговечной сущности.

Позже добавлять журнал звонков после изменений платежей стало проще: его строили поверх уже отделённых попыток и результатов, а заявка связывала их с причиной и дальнейшим действием. Время до и после мы не измеряли, это моё наблюдение.

Как пытаться опровергнуть термин с помощью AI

Просьба "придумай более точное имя" начинается слишком поздно. Сначала текущее имя нужно превратить в гипотезу и дать AI возможность атаковать её.

Я бы повторял этот приём так:

  1. Беру термин, с которым команда уже согласна.
  2. Собираю 3–5 реальных сценариев, включая исключения и повторные действия.
  3. Выписываю глаголы, кардинальности, момент создания и условие завершения.
  4. Прошу AI построить минимум две модели и найти контрпример к каждой.
  5. Отдельно прошу защитить старое имя и перечислить недостающие бизнес-факты.
  6. Проверяю последствия каждой модели в контрактах, хранении и тестах.
  7. Возвращаю спор владельцам процесса и только после этого меняю язык системы.

Рабочий запрос может выглядеть так:

Ниже описаны текущее имя объекта и реальные сценарии его использования.

1. Отдели наблюдаемые факты от интерпретаций.
2. Если текущее имя верно, какие инварианты должны выполняться?
3. Найди сценарии, которые нарушают эти инварианты.
4. Построй две конкурирующие границы модели.
5. Приведи контрпример к каждой границе.
6. Защити текущую модель: при каких недостающих фактах она всё-таки верна?
7. Перечисли, что изменится в контрактах, хранении и тестах при выборе каждой модели.
8. Не выбирай победителя без бизнес-фактов.

Релевантные примеры заметно меняют качество такой критики. В индустриальном исследовании Bashir и коллег десять примеров из похожих требований улучшили обнаружение неоднозначностей в среднем на 20,2% по сравнению с запросом без примеров. Восемь экспертов оценили объяснения модели в среднем на 3,84 из 5. Задача другая, но вывод для нашего метода полезен: хороший контекст помогает AI сформулировать сомнение, а экспертная проверка остаётся отдельным этапом.

Последний пункт защищает от приятного, но бесполезного результата. Если спросить AI только "прав ли я?", легко получить тщательно аргументированное согласие. Публичный разбор OpenAI об избыточной соглашательности модели показывает, как соглашательное поведение может остаться незамеченным в обычных офлайн-проверках и A/B-тестах. Поэтому запрос к AI-критику должен не подтверждать исходную позицию, а симметрично атаковать и защищать её.

Проверять нужно и сам критерий. В аудите SWE-Bench Pro два параллельных разбора – агентский под человеческим контролем и независимый инженерный – показали, что около 30% задач сломаны на уровне постановки или тестов. Для доменной модели автоматического эталона нет, поэтому привычный термин и существующий тест тоже нельзя заранее назначать источником истины.

AI всё равно может выдумать проблему, провести границу в удобном для кода месте или уверенно назвать неизвестное бизнес-правило. Его контрпример остаётся поводом проверить процесс, открыть контракт или поговорить с владельцем. Сам ответ ничего не ратифицирует.

Где здесь остаётся Ubiquitous Language

В первом цикле вывод был таким: когда люди понимают одно слово по-разному, нужно вытащить расхождение в разговор и закрепить общий язык в коде. Этот совет остаётся верным, но новая история начинается на следующем шаге.

Наша команда уже говорила одинаково. Значит, согласованность языка является необходимым свойством модели, но не проверкой её корректности. Общему языку тоже нужен механизм критики.

UL в этой статье работает как место фиксации принятого решения. Источником проверки служат жизненный цикл, кардинальности, реальные сценарии и инженерные последствия. Команда сначала должна опровергнуть или выдержать конкурирующую модель, и только потом делать новое слово общим.

Согласие и корректность – разные свойства

Самая опасная модель может быть не той, о которой спорят. Иногда опаснее модель, с которой давно никто не спорит.

AI полезен в такой ситуации не потому, что знает домен лучше команды. Он позволяет дёшево вернуть спор туда, где команда уже перестала спорить: собрать альтернативную декомпозицию, выписать следствия и найти сценарий, который привычное имя объясняет плохо. Это роль оппонента, а не арбитра.

Дальше начинается инженерная работа. Мы проверяем существительное его глаголами, границу – временем и кардинальностями, а затем смотрим, одинаково ли решение читается в контрактах, хранении и тестах.

Согласие делает термин общим. Проверяемость делает модель защищаемой.

Обсуждаем в моём телеграм-канале.

Что почитать

Здесь намеренно нет ещё одного списка по DDD и Ubiquitous Language. Все материалы ниже вышли в 2025–2026 годах и рассматривают AI не как генератор красивого ответа, а как участника извлечения, критики или проверки модели:

  • Bragilovski et al. – Leveraging machines to derive domain models from user stories, 2025: прямое сравнение людей, подхода на основе правил, классического машинного обучения и языковых моделей при построении моделей по пользовательским историям. Особенно полезен разбор ложных срабатываний и вывод, что автоматическая модель остаётся заготовкой для аналитика.
  • Bashir et al. – Requirements Ambiguity Detection and Explanation with LLMs: An Industrial Study, 2025: исследование на трёх индустриальных наборах требований. Оно отдельно показывает влияние релевантных примеров на обнаружение неоднозначности и даёт экспертную оценку объяснений модели.
  • OpenAI – Separating signal from noise in coding evaluations, 2026: аудит с двумя параллельными контурами – агентами под человеческим контролем и независимой группой опытных инженеров. Полезная методическая параллель: проверять приходится не только ответ, но и сам критерий.
  • Moon et al. – Don't Judge Code by Its Cover: Exploring Biases in LLM Judges for Code Evaluation, 2026: исследование о том, как поверхностные различия в именах, комментариях и форматировании влияют на оценки языковых моделей-судей даже для семантически эквивалентного кода. Полезное ограничение для любой схемы "одна модель генерирует, другая проверяет".
  • Anthropic – Demystifying evals for AI agents, 2026: практическое руководство по проверке итогов и хода работы AI-агентов. В контексте статьи важнее всего сочетание автоматической, модельной и экспертной оценки и калибровка модели по человеческому суждению.
Поделиться: Telegram X