Промпт-инжиниринг: как использовать LLM для создания приложений

Последние два месяца я посвятил разработке приложения на основе большой языковой модели (LLM). Это был захватывающий, но порой разочаровывающий опыт. В ходе проекта изменилось мое представление о промпт-инжиниринге и возможностях LLM.

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

Вот что нужно знать о проекте.

  • Разработанное нашей командой приложение VoxelGPT объединяет LLM с языком запросов компьютерного зрения FiftyOne, что обеспечивает поиск по наборам данных изображений и видео с помощью естественного языка. VoxelGPT также отвечает на вопросы о самом FiftyOne.
  • VoxelGPT имеет открытый исходный код (как и FiftyOne). Весь код доступен на GitHub.
  • Вы можете бесплатно протестировать VoxelGPT на сайте gpt.fiftyone.ai.

Разделим уроки по промпт-инжинирингу на четыре части:

  • Общие уроки.
  • Техники составления промптов.
  • Примеры.
  • Инструментарий.

Общие уроки

Промпт-инжиниринг  —  это в такой же степени экспериментирование, как и проектирование. Существует бесконечное количество способов написать промпт, начиная от формулировки конкретного вопроса и заканчивая введением информационного материала и уточнением контекста. Это может показаться невероятным, но я обнаружил, что проще всего начать с элементарного и довериться интуиции, а затем проверять гипотезы.

В компьютерном зрении каждый набор данных отличается своей схемой, типами меток и названием классов. Хотя VoxelGPT предназначен для обработки любого набора данных путем компьютерного зрения, мы для начала взяли один датасет  —  MS COCO. Сохранение всех дополнительных степеней свободы позволило в первую очередь закрепить способность LLM писать синтаксически корректные запросы.

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

Какую модель (модели) использовать?

Одной из самых важных характеристик больших языковых моделей считается их относительная взаимозаменяемость. Теоретически вы должны быть готовы заменить одну LLM на другую без существенных структурно-функциональных изменений.

Хотя это верно, что заменить LLM обычно так же просто, как изменить порядок API-вызовов, на практике возникают трудности.

  • Некоторые модели обладают гораздо меньшей длиной контекста, чем другие. Переход на модель с меньшим контекстом может потребовать серьезного рефакторинга.
  • Открытый исходный код  —  это здорово, но LLM с открытым исходным кодом (пока) не так производительны, как GPT-модели. Кроме того, при развертывании приложения с LLM с открытым исходным кодом нужно убедиться, что контейнер, в котором работает модель, имеет достаточно памяти и хранилища. Это может оказаться более хлопотным (и более дорогим), чем использование конечных API-точек.
  • Переход с GPT-4 на GPT-3.5, осуществленный для снижения стоимости использования модели, может шокировать падением производительности. Для выполнения сложных задач с генерацией кода и получения выводов больше подойдет GPT-4.

Где использовать LLM?

Большие языковые модели очень эффективны. Но то, что они способны решать определенные задачи, не означает, что вы должны (или даже обязаны) использовать их для выполнения этих задач. Разумнее всего относиться к LLM как к вспомогательным средствам. LLM не являются абсолютным решением  —  они лишь его часть. Не ожидайте, что большие языковые модели будут делать все.

Например, используемая вами LLM может (при идеальных обстоятельствах) генерировать правильно отформатированные API-вызовы. Но если вы знаете, как должна выглядеть структура API-вызова, и действительно заинтересованы в заполнении разделов API-вызова (имена переменных, условия и т. д.), то просто применяйте LLM для выполнения этих задач и используйте (должным образом обработанные) выходные данные LLM для генерации структурированных вызовов API самостоятельно. Это будет дешевле, эффективнее и надежнее.

Безусловно, полная система с LLM будет обладать структурно-функциональным единством и классической логикой, а также множеством традиционных компонентов программной инженерии и МО-инженерии. Найдите то, что больше всего подходит для вашего приложения.

Языковые модели необъективны

Языковые модели  —  это одновременно и механизмы логических выводов, и хранилища знаний. Часто аспект хранения знаний в LLM представляет большой интерес для пользователей: многие используют LLM в качестве замены поисковых систем. Однако каждый, кто пользовался LLM, знает, что они склонны выдумывать “факты”, не имеющие отношения к реальности. Это явление в ИИ называется галлюцинацией.

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

В нашем случае GPT-3.5 должен был определить соответствующие ViewStages (конвейеры логических операций), необходимые для преобразования запроса пользователя на естественном языке в правильный запрос на FiftyOne Python. Проблема заключалась в том, что GPT-3.5 знал о таких ViewStages, как “Match” и “FilterLabels”, которые уже давно существуют в FiftyOne, но его обучающие данные не включали недавно добавленные функции, в которых ViewStage “SortBySimilarity” может быть использован для поиска изображений, похожих на текстовый запрос.

