RU ▾
Получить API-ключ

Текстовые ролевые сценарии с LLM API

Обновлено

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

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

Ключевые моменты

  • Разделите работу: модель пишет историю, ваш код хранит факты, инвентарь и правила.
  • Используйте потоковую передачу ответов и держите каждый поток достаточно коротким, чтобы быстро освободить слот параллельных запросов.
  • Контекстное окно на 100 000 токенов вмещает очень длинную сессию, но суммаризация снижает затраты и дрейф.
  • Сессия из 200 реплик при 6 000 входных и 400 выходных токенах на реплику стоит около $0,22.

Начните с рассказчика, а не с сюжета

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

Хороший системный промпт для движка сценария состоит из четырёх частей. Сначала идёт роль: гейм-мастер, интервьюер, бортовой компьютер корабля, персонаж в пьесе. Далее — правила мира несколькими простыми предложениями: тон, сеттинг, что существует, а что нет. Третье — правила форматирования, так как ваш код может их парсить: например, «завершайте каждый ответ одной строкой, начинающейся с OPTIONS:». Четвёртое — границы. Текстовый API здесь настроен на ответы без отказов по контенту для легального использования взрослыми, что делает его подходящим для мрачной прозы, злодеев с реальной угрозой и морально неоднозначных сюжетов. У него всё же есть одна жёсткая граница: нет сексуального контента с участием несовершеннолетних. Включите это правило и в свои условия использования, а также настройте проверку возраста в приложении, если ваши сценарии рассчитаны на взрослых.

Держите промпт около 600 слов. Длинные списки правил выполняются частично. Если вам нужно много правил, расставьте их по приоритету и удалите нижнюю треть.

Рабочий цикл с потоковой передачей

Потоковая передача важнее в ролевых играх, чем в большинстве других применений. Игрок, читающий историю, хочет видеть слова по мере их написания моделью, а ответ на 300 токенов кажется медленнее, чем он есть на самом деле, если он приходит сразу. API потоково передаёт данные через серверные события (SSE) с stream: true, а SDK OpenAI обрабатывают парсинг. Эндпоинт совместим с OpenAI, идентификатор модели — uncensored, а базовый URL для этого сайта — https://api.sdxlapi.com/v1.

from openai import OpenAI

client = OpenAI(base_url="https://api.sdxlapi.com/v1", api_key="YOUR_KEY")

SYSTEM = """You are the narrator of a noir mystery set in a rainy harbour city.
Write in second person, present tense, 120 to 200 words per reply.
Never decide what the player says or does. Invent details freely, but stay
consistent with the STATE block you are given.
End every reply with one line: OPTIONS: a | b | c"""

state = {"place": "Pier 9 office", "time": "23:40", "inventory": ["brass key"], "facts": []}
history = []

def turn(player_text: str) -> str:
    history.append({"role": "user", "content": player_text})
    msgs = [{"role": "system", "content": SYSTEM + "\nSTATE: " + str(state)}] + history[-20:]
    stream = client.chat.completions.create(
        model="uncensored", messages=msgs, stream=True,
        temperature=0.9, top_p=0.95, max_tokens=500,
        presence_penalty=0.3,
    )
    reply = ""
    for chunk in stream:
        if chunk.choices and chunk.choices[0].delta.content:
            piece = chunk.choices[0].delta.content
            reply += piece
            print(piece, end="", flush=True)
    history.append({"role": "assistant", "content": reply})
    return reply

turn("I check the desk drawer.")

Настройки подобраны для художественных текстов. Температура около 0,8–1,0 даёт разнообразную прозу, а top_p около 0,95 отсекает самые странные варианты слов. Небольшой штраф за присутствие сдерживает рассказчика от возвращения к тем же самым образам. Установите max_tokens, чтобы ограничить длину ответа; это необязательно, но без этого болтливый рассказчик может написать гораздо больше, чем вы хотели. Для сцены, которую вы хотите воспроизвести точно во время тестирования, передайте seed и зафиксируйте другие параметры.

Пусть код владеет фактами

