
После прошлой статьи мне в личку прилетел вопрос: «А что на входе? Как именно ты подаёшь данные агенту? Просто кидаешь текст ТЗ — и всё?»
Отвечаю сразу вот он, паспорт требования, с которым работает мой агент:

Это пример паспорта для требования «Пользователь должен иметь возможность гибко настраивать отчёт». Да, в реальном пайплайне такое требование сначала разбивается на атомарные части (о чём расскажу в разделе 1), и каждая часть получает свой паспорт. Но для иллюстрации того, как выглядят признаки и что они значат, этот пример отлично подходит. Дальше в статье я покажу, как меняются паспорта после дробления, а пока просто запомните: агент работает не с текстом, а с такими структурами.
Теперь по порядку как мы к этому пришли.
1. Не ТЗ целиком, а отдельные требования
На старте эксперимента я тоже пыталась пойти простым путём: загрузить весь документ в нейросеть и попросить найти ошибки. Результат оказался предсказуемо печальным. Текст ТЗ это обычно несколько страниц, где перемешаны контекст, общие слова и конкретика. Если отдать его целиком, модель:
теряет фокус на отдельных утверждениях;
начинает смешивать требования из разных разделов;
не может дать чёткую обратную связь по каждому пункту.
Поэтому первый шаг разбить ТЗ на атомарные требования. Что я считаю требованием? Это законченная мысль, описывающая одно действие, правило или ограничение. Обычно такие фразы начинаются с «Система должна…», «Пользователь может…», «При наступлении события…».
Пример из реального ТЗ:
«В отчёте по продажам пользователь должен иметь возможность гибко настраивать отображение данных: выбирать период, группировать по контрагентам и складам, а также сохранять настройки под своим профилем.»
Я разбиваю это на отдельные требования:
Пользователь может выбирать период в отчёте.
Пользователь может группировать отчёт по контрагентам.
Пользователь может группировать отчёт по складам.
Настройки отчёта сохраняются индивидуально для каждого пользователя.
Как можно ошибиться при дроблении:
Слишком мелко: разбить «сохранять настройки» на «сохранять период», «сохранять группировку» и т.д. это избыточно, потому что в реализации эти механизмы обычно общие. Агент начнёт плодить лишние замечания.
Потерять связку: если не отметить, что все эти требования относятся к одному отчёту, можно потом проверять их независимо и пропустить, например, что настройки сохраняются только для выбранного периода, а не для всех.
Зачем мы это делаем? Чтобы каждый маленький кусочек требований можно было проверить по отдельности, а потом собрать общую картину.
Наследование контекста
Чтобы не потерять связь с «родителем», в JSON-паспорт каждого требования я добавляю поля:
parent_section— название раздела ТЗ (например, «Отчёт по продажам»)parent_object— объект, к которому относится требование (например, «Report.Sales»)
Я использую текстовые названия, потому что мне их удобно читать при отладке. В более сложной системе можно заменить на числовые идентификаторы суть не меняется.
Связанные требования (Linked)
Иногда одно требование содержит логическое «И» и неразрывно связывает несколько условий. Например: «Система должна отправлять уведомление на почту и в Telegram». Формально это два требования, но реализовывать их лучше вместе. Я помечаю такие пары как linked: true. Технически при параллельной обработке они отправляются в одном батче или помечаются флагом, а агент-критик потом проверяет целостность (например, чтобы не получилось, что одно сделали, а другое забыли).
2. Паспорт требования: от текста к числам
Дальше в дело вступает LLM-анализатор. Его задача превратить текст требования в структурированный JSON-паспорт т. е. набор числовых признаков. Почему не оставить текст? Потому что классификатору (дереву решений) нужны числа, а не слова. А LLM как раз умеет извлекать из слов нужные сигналы.
Важно: LLM здесь работает не как «чёрный ящик», а как feature extractor — она извлекает конкретные, заранее определённые признаки. Это не эмбеддинги, а интерпретируемые параметры: есть ли цифры, есть ли слова-маркеры и т.д. Такой подход даёт прозрачность и контроль.
Важное уточнение: когда я говорю, что LLM не чёрный ящик, я немного хитрю :) На самом деле она такой же чёрный ящик, как и любая нейросеть. Я не знаю, почему она сегодня считает фразу «при превышении лимита» граничным условием, а завтра нет. Это как спросить у кота, почему он сел именно на этот коврик, объяснения не будет.
Но: я хотя бы жёстко контролирую, что она должна выдать на выходе (JSON со строгими полями), и могу перепроверить её регулярками. Прозрачность здесь не в том, что я вижу её мысли, а в том, что я чётко ограничила ей рамки.
Какие признаки я использую?
В ходе экспериментов я перебрала около 15 потенциальных признаков, но остановилась на этих шести. Они давали наилучшее качество при минимальной сложности:
Признак | Что измеряет | Диапазон |
|---|---|---|
| Есть ли в требовании конкретные числа, даты, лимиты | 0/1 |
| Вес слов-маркеров размытости (гибкий, удобный, быстрый, интуитивный, качественный) | 0–1 |
| Упомянуты ли исключения (иначе, если не, в случае ошибки) | 0/1 |
| Есть ли граничные условия (пустые значения, максимумы, минимумы, отсутствие данных) | 0/1 |
| Указаны ли сроки или временные ограничения | 0/1 |
| Сколько действующих лиц упомянуто явно (пользователь, администратор). Система по умолчанию не учитывается, так как она всегда подразумевается | число |
Признаки подбирались не случайно. Я исходила из критериев хорошего ТЗ (бизнес-цель, полнота сценариев, однозначность, проверяимость), которые описала в первой статье. А потом, обучив дерево решений, посмотрела на важность признаков они действительно оказались ключевыми.
Дисклеймер о stopword_score: Оценка размытости зависит от списка слов, который я собрала на основе своей практики. В ваших проектах могут быть другие слова-паразиты. Не стесняйтесь дополнять список под свои задачи это легко настраивается в промпте.
Как LLM извлекает признаки: промпт, JSON mode, few-shot
Чтобы LLM работала стабильно и не галлюцинировала, я использую:
JSON mode — модель обязана выдавать строго структурированный JSON.
Few-shot examples — в промпт подкладываю 3–5 эталонных примеров с правильными паспортами. Это резко снижает ошибки.
Чёткая шкала для
stopword_score: 0 — нет размытых слов; 0.3 одно такое слово; 0.6 два; 0.9 три и более. Так дерево получает стабильные числа, а не «шум» от настроения модели.Почему именно 0, 0.3, 0.6, 0.9? Честно, взяла с потолка. Дерево решений умнее меня: если передавать ему не дискретные баллы, а, скажем, долю стоп-слов в тексте, оно само найдёт лучший порог. В следующей версии так и сделаю.
Пример неудачного извлечения: Однажды LLM не распознала граничное условие в требовании «При превышении лимита в 1000 строк выдавать предупреждение», она посчитала, что
boundary_conditions_mentioned=0, потому что там нет слов «пусто», «ноль», «максимум». Пришлось дополнить инструкцию примерами с «превышение лимита». После этого качество выросло.
Как ещё я борюсь с галлюцинациями: Для has_numbers иногда подключаю регулярные выражения, если LLM пропустила цифры, но они есть в тексте (например, «2 секунды»), я исправляю признак постобработкой. Это «костыль», но он повышает надёжность.
Зачем я это рассказываю? Чтобы вы понимали: промпт это живая материя, его надо докручивать под свои данные.
Упрощённый промпт сейчас выглядит так:

