LLMOps Шаблоны инженерии производственных задач с Hamilton

LLMOps Шаблоны с Hamilton

Обзор способов применения промптов на производстве с помощью Hamilton

Промпты. Как их развивать в контексте производства? Этот пост основан на материале, опубликованном здесь. Изображение с сайта pixabay.

То, что вы отправляете своей большой языковой модели (LLM), крайне важно. Малые изменения и изменения могут иметь большое влияние на результаты, поэтому по мере развития вашего продукта необходимо развивать и промпты. LLM постоянно развиваются и выпускаются, и поэтому при изменении LLM ваши промпты также должны измениться. Поэтому важно установить паттерн итерации для организации способа «внедрения» ваших промптов, чтобы вы и ваша команда могли работать эффективно, но также обеспечить минимизацию проблем производства, если не их полное отсутствие. В этом посте мы расскажем вам о лучших практиках управления промптами с помощью Hamilton, фреймворка для микрооркестрации с открытым исходным кодом, сделаем аналогии с паттернами MLOps и обсудим компромиссы по пути. Основные выводы этого поста по-прежнему актуальны, даже если вы не используете Hamilton.

Несколько вещей перед тем, как мы начнем:

  1. Я являюсь одним из со-создателей Hamilton.
  2. Не знакомы с Hamilton? Прокрутите до самого конца для получения дополнительных ссылок.
  3. Если вы ищете пост, который говорит о “управлении контекстом”, то это не тот пост. Но это пост, который поможет вам разобраться с основами того, как итерировать и создавать историю итерации «управления контекстом промпта» для производственного уровня.
  4. Мы будем использовать термины “промпт” и “шаблон промпта” взаимозаменяемо.
  5. Мы предполагаем, что эти промпты используются в “онлайн” веб-сервисе.
  6. Мы будем использовать наш пример сумматора PDF в Hamilton для проектирования наших паттернов.
  7. Какая у нас здесь достоверность? Мы потратили свою карьеру на создание инструментов самообслуживания данных/MLOps, наиболее известных для 100+ Data Scientists Stitch Fix. Так что мы видели много сбоев и подходов, которые применялись со временем.

Промпты для LLM – это то же, что гиперпараметры для моделей ML

Тезис: Промпты + LLM API аналогичны гиперпараметрам + моделям машинного обучения.

С точки зрения практик “Ops”, LLMOps все еще находится в зачаточном состоянии. MLOps немного старше, но до сих пор ни то, ни другое не получило широкого распространения, если сравнивать с тем, насколько широко распространены знания о практиках DevOps.

Практики DevOps в основном заботятся о том, как доставить код в производство, а практики MLOps – о том, как доставить код и данные артефакты (например, статистические модели) в производство. Что же насчет LLMOps? Лично я думаю, что это ближе к MLOps, поскольку у вас есть:

  1. ваш рабочий процесс LLM – это просто код.
  2. и LLM API – это данные артефакт, который можно «настроить» с помощью промптов, аналогично модели машинного обучения (ML) и ее гиперпараметрам.

Поэтому вам, скорее всего, важно версионировать LLM API + промпты тесно для обеспечения хорошей производственной практики. Например, в практике MLOps вы бы хотели иметь процесс, который позволяет проверить, что ваша модель ML по-прежнему ведет себя правильно, когда изменяются ее гиперпараметры.

Как нужно мыслить об операционализации промпта?

Чтобы быть ясным, две части, которые нужно контролировать, – это LLM и промпты. Как и в MLOps, когда меняется код или модельный артефакт, вы хотите иметь возможность определить, что именно изменилось. Для LLMOps мы хотим получить ту же дискриминацию, отделяя рабочий процесс LLM от LLM API + промптов. Важно учитывать, что LLM (собственный или API) в основном являются статическими, так как мы реже обновляем (или даже контролируем) их внутренности. Таким образом, изменение части промптов LLM API + промпты фактически аналогично созданию нового модельного артефакта.

Существуют два основных способа обработки подсказок:

  1. Подсказки как динамические переменные времени выполнения. Используемый шаблон не является статическим для развертывания.
  2. Подсказки как код. Шаблон подсказки является статическим/предопределенным для конкретного развертывания.

Основное отличие заключается в количестве составных частей, которые вам необходимо управлять, чтобы обеспечить отличную производственную историю. Ниже мы рассмотрим, как использовать Hamilton в контексте этих двух подходов.

Подсказки как динамические переменные времени выполнения

