Разработчиков нет. LLM, спасай!

 

Я архитектор в одном занимательном продукте в области документооборота…

Продукт разработан другой компанией в давние времена, а теперь унаследован нами.

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

Через строчку чередуется PHP, HTML, CSS, JS, SQL – никаких архитектурных паттернов не усматривается, даже простой слоистой архитектуры, даже MVC нет. PHP ведь позволяет такое безобразие (код выдуман).

Безобразие
<?php
// Подключение к БД
$db = new PDO("mysql:host=localhost;dbname=shop", "user", "pass");

// Получаем товары из БД
$products = $db->query("SELECT * FROM products WHERE active = 1")->fetchAll();
?>

<html>
<head>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>

<?php foreach($products as $product): ?>
    <div class="product" data-id="<?= $product['id'] ?>">
        <h2><?= htmlspecialchars($product['name']) ?></h2>
        <p class="price">$<?= number_format($product['price'], 2) ?></p>
        <button onclick="addToCart(<?= $product['id'] ?>)">Buy</button>
    </div>
<?php endforeach; ?>

<div id="cart">
    <h3>Cart (<span id="cart-count">0</span>)</h3>
    <div id="cart-items"></div>
</div>

<script>
// Добавление в корзину через AJAX
function addToCart(productId) {
    $.post('add_to_cart.php', {id: productId}, function(response) {
        updateCart(response);
    });
}

// Обновление корзины
function updateCart(data) {
    $('#cart-count').text(data.count);
    $('#cart-items').html(data.items);
}
</script>

<?php
// Обработчик add_to_cart.php
session_start();
if(isset($_POST['id'])) {
    $id = (int)$_POST['id'];
    $_SESSION['cart'][$id] = ($_SESSION['cart'][$id] ?? 0) + 1;
    
    // Получаем информацию о товаре
    $stmt = $db->prepare("SELECT * FROM products WHERE id = ?");
    $stmt->execute([$id]);
    $product = $stmt->fetch();
    
    echo json_encode([
        'count' => array_sum($_SESSION['cart']),
        'items' => "Added: " . htmlspecialchars($product['name'])
    ]);
}
?>

<style>
.product {
    border: 1px solid #ddd;
    padding: 10px;
    margin: 10px;
    display: inline-block;
}
#cart {
    position: fixed;
    right: 20px;
    top: 20px;
    background: #f5f5f5;
    padding: 15px;
}
</style>
<!--
Этот код представляет простой магазин с функционалом:
- Вывод товаров из БД
- Добавление в корзину через AJAX
- Сессионное хранение корзины
- Базовая стилизация
- Защита от XSS через htmlspecialchars()
- Подготовленные выражения для SQL

Конечно, в реальном проекте нужно разделять логику, 
добавить больше проверок и использовать более продвинутую архитектуру.
-->

На 80 строк уже безобразие, а уж на тысячах строк и полное безобразие. Поддерживать и развивать такой код очень тяжело. По статистике получалось, что этот продукт требует усилий по сопровождению по сравнению с нормальным в 5 (!) раз больше. Много времени требовалось чтобы разобраться в зависимостях между далеко и нелогично раскиданными участками кода и требовались колоссальные ментальные усилия чтобы прочитать и исправить.

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

И да, на нормальных, статически типизированных и компилируемых языках я пишу раза в 2 быстрее.

Короче, у старого продукта осталась только одна ценность: пользовательский опыт. Так привыкли пользоваться. Остальное целесообразно сжечь и сделать заново.

Главное при рефакторинге столь плохого кода не смотреть на старый код. А то, как у старьевщика захочется что-то сохранить.

Единственное, что было сильно связано с приложением целиком это БД. После неудачных попыток разделения на микросервисы решили оставить БД в качестве священной коровы. Попытки вставок кода на других языках встретили яростное сопротивление без проблеска логики. Были объяснения, что разработчики PHP стоят дешевле, но это же позор!

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

Анализ инцидентов показывал, что ошибки не в опечатках, а в самой бизнес логике, размазанной тонким слоем... повидла по всему коду. Напрашивалось решение сконцентрировать бизнес-логику в небольшом хорошо читаемом куске кода, в доменной модели. Перейти от правил поведения, привязанным к десяткам и сотням use-case (а следовательно, размазанным по всему коду) к доменной модели, единым правилам, инвариантам, архитектурным паттернам, всему вот этому вот. Да и страшный GUI поправить не помешает там где доменная модель активно используется.

А еще напрашивалась замена модулей целиком, отрезание по слабо связанным частям - паттерн фикус-душитель. Итак, поставлена задача создать доменную модель и переписать 10% кода, потом передумали на 5% кода, потом решили для начала переписать один модуль на 5000 строк.

Паттерн фикус-душитель
Паттерн фикус-душитель

Дальше пошла борьба за ресурсы.

Типичной нормой времени на рефакторинг считается 20%, а в таких запущенных случаях, очевидно целесообразно выделять до 40%. Это облегчит жизнь даже при отсутствии активной разработки новых фич.

