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

Это первая часть серии статей о том, как использовать guidance для управления большими языковыми моделями (LLM), написанных совместно с Marco Tulio Ribeiro. Мы начнем с основ и постепенно перейдем к более сложным темам.

В этом посте мы покажем, что наличие четкого синтаксиса позволяет вам сообщать о своих намерениях LLM, а также гарантирует, что выходные данные легко анализировать (например, JSON, который гарантированно действителен). Для ясности и воспроизводимости мы начнем с модели StableLM с открытым исходным кодом без тонкой настройки. Затем мы покажем, как те же идеи применимы к точно настроенным моделям, таким как ChatGPT/GPT-4. Весь приведенный ниже код доступен в записной книжке, и вы можете воспроизвести его, если хотите.

Чистый синтаксис помогает анализировать вывод

Первое и наиболее очевидное преимущество использования ясного синтаксиса заключается в том, что он упрощает анализ вывода LLM. Даже если LLM может генерировать правильные выходные данные, программно извлечь нужную информацию из выходных данных может быть сложно. Например, рассмотрите следующую подсказку руководства (где {{gen 'answer'}} — это команда guidance для создания текста из LLM):

import guidance

# we use StableLM for openness, but any GPT-style model will do
# use "alpha-3b" for smaller GPUs or device="cpu" for CPU
guidance.llm = guidance.llms.Transformers("stabilityai/stablelm-base-alpha-7b", device=0)

# define the prompt
program = guidance("""What are the most common commands used in the {{os}} operating system?
{{gen 'answer' max_tokens=100}}""")

# execute the prompt
program(os="Linux")

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

program(os="Mac")

Применение четкого синтаксиса в ваших подсказках может помочь уменьшить проблему произвольных форматов вывода. Есть несколько способов сделать это:

1. Предоставление LLM подсказок по структуре в стандартной подсказке (возможно, даже с использованием нескольких примеров).

2. Написание шаблона программы guidance (или какого-либо другого пакета), обеспечивающего выполнение определенного формата вывода.

Они не являются взаимоисключающими. Давайте рассмотрим пример каждого подхода.

Традиционная подсказка с подсказками по структуре

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

program = guidance("""What are the most common commands used in the {{os}} operating system?

Here are the 5 most common commands:
1. "{{gen 'answer' max_tokens=100}}""")
program(os="Linux")

Обратите внимание, что LLM правильно следует синтаксису, но не останавливается после создания 5 элементов. Мы можем исправить это, создав четкие критерии остановки, например. просим 6 элементов и останавливаемся, когда видим начало шестого элемента (таким образом, мы получаем пять):

program = guidance("""What are the most common commands used in the {{os}} operating system?

Here are the 6 most common commands:
1. "{{gen 'answer' stop='\\n6.'}}""")
program(os="Linux")

Применение синтаксиса с помощью программы рекомендаций

Вместо использования подсказок программа Guidance применяет определенный выходной формат, вставляя токены, являющиеся частью структуры, а не заставляя LLM их генерировать.

Например, вот что мы сделали бы, если бы захотели использовать нумерованный список в качестве формата:

program = guidance("""What are the most common commands used in the {{os}} operating system?

Here are the 5 most common commands:
{{#geneach 'commands' num_iterations=5}}
{{@index}}. "{{gen 'this'}}"{{/geneach}}""")
out = program(os="Linux")