Самая частая проблема в длинных ролевых играх — дрейф. Ключ лежит в кармане игрока на 5-м ходу и на столе на 40-м. Трактирщицу звали Марой, потом Мартой. Модель, читающая 30 000 токенов истории, часто справится с этим правильно, но «часто» — это не архитектура. Храните факты в небольшой структуре, которую обновляет ваша программа, и передавайте эту структуру в каждом запросе, как это делает строка STATE выше.

Как обновить состояние? Попросите модель отчитаться об изменениях после каждого хода, во втором, дешёвом вызове, использующем JSON-режим. Первый вызов пишет историю для игрока. Второй читает новый фрагмент и возвращает только то, что изменилось.

import json

def extract_changes(passage: str) -> dict:
    r = client.chat.completions.create(
        model="uncensored",
        response_format={"type": "json_object"},
        temperature=0,
        max_tokens=250,
        messages=[
            {"role": "system", "content":
             'Read the passage. Return JSON: {"place": str|null, "inventory_add": [str], '
             '"inventory_remove": [str], "new_facts": [str]}. Only report what the passage states.'},
            {"role": "user", "content": passage},
        ],
    )
    return json.loads(r.choices[0].message.content)

def apply(ch: dict):
    if ch.get("place"):
        state["place"] = ch["place"]
    for item in ch.get("inventory_add", []):
        if item not in state["inventory"]:
            state["inventory"].append(item)
    state["inventory"] = [i for i in state["inventory"] if i not in ch.get("inventory_remove", [])]
    state["facts"] = (state["facts"] + ch.get("new_facts", []))[-30:]

Ограничьте список фактов, как это делает срез, иначе блок состояния будет расти, пока не станет таким же тяжёлым, как история, которую он должен заменить. Если ответ JSON не удалось распарсить, пропустите обновление для этой реплики и переходите дальше; один пропущенный факт дешевле, чем сбой.

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

Память для сессий, длящихся часами

Окно контекста составляет 100 000 токенов, считая промпт и завершение вместе. Типичный ответ из 250 токенов плюс сообщение игрока из 30 токенов — это около 280 токенов на реплику, поэтому одна лишь сырая история могла бы вместить сотни реплик. Вы можете отправить всё это. Многие разработчики всё равно не делают этого по двум причинам: стоимость растёт с каждой репликой, так как вся история каждый раз оплачивается как входные данные, а старые детали могут отвлекать модель.

Разумная структура поддерживает три уровня:

  1. Закреплены: системный промпт и блок состояния. Отправляются всегда.
  2. Суммаризация: абзац-другой о том, что произошло, который переписывается каждые 10–15 реплик коротким вызовом.
  3. Недавние реплики: последние 10–20 сообщений дословно, чтобы проза сохраняла свой ритм.

Когда окно недавних реплик заполняется, сворачивайте 10 самых старых в сводку и удаляйте их. Вызов сводки может использовать низкую температуру и лимит в 300 токенов. Игроки редко замечают сжатие, пока имена, обещания и неразрешённые угрозы попадают в сводку. Явно укажите это в промпте суммаризации: «сохраните имена, долги, травмы, открытые вопросы».

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

Стоимость сессии

Стоимость составляет $0,15 за 1 млн входных токенов и $0,50 за 1 млн выходных токенов, с предоплаченным балансом и без подписки. Разберём длинную сессию. Скажем, 200 реплик, с закреплённым слоем, суммаризацией и недавними репликами, что в сумме даёт в среднем 6 000 входных токенов на запрос и 400 выходных токенов на ответ (ответ с историей плюс небольшая доля на вызовы состояния и суммаризации).

Входные данные: 200 x 6 000 = 1 200 000 токенов, что составляет 1,2 x $0,15 = $0,18. Выходные данные: 200 x 400 = 80 000 токенов, что составляет 0,08 x $0,50 = $0,04. Сессия стоит $0,22.