3. Примеры превращения текста в паспорт
Возьмём типичную размытую формулировку, которая часто встречается в ТЗ:
«Пользователь должен иметь возможность гибко настраивать отчет»
LLM-анализатор выдаст (как в самом начале статьи):

Высокий stopword_score при нулевых значениях has_numbers, has_negative_keywords, boundary_conditions_mentioned и has_deadline — классический портрет непроверяемого требования. actor_count=1 здесь не играет роли (он просто фиксирует наличие пользователя), главные маркеры размытости на месте, а конкретики и граничных условий нет.
Ещё раз: это не атомарное требование из нашего примера дробления, а отдельный случай, так часто пишут в реальных ТЗ. В составном требовании из раздела 1 мы бы разбили его на части, и каждая часть получила бы свой паспорт.
Например, атомарное требование «Пользователь может выбирать период в отчёте» выглядело бы так:

Здесь stopword_score=0, потому что слово «гибко» исчезло, и требование стало конкретным (даже без цифр оно проверяемо — достаточно убедиться, что выбор периода работает). Дерево решений, скорее всего, классифицирует его как ok (если не считать отсутствие граничных условий).
А теперь хорошее требование с негативным сценарием:
«При попытке провести документ без заполненного поля "Контрагент" система должна выдавать сообщение: "Укажите контрагента"»