Динамическая передача/загрузка подсказок

Подсказки – это просто строки. Поскольку строки являются примитивным типом в большинстве языков, это означает, что их достаточно легко передавать. Идея заключается в том, чтобы абстрагировать ваш код так, чтобы во время выполнения вы передавали требуемые подсказки. Более конкретно, вы бы «загружали/перезагружали» шаблоны подсказок каждый раз, когда появляется «обновленный» шаблон.

Аналогично MLOps, это будет автоматическая перезагрузка артефакта ML-модели (например, файла pkl), когда появляется новая модель.

Аналогия MLOps: диаграмма, показывающая, как будет выглядеть автоматическая перезагрузка модели машинного обучения. Изображение автора.
Диаграмма, показывающая, как будет выглядеть динамическая перезагрузка/запрос подсказок. Изображение автора.

Преимущество здесь заключается в том, что вы можете очень быстро выпускать новые подсказки, потому что вам не нужно повторно развертывать ваше приложение!

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

  1. Для того, кто отслеживает ваше приложение, будет неясно, когда произошло изменение и распространилось ли оно по вашим системам. Например, вы только что добавили новую подсказку, и LLM теперь возвращает больше токенов за запрос, что вызывает увеличение задержки; тот, кто отслеживает, вероятно, будет в замешательстве, если у вас нет хорошей культуры журнала изменений.
  2. Семантика отката предполагает необходимость знания о другой системе. Вы не можете просто откатить предыдущее развертывание для исправления проблем.
  3. Вам понадобится хороший мониторинг, чтобы понять, что было запущено и когда; например, когда отдел обслуживания клиентов дает вам заявку на расследование, как вы узнаете, какая подсказка использовалась?
  4. Вам нужно будет управлять и контролировать ту систему, которую вы используете для управления и хранения ваших подсказок. Это будет дополнительная система, которую вам придется поддерживать вне того, что обслуживает ваш код.
  5. Вам нужно будет управлять двумя процессами: обновление и развертывание сервиса, а также обновление и развертывание подсказок. Синхронизация этих изменений будет на вас. Например, вам нужно внести изменение в код вашего сервиса, чтобы обработать новую подсказку. Вам нужно будет согласовать изменение двух систем, чтобы это работало, что является дополнительной операционной нагрузкой для управления.

Как это будет работать с Hamilton

Наш поток суммирования PDF будет выглядеть примерно так, если удалить определения функций summarize_text_from_summaries_prompt и summarize_chunk_of_text_prompt:

summarization_shortened.py. Обратите внимание на два входа *_prompt, которые обозначают необходимые подсказки ввода для функционирования потока данных. С помощью Hamilton вы сможете определить, какие входы должны быть требуемыми для вашего шаблона подсказки, просто посмотрев на такую диаграмму. Диаграмма, созданная с помощью Hamilton. Изображение автора.

Для работы вам следует либо внедрить подсказки при запросе:

из hamilton импортировать базу, драйвер
из hamilton сократить импортировать summarization_shortend
# создать драйвер
dr = (
    driver.Builder()
    .с модулями(summarization_shortend)
    .построить()
)
# получить промпты откуда-то
summarize_chunk_of_text_prompt = """НЕКОТОРЫЙ ПРОМПТ ДЛЯ {chunked_text}"""
summarize_text_from_summaries_prompt = """НЕКОТОРЫЙ ПРОМПТ {summarized_chunks} ... {user_query}"""
# выполнить и передать промпт
result = dr.execute(
    ["summarized_text"],
    inputs={
        "summarize_chunk_of_text_prompt": summarize_chunk_of_text_prompt,
        ...
    }
)

Или вы можете изменить свой код для динамической загрузки промптов, то есть добавить функции для получения промптов из внешней системы как часть потока данных Hamilton. При каждом вызове они будут запрашивать промпт для использования (конечно, вы можете кэшировать это для улучшения производительности):

# prompt_template_loaders.py
def summarize_chunk_of_text_prompt(
    db_client: Client, other_args: str
) -> str:
    # псевдокод здесь, но вы понимаете идею:
    _prompt = db_client.query(
        "получить последний промпт X из БД", other_args
    )
    return _prompt

def summarize_text_from_summaries_prompt(
    db_client: Client, another_arg: str
) -> str:
    # псевдокод здесь, но вы понимаете идею:
    _prompt = db_client.query(
        "получить последний промпт Y из БД", another_arg
    )
    return _prompt

Код драйвера:

