LLM APIでテキストベースのロールプレイシナリオを作成
ロールプレイテキストベースのシナリオは、モデルがルール付きのナレーターを演じ、コードが状態を管理する場合に最も効果的です。この記事では、Pythonでの小さなエンジン(プロンプト、ストリーミング、メモリ、長時間セッションのコスト見積もり)を紹介します。

主要ポイント
- 分担しましょう:モデルが物語を書き、コードが事実、インベントリ、ルールを保存します。
- 返信をストリーミングし、ストリームを短く保って同時実行数のスロットをすばやく解放しましょう。
- 100,000トークンのウィンドウは非常に長いセッションを保持できますが、要約によりコストとドリフトを抑制します。
- 1ターンあたり6,000入力トークン、400出力トークンの200ターンセッションは、約$0.22かかります。
プロットではなくナレーターから始める
ロールプレイのテキストベースのシナリオを作成する人は、しばしばプロットを書くことから始めます。それは順序が逆です。シナリオはプレイヤーとナレーター間のループであり、ナレーターにはストーリーよりも前に役割説明が必要です。モデルは、誰が話すか、プレイヤーのために何を決定できるか、返信の長さはどのくらいか、そして何をすべきではないかを知らなければなりません。それ以外はすべて即興で構いません。
シナリオエンジン用の優れたシステムプロンプトには4つのパートが必要です。まずロール:ゲームマスター、インタビュアー、船のコンピュータ、演劇のキャラクターなど。次に、世界のルールを簡潔な文章で:トーン、舞台設定、存在するものと存在しないもの。次に、コードが解析する可能性があるためフォーマットルール:例えば「各返信の末尾にOPTIONS:で始まる1行を追加する」など。最後に境界線。ここでのテキストAPIは、法的な成人向け利用に対してコンテンツ拒否なしで回答するようにチューニングされており、ダークフィクション、真の脅威を持つ悪役、道徳的にグレーなプロットに適しています。ただし、未成年者を含む性的コンテンツという1つの明確な制限があります。このルールをあなたの利用規約にも記載し、シナリオが成人向けの場合、アプリに年齢確認を実装してください。
プロンプトを約600語に抑えてください。長いルールリストは半分しか従われません。多くのルールが必要な場合は、優先順位を付けて下三分の一を削除してください。
ストリーミング付きの動作ループ
ロールプレイではストリーミングが最も重要です。プレイヤーは物語を読みながらモデルが書き進める様子を見たいと考えており、300トークンの返信が一度に届くと、実際よりも遅く感じられます。APIはstream: trueでサーバー送信イベント(SSE)経由でストリーミングし、OpenAI SDKが解析を処理します。エンドポイントはOpenAI互換で、モデルIDは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モードを使用する2番目の安価な呼び出しです。最初の呼び出しはプレイヤー向けの物語を書き、2番目の呼び出しは新しい段落を読み取り、変更された部分のみを返します。
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返信の解析に失敗した場合は、そのターンの更新をスキップして次に進みます。1つの事実の見落としは、クラッシュよりも安価です。
サイコロ、体力、タイマーはプレーンコードで処理するのが最適です。プレイヤーが危険なことを試みるとき、Pythonでサイコロを振り、ナレーターに結果を伝えます:「プレイヤーの試みは失敗しました」。その後モデルが結果を書き出します。これにより、言語モデルがしばしば行うように、モデルがプレイヤーに毎回勝利を許すのを防げます。
長時間実行されるセッションのメモリ
コンテキストウィンドウは、プロンプトと補完を合わせて100,000トークンです。典型的な250トークンの返信と30トークンのプレイヤーメッセージは、1ターンあたり約280トークンとなり、生の履歴だけで何百ものターンを保持できます。すべて送信できます。多くのビルダーはまだこれを行いません。その理由は2つあります:履歴全体が毎回入力として請求されるため、コストはターンごとに増大すること、および古い詳細がモデルを混乱させる可能性があることです。
適切なレイアウトは3つのレイヤーを保持します:
- 固定:システムプロンプトとステートブロック。常に送信されます。
- 要約:発生したことの進行中の段落または2段落。短い呼び出しで10〜15ターンごとに書き直されます。
- 最近のターン:最後の10〜20メッセージをそのまま。これにより文章のリズムを維持します。
直近のターンのウィンドウがいっぱいになったら、最も古い10ターンを要約に組み込み、それらを削除します。要約呼び出しには、低い温度と300トークンの制限を使用できます。名前、約束、解決されていない脅威が要約に含まれていれば、プレイヤーは圧縮に気づくことはほとんどありません。要約用のプロンプトでそれを明示してください:「名前、借金、怪我、未解決の質問を保持する」
キャラクターに一貫した声を持たせたい場合は、固定レイヤーに短いスタイルカードを提供してください:その発話の例文3行と、絶対に言わないこと2つ。短い例は形容詞よりもトーンを形成します。
セッションのコスト
料金は入力トークン100万トークンあたり$0.15、出力トークン100万トークンあたり$0.50で、前払いクレジットが必要でサブスクリプションはありません。長いセッションを計算してみましょう。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になります。まだ手頃な価格ですが、品質の向上なしに請求額の4倍です。多くのプレイヤーがいるアプリでは、この差が利益率を決定します。
失敗または拒否されたリクエストは課金されず、クレジットは期限切れにならないため、未使用の残高は単に待機します。チャージは$20から$500までドル単位で実行でき、$50からは5%ボーナス、$100からは10%ボーナス、$200からは15%ボーナスが得られます。オプションの全リストについては、価格ページをご覧ください。
複数のプレイヤーを同時に実行する
フルキーは8つの同時リクエストと1分あたり300リクエストを許可します。ストリーミングされたリクエストはストリームが終了するまでスロットを保持するため、8つのスロットと約10秒かかる返信がある場合、他に何も実行されていないと仮定して、1分あたり約48のストリームされた返信を提供できます。これは負荷や返信の長さによって速度が異なるため、測定値ではなく大まかなルールです。重要なのはそれを見積もることです:ゲームのスロットよりも同時プレイヤーが多い場合、サーバーでリクエストをキューに入れ、一度にすべてを送信する代わりに、「ナレーターが考えています」という短い状態を表示してください。
制限を超えると、APIはHTTP 429を返します。コードは、1分あたりのリクエストに対してはrate_limited、一度に多すぎる場合はconcurrencyです。一時的な過負荷も429を返し、Retry-After: 5ヘッダーが含まれます。その5秒待機する単純なリトライだけで、カジュアルなゲームには十分です。レスポンスヘッダーも確認してください:X-RateLimit-Limit-RequestsとX-RateLimit-Limit-Concurrencyは上限を示し、X-Balance-USDは残高を示します。これは管理画面で表示できます。
状態抽出呼び出しを次のプレイヤーターンと並行して実行しないでください。それを待ってください。そうしないと、ナレーターは古い事実に基づいて書き出します。良い妥協案は、ストリームが終了した直後に抽出を開始し、プレイヤーがそれを実行している間読み取れるようにすることです。
シーンを画像パイプラインに接続する
このサイトはStable Diffusionのワークフローでテキストモデルを使用する人のために構築されており、シナリオもそれに適しています。API自体はテキストのみで、画像、音声、ビデオは生成しません。ただし、ナレーターはシーン説明を記述でき、あなたのパイプラインがそれを画像プロンプトに変換します。主要なシーン変更後にJSON呼び出しを1つ追加し、照明、雰囲気、カメラアングルを含む設定の短い視覚的説明を求め、その文字列を実行している画像ツールに渡してください。ドキュメントページにはプロンプト作成呼び出しのリクエスト形状が示されており、SDプロンプトジェネレーターに関する記事でさらに詳しく説明されています。
エンジンを試すには、アカウントを作成してトライアルを受け取ってください。新規ユーザーは、APIキーページでワンクリックで$0.50のトライアルクレジットを取得でき、カード不要で7日間有効です。上記の数字によると、これは数回分の完全なセッションに十分です。トライアルキーは同時リクエストが2件、1分あたり60件に制限されており、単独テストに適しています。
フィクションはフィクションとして扱ってください。成人向けシナリオは関係する全員が成人であれば法的な利用となります。絶対的なルールは1つだけで、未成年者を含む性的コンテンツがないことです。APIに送信されるプロンプトはトレーニングには使用されません。
その他のガイド
質問と回答
モデルはあらゆるキャラクターやジャンルを演じられますか?
合法的な成人向けリクエストを拒否しないよう調整された汎用テキストモデルであり、ホラー、犯罪、ドラマ、コメディはすべて問題ありません。唯一の制限は未成年者を含む性的コンテンツです。
コンテキストが一杯になる前にセッションはどれくらい続けられますか?
コンテキストウィンドウはプロンプトとcompletionを合わせて100,000トークンです。要約レイヤーがあれば、古いターンを圧縮できるため、実際にはその先まで実行できます。
返信はストリーミングすべきですか?
はい、人がリアルタイムで読むあらゆるコンテンツに適用できます。ストリーミングにより最終的な使用量が提供され、ストリームは完了するまで同時実行数のスロットを1つ占有します。
シーンを正確に繰り返す方法はありますか?
同じメッセージと設定で同じシードを渡してください。これはテスト用の補助機能であり、バイト単位の完全一致を保証するものではありません。
キーはフォーム1つで手に入ります
アカウントを作成し、キーをコピーし、ベースURLを変更します。これでセットアップは完了です。