Здесь есть и негативный сценарий (has_negative_keywords=1), и граничное условие (поле не заполнено). Даже без чисел требование качественное.
Ещё пример с числами:
«Время формирования отчёта не должно превышать 2 секунд при объёме данных до 10 000 записей»

Числа есть, граничное условие (до 10 000) есть. actor_count=0, потому что в требовании нет явно упомянутых людей — только система (она по умолчанию не учитывается). Всё по правилу.
4. Откуда берутся метки (таргет)
Чтобы обучить классификатор, нужен размеченный датасет. Я взяла 90 ТЗ (30 отличных, 30 средних, 30 проблемных), разбила их на 270 отдельных требований и каждому вручную присвоила метку тип ошибки или ok.
Методология разметки: Я размечала все примеры сама, опираясь на свой чек-лист из первой статьи. Чтобы снизить субъективность, я перепроверяла спорные случаи через месяц и если мнение менялось, пересматривала метку. Коллег не привлекала, потому что это был личный эксперимент, но для продукта стоит использовать несколько экспертов и считать согласованность.
Метки:
ok— требование хорошее, ошибок нет.unverifiable— непроверяемое (размытые формулировки, нет критериев).no_negative— описаны только позитивные сценарии, нет исключений.no_boundary— отсутствуют граничные условия.ambiguity— неоднозначная формулировка (можно трактовать по-разному).
Распределение получилось примерно таким:
ok— 80unverifiable— 60no_negative— 50no_boundary— 40ambiguity— 40
Классы слегка несбалансированные (больше ok), но для дерева решений я использовала взвешивание классов (параметр class_weight='balanced'), чтобы модель не игнорировала редкие ошибки.
Пограничный случай: Например, требование «Система должна автоматически сохранять данные каждые 5 минут». С одной стороны, есть число (5 минут), с другой нет негативных сценариев (что делать, если сохранение не удалось?). Я отнесла его к no_negative, потому что отсутствие обработки ошибок критичнее.
5. Репрезентативность выборки: почему без неё всё летит в помойку
Главный критерий успеха любых данных — это репрезентативная выборка. Что это значит? Объясняю на пальцах.
Представьте, что вы хотите оценить качество еды в ресторане. Если вы закажете только свои любимые десерты и коктейли это НЕ репрезентативная выборка. Вы ничего не узнаете о супах, горячих блюдах или закусках. Ваша оценка будет смещённой, и вы удивитесь, когда придёте с друзьями, а они закажут борщ и останутся голодными.
Чтобы выборка была репрезентативной, нужно попробовать все ключевые категории блюд: холодные закуски, супы, горячее, десерты, напитки. Не обязательно есть каждое блюдо из меню, но важно, чтобы были представлены все группы. Тогда вы сможете честно сказать: «В этом ресторане в целом вкусно» или «Супы так себе, но десерты божественны».
С ТЗ то же самое. Если я соберу документы только из торговли и только от себя любимой — это «десерты и коктейли». А чтобы агент научился работать с любыми ТЗ, в выборке должны быть:
Разные отрасли: торговля, производство, услуги, логистика. В каждой — свои типичные ошибки.
Разные авторы: я, мои коллеги, заказчики, внешние аналитики. У каждого свой стиль и слепые зоны.
Разный объём: от мелких доработок (2–3 страницы) до больших внедрений (50+ страниц). В маленьких ТЗ часто нет контекста, в больших — противоречия между разделами.
Разные годы: старые проекты (5–7 лет назад) и свежие. Требования к ТЗ меняются, заказчики умнеют, разработчики наглеют :) Нужно, чтобы агент понимал и «олдскульные» формулировки, и современные.
Важно: моя выборка репрезентативна именно для моего домена торговля, производство, услуги. Как только я столкнулась с проектом из HoReCa (ресторанный бизнес), агент споткнулся: там своя терминология и типовые ошибки. Пришлось дообучать на новых примерах. Имейте это в виду, если будете повторять эксперимент: при смене предметной области данные придётся расширять.
Я перепроверяла свою выборку по этим пунктам трижды. Репрезентативность это не про количество, а про разнообразие. Лучше 30 ТЗ, закрывающих все грани задачи, чем 300 однотипных.
6. Зачем классификатор, а не просто правила if-else?
Этот вопрос мне задали в комментариях, и он очень правильный. Действительно, можно было бы написать что-то вроде:
if has_numbers == 0 and stopword_score > 0.7: return "unverifiable" if has_numbers == 1 and boundary_conditions_mentioned == 0: return "no_boundary"
Но в моём случае, с ростом числа примеров и признаков, поддерживать такие правила вручную стало сложно. Почему:
Представьте, что вы описываете рецепт словами «добавить щепотку соли». Что значит «щепотка»? Для дерева решений это не проблема: оно посмотрит на тысячи примеров и само поймёт, что щепотка — это примерно 2–3 грамма, а если солить в первый раз лучше недосолить.
С нашими признаками так же. Вручную писать правила «если есть числа, но нет граничных условий → ошибка» можно, но когда признаков станет больше, эта простыня из if-else будет весить сотни строк. А дерево само разложит всё по полочкам
Нелинейность связей. Комбинации признаков сложнее, чем кажутся. Например, требование с
has_numbers=1может бытьok, если есть ещё и негативные сценарии, а может бытьno_negative, если негативных сценариев нет. Дерево решений автоматически находит такие нелинейные зависимости.Автоматический подбор порогов. Для непрерывного признака
stopword_scoreнужно где-то взять порог (0.7?). Дерево само подбирает оптимальный порог, минимизируя энтропию, а не на глазок.Избежание «лапши» из условий. Правила вручную привели бы к сотням строк кода, которые сложно поддерживать. А дерево компактно и легко интерпретируется.
Лёгкость обновления. Если я нахожу новый тип ошибки или хочу уточнить правила, мне достаточно добавить примеры в датасет и переобучить модель заново (деревья решений не поддерживают частичное дообучение, только полное переобучение на расширенной выборке).
Признание: для очень маленького датасета (скажем, 20–30 примеров) правила могут быть не хуже и даже проще. Если у вас мало данных начните с правил, они прозрачнее. А когда данных станет много переходите на дерево.
Метрики качества: На тестовой выборке (20% данных) accuracy держится около 82%. Но важнее смотреть на recall по редким классам:
для
ambiguity— около 70% (иногда неоднозначность похожа наok);для
no_negative— 75%;для
no_boundary— 80% (после добавления этого признака recall вырос с 60%).
Для тех, кто помнит мою первую статью: там я отказывалась от accuracy, потому что оценивала качество текстовых отчётов агента. Здесь же мы оцениваем работу классификатора — это строгая задача классификации, и для неё accuracy и recall вполне подходят. Так что никакой шизофрении, просто разные объекты измерения.
Такой уровень меня устраивает для прототипа.
7. Арбитраж: агент-критик как судья
Классификатор работает на уровне отдельных требований (микроанализ). Он выдаёт «сырые» метки: «это требование непроверяемое», «это ok». Но есть вещи, которые на этом уровне не видны:
Противоречия между разными требованиями (например, в одном разделе склад обязателен, в другом нет).
Пропущенные права доступа (кто может видеть кнопку?).
Маппинг данных для интеграций.
Здесь в игру вступает агент-критик та же локальная LLM (запущенная через Ollama), но с другим промптом, которая получает:
весь исходный текст ТЗ;
список требований с их JSON-паспортами и предсказанными метками;
контекстные связи (родительские разделы, связанные требования).
Зачем критику паспорта, если он читает текст? Хороший вопрос. Паспорта для него как шпаргалка. Он сразу видит, где классификатор уже нашёл проблемы, и может специально в этих местах покопаться.
Но противоречия между требованиями он ловит только читая текст. Если в одном месте сказано «склад обязателен», а в другом «можно без склада», ни один паспорт это не покажет. Тут без живого чтения не обойтись.
Как критик использует паспорта: Он может видеть, что требование помечено как ok, но при этом в соседнем требовании есть противоречие. Тогда он перепроверяет исходный текст и выдаёт замечание.
Конкретный пример: В одном ТЗ было требование «Поле "Склад" обязательно для заполнения», а в другом — «При импорте данных поле "Склад" может быть пустым, тогда склад определяется по умолчанию». Классификатор по отдельности каждое требование счёл ok (в каждом есть логика). Но критик увидел противоречие и написал в отчёте: «Внимание: в разделе 2.1 склад обязателен, а в разделе 3.4 допускается пустое значение с подстановкой по умолчанию. Уточните, какое поведение верное».
8. Скорость и масштаб: параллельная обработка
Если в ТЗ 50–100 требований, последовательно прогонять каждое через LLM-анализатор долго (секунды на требование — и уже минуты). Но поскольку требования атомарны и независимы (с учётом наследования контекста), их можно обрабатывать параллельно. Я использую ThreadPoolExecutor (в Python) и отправляю запросы к локальной Ollama одновременно.
Почему Ollama? Она позволяет запускать модели (например, Mistral или Llama 3 8B) локально, бесплатно и без доступа в интернет — идеально для экспериментов и прототипов. Никаких затрат на API, никаких рисков утечки данных.
Важное примечание для тех, у кого игровая карта:
Кстати, если у вас игровая видеокарта (спасибо мужу-девопсу, который собрал правильную машину!) вы уже вооружены для экспериментов. Моя, например, спокойно переваривает 4–6 параллельных запросов к Ollama, и я даже не замечала проблем, пока не начала замерять скорость. Так что если у вас есть игровой ПК — считайте, что у вас уже есть готовая лаборатория для локальных моделей. И никаких дополнительных вложений!
Технические детали:
Размер пула — 4–6 потоков, чтобы не перегружать видеокарту.
Для связанных требований (
linked) они отправляются в одном потоке, чтобы сохранить порядок.
Проблемы: Иногда модель не справляется с параллельными запросами и начинает тормозить — приходится уменьшать число потоков. Но в целом ускорение в 3–4 раза на 100 требованиях достижимо.
Зачем это нужно? Чтобы анализ ТЗ не превращался в чаепитие, а занимал разумное время.
9. Итог: что даёт такая структура данных
Прозрачность. Мы точно знаем, на каком основании агент ругается на требование: LLM извлекла признаки, дерево их скомбинировало.
Контроль. Можно добавить новый признак (например,
has_ui_mockups) и переобучить модель, не ломая код.Экономию. LLM используется только для извлечения признаков (с few-shot и JSON mode это недорого) и для макроанализа. Основная масса классификации делается деревом.
Масштабируемость. Новые примеры просто добавляются в датасет, и модель становится умнее без переписывания правил.
Полноту. Атомарные требования не теряют контекст благодаря наследованию, а связанные требования проверяются в паре.
Честное ограничение подхода
Чтобы вы не думали, что я продаю вам идеальный продукт, — вот список проблем, с которыми я столкнулась:
Дерево ошибается в 15% случаев. Особенно на требованиях, где смешаны несколько типов ошибок (например, одновременно размытость и отсутствие граничных условий). На тестовой выборке accuracy держится в районе 80–85%, и это потолок для моего датасета.
LLM иногда галлюцинирует признаки. Как в примере с «превышением лимита» — пришлось докручивать few-shot и добавлять постобработку.
Субъективность разметки. 270 требований размечала я одна. Если бы это делали три разных аналитика, метки могли бы разойтись в 10–15% случаев. Для моего личного инструмента это ок, для продукта — потребовалась бы валидация.
Подход не работает для ультракоротких ТЗ. Если в документе 1 страница и 5 размытых фраз — дробление не даст статистики, а дереву не на чем учиться. Тут уже нужны другие методы (или просто идти пить кофе с автором, обсуждая, как он умудрился написать ТЗ на салфетке).
Главный вывод: весь этот пайплайн сплошной компромисс. Это как выбрать между готовым тортом из магазина (не знаешь, что внутри, но быстро) и домашним пирогом по бабушкиному рецепту (долго, муторно, но каждый ингредиент знаком).
Для прототипа — ок. Для продакшена — надо думать.
Что я сделала бы иначе сейчас
Оглядываясь назад, я понимаю, сколько шишек я набила.. Вот мои главные выводы — возможно, они помогут вам не повторять моих ошибок.
1. Сразу ввела бы больше признаков. Например, has_mockup, has_business_goal. И собирала бы статистику по ним с первого дня. Это избавило бы от необходимости переразмечать часть данных позже.
2. Автоматизировала бы проверку согласованности разметки. Сейчас я перепроверяла спорные случаи вручную через месяц. Лучше было бы попросить коллегу переразметить 10% примеров и посчитать согласованность — это показало бы реальную субъективность моей схемы.
3. Настроила бы мониторинг качества в реальном времени. Чтобы каждый новый прогон агента сразу показывал, на каких требованиях он ошибается, а не копить ошибки до итерации.
4. Не надеялась бы, что теорию легко применить на практике. Я знала про выборки, про деревья решений, про важность признаков в общих чертах. Но когда дело дошло до реальной работы, оказалось, что знать и уметь, это две большие разницы. Я аналитик, а не программист и не дата-сайентист. Пришлось разбираться, как эти деревья решений реально работают в коде, почему они выдают именно такие split-ы и что делать, когда accuracy не растёт. Документация на английском, а с ним у меня так себе (разговорный есть, а технические тексты — боль).
Первые 4 месяца разметки 90 ТЗ дали результат, близкий к нулю агент тупил, ошибался и придумывал несуществующие проблемы. Пару раз я была готова плюнуть и забыть. Удержала только злость на потраченное время: «Я столько вбухала, теперь уже нельзя отступать».
И всё-таки, знаете... Самое трудное было когда я НЕ ЗНАЛА, как подступиться. Казалось, что машинное обучение это какая-то магия для избранных, а все вокруг уже давно всё понимают. Это чувство «я не в своей тарелке» выматывало сильнее, чем любые технические сложности.
А теперь, когда я прошла этот путь, магия рассеялась. Я вижу, сколько ещё нужно сделать: докрутить признаки, расширить датасет, перепроверить разметку, улучшить промпты. До идеального продукта как до луны пешком. Но самое главное страх неизвестности ушёл.
Я понимаю, ЧТО именно нужно улучшать и КАК это делать. И это чувство стоит всех потраченных нервов.
Так что мой главный совет: не бойтесь начинать. Трудно только до тех пор, пока не поймёшь. А когда поймёшь, вас уже не остановишь.
Если решите повторить, не жалейте времени на сбор данных это основа всего. И обязательно делитесь опытом!
А какие признаки важны в ваших ТЗ? Какие формулировки чаще всего вызывают споры? Делитесь в комментариях — соберём банк идей !