из hamilton импортировать базу, драйвер
import prompt_template_loaders  # <-- загрузите это для предоставления ввода промпта
из hamilton сократить импортировать summarization_shortend
# создать драйвер
dr = (
    driver.Builder()
    .с модулями(
        prompt_template_loaders,  # <-- Hamilton будет вызывать вышеперечисленные функции
        summarization_shortend,
    )
    .построить()
)
# выполнить и передать промпт
result = dr.execute(
    ["summarized_text"],
    inputs={
        # в этой версии не нужно передавать промпты
    }
)

Как я могу записывать используемые промпты и отслеживать потоки?

Здесь мы рассмотрим несколько способов отслеживать происходящее.

  • Записывайте результаты выполнения. Это означает выполнение Hamilton, а затем отправку информации туда, куда вы хотите.
result = dr.execute(
    ["summarized_text", "summarize_chunk_of_text_prompt", ... # и все остальное, что вы хотите получить
    "summarize_text_from_summaries_prompt"],
    inputs={
        # в этой версии не нужно передавать промпты
    }
)
my_log_system(result)  # отправьте то, что вы хотите сохранить для безопасности в свою систему.

Примечание. В вышеприведенном примере Hamilton позволяет запрашивать любые промежуточные результаты, просто запросив “функции” (то есть узлы на диаграмме) по имени. Если мы действительно хотим получить все промежуточные результаты всего потока данных, мы можем сделать это и записать их где угодно!

  • Используйте регистраторы внутри функций Hamilton (чтобы увидеть силу этого подхода, посмотрите мою старую презентацию о структурированных журналах):
import logging
logger = logging.getLogger(__name__)

def summarize_text_from_summaries_prompt(
    db_client: Client, another_arg: str
) -> str:
    # псевдокод здесь, но вы понимаете идею:
    _prompt = db_client.query(
        "получить последний промпт Y из БД", another_arg
    )
    logger.info(f"Используемый промпт: [{_prompt}]")
    return _prompt
  • Расширьте Hamilton, чтобы он выводил эту информацию. Вы используете Hamilton для получения информации из выполненных функций, то есть узлов, не вставляя операторы регистрации в тело функции. Это способствует повторному использованию, поскольку вы можете переключать регистрацию между настройками разработки и продакшена на уровне драйвера. См. GraphAdapters или напишите собственный декоратор на Python для обертывания функций для мониторинга.

В любом из приведенных выше кодов вы можете легко использовать инструменты сторонних разработчиков для отслеживания и мониторинга кода, а также внешнего вызова API.

Промпты как код

Промпты как статические строки

Поскольку промпты – это просто строки, их также очень удобно хранить вместе с вашим исходным кодом. Идея состоит в том, чтобы хранить любое количество версий промптов в вашем коде, чтобы при запуске набор доступных промптов был фиксированным и детерминированным.

Аналогия MLOps заключается в том, что вместо динамической перезагрузки моделей вы вместо этого встраиваете модель машинного обучения в контейнер или жестко закрепляете ссылку. После развертывания вашего приложения есть все необходимое. Развертывание является неизменным; ничего не изменяется после его запуска. Это упрощает отладку и определение того, что происходит.

Аналогия MLOps: создание неизменного развертывания путем закрепления модели для развертывания вашего приложения. Изображение автора.
Диаграмма, показывающая, как обработка подсказок как кода позволяет использовать CI/CD и создавать неизменное развертывание для работы с вашим API LLM. Изображение автора.

У этого подхода есть множество операционных преимуществ:

  1. Каждый раз, когда появляется новая подсказка, это приводит к новому развертыванию. Если возникает проблема с новой подсказкой, ясно определены семантики отката.
  2. Вы можете отправить запрос на слияние (PR) для исходного кода и подсказок одновременно. Становится проще просматривать, в чем заключается изменение, и какие зависимости будут затронуты/взаимодействовать с этими подсказками.
  3. Вы можете добавить проверки в свою систему CI/CD, чтобы гарантировать, что плохие подсказки не попадут в продакшн.
  4. Отладка проблемы становится проще. Просто получите (Docker) контейнер, который был создан, и вы сможете быстро и легко воспроизвести любую проблему клиента.
  5. Не требуется поддержка или управление другой “системой подсказок”. Упрощение операций.
  6. Не исключается добавление дополнительного мониторинга и видимости.

Как это будет работать с Hamilton

Подсказки будут закодированы в функции в поток данных/направленный ациклический граф (DAG):