Мы попробовали передать модели определение “SortBySimilarity”, подробную информацию с примерами об использовании этого ViewStage. Мы даже пытались проинструктировать GPT-3.5, что он не должен использовать такие ViewStages, как “Match” и “FilterLabels”, иначе будет наказан. Но что бы мы ни предпринимали, модель все равно ориентировалась на то, что знает, в ущерб корректности вывода.

В итоге нам пришлось решать эту проблему в процессе постобработки.

Болезненная постобработка неизбежна

Неважно, насколько хороши примеры. Неважно, насколько точны промпты. Большие языковые модели неизбежно будут галлюцинировать, давать неправильно отформатированные ответы и устраивать истерики при непонимании входной информации. Самое предсказуемое свойство LLM  —  это непредсказуемость их результатов.

Я потратил немыслимое количество времени на написание процедур для поиска и исправления галлюцинаций в синтаксисе. В итоге файл постобработки содержал почти 1600 строк Python-кода.

Одни из этих подпрограмм были такими же простыми, как добавление скобок или замена “and” и “or” на “&” и “|” в логических выражениях. Другие подпрограммы были намного сложнее, например проверка имен сущностей в ответах LLM, преобразование одного ViewStage в другой при выполнении определенных условий, проверка правильности количества и типов аргументов методов.

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

  • Напишите свой синтаксический анализатор ошибок, используя Abstract Syntax Trees (модуль ast в Python).
  • Если результаты синтаксически некорректны, передайте LLM сгенерированное сообщение об ошибке и попросите модель повторить попытку.
  • Этот подход не позволяет решить более коварную проблему, когда синтаксис корректен, но результаты неверны.

Техники составления промптов

Чем больше, тем лучше

Чтобы создать VoxelGPT, я использовал, казалось, все возможные техники составления промптов:

  • “Представь, что ты эксперт”;
  • “Твоя задача  —  это…”;
  • “Ты должен”;
  • “Ты будешь наказан”;
  • “Вот правила”.

Никакая комбинация таких фраз не обеспечит определенный тип поведения. Умных промптов просто недостаточно.

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

Примеры важнее документации

Сейчас уже общеизвестно (и общепризнанно) то, что примеры и другая контекстная информация, например документация, помогают получить лучшие ответы от большой языковой модели. Я убедился в этом на примере VoxelGPT.

Но что, если после добавления всех примеров и документации, непосредственно относящихся к делу, в контекстном окне останется свободное место? Доверьтесь моему опыту: косвенные примеры имеют большее значение, чем косвенная документация.

Модульность лучше монолитности

Чем детальнее вы можете разбить всеобъемлющую задачу на более мелкие подзадачи, тем лучше. Вместо того чтобы передавать схему набора данных и список сквозных примеров, гораздо эффективнее определить отдельные этапы отбора и вывода (селективно-результативного промптинга) и передавать только соответствующую информацию на каждом этапе.

Это предпочтительнее по трем причинам:

  1. LLM лучше справляются с одной задачей за раз, чем с несколькими задачами одновременно.
  2. Чем меньше шагов, тем легче проводить санитарную обработку входов и выходов.
  3. Инженеру необходимо понимать логику приложения. Смысл LLM не в том, чтобы сделать мир “черным ящиком”. Он заключается в том, чтобы открыть возможности для новых рабочих процессов.

Примеры

Сколько их нужно?

Большая часть промпт-инжиниринга  —  определение того, сколько примеров вам нужно для конкретной задачи. Это в значительной степени зависит от специфики той или иной цели.

Некоторые задачи (эффективная генерация запросов и ответы на вопросы на основе документации FiftyOne) позволили нам обойтись без примеров. Другие (выбор тегов, определение релевантности истории чата и распознавание именованных сущностей для классов меток) потребовали всего нескольких примеров. Однако наша основная задача по выводу содержит почти 400 примеров (и это по-прежнему является ограничивающим фактором общей производительности), поэтому мы передаем только самые важные примеры во время вывода.

В процессе генерирования примеров следуйте двум рекомендациям:

  1. Будьте крайне информативны. Попытайтесь предоставить LLM хотя бы один пример для каждого случая. В работе с VoxelGPT мы старались подбирать минимум один пример для каждого синтаксически правильного способа применения каждого ViewStage (а чаще несколько примеров), чтобы модель могла успешно выполнять поиск по шаблону.
  2. Будьте как можно более последовательны. Если вы разбиваете задачу на несколько подзадач, убедитесь, что примеры в этих подзадачах не противоречат друг другу. Примеры можно использовать повторно.

