KO ▾
API 키 받기

LLM API로 텍스트 기반 역할극 시나리오 구축하기

업데이트됨

역할극 텍스트 기반 시나리오는 모델이 규칙에 따라 내레이터 역할을 수행하고, 코드에서 상태를 관리할 때 가장 잘 작동합니다. 이 글에서는 Python으로 프롬프트, 스트리밍, 메모리, 긴 세션 비용 추정을 다루는 작은 엔진을 구현하는 방법을 소개합니다.

텍스트 역할극 시나리오를 나타내는 빛나는 말풍선이 있는 작은 무대 디오라마

주요 포인트

  • 작업을 분할하세요: 모델이 이야기를 작성하고 코드가 사실, 인벤토리 및 규칙을 저장합니다.
  • 답변을 스트리밍하고 각 스트림이 동시성 슬롯을 빠르게 해제할 수 있도록 짧게 유지하세요.
  • 100,000 토큰의 컨텍스트 창은 매우 긴 세션을 유지할 수 있지만, 요약을 통해 비용과 드리프트를 낮게 유지할 수 있습니다.
  • 턴당 입력 토큰 6,000개, 출력 토큰 400개로 구성된 200턴 세션의 비용은 약 $0.22입니다.

플롯보다 내레이터로 시작하세요

역할극 텍스트 기반 시나리오를 구축하는 사람들은 종종 플롯 작성부터 시작합니다. 이는 순서가 바뀐 접근입니다. 시나리오는 플레이어와 내레이터 간의 루프이며, 내레이터는 이야기보다 먼저 직무 설명이 필요합니다. 모델은 누가 말하는지, 플레이어를 위해 무엇을 결정할 수 있는지, 답변이 얼마나 길어야 하는지, 그리고 무엇을 절대 하지 말아야 하는지 알아야 합니다. 나머지는 모두 즉흥적으로 처리할 수 있습니다.

시나리오 엔진을 위한 좋은 시스템 프롬프트는 네 부분으로 구성됩니다. 첫째, 역할입니다: 게임 마스터, 인터뷰어, 선박 내 컴퓨터, 연극 속 캐릭터 등입니다. 둘째, 몇 문장으로 된 세계 규칙입니다: 톤, 배경, 존재하는 것과 존재하지 않는 것 등입니다. 셋째, 코드가 구문 분석할 수 있는 형식 규칙입니다. 예를 들어 "각 답변 끝에 OPTIONS:로 시작하는 한 줄을 추가하세요"와 같습니다. 넷째, 경계입니다. 여기서의 텍스트 API는 합법적인 성인 사용에 대해 콘텐츠 거부 없이 응답하도록 조정되어 있어, 다크 픽션, 실제 위압감을 가진 악당 및 도덕적으로 회색 지대의 플롯에 적합합니다. 여전히 하나의 명확한 제한이 있습니다: 미성년자가 포함된 성적인 콘텐츠는 금지됩니다. 이 규칙을 자체 이용약관에도 명시하고, 시나리오가 성인을 대상으로 한다면 앱에 연령 확인을 적용하세요.

프롬프트는 약 600 단어 미만으로 유지하세요. 긴 규칙 목록은 절반만 따르는 경향이 있습니다. 규칙이 많다면 우선순위를 정하고 가장 중요도가 낮은 하위 3분의 1을 삭제하세요.

스트리밍이 포함된 작동 루프

스트리밍은 역할극에서 대부분의 사용 사례보다 더 중요합니다. 이야기를 읽는 플레이어는 모델이 작성하는 동안 단어가 표시되기를 원하며, 300 토큰의 답변이 한 번에 도착하면 실제로보다 느리게 느껴집니다. API는 stream: true를 사용하여 서버 전송 이벤트를 통해 스트리밍하며, OpenAI SDK들은 구문 분석을 처리합니다. 엔드포인트는 OpenAI와 호환되며, 모델 ID는 uncensored이고 이 사이트의 base 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 사이의 temperature는 다양한 문체를 제공하며, 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개를 요약에 병합하고 삭제합니다. 요약 호출은 낮은 temperature와 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개와 분당 300개의 요청을 허용합니다. 스트리밍 요청은 스트림이 끝날 때까지 슬롯을 점유하므로, 8개의 슬롯과 약 10초가 걸리는 응답을 가정할 때 다른 작업이 없다고 전제하면 분당 약 48개의 응답 스트림을 처리할 수 있습니다. 이는 부하와 응답 길이에 따라 속도가 달라지므로 측정된 수치가 아닌 대략적인 계산입니다. 핵심은 이를 계획하는 것입니다. 게임의 동시 플레이어 수가 슬롯보다 많다면 서버에서 요청을 큐에 넣고, 한 번에 모두 전송하는 대신 짧은 '내레이터 생각 중' 상태를 표시하세요.