Как выглядит файл summarization.py в примере суммирования PDF. Шаблоны подсказок являются частью кода. Диаграмма, созданная с помощью Hamilton. Изображение автора.

Совместное использование этого кода с Git позволяет вам иметь легковесную систему версионирования для всего потока данных (т.е. “цепи”), чтобы вы всегда могли определить, в каком состоянии находился мир при определенном SHA коммита. Если вы хотите управлять и иметь доступ к нескольким подсказкам в любой момент времени, Hamilton имеет два мощных абстракции, которые позволяют вам это сделать: @config.when и модули Python. Это позволяет вам сохранять и иметь доступ ко всем старым версиям подсказок вместе и указывать, какую использовать с помощью кода.

@config.when (документы)

У Hamilton есть понятие декораторов, которые являются аннотациями к функциям. Декоратор @config.when позволяет указать альтернативные реализации функций, т.е. “узлов”, в вашем потоке данных. В данном случае мы указываем альтернативные подсказки.

from hamilton.function_modifiers import config@config.when(version="v1")def summarize_chunk_of_text_prompt__v1() -> str:    """V1 подсказка для суммирования блоков текста."""    return f"Суммировать этот текст. Извлекать ключевые моменты с обоснованием.\n\nСодержание:"@config.when(version="v2")def summarize_chunk_of_text_prompt__v2(content_type: str = "научная статья") -> str:    """V2 подсказка для суммирования блоков текста."""    return f"Суммировать этот текст из {content_type}. Извлечь ключевые моменты с обоснованием. \n\nСодержание:"

Вы можете продолжать добавлять функции, аннотированные как @config.when, позволяя вам переключаться между ними с использованием переданной конфигурации в Hamilton Driver. При создании экземпляра Driver будет создан поток данных, использующий реализацию подсказок, связанную со значением конфигурации.

from hamilton import base, driver
import summarization

# create driver
dr = (
    driver.Builder()
    .with_modules(summarization)
    .with_config({"version": "v1"}) # V1 is chosen. Use "v2' for V2.
    .build()
)

Переключение модулей

Вместо использования @config.when вы можете разместить ваши различные реализации подсказок в разных модулях Python. Затем при создании объекта Driver передайте правильный модуль для контекста, который вы хотите использовать.

Итак, у нас есть один модуль, в котором находится V1 нашей подсказки:

# prompts_v1.pydef summarize_chunk_of_text_prompt() -> str:    """V1 подсказка для резюмирования текстовых блоков."""    return f"Резюмируйте этот текст. Извлеките любые ключевые моменты с обоснованием.\n\nСодержание:"

А здесь у нас есть один модуль, в котором находится V2 (посмотрите, как они немного отличаются):

# prompts_v2.pydef summarize_chunk_of_text_prompt(content_type: str = "научная статья") -> str:    """V2 подсказка для резюмирования текстовых блоков."""    return f"Резюмируйте этот текст из {content_type}. Извлеките ключевые моменты с обоснованием. \n\nСодержание:"

В коде драйвера ниже мы выбираем правильный модуль для использования на основе некоторого контекста.

# run.pyfrom hamilton import driverimport summarizationimport prompts_v1import prompts_v2# create driver -- passing in the right module we wantdr = (    driver.Builder()    .with_modules(        prompts_v1,  # или prompts_v2        summarization,    )    .build())

Использование подхода с модулями позволяет нам инкапсулировать и версионировать целые наборы подсказок вместе. Если вы хотите вернуться в прошлое (с помощью git) или посмотреть, какая подсказка была использована, вам просто нужно перейти к правильному коммиту, а затем посмотреть в правильном модуле.

Как я могу записывать использованные подсказки и отслеживать потоки?

Предполагая, что вы используете git для отслеживания вашего кода, вам не понадобится записывать, какие подсказки использовались. Вместо этого вам просто нужно знать, какой коммит SHA git развернут, и вы сможете одновременно отслеживать версию вашего кода и подсказок.

Для отслеживания потоков, как и в предыдущем подходе, у вас есть те же самые доступные крючки мониторинга, и я не буду повторять их здесь, но они включают:

  • Запрос промежуточных результатов и их собственная запись в лог вне Hamilton.
  • Запись в лог из самой функции, или создание декоратора на Python / GraphAdapter, чтобы это делать на уровне фреймворка.
  • Интеграция инструментов сторонних разработчиков для мониторинга вашего кода и вызовов LLM API.
  • или все вышеперечисленное!