Синтетические примеры

Генерация примеров  —  трудоемкий процесс, а примеры, созданные вручную, могут завести вас очень далеко. Невозможно заранее продумать все возможные сценарии. Когда вы развернете приложение, сможете регистрировать запросы пользователей и использовать их для улучшения набора примеров.

Однако до развертывания лучше всего сгенерировать синтетические примеры.

Ниже приведены два подхода к созданию синтетических примеров:

  1. Использование LLM для генерации примеров. Можно попросить LLM изменить язык или даже сымитировать стиль речи потенциальных пользователей! Это не сработало в нашем случае, но я убежден, что может стать полезной практикой при создании многих приложений.
  2. Программное генерирование примеров на основе элементов входного запроса. Для VoxelGPT это означает генерацию примеров на основе полей пользовательского набора данных. Мы находимся в процессе внедрения этой технологии в конвейер, и до сих пор результаты были многообещающими.

Инструментарий

LangChain

Библиотека LangChain не случайно стала популярной: она позволяет легко соединять входы и выходы LLM сложными способами, упрощая сложные детали. Особенно эффективны ее модули Models и Prompts.

Тем не менее LangChain находится в процессе разработки: модули Memories, Indexes и Chains имеют существенные недостатки. Вот лишь некоторые из проблем, с которыми я столкнулся при попытке использовать LangChain.

  1. Загрузчики документов (Document Loaders) и сплиттеры текста (Text Splitters). В LangChain Document Loaders должны преобразовывать данные из различных форматов файлов в текст, а Text Splitters  —  разделять текст на семантически значимые фрагменты. VoxelGPT отвечает на вопросы о документации FiftyOne, извлекая наиболее подходящие фрагменты документации и передавая их в запрос. Чтобы генерировать осмысленные ответы на вопросы о документации FiftyOne, мне пришлось создавать пользовательские загрузчики и сплиттеры, поскольку LangChain не обеспечивала должной гибкости.
  2. Векторные хранилища. LangChain предлагает интеграцию с Vectorstore и Retrievers (ретриверами) на основе Vectorstore, помогающими находить необходимую информацию для включения в промпты для LLM. В теории это здорово, но реализации не хватает гибкости. Мне пришлось написать собственную реализацию с помощью ChromaDB, чтобы передавать векторы встраивания заранее и не пересчитывать их при каждом запуске приложения. Мне также пришлось написать собственный ретривер для реализации необходимой мне предварительной фильтрации.
  3. Ответы на вопросы с использованием источников. При создании ответов на вопросы по документации FiftyOne я пришел к разумному решению, используя LangChain-цепочку “RetrievalQA”. Желая добавить источники, я подумал, что это будет так же просто, как поменять эту цепочку на LangChain-цепочку “RetrievalQAWithSourcesChain”. Однако плохая техника промптинга привела к тому, что эта цепочка продемонстрировала несколько неудачных действий, включая галлюцинации о Майкле Джексоне. И снова мне пришлось взять дело в свои руки.

Что все это значит? Возможно, проще просто собрать компоненты самому.

Векторные базы данных

Векторный поиск  —  отличный подход, но это не значит, что он необходим для вашего проекта. Изначально я реализовал процедуру поиска похожих примеров с помощью ChromaDB, но поскольку у нас были только сотни примеров, я перешел на точный поиск ближайших соседей. Мне пришлось самому заниматься фильтрацией метаданных, но в результате процедура стала быстрее и с меньшим количеством зависимостей.

TikToken

Внедрить TikToken в проект было невероятно просто. Всего TikToken добавил менее 10 строк кода, но оптимизировал точность при подсчете токенов и вмещении как можно больше информации в длину контекста. Это единственное решение, без которого не обойтись, когда речь заходит об инструментах.

Заключение

Существует множество LLM, инструментов и методов промпт-инжиниринга. Ключ к созданию приложения с помощью промпт-инжиниринга можно свести к следующему.

  1. Разбейте большую задачу на части и постепенно приходите к решению.
  2. Рассматривайте LLM как подручные средства, а не как комплексные решения.
  3. Используйте инструменты, только когда это может облегчить вам жизнь.
  4. Не пренебрегайте экспериментами.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jacob Marks, Ph.D.: What I Learned Pushing Prompt Engineering to the Limit

Предыдущая статьяPandas 2.0.0  —  геймчейнджер в работе дата-сайентистов?
Следующая статьяПрограммист как пользователь инструментов