Летел я из Джакарты домой в Сингапур. Как обычно:
- на Netflix ничего не скачано
- Wi-Fi в самолёте стоит как крыло от него самого
В общем, надо чем-то себя занять.
Решил пописать код. Тем более была вполне интересная задача: написать небольшой rule engine для банка. Но есть нюанс:
- интернета нет
- локальных LLM нет
- писать всё руками, как наши деды в 2022-м, тоже не хотелось
И тут мне пришла идея: а что если поиграть с LLM в TDD ping-pong? То есть я руками пишу самое важное — спецификацию в виде тестов.
А модель потом сама генерирует реализацию под этот контракт.
Получалось примерно так:
@Test
fun `when term deposit created allow only deposit`() {
val awaitFundingTd =
accountRuleSet {
withdrawal {
default { denyAny() }
}
deposit {
allowAny() { balances("funded") }
}
}
awaitFundingTd
.makeDecision(Withdrawal, "ATM Withdrawal")
.shouldBeDenied()
}
Сразу захотелось сделать красивый DSL для правил. Потому что если делать DSL — то обязательно универсальный, расширяемый, чтобы потом переиспользовать для всех продуктов (да-да, конечно). Иначе зачем вообще начинать 😄
В итоге за полтора часа полёта я написал 7 тестов, 0 строк реализации и абсолютно ничего не компилировалось. То есть классический успешный TDD session. А уже дома вместо огромного промпта на 3 экрана с объяснениями что я вообще хочу я просто сказал Cursor "Сделай так, чтобы тесты проходили"
И получилось… идеально. С первого раза все тесты зазеленели, классы получились маленькими, интерфейсы аккуратные и никакой магической лапши. И тут до меня дошла довольно простая мысль: спецификацию можно задать не словами, используя расплывчатые формулировки вида "гибко, красиво и удобно", а сразу запердолить контракт в виде тестов.
Выходит, что LLM намного проще быть хорошим инженером, когда ты перестаёшь разговаривать с ней как продакт и начинаешь как разработчик