Что насчет A/B-тестирования моих подсказок?

При любой инициативе по машинному обучению важно измерять бизнес-влияние изменений. Точно так же, с LLM + подсказками, важно тестировать и измерять изменения по важным бизнес-метрикам. В мире MLOps вы бы проводили A/B-тестирование моделей машинного обучения для оценки их бизнес-ценности, распределяя трафик между ними. Чтобы обеспечить необходимую случайность для A/B-тестов, вы не будете знать во время выполнения, какую модель использовать, пока не будет подброшена монетка. Однако, чтобы получить эти модели, обе они должны пройти процесс квалификации. Так что для подсказок мы должны думать таким же образом.

Вышеприведенные два шаблона проектирования подсказок не исключают возможности A/B-тестирования подсказок, но это означает, что вам нужно управлять процессом, чтобы включить параллельное тестирование стольких шаблонов подсказок, сколько вам нужно. Если вы также изменяете пути кода, наличие их в коде будет проще определить и отладить, что происходит, и вы можете использовать декоратор `@config.when` или замену модуля Python для этой цели. В отличие от необходимости полагаться на ваш стек журналирования/мониторинга/наблюдаемости, чтобы узнать, какая подсказка использовалась, если вы их динамически загружаете/передаете, а затем вам нужно сопоставить, какие подсказки соответствуют каким путям кода.

Обратите внимание, что все это становится сложнее, если вам потребуется изменить несколько подсказок для A/B-теста, потому что у вас есть несколько из них в потоке. Например, у вас есть две подсказки в вашем рабочем процессе, и вы меняете LLM, вы захотите провести A/B-тестирование изменения в целом, а не отдельно для каждой подсказки. Наш совет: поместите подсказки в код, и ваша операционная деятельность станет проще, поскольку вы будете знать, какие две подсказки принадлежат к каким путям кода, не прибегая к ментальному отображению.

Сводка

В этом посте мы объяснили два подхода к управлению подсказками в рабочей среде с помощью Hamilton. Первый подход рассматривает подсказки как динамические переменные времени выполнения, в то время как второй подход рассматривает подсказки как код для настроек производства. Если вам важно снижение операционной нагрузки, то наш совет – закодируйте подсказки как код, так как это более просто с операционной точки зрения, если только скорость их изменения действительно имеет значение для вас.

Для резюмирования:

  1. Подсказки как динамические переменные времени выполнения. Используйте внешнюю систему для передачи подсказок в ваши потоки данных Hamilton или используйте Hamilton для их извлечения из базы данных. Для отладки и мониторинга важно иметь возможность определить, какая подсказка была использована для определенного вызова. Вы можете интегрировать инструменты с открытым исходным кодом или использовать что-то вроде платформы DAGWorks, чтобы убедиться, что вы знаете, что было использовано для любого вызова вашего кода.
  2. Подсказки как код. Кодирование подсказок как кода позволяет легко версионировать их с помощью git. Управление изменениями может осуществляться с помощью запросов на включение изменений и проверок CI/CD. Он хорошо работает с функциями Hamilton, такими как @config.when и переключение модуля на уровне драйвера, потому что четко определяет, какая версия подсказки используется. Этот подход усиливает использование любых инструментов, которые вы можете использовать для мониторинга или отслеживания, таких как платформа DAGWorks, так как подсказки для развертывания являются неизменными.

Мы хотим услышать вас!

Если вы взволнованы чем-то из этого или имеете сильные мнения, оставьте комментарий или зайдите в наш канал на Slack! Некоторые ссылки для похвалы/жалобы/общения:

  • 📣 присоединяйтесь к нашему сообществу в Slack – мы с удовольствием поможем ответить на ваши вопросы или помочь вам начать работу.
  • ⭐️ добавьте нас в избранное на GitHub.
  • 📝 оставьте нам сообщение, если что-то найдете.
  • 📚 прочитайте нашу документацию.
  • ⌨️ интерактивно изучайте Hamilton в вашем браузере.

Другие ссылки/посты о Hamilton, которые могут вас заинтересовать:

  • tryhamilton.dev – интерактивное руководство в вашем браузере!
  • Hamilton + Lineage за 10 минут
  • Как использовать Hamilton с Pandas за 5 минут
  • Как использовать Hamilton с Ray за 5 минут
  • Как использовать Hamilton в среде Notebook
  • Общая история и введение в Hamilton
  • Преимущества создания потоков данных с помощью Hamilton (Органический пользовательский пост о Hamilton!)