Принцип III: Модульность запросов
Ужасное чудовище. Каждый опытный инженер видел такое: код, который настолько громоздкий, рискованный и трудный для понимания, что никто не осмеливается к нему прикоснуться. Нет модульных тестов, любое изменение вызывает легкий сердечный приступ. Единственные, кто осмеливается подойти к нему, — это старожилы, которые были свидетелями его создания, и они подходят лишь тогда, когда нет альтернативы. Он устарел, не имеет модульной структуры, и зависимости от него устарели. Компонент слишком опасен для серьезных изменений.
Я помню первое чудовище, с которым столкнулся. Функция на 5000 строк, которая была центральной для операций бизнеса, стоившего сотни миллионов долларов; почти никто не осмеливался к ней прикасаться. Когда она ломалась, целые команды просыпались среди ночи. Вся разработка в компании замедлялась из-за зависимости от этого ключевого компонента. Миллионы долларов были потрачены на попытки справиться с этим монстром.
При чем тут запросы LLM? Они тоже могут стать чудовищами! Так страшно менять, что никто к ним не прикасается. Или наоборот, команды пытаются их исправить и вызывают лавину инцидентов.
Что нужно клиентам
Клиенты не хотят платить за программное обеспечение, которое работает корректно только по вторникам и четвергам; они требуют постоянной надежности и потока новых функций. При создании долгосрочных систем с высокой надежностью важно, чтобы приложение могло развиваться, при этом постоянно поддерживая работу. Это касается приложений на базе генерируемого ИИ так же, как и традиционного программного обеспечения.
Итак, как получить здоровое приложение на базе ИИ, а не чудовище? Существует больше десятка подходов, которые рассматриваются в этой серии. Все они начинаются с одного принципа: вместо одного гигантского запроса вам нужно несколько более мелких, сфокусированных запросов, каждый из которых нацелен на решение одной проблемы.
Что такое модульность
Модульность — это практика разбивки сложной системы на более мелкие, самодостаточные и многоразовые компоненты. В традиционной разработке программного обеспечения это означает написание функций, классов и сервисов, каждый из которых выполняет конкретную задачу. В контексте разработки запросов для LLM модульность означает разбивку большого монолитного запроса на более мелкие, сфокусированные запросы—каждый из которых предназначен для выполнения одной четко определенной задачи.
Преимущества модульности
Модульность позволяет безопасно вводить изменения в вашу систему со временем. Ее важность возрастает, когда:
- Увеличивается срок службы приложения.
- Увеличивается количество и сложность ожидаемых функций.
- Требования к надежности системы становятся более строгими.
Все эти параметры необходимо учитывать при планировании системы.
Но как именно модульность помогает поддерживать систему? Основные преимущества описаны ниже.
Снижение рисков
Производительность запроса LLM по своей природе нестабильна. Его суть такова, что любое изменение может повлиять на выходные данные непредсказуемым образом. Вы можете управлять этим риском, разбивая большие запросы на компоненты, где изменение может затронуть только часть системы. Даже если один запрос сломается, остальная часть системы будет работать как раньше.
Но что, если запросы функционируют как цепочка? Разве разбивка одного компонента не сломает цепь? Да, сломает, но в этом сценарии ущерб все еще будет уменьшен. Ошибочные выходные данные в цепочке запросов могут предоставить нижестоящим запросам ошибочные входные данные, но каждый компонент все равно будет работать как раньше на наборе действительных входных данных. В отличие от изменения гигантского запроса – изменение может (и будет!) затрагивать каждый элемент логики, закодированной в этом запросе. Вы не сломали один аспект системы – вы потенциально сломали каждую ее часть.
(Безопасное управление цепочками запросов — это будущая глава в серии. Вам нужно планировать различные типы сбоев и иметь запасные планы. Но это выходит за рамки данного обсуждения)
Улучшенная тестируемость
Каждый, кто писал модульные тесты, знает, что простую функцию, которая выполняет одну задачу, намного легче тестировать, чем сложную функцию, которая пытается выполнять множество различных задач. То же самое относится и к запросам – небольшой, сфокусированный запрос можно протестировать гораздо тщательнее как вручную, так и в полностью автоматическом режиме.
Лучшая производительность
Широкое количество доказательств показывает, что более короткие запросы, как правило, превосходят более длинные: 1, 2, 3.
Исследования влияния многозадачности на производительность запросов более разнообразны: 4, 5. Идеально оптимизированный запрос может, при определенных условиях, выполнять несколько задач одновременно. На практике же намного легче оптимизировать сфокусированные запросы, где вы можете отслеживать производительность по одной основной метрике. Вы должны стремиться к более сфокусированным запросам, где это возможно.
Упрощение обмена знаниями
Объяснить все тонкости супер запроса на 3000 слов новому члену команды — это путешествие. И сколько бы вы ни объясняли, лишь те, кто уже почувствовал природу этого зверя, будут вносить свой вклад.
Система запросов, когда каждая часть относительно проста, может быть быстрее освоена; инженеры начнут быть продуктивными раньше.
Оптимизация расходов
Используя разные модели в различных частях системы, вы можете добиться значительной экономии средств и времени без ущерба для качества ответа.
Например, запрос, который определяет вводимый язык, не должен быть особенно умным — он не требует вашей последней и самой дорогой модели. С другой стороны, запрос, который генерирует ответ на основе документации, может выиграть от встроенного логического вывода, встроенного в высококачественные модели.
Когда НЕ модульно разбивать
Большинство приложений на основе программного обеспечения требуют безопасного добавления функций на протяженном промежутке времени. Однако существует исключение. Прототипные приложения не предназначены для длительного обслуживания; они не будут получать новые функции и не предназначены для высокой надежности. Поэтому не тратьте время на модульность, создавая прототипы. На самом деле, большинство шаблонов в этой серии не применимы к прототипным приложениям. При создании прототипа – действуйте быстро, подтвердите критические неизвестные и потом выкиньте код.
Еще одно соображение – знать, когда прекратить модульность. Существуют накладные расходы на управление дополнительными запросами, и если преимущества дальнейшей модульности невелики – вам стоит прекратить разбивку системы.
Инфраструктура для модульности
Если бы модульная разбивка запросов была тривиальной – этим занимались бы все. Чтобы управлять множеством запросов в системе, вам нужно инвестировать в инфраструктуру – без нее вы получите хаос. Вот минимальные требования к инфраструктуре для запросов LLM:
- Возможность быстро и безболезненно добавлять запросы стандартным образом. Особенно важно, когда запросы загружаются извне кодовой базы. См. Принцип II: Загружайте запросы безопасно (если вы действительно должны).
- Возможность автоматизированного развертывания запросов.
- Возможность логирования и мониторинга входных/выходных данных отдельных запросов.
- Возможность добавления автоматизированных тестов, охватывающих запросы.
- Способ легко отслеживать расходы на токены/$ для различных запросов.
Кейс-исследование
Давайте посмотрим, как построение системы на базе генерируемого ИИ выглядит на практике с модульностью и без нее.
Без модульности
Вы разрабатываете приложение технической поддержки и решили реализовать его с помощью одного запроса. В самой простой версии вы можете представить себе монолитный запрос, который генерирует ответы, загружая соответствующую документацию через RAG.
Выглядит неплохо и просто, не правда ли? Но по мере добавления функций возникают проблемы с этой архитектурой:
- Вы хотите отвечать на сообщения на фиксированном списке языков, но не обрабатывать другие. Для этого вы добавляете инструкции к запросу, чтобы он отвечал только на определенных языках, и заставляете LLM возвращать поле “язык” для отчетности.
- Вы хотите, чтобы все разговоры были классифицированы. Добавьте поле “метка” к выходным данным запроса.
- Когда пользователь недоволен – передайте дело к человеческой поддержке. Добавьте переменную вывода “перевести_на_человека” вместе с инструкциями в запросе.
- Необходим перевод всех сообщений для внутреннего аудита. Верните поле “переведено” с сообщением на английском.
- Необходима защита, чтобы приложение никогда не спрашивало у пользователей о их местоположении и о том, за кого они голосовали на последних выборах. Добавьте инструкции к запросу и протестируйте это вручную.
- Нужен резюме для каждого разговора? Добавьте поле “резюме” к каждому выходному значению.
Возможно, вы начинаете видеть проблему – у этого запроса теперь шесть выходных значений. Тестирование его станет настоящим кошмаром. Вы добавляете поддержку еще одного языка, и вдруг ваше приложение начинает возвращать резюме на испанском, а не на английском. Почему? Кто знает, выходные данные LLM нестабильны, поэтому изменение запроса приводит к непредсказуемым результатам.
Поздравляем – вы создали чудовище! Со временем оно вырастет и вызовет еще больше проблем.
С модульностью
Используются как Цепочка запросов, так и полностью отдельный запрос на классификацию. Исходный большой запрос модульно разбивается настолько, насколько это возможно.
Один запрос определяет язык, другой предоставляет перевод, третий определяет, недоволен ли пользователь, и передает дело людям, запрос на ответ генерирует ответ, а защитный контроль проверяет соблюдение правил ответа. Выходные данные одного запроса становятся входными для следующего; традиционный код может работать между этими запросами, например, чтобы проверить право на язык, не вовлекая LLM.
Изменение все равно может сломать данный запрос, но риски значительно снижаются, потому что:
- Изменение одной части не угрожает сломать всю логику приложения.
- Тестирование значительно проще, и вероятность раннего выявления сбоев высока.
- Каждый запрос относительно прост, поэтому его легче понять, и вы менее вероятно нанесете ущерб при изменении.
- Изменения легче проверить.
Вы получаете все преимущества генерируемого ИИ, но риски значительно снижены. Плюс, вы можете использовать более дешевые модели для некоторых компонентов, чтобы сэкономить деньги.
Заключение
Модульность позволяет вам изолировать ошибки, улучшить поддерживаемость и построить более надежную систему. Даже приложения умеренного размера будут включать десятки, если не сотни, компонентов запросов. Разделяйте запросы до тех пор, пока каждый из них не будет выполнять одну задачу, и пока преимущества дальнейшей модульности не будут перевешены добавленной операционной сложностью. Модульная разбивка ваших запросов необходима, если ваши приложения на базе ИИ должны оставаться надежными и продолжать добавлять функции в долгосрочной перспективе. Уже существует множество систем-«чудовищ» – постарайтесь не создавать новые!
Если вам понравилась эта серия – подписывайтесь на новые посты.