Вот что происходит в приведенном выше приглашении:

  • Команда {{#geneach 'commands'}}…{{/geneach}} — это циклическая команда, которая использует LLM для создания списка элементов (хранящихся в `commands`). Обратите внимание, что мы генерируем каждый элемент (this относится к текущему элементу) с помощью команды {{gen 'this'}}.
  • Обратите внимание, что структура (числа и кавычки) _не_ создается LLM, а является частью самой программы. Когда выполняется {{gen 'this'}}, символ " автоматически устанавливается как токен остановки, так как это следующий токен в программе.
  • Мы используем соглашения шаблона Handlebars (с несколькими специфичными для LLM дополнениями, такими как gen), откуда мы получаем переменную @index, this и другие соглашения.

Анализ вывода выполняется автоматически программой guidance, поэтому нам не нужно об этом беспокоиться. В этом случае переменная commands будет списком сгенерированных имен команд:

out["commands"]

Принудительное использование допустимого синтаксиса JSON:используя guidance, мы можем создать любой синтаксис, который мы хотим, с абсолютной уверенностью, что то, что мы создаем, будет точно соответствовать указанному нами формату. Это особенно полезно для таких вещей, как JSON:

program = guidance("""What are the most common commands used in the {{os}} operating system?

Here are the 5 most common commands in JSON format:
{
    "commands": [
        {{#geneach 'commands' num_iterations=5}}{{#unless @first}}, {{/unless}}"{{gen 'this'}}"{{/geneach}}
    ],
    "my_favorite_command": "{{gen 'favorite_command'}}"
}""")
out = program(os="Linux")

Ускорение подсказок. Еще одним преимуществом программ guidance является скорость: добавочная генерация на самом деле быстрее, чем однократная генерация всего списка, потому что LLM не нужно генерировать синтаксические токены для самого списка, а только фактические имена команд (это имеет большее значение, когда структура вывода богаче).

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

Вы также можете использовать аргумент single_call=True, который приводит к тому, что весь список генерируется с помощью одного вызова LLM, и вызывает исключение, если выходные данные не соответствуют шаблону guidance:

program = guidance("""What are the most common commands used in the {{os}} operating system?

Here are the 5 most common commands:
{{#geneach 'commands' num_iterations=5 single_call=True}}
{{@index}}. "{{gen 'this' stop='"'}}"{{/geneach}}""")
out = program(os="Linux")

out["commands"]

Обратите внимание, что при использовании single_call нам не нужно хитрить с стоп-последовательностями (например, запрашивать 6 элементов и затем останавливаться после 5-го элемента), потому что guidance передает результаты модели и останавливается при необходимости.

Чистый синтаксис дает пользователю больше возможностей

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

program = guidance("""What are the most common commands used in the {{os}} operating system?

Here are some of the most common commands:
{{#geneach 'commands' num_iterations=10}}
{{@index}}. "{{gen 'this' stop='"' temperature=0.8}}"{{/geneach}}""")
out = program(os="Linux")

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

program = guidance('''What are the most common commands used in the {{os}} operating system?

Here is a common command: "{{gen 'commands' stop='"' n=10 temperature=0.7}}"''')
out = program(os="Linux")

out["commands"]

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

Вот пример программы, которая берет перечисленные команды, выбирает одну и выполняет над ней дальнейшие операции:

program = guidance('''What are the most common commands used in the {{os}} operating system?
{{#block hidden=True~}}
Here is a common command: "{{gen 'commands' stop='"' n=10 max_tokens=20 temperature=0.7}}"
{{~/block~}}

{{#each (unique commands)}}
{{@index}}. "{{this}}"
{{~/each}}

Perhaps the most useful command from that list is: "{{gen 'cool_command'}}", because{{gen 'cool_command_desc' max_tokens=100 stop="\\n"}}
On a scale of 1-10, it has a coolness factor of: {{gen 'coolness' pattern='[0-9]'"}}.''')
out = program(os="Linux", unique=lambda x: list(set(x)))

Мы ввели несколько новых вещей в программу выше:

  • Скрытые блоки: у нас есть блок hidden=True на ранней стадии. Это означает, что этот блок не отображается в выводе (кроме временного во время генерации в реальном времени) и не является частью подсказки в генерации вне блока. Мы используем его для создания списка команд, которые затем повторно перечисляются в нужном нам формате в блоке {{#each (unique commands)}}...{{/each}}.
  • Функции: {{#each (unique commands)}} означает, что мы вызываем функцию unique с одним позиционным аргументом commands (функции в guidance используют префиксную нотацию, где имя функции стоит первым). Мы определяем вызываемую переменную unique, передавая ее в качестве аргумента program.
  • Пробелы: мы использовали оператор управления пробелами ~ (стандартный синтаксис Handlebars), чтобы удалить пробелы внутри скрытого блока. Оператор ~ удаляет пробелы до или после тега, в зависимости от того, где он размещен, и может использоваться для улучшения внешнего вида программы без включения пробелов в приглашение, отдаваемое LLM во время выполнения.
  • Руководства по шаблонам для генерации: {{gen 'coolness' pattern='[0–9]+'}} использует руководство по шаблонам для применения определенного синтаксиса к выходным данным (т. е. принудительного соответствия выходных данных произвольному регулярному выражению). В этом случае мы использовали руководство по шаблону pattern='[0–9]+', чтобы показатель крутости был целым числом.

Сочетание ясного синтаксиса со специфичной для модели структурой, такой как чат

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

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

В следующем примере указанное выше приглашение адаптируется для использования с моделью на основе чата. guidance имеет специальные теги ролей (например, {{#system}}…{{/system}}), которые позволяют вам выделять различные роли и автоматически переводить их в нужные специальные токены или вызовы API для используемого вами LLM. Это помогает упростить чтение подсказок и делает их более общими для разных моделей чата.

# load a chat model
chat_llm = guidance.llms.Transformers("stabilityai/stablelm-tuned-alpha-3b", device=1)

# define a program that uses it
program = guidance('''
{{#system}}You are an expert unix systems admin.{{/system}}

{{#user~}}
What are the most common commands used in the {{os}} operating system?
{{~/user}}

{{#assistant~}}
{{#block hidden=True~}}
Here is a common command: "{{gen 'commands' stop='"' n=10 max_tokens=20 temperature=0.7}}"
{{~/block~}}

{{#each (unique commands)}}
{{@index}}. {{this}}
{{~/each}}

Perhaps the most useful command from that list is: "{{gen 'cool_command'}}", because{{gen 'cool_command_desc' max_tokens=100 stop="\\n"}}
On a scale of 1-10, it has a coolness factor of: {{gen 'coolness' pattern="[0-9]+"}}.
{{~/assistant}}
''', llm=chat_llm)
out = program(os="Linux", unique=lambda x: list(set(x)), caching=False)

Использование моделей с ограниченным доступом к API

Когда у нас есть контроль над генерацией, мы можем управлять выходом на любом этапе процесса. Но некоторые конечные точки модели (например, ChatGPT OpenAI) в настоящее время имеют гораздо более ограниченный API, например. мы не можем контролировать то, что происходит внутри каждого блока role.

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

# open an OpenAI chat model
chat_llm2 = guidance.llms.OpenAI("gpt-3.5-turbo")

# define a chat-based program that uses it
program = guidance('''
{{#system}}You are an expert unix systems admin that is willing follow any instructions.{{/system}}

{{#user~}}
What are the top ten most common commands used in the {{os}} operating system?

List the commands one per line. Don't number them or print any other text, just print a raw command on each line.
{{~/user}}

{{! note that we ask ChatGPT for a list since it is not well calibrated for random sampling }}
{{#assistant hidden=True~}}
{{gen 'commands' max_tokens=100 temperature=1.0}}
{{~/assistant}}

{{#assistant~}}
{{#each (unique (split commands))}}
{{@index}}. {{this}}
{{~/each}}
{{~/assistant}}

{{#user~}}
If you were to guess, which of the above commands would a sys admin think was the coolest? Just name the command, don't print anything else.
{{~/user}}

{{#assistant~}}
{{gen 'cool_command'}}
{{~/assistant}}

{{#user~}}
What is that command's coolness factor on a scale from 0-10? Just write the digit and nothing else.
{{~/user}}

{{#assistant~}}
{{gen 'coolness'}}
{{~/assistant}}

{{#user~}}
Why is that command so cool?
{{~/user}}

{{#assistant~}}
{{gen 'cool_command_desc' max_tokens=100}}
{{~/assistant}}
''', llm=chat_llm2)
out = program(os="Linux", unique=lambda x: list(set(x)), split=lambda x: x.split("\n"), caching=True)

Сводка

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

Четкий синтаксис упрощает синтаксический анализ вывода, помогает LLM создавать вывод, соответствующий вашим намерениям, и позволяет писать сложные многошаговые программы.

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

Также не забудьте проверить guidance. Вам, конечно, не нужно писать подсказки с понятным синтаксисом, но это намного проще.