Как у нас делается управление портфелем проектов?

Как только один проект перестает "гореть" ресурсы из него перекидывают на другие проекты. В результате, плохо всё.

Итак, все ресурсы перекинули на другие проекты (хотя, чего уж хуже этого проекта?) и рефакторинг предложили выполнить мне самому.

О_о

Издевательство
Издевательство

Офигеть. На проект звали в роли архитектора, а тут разработка на незнакомом языке, да еще и полно фронт-энда. Тем более, что вкладывать силы в изучение PHP я не хочу в принципе.

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

Делов то! Что может пойти не так?
Делов то! Что может пойти не так?

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

Да, первое на что я налетел это противоречие требования не менять пользовательский опыт для старых пользователей с желанием (и моим и техподдержки) сделать нормальный UX/UI чтобы облегчить работу новых пользователей. Это требование было неявным и, посыпаю голову пеплом, я должен был сразу это заметить. В результате получились долгие прыгания между вариантами реализации с невозможностью принять обоснованное и окончательное решение. Что использовать? Чистый HTML? JQuery (select2)? чистый JS? а может и вообще react? Из любопытства я попробовал все варианты. Получилась реализация на Select2, причем из-за кастомизации на каждый выпадающий список понадобилось аж 200 строк кода с магией (странностями и нелогичностью) и зависимостью от библиотек. Готовые решения хороши до момента кастомизации, а дальше только тормозят разработку. Лучше бы 400 строк на чистом JS написал, но если бы не знал про select2, то спроектировал бы плохой UI.

UX/UI это вкусовщина и mind games. Особенно, если хочется сделать хорошо красиво.

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

Я хотел писать программу так:

  • Я имею компетенции в архитектуре и требованиях, я подаю на вход архитектуру, требования, ограничения, проверяю код и принимаю результат.

  • LLM имеет компетенции в технологиях. Остается это заставит работать вместе.

Я не могу сформулировать полные требования сразу, они уточняются по ходу реализации. Итак, часть требований остается у меня в голове и их инкрементально надо передать. Знания о технологиях "в голове" у LLM но он тоже не может передать все. Зафиксировать все не получается, все расползается и забывается. ТЗ как один документ не удобен, а если вместе со старым кодом то он не помещается ни в контекстном окне LLM ни в моей памяти (но я хотя бы могу зафиксировать в документе).

Маленькое и короткоживущее контекстное окно LLM
Маленькое и короткоживущее контекстное окно LLM

Сформулирую проблему

  • Потери контекста у LLM

  • Сложности документирования для человека

  • Необходимости быстрого восстановления контекста

  • Отслеживания связей между требованиями и реализацией

    Контекст (требования и ограничения, выбранные технологические решения) оказалось удобно хранить не в одном большом ТЗ, а маленькими кусочками уровня архитектуры, общий список ФТ, НФТ, требований к модулю (например, визарду), к классу контроллера, к модели, к вьюшке, к методу класса , к визуальному компоненту:

Структура документации удобная для LLM:

Документация по уровням кода:

 

Общая структура проекта:

project/
├── docs/
│   ├── requirements/
│   │   ├── functional.md
│   │   ├── nonfunctional.md
│   │   └── usecases.md
│   ├── testing/
│   │   ├── testcases.md
│   │   └── userstories.md
│   └── architecture/
│       ├── components.md
│       └── integrations.md
├── wizard/
│   ├── README.md
│   └── DeliveryForm/
│       ├── README.md
│       └── CitySelector.tsx

А. Уровень модуля (wizard/README.md):

# Wizard Module
Requirements:
- FR1: Процесс заказа за 4 шага
Architecture:
- State Management: Redux

Б. Уровень контроллера:

/**
* DeliveryFormController
* Requirements:
* - FR2.1: Сохранение адреса доставки
* Test Cases:
* - TC2.1: Успешное сохранение формы
*/

В. Уровень модели:

/**
* DeliveryFormModel
* Requirements:
* - FR2.3: Структура адреса доставки
* Validation Rules:
* - city: required
*/

Г. Уровень компонента:

/**
* CitySelector
* UI/UX Requirements:
* - Автокомплит после 3 символов
* Test Cases:
* - TC2.1.3: Автокомплит работает
*/

Матрица трассировки:

FR2.1 Сохранение адреса:
- DeliveryForm/CitySelector.tsx
- TC2.1: Тест валидации
NFR1 Производительность:
CitySelector.handleSearch()
TC2.3: Тест производительности

Ключевые преимущества такой структуры:

  • Многоуровневая документация с чётким разделением ответственности

  • "Хлебные крошки" в коде для быстрого восстановления контекста

  • Связь между требованиями, кодом и тестами

  • Возможность быстрой навигации по документации

  • Удобство коммуникации с LLM благодаря структурированности

 

Я даже дернулся самому реализовать запоминание контекста в долговременной памяти и защита от его искажения. Но, задача сложная.