한도를 초과하면 API는 HTTP 429를 반환합니다. 분당 요청에 대한 코드 rate_limited과 동시성 초과에 대한 코드 concurrency이 있습니다. 일시적인 과부하도 429를 반환하며, Retry-After: 5 헤더가 포함됩니다. 5초를 기다리는 간단한 재시도는 캐주얼 게임에 충분합니다. 응답 헤더도 읽으세요: X-RateLimit-Limit-Requests와 X-RateLimit-Limit-Concurrency은 한도를 보여주며, X-Balance-USD은 관리 화면에 표시할 수 있는 잔액을 보여줍니다.

상태 추출 호출을 다음 플레이어 턴과 병렬로 실행하지 마세요. 기다리세요. 그렇지 않으면 내레이터가 오래된 사실에 기반하여 작성합니다. 좋은 절충안은 스트림이 끝나는 즉시 추출을 시작하고 플레이어가 읽도록 하는 것입니다.

장면을 이미지 파이프라인에 연결하기

이 사이트는 Stable Diffusion 워크플로우 주변에서 텍스트 모델을 사용하는 사람들을 위해 구축되었으며, 시나리오는 이에 잘 맞습니다. API 자체는 텍스트 전용입니다: 이미지, 오디오 또는 비디오를 생성하지 않습니다. 그러나 내레이터는 파이프라인이 이미지 프롬프트로 변환할 장면 설명을 작성할 수 있습니다. 주요 장면 변경 후 이미지 도구 중 하나에 전달할 설정의 짧은 시각적 설명(조명, 분위기, 카메라 앵글 포함)을 요청하는 JSON 호출을 하나 추가하세요. 문서 페이지에는 프롬프트 작성 호출의 요청 형태가 나와 있으며, SD 프롬프트 생성기에 대한 글은 해당 측면에 대해 더 깊이 다룹니다.

엔진을 시도하려면 계정을 생성하고 체험판을 신청하세요. 신규 사용자는 API 키 페이지에서 한 번의 클릭으로 $0.50의 체험 크레딧을 받을 수 있으며, 카드 없이 7일 동안 유효합니다. 위의 숫자에 따르면 이는 몇 번의 완전한 세션에 충분합니다. 체험 키는 동시 요청 2개와 분당 60개로 제한되며, 이는 솔로 테스트에 적합합니다.

픽션은 픽션으로 유지하세요. 성인 테마 시나리오는 모든 참여자가 성인일 경우 합법적인 사용입니다. 절대 지켜야 할 유일한 규칙은 미성년자가 포함된 성적인 콘텐츠를 금지하는 것입니다. API로 전송된 프롬프트는 학습에 사용되지 않습니다.

질문과 답변

모델은 모든 캐릭터나 장르를 연기할 수 있나요?

합법적인 성인 요청을 거부하지 않도록 조정된 일반 텍스트 모델이므로 호러, 범죄, 드라마, 코미디 모두 적합합니다. 유일한 엄격한 제한은 미성년자가 포함된 성적인 콘텐츠입니다.

컨텍스트 창이 가득 차기 전에 세션을 얼마나 오래 유지할 수 있나요?

프롬프트와 완성을 합쳐 컨텍스트 창은 100,000 토큰입니다. 요약 레이어를 사용하면 이전 턴을 압축하므로 실제로는 그 훨씬 더 오래 실행할 수 있습니다.

답변을 스트리밍해야 하나요?

네, 사람이 실시간으로 읽는 모든 경우에 해당합니다. 스트리밍을 사용하면 사용량이 최종적으로 제공되며, 스트림은 완료될 때까지 동시성 슬롯 중 하나를 점유합니다.

장면을 정확히 반복하는 방법이 있나요?

일치하는 메시지와 설정으로 동일한 시드를 전달하세요. 이는 테스트용 보조 도구이며 바이트 단위로 완전히 동일한 출력을 보장하지는 않습니다.

키는 양식 하나만 채우면 받을 수 있습니다

계정을 생성하고 키를 복사한 후 기본 URL을 변경하세요. 설정은 이것으로 끝입니다.

API 키 받기