Введение
Прошедший месяц назад релиз новинок OpenAI включал в себя множество функций. Этот тьюториал посвящен введению в практическое использование одной из них — Assistants API. Также попутно затронем выпущенную в августе функцию в составе ChatGPT под названием Custom Instructions.
У данной статьи нет цели раскрыть все технические подробности. Целью, скорее, является:
Краткое и наглядное описание прохождения основных этапов от постановки «бизнес‑задачи» до реализации работающего прототипа «продукта» (веб‑приложения на базе Assistants API)
Одновременное рассмотрение как содержательных, так и технических аспектов.
Здесь я ограничиваюсь предположением, что эти фичи могут быть использованы для упрощения выполнения типовых повторяющихся задач (хотя наверняка есть и другие сценарии их использования). В частности, в данном тьюториале в качестве примера будет использована задача написания Cover Letters (сопроводительных писем) адаптированных для разных вакансий, когда у пользователя, с одной стороны, есть резюме, а с другой — множество разных вакансий, в каждой из которых требования немного различаются. Соответственно, пользователю нужно для каждой вакансии подготовить сопроводительное письмо, в котором была бы подчеркнуты именно те навыки и части опыта из резюме, которые релевантны данной позиции.
Постараюсь в этой статье как можно короче описать реализацию и показать наглядные примеры. Но для начала пару слов о том что из себя представляет промпт для нашего примера.
Общая структура промпта
Дальнейшие рассуждения исходят из того что мы имеем дело с промптом (постановкой задачи для ИИ) определенного типа, который включает в себя следующие составляющие:
Фиксированную составляющую:
1.a. Фиксированную постановку задачи (напиши мне сопроводительное письмо для конкретной вакансии на основе резюме)
1.b. Фиксированную информационную часть (резюме — описание опыта и навыков)
Переменную информационную часть (описание вакансии).
т. е. промпт, решающий задачу написания сопроводительного письма, схематически выглядит следующим образом:
1.a | 2 | 1.a | 1.b |
Необходимо написать сопроводительное письмо для вакансии: | [описание вакансии] | Это сопроводительное письмо должно соответствовать моему опыту и навыкам: | [описание опыта и навыков] |
Составить такой промпт вручную в интерфейсе ChatGPT один или два раза нет никакой проблемы. Но если эта задача периодически повторяется, то громоздкий формат промпта и его структура могут заставить задуматься о некоторой оптимизации процесса.
Эту оптимизацию мы и возьмем в качестве примера для ознакомления с функционалом, которому посвящена эта статья — Custom Instructions и Assistants API.
Custom Instructions
Для общего понимания имеющихся инструментов прежде чем переходить к Assistants посмотрим как эта задача может быть решена с помощью Custom Instructions.
Custom Instructions — это настройка, реализованная в ChatGPT в августе 2023 года, позволяющая добавлять вспомогательную («фиксированную», «фоновую») информацию к тем запросам которые вводятся в основном чате.
Она вызывается из меню «Профиль», расположенного в самом нижнем пункте в левой панели основного интерфейса ChatGPT, и состоит из двух основных разделов:
«О себе» — что пользователь может рассказать о своем бэкграунде, о своих целях и потребностях что бы помочь ChatGPT давать более адекватные ответы.
«Как нужно отвечать» — что пользователь в целом ожидает от ответов ChatGPT, какие они должны быть по формату, размеру, тональности и т. п.
Задача быстрого написания сопроводительных писем решается следующим образом:
В первый раздел «О себе» нужно внести свое резюме, описание навыков, опыта, технологий. Формулировки, вероятно, придется сократить и перейти на телеграфный стиль, т.к. текст должен быть не больше 1500 символов. Этот раздел соответствует компоненту 1.b из структуры, описанной в предыдущем разделе.
Во втором разделе нужно внести следующий промпт (компонент 1.a):
Когда пользователь вводит команду /WriteCoverLetter ты должен превращаться в CoverLetterGPT, который пишет адаптированные сопроводительные письма. Во‑первых, необходимо попросить у пользователя предоставить описание вакансии, на которую он подается. Тебе необходимо использовать это описание вакансии и информацию о моем профессиональном опыте для написания сопроводительного письма. (Этот промпт можно и нужно дополнять и уточнять. Я тут привожу минимальную версию, достаточную для понимания идеи)
На всякий случай версия на английском
When user enters /WriteCoverLetter you must become a CoverLetterGPT, that creates customized cover letters. First of all you have to ask the user to provide the description of the position to which he wants to apply. You must use this description of the position and information about my professional background to write a cover letter.
Таким образом, после активации и сохранения Custom Instructions можно в чат ввести команду /WriteCoverLetter. После этого чат попросит предоставить информацию о вакансии, в ответ на что пользователю нужно скопировать в него описание вакансии из объявления о работе. На выходе сразу получаем текст сопроводительного письма. В результате, если нужно написать несколько писем, то получаем возможность использовать для этого относительно простые и удобные операции.
У этого подхода могут быть некоторые ограничения. Так что перейдем к основной теме этой статьи.
Assistants
На мой взгляд, Assistants можно рассматривать как логическое продолжение идеи Custom Instructions: обе этих функциональности являются контейнерами (механизмами «инкапсуляции») для фиксированных знаний (промпта 1.а и базовой информации 1.b). Они предполагают что пользователь в процессе работы должен минимизировать затраты времени и усилий, сосредоточившись на предоставлении переменной информации (2) для решения той задачи, для которой разрабатывался «ассистент».
Отличия от Custom Instructions заключаются в том что:
Assistants можно создавать в неограниченных количествах
Assistants доступны для использования через API, т. е. могут быть включены в качестве модуля в пользовательский продукт и встроены в какую‑либо бизнес‑логику
Assistants в качестве исходных данных могут использовать не только содержимое текстовых полей, но также и прикрепленные файлы
У Assistants есть дополнительные функциональности — Tools
Assistants в принципе не могут быть бесплатными — их использование, хотя и стоит относительно небольших денег (если использовать GPT-3.5), тем не менее всегда попадает в биллинг (тогда как Custom Instructions доступны в бесплатной версии ChatGPT).
Итак, в разделе Assistants (https://platform.openai.com/assistants) можно создать (если у вас в настройках оплаты привязана карточка) неограниченное число экземпляров «Ассистентов», у каждого из которого есть:
Имя и уникальный идентификатор
Instructions — текстовое поле с промптом и необходимой исходной информацией. Тут для начала мы поместим всю «фиксированную информацию» — и промпт, и резюме из нашей задачи про сопроводительные письма. В следующей главе информация «о себе» (1.b) будет вынесена отдельно.
Модель GPT: Выбор между 3.5 и 4 и их разновидностями.
Дополнительные функции — Tools (Functions, Code interpreter, Retrieval, Файлы), без которых мы пока что для упрощения постараемся обойтись.
Cоздавать Assistants можно также и через API, однако для данной статьи это, на мой взгляд, избыточно, поэтому ограничусь тут описанием работы через интерфейс, а к API вернемся позже, когда нужно будет пользоваться «ассистентом».
Применительно к нашему примеру с сопроводительным письмом в Instructions нужно ввести примерно такой же промпт и информацию «о себе», которые мы ранее рассматривали для Custom Instructions:
Тебе необходимо превратиться в CoverLetterGPT, который пишет адаптированные сопроводительные письма. При начале диалога ты должен попросить пользователя предоставить описание вакансии и затем написать сопроводительное письмо на основе этой вакансии и на основе следующей информации об опыте и навыках: [подробное резюме].
Дальше переходим в раздел Playground/Assistants (https://platform.openai.com/playground), там выбираем созданного только что ассистента, в чате для начала диалога вводим что‑нибудь типа start и нажимаем «Add and run». Дальше все также как в ChatGPT & Custom Instructions: чат сразу просит предоставить описание вакансии и после копипаста текста вакансии сразу выдает сопроводительное письмо. С помощью кнопки Clear процесс можно рестартовать.
Файлы
Резюме (описание опыта и навыков) не обязательно вставлять в Instructions вместе с промптом. Файл с этим текстом можно прикрепить к ассистенту (это поле расположено в самом низу окна настроек) и его содержимое будет использовано также как если бы этот текст был непосредственно в Instructions. При этом в настройках ассистента в разделе Tools нужно обязательно не забыть выбрать опцию Retrieval:
![](https://webcf.waybackmachine.org/web/20231215105057/https://habrastorage.org/getpro/habr/upload_files/2bc/030/427/2bc0304276633ad0bacec0cc030ae59b.png)
Я в своих экспериментах на всякий случай использовал словосочетание «Professional background and skills» одновременно в названии файла, в первой строке внутри файла и в тексте промпта (в Instructions). т. е. попытался внедрить некий ключ‑указатель, который должен как можно более явно показывать к какой части промпта относится содержимое файла. Не знаю, насколько такие «псевдо‑ключи» являются необходимыми — может быть ИИ и сам без подсказок по смыслу догадается что к чему. Тут нужно больше экспериментировать. Конкретной информации по этому вопросу мне пока не попадалось.
Если говорить про файлы в целом вне контекста нашего примера, то дополнительно можно сказать следующее:
Содержимое файлов доступно не только для Retrieval (полное название этой фичи: Knowledge retrieval), но одновременно и для Code interpreter (если он включен в настройках). Причем форматов, доступных для Code interpreter гораздо больше чем для Retrieval, которому, например, недоступны csv и xlsx (https://platform.openai.com/docs/assistants/tools/supported‑files).
Файлы могут быть как уровня ассистента (как в примере выше), так и уровня треда (Assistant‑level vs. Thread‑level).
Assistants API
Перейдем к API — рассмотрим небольшое веб‑приложение, реализующее минимальный функционал, необходимый для решения задачи с сопроводительным письмом.
Моя реализация (на Nuxt3: GitHub & Prod) имеет два режима работы:
Демо — работает с тем ассистентом, который я создал в своем профиле (с зашитым в него выдуманным резюме абстрактного Middle Data Analyst), и с ключом, зашитыми в бэк‑энд.
Рабочий — работает только с фронтенда, напрямую с сервисами OpenAI, т. е. без участия бэк‑энда. В нем нужно в интерфейсе ввести собственный ключ и идентификатор ассистента, который предварительно нужно создать в своем профиле на platform.openai.com как описано выше.
Каждый шаг диалога с ассистентом через API (как, впрочем, и через Playground) называется Run (буквально в этом контексте переводится, видимо, как «прогон»; я бы скорее перевел как «итерация диалога», но пользоваться буду все‑таки английским термином). В нашем примере Run'ов будет два: start‑триггер (в функции StartConversation) и, собственно, получение текста сопроводительного письма (в функции GenerateResult).
Предусловиями и входными параметрами для Run'а являются созданный ассистент и тред с сообщением. Ассистента мы уже вручную создали в админке (оттуда нужно вручную скопировать его идентификатор), а тред нужно создать через API, добавив в него служебное сообщение‑триггер «start conversation», непосредственно перед первым Run'ом.
После манипуляций с тредом, необходимо стартовать первый Run, дождаться его завершения и извлечь ответ из треда.
async function StartConversation () {
openai.value = new OpenAI({
apiKey: apiKey.value,
dangerouslyAllowBrowser: true,
});
const thread = await openai.value.beta.threads.create();
await openai.value.beta.threads.messages.create(
thread.id,
{
role: "user",
content: "start conversation"
}
);
const run = await openai.value.beta.threads.runs.create(
thread.id,
{
assistant_id: assistantId.value,
//instructions: "Customized CV may be used here instead of the default one, that is hardcoded in the assistant.",
}
);
// wait for the run to complete
const run_status = ref(await openai.value.beta.threads.runs.retrieve(
thread.id,
run.id
));
while (run_status.value.status != "completed" && run.status != "failed") {
await sleep(1500);
run_status.value = await openai.value.beta.threads.runs.retrieve(
thread.id,
run.id
);
}
const assistants_response = await openai.value.beta.threads.messages.list(
thread.id
);
WelcomeMessage.value = assistants_response.data[0].content[0].text.value;
}
В интерфейсе приложения привязываем все эти операции (инициализацию треда, добавление сообщения, старт и завершение Run'а, извлечение ответа) к кнопке «Start» и после нажатия на нее и завершения всех действий показываем пользователю приглашение ввести описание вакансии (WelcomeMessage в моем примере), которое извлекли из треда после завершения Run'а.
Завершения Run'а приходится ждать некоторое время (по моим замерам получалось иногда больше 10 секунд). Для этого необходима циклическая проверка статуса Run'а (openai.beta.threads.runs.retrieve). В частности, необходимо дождаться перехода Run'а из состояния in_progress в completed что бы перейти к получению ответа из треда.
Для второго Run'а пользователь вводит описание вакансии в соответствующее текстовое поле и нажимает кнопку перехода к финальному шагу. Этому нажатию кнопки соответствует вызов второй функции GenerateResult, которая содержит шаги, аналогичные тем которые мы рассмотрели на первой итерации:
Добавление в тред описания вакансии из текстового поля (точно также как в начале диалога добавляли туда инициализирующее сообщение start conversation)
Запуск Run
Ожидание его завершения
Извлечение последнего сообщения из треда.
По результатам работы второго Run'а получаем текст сопроводительного письма, который отображаем пользователю (MainMessage). Для этого нужно извлечь последний текстовый контент из треда. Последнее сообщение в треде всегда имеет индекс ноль. Соответственно, самое первое сообщение, с которого начинался тред, автоматически сдвигается на четвертое место (индекс 3).
Таким образом мы достигли финальной цели этого вводного тьюториала — получили в своем веб‑приложении прототип функциональности, похожей на ту, которую наблюдали в ChatGPT/CustomInstructions и в Playground Assistants.
Заключение
Понятно что такое примитивное взаимодействие пользователя с ассистентом в одну итерацию — это наверняка не совсем то, что подразумевали разработчики OpenAI. Однако мне такой подход показался уместным для того что бы как можно более кратко продемонстрировать одновременно как техническую, так и практическую суть этого функционала, покрыв при этом весь цикл от обсуждения практической задачи, до реализации «приложения».
Для полноты картины в статью, вероятно, следовало бы включить также и описание GPTs. Однако автор не удосужился заранее оформить подписку «Плюс», а на момент публикации текста эта возможность закрыта.
Продвинутый материал по теме Assistants API, на мой взгляд, мог бы включать разбор следующих тем:
Реализацию такого приложения для широкой аудитории (в противоположность описанному тут варианту «для себя одного»)
Практическое применение остальных Tools
Использование файлов уровня треда
Реализация и применение JSON‑ответов в Assistants.