Задача сложная

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

  1. Персистентное хранение:

  • БД для хранения контекста

  • Специализированные решения как LangChain Memory

  1. Проекты по управлению состоянием:

  • MemGPT - система управления памятью для LLM

  • AutoGPT - долгосрочное планирование и память

  • HuggingFace TokenFlow - управление контекстом

  1. Экспериментальные подходы:

  • Constitutional AI - встраивание правил и ограничений

  • Fine-tuning с акцентом на память

  • Гибридные системы с внешними базами знаний

Основные проблемы:

  • Согласованность между сессиями

  • Верификация сохраненной информации

  • Приоритизация важной информации

  • Разрешение конфликтов

  • Масштабируемость решения

Это сложная задача, так как требует:

  1. Надежного механизма сохранения

  2. Контроля достоверности

  3. Эффективного поиска

  4. Управления правами доступа

Активные исследования продолжаются, но универсального решения пока нет.

То есть, человеку придется активно участвовать в сохранении контекста

Разделение ответственности

LLM хорош как помощник для:

  • Генерации идей

  • Проработки деталей

  • Анализа отдельных аспектов

  • Консультаций по конкретным вопросам

Человек незаменим для:

  • Принятия ключевых архитектурных решений

  • Оценки рисков и последствий

  • Согласования противоречивых требований

  • Контроля целостности проекта, неискаженности документации

  • Валидации результатов

Даже с идеальной "памятью", LLM остается инструментом, а не заменой системному архитектору или техлиду. Попытка переложить на него функции контроля и валидации - это риск:

  • Потери управляемости проекта

  • Пропуска критических ошибок

  • Принятия неоптимальных решений

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

Таким образом, может разбивать разработку на куски микроспринты по 15-30 шагов. На вход будет требования, ограничения и выбранные ранее решения, на выходе решение текущей задачи и обновленные требования, ограничения, выбранные технические решения. Человек должен обеспечить сохранение контекста. Что-то вроде задачи архитектора и Лида разработки

По сути это создание управляемых итераций, где:

  1. Человек выступает как:

  • Хранитель контекста между микроспринтами

  • Валидатор технических решений

  • Координатор последовательности задач

  • Контролер соответствия общему видению

  1. LLM работает как:

  • Генератор детальных решений

  • Помощник в проработке деталей

  • Инструмент для быстрого прототипирования идей

Структура микроспринта:
Вход:

  • Текущие требования

  • Существующие ограничения

  • Принятые тех.решения

  • Конкретная задача

Процесс:

  • Проработка решения с LLM

  • Уточнение деталей

  • Документирование

Выход:

  • Решение задачи

  • Обновленный контекст

  • Новые ограничения/требования

Процесс очень похож на управление командой разработчиков, но с важным отличием - "срок жизни" контекста у LLM критично краток и подвержен искажениям.

Сравнение:

Человек-разработчик:

  • Помнит контекст проекта недели/месяцы

  • Сам отслеживает противоречия

  • Задает уточняющие вопросы

  • Сохраняет понимание общей архитектуры

  • "Забывание" происходит постепенно

LLM:

  • Контекст живет только в рамках сессии

  • Не имеет "долгой памяти"

  • Не может сам выявить противоречия со старыми решениями

  • Требует постоянного "освежения" контекста

  • "Забывание" происходит мгновенно при новой сессии

Поэтому микроспринты с LLM требуют:

  • Более частой синхронизации контекста

  • Более тщательной документации решений

  • Более строгой валидации на соответствие предыдущим решениям

По сути, мы применяем знакомые практики управления разработкой, но в более интенсивном режиме из-за особенностей работы с LLM.

Можно ли реализовать без людей разработчиков код больше чем 5000 строк связанного (слабо поддающегося декомпозиции) кода? Думаю, 5-10.000 это предел. Усилий чтобы обуздать этого норовистого коня слишком много.

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

Вместо эпилога:

Я бы не занимался такой фигней без бюджетирования, почти только на энтузиазме. Но уж до печенок не люблю кривую архитектуру, код и UX/UI.

Всё сделано по эстетическим соображениям.

P.S. добавление от руководителя проекта:

Упоминай меня ????
Что я был с тобой (в борьбе за рефакторинг)

P.P.S. Владельцем ресурсов (руководителем отдела) я тоже работал и ясно представляю соображения той стороны - риски потратить ресурсы и долго иметь результат хуже изначального, тратя все новые ресурсы на "обработку напильником". Но тут уже совсем терминальный случай когда Сова становится классовым врагом Зайца и вредит системе.

P.P.P.S.

Картинка про нулевой горизонт планирования эффективными менеджерами

Комментарии (2)


  1. jackshrike
    10.11.2024 05:49

    противоречие требования не менять пользовательский опыт для старых пользователей с желанием (и
    моим и техподдержки) сделать нормальный UX/UI чтобы облегчить работу новых пользователей.

    делается 2 (два) юзер интерфейса, переключаемых самим юзером по одной кнопке. A/B тесты, сбор статистики, снятие с поддержки старого, удаление старого.


  1. vadimr
    10.11.2024 05:49

    LLM имеет компетенции в технологиях.

    Вот значит даже как.