У нас в проекте четыреста пул-реквестов в месяц. Половина комментариев — «поправь отступ», ещё четверть — «LGTM». Я хотел научить машину отличать полезное от шума. Машина научилась. А потом я полез смотреть, где она ошибается — и три недели думал об этом перед сном.
Вы наверняка знаете это состояние: открываешь чужой PR на ревью, а там сорок два комментария. Глаза разбегаются. Первый — отступ, второй — пробел, третий — «ок», четвёртый — «ок», пятый — переименуй переменную. Где-то между ними спрятано замечание про гонку данных, которая уложит продакшен через неделю, но ты его благополучно пропустил, потому что к двадцатому «ок» мозг уже не здесь.
Код-ревью давно превратилось в ритуал. Галочка в Jira. Запятые обсуждаем час, архитектуру — никогда. Я решил это починить.
Задача выглядела обманчиво простой: берём комментарий, классифицируем — сигнал или шум.
«Тут будет падение, если массив пустой» — сигнал.
«LGTM» — шум.
«Переименуй в getUserData» — шум.
«А что если пользователь отправит null?» — сигнал.
Я собрал сорок семь открытых репозиториев с публичной историей — почти одиннадцать тысяч комментариев. Три тысячи разметил вручную, остальное полуавтоматом с выборочной проверкой. Модель обучилась за вечер и угадывала правильно в восьмидесяти семи случаях из ста.
Устроено внутри сознательно просто — хотел видеть, за что модель цепляется. Никаких эмбеддингов, никаких трансформеров. Из каждого комментария вытаскиваю шесть признаков руками:
EMPTY_APPROVALS = {"lgtm", "ok", "ок", "+1", "👍", "норм", "ладно", "looks good", "принято", "ship it"} def extract(comment: str) -> dict: words = comment.split() return { "length": len(comment), "has_question": "?" in comment, "has_code_ref": bool(re.search(r'`[^`]+`|\.py|\.js|line\s*\d+', comment)), "has_suggestion": bool(re.search(r'а если|лучше|consider|what if', comment, re.I)), "is_empty_approval": comment.strip().lower() in EMPTY_APPROVALS, "specificity": len(re.findall(r'\w+\.\w+|\w+\(', comment)) / (len(words) + 1), }
Есть ли в комментарии вопрос. Ссылается ли на конкретный код. Предлагает ли альтернативу. Или это просто «ок» — одно слово, галочка, дальше. Последний признак — specificity — считает, как часто человек упоминает конкретные функции и файлы относительно длины текста. Чем конкретнее комментарий, тем выше шанс, что в нём есть сигнал.
Классификатор — логистическая регрессия. Пробовал деревья, пробовал бустинг — разница полтора процента, а регрессию можно открыть и посмотреть глазами, какие признаки тянут. has_code_ref и specificity оказались самыми сильными. Длина сама по себе — почти бесполезна: бывают длинные пустые комментарии и короткие точные попадания в баг.
Всё! Можно было писать туториал, собирать лайки и закрывать задачу.
Но я сделал то, что делает любой нормальный инженер — открыл список ошибок.
Первые двадцать ошибок — ерунда: пограничные случаи, сарказм, который машина не распознала, вопросы, которые можно трактовать и так и так.
А потом я заметил кое-что странное. Модель систематически сомневалась на одних и тех же людях. Их комментарии выглядели нормально — короткие, по делу, формально полезные. Но там, где обычно модель уверена на девяносто процентов, на этих авторах она выдавала пятьдесят на пятьдесят.
Первая мысль — баг в разметке. Проверил, нет. Вторая — особенность стиля, может они просто пишут иначе. Проверил — да, пишут иначе. Но почему именно эта группа?
Полез смотреть, кто эти авторы. Поднял историю коммитов. И нашёл общее: все они ушли из проекта. Через месяц, два, три после тех самых комментариев — последний коммит, и тишина.
Вот конкретный человек. Полтора года в проекте, сто сорок пул-реквестов, активный, въедливый, спорит на каждом ревью. А вот его последние восемь недель: комментарии становятся короче, вопросов всё меньше, «ок» вместо развёрнутых абзацев. И потом — тишина.
Модель не ошибалась. Она видела то, чего я не искал: угасание. Задолго до того, как человек написал заявление. Возможно, задолго до того, как он сам понял, что уходит.
Следующие две недели, по вечерам, я этим и занимался. Выделил восемьдесят девять человек, которые ушли и не вернулись. Ушли — значит последний коммит, потом тишина минимум на год. Не знаю, уволились ли они, перешли в другой проект или просто потеряли интерес — из публичных данных это не видно. Но паттерн угасания перед уходом одинаковый, независимо от причины. Потом сравнил их последние комментарии с тем, что они писали раньше. Паттерны оказались пугающе одинаковыми — у всех.
Не резко — постепенно, как громкость, которую убавляют по миллиметру в день. Полгода назад человек пишет: «Тут лучше вынести в отдельный сервис, потому что эта логика будет расти — я уже видел такое в модуле биллинга, потом три месяца распутывали». За месяц до ухода: «Ок». Среднее падение — до сорока процентов от прежней длины. Стабильно, у всех.
Человек перестаёт спрашивать «почему так?», перестаёт предлагать «а если иначе?». Принимает всё, кивает, не спорит. Вопрос — это инвестиция: ты тратишь энергию, потому что тебе важен ответ. Когда перестаёт быть важно — вопросы заканчиваются. Падение на семьдесят процентов за шесть недель.
«LGTM» без единого замечания на код в пятьсот строк. Раньше такого не было — раньше этот человек всегда находил хоть что-то. Теперь — галочка и дальше. Доля таких «пустых» ревью подскакивает с пятнадцати процентов до шестидесяти.
Время от назначения ревью до первого комментария увеличивается вдвое. Не потому что человек стал объективно занятее — нагрузка в команде у всех примерно одинаковая. А потому что задача переместилась в конец внутреннего списка приоритетов. Проще говоря, стало всё равно.
Исчезают восклицательные знаки, «круто!», «ого», «спасибо», эмодзи. Раньше: «Блин, точно! Не подумал. Спасибо что поймал». Теперь: «Исправлю». Ровный, стерильный текст — как кардиограмма, которая выпрямляется в линию.
Каждый из этих признаков по отдельности — ничего особенного: плохая неделя, усталость, проблемы дома. Но когда все пять совпадают — это не плохая неделя. Это решение, которое человек ещё не произнёс вслух.
Я должен был проверить. Построил простую модель — не для продукта, для себя. Пять признаков, скользящее окно в две недели. Для каждого автора — его собственная базовая линия за предыдущие полгода. Не «длинный ли комментарий», а «стал ли он короче, чем обычно у этого человека». Не «быстро ли ответил», а «медленнее ли, чем он сам отвечал раньше».
def attrition_signal(author_id, window_days=14): recent = get_comments(author_id, last_n_days=window_days) baseline = get_comments(author_id, last_n_days=180) if len(baseline) < 10 or len(recent) < 3: return None # мало данных — лучше промолчать return { "length_ratio": mean_len(recent) / mean_len(baseline), "question_rate_delta": question_rate(recent) - question_rate(baseline), "empty_approval_rate": empty_rate(recent), "response_time_ratio": median_hours(recent) / median_hours(baseline), "sentiment_delta": mean_sentiment(recent) - mean_sentiment(baseline), } # вспомогательные функции считают среднее/медиану по окну, логика тривиальная
Каждая строчка — один из пяти признаков. length_ratio ниже единицы — комментарии сжимаются. question_rate_delta уходит в минус — вопросы исчезают. empty_approval_rate растёт — всё больше «ок» без единого замечания. response_time_ratio выше единицы — человек отвечает медленнее. sentiment_delta падает — тон выравнивается, эмоции уходят.
Тут важный момент. Классы несбалансированы: ушли восемьдесят девять человек из шестисот с лишним — примерно пятнадцать процентов. Если модель каждому скажет «останется», она угадает в восьмидесяти пяти случаях из ста и не поймает ни одного ухода. Красивая цифра, бесполезная модель. Поэтому простая точность тут врёт — нужно смотреть, сколько уходов модель нашла и сколько раз подняла ложную тревогу.
|
Что измеряем |
Результат |
|---|---|
|
Из тех, на кого модель указала — действительно ушли (precision) |
52% |
|
Из тех, кто реально ушёл — модель поймала (recall) |
73% |
|
Общее качество ранжирования (ROC AUC) |
0.84 |
Половина ложных тревог — много. Из десяти, на кого модель показала пальцем, пятеро действительно ушли, пятеро — нет: передумали, получили повышение, влюбились, или паттерн объяснялся чем-то, о чём мы не знаем и не должны знать. Зато из тех, кто реально ушёл, модель заметила почти три четверти. За шесть недель. По комментариям к коду.
Для сравнения: если просто смотреть, стал ли человек писать короче, — качество ранжирования около 0.65. Пять признаков вместе — 0.84. Случайное угадывание — 0.50. Проверял на пятикратной кросс-валидации, стратифицированной — чтобы не обманывать самого себя.
Модель не видит причину. Только симптом. Но симптом — видит увереннее, чем я ожидал. Данные — GitHub API, тональность считал через TextBlob/dostoevsky, классификатор — логрег. Пайплайн воспроизводим за вечер на любых 40+ публичных репозиториях.
Тут я не мог понять, что с этим делать. Потому что есть три способа смотреть на эту штуку — и все три одновременно правда.
Менеджер видит сигнал заранее. Подходит, спрашивает: «Эй, всё нормально? Чем помочь?» — не «почему ты уходишь», а именно «чем помочь». Человек хотел уйти, потому что его не слышали, а его услышали — и он остался. Выгорание, пойманное за шесть недель, это выгорание, которое ещё можно вылечить. За шесть часов до заявления — уже нет.
Человек имеет право уходить молча. Не объяснять, не сигнализировать, не отчитываться о своих эмоциях через длину комментариев. Если каждый твой «ок» анализируется на предмет лояльности — ты живёшь в паноптикуме. Тюрьма, которую два века назад придумал Бентам: надзиратель видит всех, его не видит никто. Ты не знаешь, следят ли за тобой — и ведёшь себя так, будто следят всегда. HR-отдел с моделью предсказания увольнений — это паноптикум, только с нейросетями вместо вертухаев.
Данные уже существуют. История коммитов, время ответа, длина сообщений — всё это логируется годами в каждой компании. Вопрос не в том, можно ли это анализировать, а кто сделает это первым. Если не вы — то стартап, который продаст это вашему начальству как «платформу аналитики персонала». Красивые дашборды, метрики удержания, прогнозы оттока. Без вашего участия и без этических вопросов.
Вот что я понял точно: не нужна модель, чтобы видеть угасание. Нужно просто смотреть.
Если коллега перестал задавать вопросы на ревью — задайте ему вопрос. Не «ты уходишь?» — а «что думаешь про эту архитектуру?». Верните его в разговор.
Если комментарии стали короче — не списывайте это на эффективность. Это потеря интереса. «Ок» от человека, который вам доверяет, и «ок» от человека, которому всё равно, выглядят одинаково, но значат противоположное.
Если время ответа растёт — не дёргайте. Спросите, что случилось. Не в рабочем чате — лично.
Модель увидела паттерн за шесть недель. Вам достаточно одной, чтобы заметить. Разница в том, что вы можете не просто предсказать уход — вы можете сказать: «Эй, ты как?»
Я искал мусор в комментариях, а нашёл людей, которым плохо. Модель оказалась внимательнее меня — не умнее, именно внимательнее. Потому что у неё нет дедлайнов, нет усталости, нет привычки не замечать медленные изменения.
В этом и штука: мы адаптируемся. Коллега стал тише — привыкли. Реже спорит — привыкли. Не шутит на созвонах — привыкли. Каждый день почти как вчера, разница в полтона, а через месяц мы уже не слышим. Модель не привыкает — для неё каждый комментарий это точка на графике, и она видит тренд просто потому, что не устаёт смотреть.
Миф про лягушку в кипятке — что она не выпрыгнет, если нагревать медленно — неправда, лягушка выпрыгнет. А мы нет. Сидим в воде, которая теплеет по градусу в неделю, и не замечаем, что кто-то рядом уже сварился.
У Лема в «Голосе Неба» учёные думали, что расшифровывают сигнал из космоса. Строили гипотезы, спорили о значении. А потом поняли: сигнал изучал их — показывал, кто они такие, по тому, как они его интерпретируют.
Я думал, что классифицирую комментарии к коду. А изучил — как мы перестаём замечать друг друга.
Паттерн всегда был в данных. Мы просто не искали. Может, не хотели.
В канале токены на ветер раскрываю вопрос: модель видит, когда человек уходит. А когда выгорает, но остаётся?
Спойлер: видит. И это страшнее.
Источник


