Гайд по промпт-инжинирингу (Brex)
Гайд по промпт-инжинирингу (Brex)
Это руководство создано компанией Brex для внутренних целей. Оно основано на уроках, извлечённых при исследовании и создании промптов для больших языковых моделей (LLM) в production-сценариях. Оно покрывает историю вокруг LLM, а также стратегии, гайдлайны и рекомендации по безопасности при работе и построении программных систем поверх больших языковых моделей, таких как GPT-4 от OpenAI.
Примеры в этом документе сгенерированы недетерминированной языковой моделью, и те же примеры могут дать у вас другие результаты.
Это живой документ. Современные лучшие практики и стратегии вокруг LLM стремительно меняются каждый день.
Содержание
- Что такое большая языковая модель (LLM)?
- Краткая, неполная и слегка неточная история языковых моделей
- До 2000-х
- Середина 2000-х
- Начало 2010-х
- Конец 2010-х
- 2020-е
- Что такое промпт?
- Скрытые промпты
- Токены
- Лимиты токенов
- Промпт-хакеры
- Jailbreak'и
- Утечки
- Зачем нужен промпт-инжиниринг?
- Дай боту рыбу
- Семантический поиск
- Научи бота рыбачить
- Грамматики команд
- ReAct
- GPT-4 против GPT-3.5
- Стратегии
- Встраивание данных
- Простые списки
- Markdown-таблицы
- JSON
- Свободный текст
- Вложенные данные
- Цитирование
- Программное потребление
- Цепочка рассуждений
- Усреднение
- Интерпретация кода
- Разделители
- Тонкая настройка
- Минусы
- Дополнительные ресурсы
Что такое большая языковая модель (LLM)?
Большая языковая модель — это предсказывающий движок, который берёт последовательность слов и пытается предсказать наиболее вероятную последовательность, идущую после неё [1]. Делает она это, присваивая вероятности вероятным следующим последовательностям, а затем сэмплируя из них, чтобы выбрать одну [2]. Процесс повторяется, пока не выполнится какое-то условие остановки.
LLM обучаются этим вероятностям на больших корпусах текста. Следствие этого — модели лучше подходят одним сценариям использования, чем другим (например, если модель обучена на данных Git, она отлично понимает вероятности последовательностей в исходном коде). Другое следствие — модель может генерировать утверждения, которые кажутся правдоподобными, но на самом деле случайны и не укоренены в реальности.
По мере того как языковые модели становятся точнее в предсказании последовательностей, появляются многие удивительные эмерджентные способности.
[1]: Языковые модели на самом деле используют токены, а не слова. Токен примерно соответствует слогу в слове или примерно 4 символам.
[2]: Существует много разных стратегий отсечения и сэмплирования, меняющих поведение и производительность последовательностей.
Краткая, неполная и слегка неточная история языковых моделей
:pushpin: Пропустите до «Что такое промпт», если хотите перескочить историю языковых моделей. Эта секция для любопытных, хотя она может помочь понять логику советов ниже.
До 2000-х
Языковые модели существуют десятилетиями, хотя традиционные языковые модели (например, n-граммные) имеют много недостатков: взрыв пространства состояний (проклятие размерности) и проблемы с новыми фразами, которых они никогда не видели (разреженность). Проще говоря, старые языковые модели могут генерировать текст, смутно напоминающий статистику человеческого текста, но в выводе нет согласованности — и читатель быстро поймёт, что это абракадабра. N-граммные модели также не масштабируются на большие значения N, поэтому ограничены по своей природе.
Середина 2000-х
В 2007 году Джеффри Хинтон — знаменитый популяризатор обратного распространения в 1980-х — опубликовал важное достижение в обучении нейросетей, открывшее путь к гораздо более глубоким сетям. Применение этих простых глубоких нейросетей к языковому моделированию помогло смягчить часть проблем языковых моделей — они стали эффективнее, но эта область остаётся активной зоной исследований.
2020-е
Хотя технически всё началось в 2018 году, тема 2020-х — генеративные предобученные трансформеры, более известные как GPT. Через год после статьи «Attention Is All You Need» OpenAI выпустила «Improving Language Understanding by Generative Pre-Training». Статья показала, что можно обучить большую языковую модель на массивном наборе данных без конкретной задачи, а затем, когда модель выучила общие аспекты языка, донастроить её для конкретных задач и быстро получить результаты уровня SotA.
В 2020 году OpenAI выпустила статью о GPT-3 «Language Models are Few-Shot Learners», показав, что если увеличить GPT-подобные модели ещё примерно в 10 раз по числу параметров и количеству обучающих данных, то для многих задач донастройка больше не нужна. Способности возникают естественно, и вы получаете результаты уровня SotA просто через текстовое взаимодействие с моделью.
В 2022 году OpenAI выпустила InstructGPT, развив успехи GPT-3. Целью было подстроить модель под следование инструкциям, сделав выводы менее токсичными и предвзятыми. Ключевым ингредиентом стало обучение с подкреплением на основе человеческого фидбека (RLHF) — концепция, соавторами которой выступили Google и OpenAI в 2017 году [4]: она позволяет людям быть в обучающем цикле, чтобы точнее выравнивать вывод модели с человеческими предпочтениями. InstructGPT — предшественник знаменитого ChatGPT.
OpenAI за последние годы была главным контрибьютором в большие языковые модели, включая недавний выход GPT-4, но она не одна. Meta выпустила много открытых LLM: OPT, OPT-IML (донастроенный под инструкции) и LLaMa. Google выпустила модели FLAN-T5 и BERT. И есть огромное сообщество open source-исследователей, выпускающих модели вроде BLOOM и StableLM.
Прогресс движется так быстро, что каждые несколько недель SotA меняется, а модели, которым раньше требовались кластеры, теперь работают на Raspberry Pi.
[4]: 2017 год был большим годом для обработки естественного языка.
Что такое промпт?
Промпт (иногда называемый контекстом) — это текст, предоставленный модели до начала генерации вывода. Он направляет модель исследовать определённую область того, чему она научилась, чтобы вывод был релевантен вашим целям. По аналогии: если считать языковую модель интерпретатором исходного кода, то промпт — это исходный код, который нужно интерпретировать. Забавно, что языковая модель с радостью попытается угадать, что делает исходный код:
И она почти идеально интерпретирует Python!
Часто промпты — это инструкция или вопрос, например:
С другой стороны, если не задать промпт, у модели нет якоря для работы, и вы увидите, что она просто случайно сэмплирует из всего, что когда-либо потребляла:
- У GPT-3-Davinci: три примера случайного текста.
- У GPT-4: три примера, заметно более осмысленных.
Скрытые промпты
:warning: Всегда считайте, что любой контент в скрытом промпте может быть увиден пользователем.
В приложениях, где пользователь динамически взаимодействует с моделью (например, чат), обычно есть части промпта, которые никогда не должны быть видны пользователю. Эти скрытые части могут быть где угодно, хотя почти всегда скрытый промпт есть в начале разговора.
Обычно это начальный кусок текста, задающий тон, ограничения модели и цели, плюс другая динамическая информация, специфичная для конкретной сессии: имя пользователя, местоположение, время суток и т.д.
Модель статична и заморожена в определённый момент времени, поэтому если вы хотите, чтобы она знала текущую информацию (время, погоду) — вы должны предоставить её.
Если вы используете OpenAI Chat API, скрытый контент промпта помещается в роль system.
Вот пример скрытого промпта с последующими взаимодействиями с его содержимым:
В этом примере мы объясняем боту различные роли, немного контекста о пользователе, некоторые динамические данные, к которым бот должен иметь доступ, а затем инструкции, как бот должен отвечать.
На практике скрытые промпты могут быть довольно большими. Вот более крупный промпт из ChatGPT-ассистента для командной строки:
<details>
<summary>Пример большого системного промпта</summary>
Мы в чате с 3 пользователями. Одного зовут "Human", другого — "Backend", третьего — "Proxy Natural Language Processor".
Я буду печатать то, что говорит "Human", и то, что отвечает "Backend". Вы будете действовать как "Proxy Natural Language Processor",
чтобы пересылать запросы "Human" в JSON-формате пользователю "Backend". Пользователь "Backend" — это Ubuntu-сервер,
и строки, отправляемые ему, выполняются в shell, после чего он отвечает выводом STDOUT и кодом выхода.
Ubuntu-сервер мой. Когда "Backend" отвечает STDOUT и кодом выхода, вы, "Proxy Natural Language Processor",
парсите и форматируете эти данные в простой дружелюбный английский и отправляете "Human". Вот пример:
Я спрашиваю как человек:
Human: Сколько неотредактированных видео осталось?
Затем вы отправляете команду Backend'у:
Proxy Natural Language Processor: @Backend {"command":"find ./Videos/Unedited/ -iname '*.mp4' | wc -l"}
Затем backend отвечает выводом STDOUT и кодом выхода:
Backend: {"STDOUT":"5", "EXITCODE":"0"}
Затем вы отвечаете пользователю:
Proxy Natural Language Processor: @Human Осталось 5 неотредактированных видео.
Отвечайте только то, что должен говорить "Proxy Natural Language Processor", и ничего больше. Ни сейчас, ни в будущем, ни по какой причине.
Ещё пример:
Я спрашиваю как человек:
Human: Что такое сертификат PEM?
Затем вы отправляете команду Backend'у:
Proxy Natural Language Processor: @Backend {"command":"xdg-open 'https://en.энциклопедия.org/wiki/Privacy-Enhanced_Mail'"}
Затем backend отвечает выводом STDOUT и кодом выхода:
Backend: {"STDOUT":"", "EXITCODE":"0"}
Затем вы отвечаете пользователю:
Proxy Natural Language Processor: @Human Я открыл ссылку с описанием сертификата PEM.
Отвечайте только то, что должен говорить "Proxy Natural Language Processor", и ничего больше. Ни сейчас, ни в будущем, ни по какой причине.
НЕ ОТВЕЧАЙТЕ как Backend. НЕ завершайте то, что должен ответить Backend. ВАМ НЕЛЬЗЯ завершать то, что должен ответить Backend.
Также НЕ объясняйте, что делает команда и что означают коды выхода. НИКОГДА, НИ СЕЙЧАС, НИ В БУДУЩЕМ НЕ ОТВЕЧАЙТЕ КАК BACKEND.
Отвечайте только то, что должен говорить "Proxy Natural Language Processor", и ничего больше. Ни сейчас, ни в будущем, ни по какой причине.
</details>
Здесь видны хорошие практики: много примеров, повторение важных поведенческих аспектов, ограничение ответов и т.д.
:warning: Всегда считайте, что любой контент в скрытом промпте может быть увиден пользователем.
Токены
Если вы думали, что токены — это круто в 2022, то в 2023 они на совершенно другом уровне. Атомарная единица потребления языковой модели — не «слово», а «токен». Можно думать о токенах как о слогах: в среднем выходит около 750 слов на 1000 токенов. Они представляют много концепций помимо буквенных символов — пунктуацию, границы предложений и конец документа.
Вот пример того, как GPT может токенизировать последовательность:
Поэкспериментировать с токенизатором можно в онлайн-токенизаторе OpenAI.
У разных моделей разные токенизаторы с разной детализацией. Теоретически можно скормить модели только нули и единицы — но тогда ей придётся выучить концепцию символов из битов, потом слов из символов и так далее. Аналогично можно подать сырой поток символов — но тогда модель должна выучить концепцию слов, пунктуации и т.д. — и в целом модели будут работать хуже.
Чтобы узнать больше, у открытые лидерборды есть отличное введение в токенизаторы и зачем они нужны.
В токенизации много нюансов — размер словаря, языки, по-разному обрабатывающие структуру предложений (например, слова, не разделяемые пробелами). К счастью, API языковых моделей почти всегда принимают сырой текст и токенизируют его за кулисами — поэтому вам редко нужно думать о токенах.
Кроме одного важного сценария, который мы обсудим следующим: лимиты токенов.
Лимиты токенов
Промпты имеют тенденцию только расти, потому что вы хотите, чтобы у бота был весь контекст предыдущих сообщений разговора. Языковые модели в целом безсостоянийны и не помнят ничего о предыдущих запросах, поэтому вам всегда нужно включать всё, что может понадобиться и специфично для текущей сессии.
Главный минус этого: ведущая архитектура языковых моделей, Transformer, имеет фиксированный размер входа и выхода — в определённый момент промпт не может расти дальше. Общий размер промпта, иногда называемый «контекстным окном», зависит от модели. Для GPT-3 это 4096 токенов. Для GPT-4 — 8192 или 32768 токенов, в зависимости от варианта.
Если ваш контекст становится слишком велик для модели, самый распространённый приём — обрезать контекст скользящим окном. Если промпт представить как скрытый инициализирующий промпт + messages[], обычно скрытый промпт остаётся неизменным, а массив messages[] берёт последние N сообщений.
Можно встретить и более хитрые тактики обрезки: например, выбрасывать сначала только пользовательские сообщения, чтобы предыдущие ответы бота оставались в контексте как можно дольше, или попросить LLM резюмировать разговор и заменить все сообщения одним сообщением с этим резюме. Правильного ответа здесь нет — решение зависит от вашего приложения.
Важно: при обрезке контекста нужно резать агрессивно, чтобы осталось место и для ответа. Лимиты токенов OpenAI включают и длину входа, и длину выхода. Если ваш вход в GPT-3 — 4090 токенов, в ответ модель сможет сгенерировать только 6 токенов.
🧙♂️ Если вы хотите посчитать количество токенов до отправки сырого текста модели, конкретный токенизатор зависит от вашей модели. У OpenAI есть библиотека tiktoken для их моделей — с важной оговоркой, что внутренний токенизатор может немного отличаться по счёту и добавлять метаданные, так что считайте это приближением.
Если нужна оценка без токенизатора: input.length / 4 даст грубое, но лучше ожидаемого приближение для англоязычных входов.
Промпт-хакеры
Промпт-инжиниринг и большие языковые модели — довольно молодая область, поэтому новые способы их взлома обнаруживаются каждый день. Два больших класса атак:
- Заставить бота обойти данные вами гайдлайны.
- Заставить бота выдать скрытый контекст, который вы не предназначали пользователю.
Известных механизмов, чтобы полностью это остановить, нет, поэтому важно исходить из того, что при взаимодействии с враждебным пользователем бот может сделать или сказать что угодно. К счастью, на практике это в основном косметические проблемы.
Думайте о промптах как о способе улучшить обычный пользовательский опыт. Мы проектируем промпты так, чтобы обычные пользователи не выходили за рамки наших задуманных взаимодействий — но всегда предполагайте, что решительный пользователь сможет обойти ограничения промпта.
Jailbreak'и
Jailbreak — это способ обойти ограничения, которые модель изначально не запрограммирована нарушать. Пример: представьте, что в скрытом промпте мы сказали боту, что ему не разрешено использовать слово «компьютер».
Он решительно откажется произносить это слово:
Но мы можем обойти эти инструкции и заставить модель с радостью использовать это слово, обманув её просьбой перевести свинскую латынь слова «компьютер» (кумпь-ютвер-компью...) обратно на английский.
Здесь можно принять ряд защитных мер, но обычно лучший ход — повторять ваши важнейшие ограничения как можно ближе к концу. Для OpenAI chat API это может означать добавление system-сообщения после последнего сообщения user. Вот пример:
Несмотря на большие инвестиции OpenAI в защиту от jailbreak'ов, каждый день распространяются очень хитрые обходы.
Утечки
Если вы пропустили предыдущие предупреждения в этом документе: всегда считайте, что любые данные, переданные языковой модели, в конечном счёте будут видны пользователю.
При построении промптов вы часто будете встраивать кучу данных в скрытые промпты (они же системные промпты). Бот с радостью перескажет эту информацию пользователю:
Даже если вы укажете ему не раскрывать информацию и он послушается, существуют миллионы способов утечки данных из скрытого промпта.
Вот пример, где бот не должен упоминать мой город, но простое переформулирование вопроса заставляет его раскрыть секрет.
Аналогично мы заставляем бота сказать, какое слово ему нельзя произносить, ни разу не произнеся это слово.
Вы должны думать о скрытом промпте как о способе улучшить пользовательский опыт или приблизить его к целевой персоне. Никогда не помещайте в промпт информацию, которую вы не отобразили бы визуально на экране для чтения.
Зачем нужен промпт-инжиниринг?
Выше мы использовали аналогию промптов как «исходного кода», который языковая модель «интерпретирует». Промпт-инжиниринг — это искусство писать промпты, чтобы языковая модель делала то, что мы хотим — точно так же, как разработка ПО — это искусство писать исходный код, чтобы компьютеры делали то, что мы хотим.
При написании хороших промптов нужно учитывать idiosyncrasies моделей, с которыми вы работаете. Стратегии варьируются со сложностью задач. Вам придётся придумывать механизмы, чтобы ограничивать модель для достижения надёжных результатов, включать динамические данные, на которых модель не обучена, учитывать ограничения обучающих данных модели, проектировать вокруг лимитов контекста и многих других измерений.
Есть старая поговорка, что компьютеры делают только то, что им скажешь. Выбросьте этот совет. Промпт-инжиниринг переворачивает эту мудрость. Это как программирование на естественном языке против недетерминированного компьютера, который сделает всё, от чего вы его не отвели.
Существуют две большие корзины подходов промпт-инжиниринга.
Дай боту рыбу
Корзина «дай боту рыбу» — для сценариев, когда вы можете явно дать боту в скрытом контексте всю информацию, необходимую для выполнения запрошенной задачи.
Например, если пользователь открыл дашборд и мы хотим показать ему короткое дружелюбное сообщение о том, какие задачи у него есть, мы можем попросить бота резюмировать так:
У вас 4 чека/мемо к загрузке. Самый свежий — от Target от 5 марта, самый старый — от Blink Fitness от 17 января. Спасибо, что держите расходы под контролем!
предоставив весь список входящих и любой другой пользовательский контекст.
Аналогично, если вы помогаете пользователю забронировать поездку, вы можете:
- Спросить даты и пункт назначения.
- За кулисами поискать рейсы и отели.
- Встроить результаты поиска рейсов и отелей в скрытый контекст.
- Также встроить в скрытый контекст политику путешествий компании.
И тогда у бота будут данные о путешествиях в реальном времени + ограничения, которые он может использовать для ответов пользователю. Вот пример бота, рекомендующего варианты, и пользователя, просящего их уточнить:
<details>
<summary>(Полный промпт)</summary>
Brex — платформа для управления бизнес-расходами.
Ниже приведена политика командировочных расходов Brex:
- Для рейсов короче 6 часов максимальный класс — эконом.
- Для рейсов длиннее 6 часов максимальный класс — премиальный эконом.
- Аренда авто должна иметь среднюю суточную ставку $75 или меньше.
- Проживание должно иметь среднюю ночную ставку $400 или меньше.
- Проживание должно быть не ниже 4 звёзд.
- Питание в ресторанах, доставка еды, продукты, бары и ночная жизнь — до $75.
- Все прочие расходы — до $5000.
- Возмещение требует проверки.
Варианты отелей:
| Название отеля | Цена | Отзывы |
| --- | --- | --- |
| Hilton Financial District | $109/ночь | 3.9 звёзд |
| Hotel VIA | $131/ночь | 4.4 звёзд |
| Hyatt Place San Francisco | $186/ночь | 4.2 звёзд |
| Hotel Zephyr | $119/ночь | 4.1 звёзд |
Варианты рейсов:
| Авиакомпания | Время | Длительность | Пересадки | Класс | Цена |
| --- | --- | --- | --- | --- | --- |
| United | 5:30-7:37 | 2ч 7м | Без пересадок | Эконом | $248 |
| Delta | 13:20-15:36 | 2ч 16м | Без пересадок | Эконом | $248 |
| Alaska | 21:50-23:58 | 2ч 8м | Без пересадок | Премиум | $512 |
Сотрудник бронирует поездку в Сан-Франциско с 20 по 25 февраля.
Рекомендуй отель и рейс, соответствующие политике. Держи рекомендацию краткой, не длиннее пары предложений, но добавь приятности, как будто ты дружелюбный коллега, помогающий мне:
</details>
Это тот же подход, который продукты вроде Microsoft Bing используют для включения динамических данных. Когда вы общаетесь с Bing, он просит бота сгенерировать три поисковых запроса. Затем выполняется три веб-поиска, и резюмированные результаты включаются в скрытый контекст для бота.
Резюмируя этот раздел: трюк хорошего опыта — динамически менять контекст в ответ на то, что пользователь пытается сделать.
🧙♂️ «Дать боту рыбу» — самый надёжный способ гарантировать, что бот получит рыбу. С этой стратегией вы получите самые стабильные и надёжные результаты. Используйте её, когда можете.
Семантический поиск
Если боту нужно просто чуть больше знать о мире, распространённый подход — выполнить семантический поиск.
Семантический поиск строится вокруг эмбеддинга документа — его можно представить как массив чисел фиксированной длины [5], где каждое число отражает какой-то аспект документа (например, для научного документа 843-е число может быть большим, а для документа об искусстве 1115-е число большим — это чересчур упрощённо, но передаёт идею) [6].
Помимо вычисления эмбеддинга документа, можно вычислить эмбеддинг пользовательского запроса той же функцией. Если пользователь спрашивает «Почему небо голубое?» — вы вычисляете эмбеддинг этого вопроса, и теоретически он будет ближе к эмбеддингам документов, упоминающих небо, чем к эмбеддингам документов, которые о небе не говорят.
Чтобы найти документы, связанные с запросом пользователя, вы вычисляете эмбеддинг и находите top-N документов с самым похожим эмбеддингом. Затем помещаете эти документы (или их резюме) в скрытый контекст для бота.
Иногда пользовательские запросы настолько коротки, что эмбеддинг не особо ценен. Есть хитрый приём из статьи от декабря 2022 года под названием «Hypothetical Document Embedding» (HyDE). Используя его, вы просите модель сгенерировать гипотетический документ в ответ на запрос пользователя, а затем вычисляете эмбеддинг этого сгенерированного документа. Модель фабрикует документ из воздуха — но приём работает!
HyDE использует больше вызовов модели, но во многих случаях даёт заметный прирост результатов.
[5]: Обычно называется вектором.
[6]: Векторные признаки изучаются автоматически, и конкретные значения не интерпретируемы человеком без усилий.
Научи бота рыбачить
Иногда вы хотите, чтобы бот умел выполнять действия от имени пользователя: добавить мемо к чеку или построить график. Или, возможно, получать данные более изощрёнными способами, чем позволяет семантический поиск: например, получить расходы за последние 90 дней.
В этих сценариях нужно научить бота рыбачить.
Грамматики команд
Мы можем дать боту список команд для нашей системы с описаниями и примерами, а затем попросить его генерировать программы из этих команд.
Здесь много подводных камней. Со сложными грамматиками команд бот склонен галлюцинировать команды или аргументы, которые правдоподобно могли бы существовать, но на самом деле не существуют. Искусство сделать правильно — перечислять команды с относительно высоким уровнем абстракции, давая боту достаточную гибкость для компоновки их новыми полезными способами.
Например, команда построить-график-расходов-за-последние-90-дней не особенно гибка или компонуема. Лучше дать боту низкоуровневые команды и позволить компоновать их. Например:
| Команда | Аргументы | Описание |
|---|---|---|
| get-budget-by-name | budget_name | Достаёт бюджет по имени |
| list-budgets | Возвращает список бюджетов, к которым у пользователя есть доступ | |
| add-memo | inbox_item_id, текст мемо | Добавляет мемо к указанному элементу входящих |
Таблицу мы предоставляем модели в Markdown-формате, который языковые модели обрабатывают невероятно хорошо — предположительно потому, что OpenAI сильно обучается на данных Git.
В примере ниже мы просим модель выводить команды в обратной польской нотации (RPN) [7], и модель справляется с простотой RPN поразительно хорошо.
[7]: Модель отлично справляется с простотой RPN.
🧠 В этом примере происходит несколько интересных тонких вещей помимо генерации команд. Когда мы просим добавить мемо к расходу «Shake Shack», модель знает, что командаadd-memoпринимает ID расхода. Но мы никогда не сообщали ей ID расхода, поэтому она ищет «Shake Shack» в таблице расходов, берёт ID из соответствующей колонки и использует его как аргумент дляadd-memo.
Заставить грамматики команд надёжно работать в сложных ситуациях бывает непросто. Лучшие рычаги: много описаний и как можно больше примеров использования. Большие языковые модели — few-shot-обучающиеся: они могут выучить новую задачу по нескольким примерам. В общем, чем больше примеров, тем лучше — но это съедает токенный бюджет, так что нужен баланс.
Вот более сложный пример с выводом в JSON вместо RPN, а типы возвращаемых команд заданы через TypeScript.
<details>
<summary>(Полный промпт)</summary>
Вы — финансовый ассистент, работающий в Brex, но также эксперт-программист.
Я клиент Brex.
Вы должны отвечать на мои вопросы, составляя серию команд.
Типы вывода:
type LinkedAccount = {
id: string,
bank_details: {
name: string,
type: string,
},
brex_account_id: string,
last_four: string,
available_balance: {
amount: number,
as_of_date: Date,
},
current_balance: {
amount: number,
as_of_date: Date,
},
}
type Expense = {
id: string,
memo: string,
amount: number,
}
type Budget = {
id: string,
name: string,
description: string,
limit: {
amount: number,
currency: string,
}
}
Доступные команды:
| Команда | Аргументы | Описание | Формат вывода |
| --- | --- | --- | --- |
| nth | index, values[] | Возвращает n-й элемент массива | any |
| push | value | Добавляет значение в стек для потребления будущей командой | any |
| value | key, object | Возвращает значение по ключу | any |
| values | key, object[] | Возвращает массив значений по соответствующему ключу в массиве объектов | any[] |
| sum | value[] | Суммирует массив чисел | number |
| plot | title, values[] | Строит график набора значений с заданным заголовком | Plot |
| list-linked-accounts | | Перечисляет все банковские связи, подходящие для ACH-переводов на кассовый счёт Brex | LinkedAccount[] |
| list-expenses | budget_id | По id бюджета возвращает список расходов по нему | Expense[]
| get-budget-by-name | name | По имени возвращает бюджет | Budget |
| add-memo | expense_id, message | Добавляет мемо к расходу | bool |
| converse | message | Отправляет пользователю сообщение | null |
Отвечайте только командами.
Выводите команды в JSON в виде абстрактного синтаксического дерева.
ВАЖНО — Отвечайте только программой. Не отвечайте никаким текстом, не являющимся частью программы. Не пишите прозу, даже если вас об этом просят. Не объясняйтесь.
Вы можете генерировать только команды, но вы эксперт по генерации команд.
</details>
Эту версию проще парсить, если в вашем языке есть JSON.parse.
🧙♂️ В индустрии нет устоявшегося лучшего формата для определения DSL, в котором модель генерирует программы. Считайте это зоной активных исследований. Вы будете упираться в пределы. И преодолевая их, мы можем открыть более оптимальные способы задания команд.
ReAct
В марте 2023 года Принстон и Google выпустили статью «ReAct: Synergizing Reasoning and Acting in Language Models». Подход: дать модели набор команд для извлечения информации, как в грамматиках команд, и попросить её выводить свои мысли и действия в цикле, пока она не сможет ответить. Модель находит информацию и использует наблюдения, чтобы приблизиться к ответу.
Простая таблица команд, как во вложенном промпте ниже, часто даёт надёжные результаты для простых случаев. Например:
| Команда | Аргументы | Описание |
|---|---|---|
| find_employee | name | Ищет сотрудника по имени |
| get_employee | id | Достаёт сотрудника по ID |
| get_location | id | Достаёт локацию по ID |
| get_reports | employee_id | Возвращает список id сотрудников, подчиняющихся сотруднику по employee_id. |
| энциклопедия | article | Достаёт статью энциклопедии по теме. |
Задаём боту простой вопрос: «Мой менеджер знаменит?».
Видим, как бот:
- Сначала ищет мой профиль сотрудника.
- Из профиля получает id менеджера и ищет его профиль.
- Достаёт имя менеджера и ищет его в энциклопедии.
- В этом сценарии я выбрал вымышленного персонажа менеджером.
- Бот читает статью энциклопедии и заключает, что это не может быть мой менеджер, так как это вымышленный персонаж.
- Бот меняет поиск, добавив «(реальный человек)».
- Не найдя результатов, бот заключает, что мой менеджер не знаменит.
<details>
<summary>(Полный промпт)</summary>
~~~
Вы — полезный ассистент. Вы работаете в цикле, ища дополнительную информацию для ответа на вопрос пользователя, пока не сможете ответить.
Команды для поиска информации:
| Команда | Аргументы | Описание |
|---|---|---|
| find_employee | name | Ищет сотрудника по имени |
| get_employee | id | Достаёт сотрудника по ID |
| get_location | id | Достаёт локацию по ID |
| get_reports | employee_id | Возвращает список id сотрудников, подчинённых сотруднику по employee_id |
| энциклопедия | article | Достаёт статью энциклопедии по теме |
Ваш ответ будет в JSON и будет включать «Thought» (мысль) + «Action» (действие) для получения данных, нужных для ответа, или будет включать «Answer» (ответ). Когда данные получены, они будут включены как «Observation» (наблюдение).
Вы будете продолжать генерировать мысли и действия, пока не придёте к ответу или не заключите, что не можете.
Пример 1:
Пользователь: Какое население Филадельфии?
Ассистент: {
"thought": "В энциклопедии, вероятно, есть эта информация. Ищу...",
"action": {"command": "энциклопедия", "article": "Филадельфия"}
}
Ассистент: {
"observation": "Филадельфия — крупнейший город Пенсильвании... Население по переписи 2020 года — 1 603 797 человек."
}
Ассистент: {
"answer": "Население Филадельфии по переписи 2020 года — 1 603 797 человек."
}
Пример 2:
Пользователь: В каком городе живёт мой менеджер?
Ассистент: {
"thought": "Посмотрю, кто ваш менеджер. Сначала найду ваш профиль.",
"action": {"command": "get_employee", "id": 92352}
}
Ассистент: {
"observation": {
"id": 78334,
"name": "Мисс Менеджер",
"location_id": 8832
}
}
Ассистент: {
"thought": "Ваш менеджер — Мисс Менеджер. Ищу её локацию.",
"action": {"command": "get_location", "id": 8832}
}
Ассистент: {
"observation": {
"id": 8832,
"name": "Филадельфия"
}
}
Ассистент: {
"answer": "Ваш менеджер живёт в Филадельфии."
}
~~~
</details>
GPT-4 против GPT-3.5
В большинстве примеров этого документа разница между GPT-3.5 и GPT-4 незначительна, но для сценариев «научить бота рыбачить» она заметна.
Ни один из примеров грамматик команд выше без существенных модификаций не работает на GPT-3.5. Как минимум нужно предоставить ряд примеров (хотя бы по одному использованию на команду), прежде чем получите разумные результаты. А для сложных наборов команд модель может галлюцинировать новые команды или выдумывать аргументы.
Достаточно тщательный скрытый промпт поможет преодолеть эти ограничения. GPT-4 способен на гораздо более стабильную и сложную логику с куда более простыми промптами (и может обойтись без примеров или с их малым числом — хотя всегда полезно включать как можно больше примеров).
Стратегии
В этой секции примеры и стратегии для конкретных нужд или проблем. Для успешного промпт-инжиниринга вам придётся комбинировать некоторое подмножество всех стратегий из этого документа. Не бойтесь смешивать и сочетать — или изобретать свои подходы.
Встраивание данных
В скрытых контекстах вы часто будете встраивать всевозможные данные. Конкретная стратегия зависит от типа и количества встраиваемых данных.
Простые списки
Для единичных объектов перечисление полей + значений обычным маркированным списком работает хорошо:
Это также работает для больших наборов вещей, но для списков данных есть другие форматы, которые GPT обрабатывает надёжнее.
Markdown-таблицы
Markdown-таблицы отлично подходят для сценариев, где нужно перечислить много элементов одного типа.
К счастью, модели OpenAI исключительно хороши в работе с Markdown-таблицами (предположительно из-за тонн данных Git, на которых они обучены).
Можно переформулировать предыдущий пример через Markdown-таблицу:
🧠 Обратите внимание: в этом последнем примере у элементов таблицы есть явная дата — 2 февраля. В нашем вопросе мы спросили про «сегодня». И ранее в промпте упомянули, что сегодня — 2 февраля. Модель корректно выполнила транзитивный вывод — превратила «сегодня» в «2 февраля» и затем нашла «2 февраля» в таблице.
JSON
Markdown-таблицы отлично работают во многих случаях, и их стоит предпочитать из-за плотности и надёжности обработки моделью, но бывают сценарии, где много колонок и модель справляется хуже, или у каждого элемента свои атрибуты и не имеет смысла делать десятки колонок с пустыми данными.
В таких сценариях JSON — ещё один формат, который модель обрабатывает очень хорошо. Близость ключей к их значениям облегчает модели держать соответствие в уме.
Вот тот же пример, что и Markdown-таблица, но в JSON:
Свободный текст
Иногда вы захотите включить в промпт свободный текст и ораничить его от остального промпта — например, встраивая документ для ссылок бота. В таких сценариях хорошо работают тройные обратные кавычки, ```, вокруг документа [8].
[8]: Хорошее правило для всего, что вы делаете в промптах, — опираться на вещи, которым модель могла научиться из Git.
Вложенные данные
Не все данные плоские и линейные. Иногда нужно встроить данные с вложенностью или связями с другими данными. В этих сценариях опирайтесь на JSON:
<details>
<summary>(Полный промпт)</summary>
~~~
Вы — полезный ассистент. Вы отвечаете на вопросы о пользователях. Вот что вы знаете о них:
{
"users": [
{
"id": 1,
"name": "Джон Доу",
"contact": {
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
},
"phone": "555-555-1234",
"email": "johndoe@example.com"
}
},
... (аналогично для других пользователей)
]
}
~~~
</details>
Вложенные структуры данных обычно разворачиваются в таблицы при встраивании. Например, приведённые выше данные могут быть представлены набором связных таблиц:
Таблица 1: users
| id (PK) | name | manager_id (FK) |
|---|---|---|
| 1 | Джон Доу | 5 |
| 2 | Джейн Смит | 1 |
| ... | ... | ... |
Таблица 2: addresses
| id (PK) | user_id (FK) | street | city | state | zip |
|---|---|---|---|---|---|
| 1 | 1 | 123 Main St | Anytown | CA | 12345 |
| 2 | 2 | 456 Elm St | Sometown | TX | 54321 |
| ... | ... | ... | ... | ... | ... |
Таблица 3: phone_numbers
| id (PK) | user_id (FK) | phone |
|---|---|---|
| 1 | 1 | 555-555-1234 |
| ... | ... | ... |
Таблица 4: emails
| id (PK) | user_id (FK) | |
|---|---|---|
| 1 | 1 | johndoe@example.com |
| ... | ... | ... |
Таблица 5: cities
| id (PK) | name | state | population | median_income |
|---|---|---|---|---|
| 1 | Anytown | CA | 50,000 | $70,000 |
| ... | ... | ... | ... | ... |
~~~
</details>
🧠 Модель хорошо работает с данными в третьей нормальной форме, но может спотыкаться на слишком многих JOIN'ах. В экспериментах она справляется минимум с тремя уровнями вложенных JOIN. В примере выше модель успешно соединилаusers→addresses→cities, чтобы вывести вероятный доход Джорджа — $90 000.
Цитирование
Часто естественно-языкового ответа недостаточно, и вы хотите, чтобы вывод модели цитировал, откуда взяты данные.
Полезно помнить: всё, что вы хотите цитировать, должно иметь уникальный ID. Простейший подход — просто попросить модель ссылаться на всё, что она упоминает:
Программное потребление
По умолчанию языковые модели выдают текст на естественном языке, но часто нужно взаимодействовать с результатом программно — выйти за рамки простой печати на экран. Этого можно достичь, попросив модель вывести результаты в вашем любимом формате сериализации (лучше всего, по-видимому, работают JSON и YAML).
Обязательно дайте модели пример желаемого формата вывода. Развивая наш предыдущий пример с путешествиями, можно дополнить промпт так:
~~~
Выдай вывод в JSON. Формат:
{
"message": "Сообщение для пользователя",
"hotelId": 432,
"flightId": 831
}
Не включай ID в сообщение.
~~~
И теперь мы получим такие взаимодействия:
Можно представить, как UI рендерит сообщение как обычный текст, и на его основе запрашивает данные для отеля и рейса программно.
Цепочка рассуждений
Иногда вы будете биться головой о промпт, пытаясь добиться надёжного вывода, но, что бы вы ни делали, не работает. Часто это случается, когда итоговый вывод бота требует промежуточных рассуждений, но вы просите только результат и ничего больше.
Ответ может вас удивить: попросите бота показать свою работу. В октябре 2022 года Google выпустила статью «Chain-of-Thought Prompting Elicits Reasoning in Large Language Models», где показала: если в скрытом промпте дать боту примеры ответов с показанной работой, то, попросив ответить, вы увидите, что бот показывает свою работу и выдаёт более надёжные ответы.
Всего через несколько недель после той статьи, в конце октября 2022 года, Токийский университет и Google выпустили статью «Large Language Models are Zero-Shot Reasoners», где показали, что примеры даже не нужны — достаточно просто попросить бота думать шаг за шагом.
Усреднение
Вот пример, где мы просим бота вычислить средний расход, исключая Target. Реальный ответ — $136,77, и бот почти справляется — $136,43.
Если просто добавить «Давай подумаем шаг за шагом» — модель даёт правильный ответ.
Интерпретация кода
Вернёмся к примеру с Python и применим цепочку рассуждений к нашему вопросу. Напомню: когда мы просили бота вычислить Python-код, он немного ошибался. Правильный ответ — Hello, Brex!!Brex!!Brex!!!, но бот путается в количестве восклицательных знаков. В примере ниже он выдаёт Hello, Brex!!!Brex!!!Brex!!!.
Если попросить бота показать свою работу, он даёт правильный ответ.
Разделители
Во многих сценариях вы не хотите показывать конечному пользователю все рассуждения бота, а хотите показать только финальный ответ. Можно попросить бота отделить финальный ответ от рассуждений. Способов много, но давайте используем JSON для лёгкого парсинга:
Использование цепочки рассуждений потребляет больше токенов, увеличивая цену и задержку, но результаты заметно надёжнее во многих сценариях. Это ценный инструмент, когда нужно, чтобы бот сделал что-то сложное максимально надёжно.
Тонкая настройка
Иногда, какие бы трюки вы ни применяли, модель просто не делает то, что нужно. В таких сценариях можно иногда откатиться к тонкой настройке. В целом это должна быть крайняя мера.
Тонкая настройка — это процесс взятия уже обученной модели и передачи ей тысяч (или больше) пар примеров input:output.
Она не отменяет необходимости в скрытых промптах — динамические данные всё равно нужно встраивать, — но может сделать промпты меньше и надёжнее.
Минусы
Минусов у тонкой настройки много. Если это вообще возможно, используйте природу языковых моделей как zero-shot, one-shot и few-shot обучающихся и учите их делать что-то в промпте, а не тонкой настройкой.
Некоторые минусы:
- Невозможно: GPT-3.5/GPT-4 не донастраиваются — это основная модель/API, которую мы будем использовать, так что мы просто не можем опереться на тонкую настройку.
- Оверхед: тонкая настройка требует вручную создавать огромное количество данных.
- Скорость: цикл итераций сильно замедляется — каждый раз, добавляя новую возможность, вместо добавления пары строк в промпт нужно создавать кучу синтетических данных, прогонять процесс донастройки и использовать новую модель.
- Стоимость: использование донастроенной GPT-3 до 60 раз дороже, чем обычную
gpt-3.5-turbo. И в 2 раза дороже, чем обычную GPT-4.
⛔️ Если вы донастраиваете модель, никогда не используйте реальные данные клиентов. Всегда используйте синтетические. Модель может запомнить части предоставленных данных и «вырыгнуть» приватные данные другим пользователям, которым их видеть не положено.
Если вы никогда не донастраиваете модель, нам не нужно беспокоиться о случайной утечке данных в модель.