Сравните ленивую версию, которая отправляет всю сырую историю. К 200-й реплике история составляет около 56 000 токенов, а в среднем по сессии входные данные составляют около 28 000 токенов на запрос. Это 200 x 28 000 = 5 600 000 входных токенов, или 5,6 x $0,15 = $0,84, плюс такие же $0,04 за выходные данные, что составляет $0,88. Всё ещё доступно, но счёт в четыре раза выше без улучшения качества. Для приложения с множеством игроков эта разница определяет вашу маржу.

Неудачные или отклонённые запросы не оплачиваются, а баланс не сгорает, поэтому неиспользованный баланс просто ждёт. Пополнения от $20 до $500 целыми долларами, с бонусом 5% от $50, 10% от $100 и 15% от $200. См. страницу с ценами для полного списка опций.

Запуск множества игроков одновременно

Полный ключ позволяет 8 параллельных запросов и 300 запросов в минуту. Потоковый запрос удерживает свой слот до конца потока, поэтому с 8 слотами и ответами, занимающими около десяти секунд, вы можете обслуживать примерно 48 потоков ответов в минуту, если ничего другого не работает. Это грубое правило, а не измеренная цифра, так как скорость варьируется в зависимости от нагрузки и длины ответа. Цель — спланировать это: если в вашей игре больше одновременных игроков, чем слотов, поставьте запросы в очередь на вашем сервере и покажите короткое состояние «рассказчик думает», вместо того чтобы отправлять всё сразу.

Когда вы превышаете лимит, API возвращает HTTP 429. Код ошибки rate_limited для запросов в минуту и concurrency для слишком большого числа одновременных запросов. Временная перегрузка также даёт 429 с заголовком Retry-After: 5. Простая повторная попытка с ожиданием этих пяти секунд достаточна для неспешной игры. Также читайте заголовки ответа: X-RateLimit-Limit-Requests и X-RateLimit-Limit-Concurrency показывают ваши лимиты, а X-Balance-USD показывает остаток, который можно отобразить на экране администратора.

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

Подключение сцен к вашему конвейеру изображений

Этот сайт создан для людей, которые используют текстовые модели в рабочих процессах Stable Diffusion, и сценарии хорошо для этого подходят. Сам API работает только с текстом: он не генерирует изображения, аудио или видео. Но рассказчик может написать описание сцены, которое ваш конвейер превратит в промпт для изображения. После крупного изменения сцены сделайте ещё один вызов JSON, запросив краткое визуальное описание окружения с освещением, настроением и углом камеры, а затем передайте эту строку любому инструменту для изображений, который вы используете. На странице документации показана структура запроса для вызовов генерации промптов, а статья о генераторе промптов SD раскрывает этот вопрос глубже.

Чтобы попробовать движок, создайте аккаунт и получите пробный баланс. Новые пользователи могут получить $0,50 пробного баланса одним кликом на странице API-ключа, срок действия — 7 дней, карта не нужна. Судя по приведённым выше цифрам, этого хватит на несколько полных сессий. Пробные ключи ограничены 2 параллельными запросами и 60 запросами в минуту, что подходит для одиночного тестирования.

Не выводите вымысел за пределы вымысла. Сценарии для взрослых — это законное использование, когда все участники взрослые; единственное правило, не допускающее исключений: нет сексуального контента с участием несовершеннолетних. Промпты, отправляемые в API, не используются для обучения.

Вопросы и ответы

Может ли модель играть любую роль или жанр?

Это универсальная текстовая модель, настроенная так, чтобы не отказывать в законных запросах для взрослых, поэтому хоррор, криминал, драма и комедия — всё подходит. Единственное строгое ограничение — сексуальный контент с участием несовершеннолетних.

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

Окно составляет 100 000 токенов для промпта плюс завершения. Со слоем сводки на практике можно выйти значительно дальше, так как старые реплики сжимаются.

Стоит ли использовать потоковую передачу ответов?

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

Есть ли способ заставить сцену повториться точно?

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

Ваш ключ — в одной форме от вас

Создайте аккаунт, скопируйте ключ, измените базовый URL. Это вся настройка.

Получить API-ключ