DE ▾
API-Schlüssel erhalten

Textbasierte Rollenspiele mit einer LLM-API

Aktualisiert

Textbasierte Rollenspiele funktionieren am besten, wenn das Modell einen Erzähler mit Regeln spielt und dein Code den Zustand verwaltet. Dieser Artikel zeigt eine kleine Engine in Python: Prompt, Streaming, Speicher und eine Kostenschätzung für eine lange Sitzung.

Diorama einer kleinen Bühne mit leuchtenden Sprechblasen, die ein Text-Rollenspiel-Szenario darstellen

Wichtige Punkte

  • Teile die Arbeit: Das Modell schreibt die Geschichte, dein Code speichert Fakten, Inventar und Regeln.
  • Stream die Antworten und halte jeden Stream kurz genug, um seinen Concurrency-Slot schnell freizugeben.
  • Ein 100.000-Token-Fenster fasst eine sehr lange Sitzung auf, aber Zusammenfassungen halten Kosten und Drift niedrig.
  • Eine 200-Züge-Sitzung mit 6.000 Input- und 400 Output-Token pro Zug kostet etwa $0,22.

Beginne mit dem Erzähler, nicht mit der Handlung

Leute, die textbasierte Rollenspiele entwickeln, beginnen oft damit, eine Handlung zu schreiben. Das ist verkehrt. Ein Szenario ist eine Schleife zwischen einem Spieler und einem Erzähler, und der Erzähler braucht eine Stellenbeschreibung, bevor er eine Geschichte braucht. Das Modell muss wissen, wer spricht, was es für den Spieler entscheiden darf, wie lang eine Antwort sein sollte und was es niemals tun soll. Alles andere kann improvisiert werden.

Ein gutes System-Prompt für eine Szenario-Engine hat vier Teile. Erstens die Rolle: Spielleiter, Interviewer, Schiffscomputer, eine Figur in einem Stück. Zweitens die Weltregeln in wenigen klaren Sätzen: Tonfall, Schauplatz, was existiert und was nicht. Drittens Formatierungsregeln, weil dein Code sie möglicherweise parst: zum Beispiel „Beende jede Antwort mit einer Zeile, die mit OPTIONS: beginnt“. Viertens die Grenzen. Die Text-API hier ist darauf abgestimmt, ohne Inhaltsablehnungen für legale Erwachsenen-Nutzung zu antworten, was sie für düstere Fiktion, Schurken mit echter Bedrohung und moralisch graue Handlungen geeignet macht. Es gibt immer noch eine harte Grenze: kein sexueller Inhalt, der Minderjährige einbezieht. Nimm diese Regel auch in deine eigenen Nutzungsbedingungen auf und setze in deiner App eine Altersprüfung voraus, wenn deine Szenarien auf Erwachsene abzielen.

Halte den Prompt unter etwa 600 Wörter. Lange Regelauflistungen werden nur teilweise befolgt. Wenn du viele Regeln benötigst, sortiere sie und entferne das untere Drittel.

Ein funktionierender Loop mit Streaming

Streaming ist im Rollenspiel wichtiger als in den meisten Anwendungen. Ein Spieler, der eine Geschichte liest, möchte Wörter erscheinen sehen, während das Modell schreibt, und eine Antwort mit 300 Token fühlt sich langsamer an, als sie ist, wenn sie auf einmal eintrifft. Die API streamt über Server-Sent Events mit stream: true, und die OpenAI SDKs übernehmen das Parsing. Der Endpunkt ist OpenAI-kompatibel, die Modell-ID ist uncensored, und die Basis-URL für diese Website ist 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.")

Die Einstellungen sind für Fiktion abgestimmt. Eine Temperatur von etwa 0,8 bis 1,0 ergibt abwechslungsreichen Text, und top_p nahe 0,95 schneidet die seltsamsten Wortauswahlen ab. Ein kleiner Presence-Penalty spricht den Erzähler davon ab, zu denselben Bildern zurückzukehren. Stelle max_tokens ein, um die Antwortlänge zu begrenzen; das ist optional, aber ohne ihn kann ein gesprächiger Erzähler weit mehr schreiben, als du wolltest. Für eine Szene, die du während des Tests exakt wiederholen möchtest, übergebe seed und halte die anderen Parameter konstant.

Lass den Code die Fakten verwalten

Der häufigste Fehler beim langen Rollenspiel ist Kontextdrift. Der Schlüssel ist in der Tasche des Spielers in Runde 5 und auf dem Tisch in Runde 40. Der Wirt wurde Mara genannt, dann Marta. Ein Modell, das 30.000 Token Verlauf liest, wird das oft richtig machen, aber „oft“ ist kein Design. Halte die Fakten in einer kleinen Struktur, die dein Programm aktualisiert, und gib diese Struktur in jeder Anfrage ein, wie es die STATE-Zeile oben tut.

Wie aktualisierst du den Zustand? Bitte das Modell, Änderungen nach jeder Runde zu melden, in einem zweiten, günstigen Aufruf, der JSON-Modus verwendet. Der erste Aufruf schreibt die Geschichte für den Spieler. Der zweite liest den neuen Abschnitt und gibt nur zurück, was sich geändert hat.

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:]

Begrenze die Liste der Fakten, wie der Slice es tut, oder der Zustandsblock wächst, bis er so schwer ist wie der Verlauf, den er ersetzen sollte. Wenn eine JSON-Antwort nicht geparst werden kann, überspringe die Aktualisierung für diesen Zug und fahre fort; eine verpasste Tatsache ist günstiger als ein Absturz.

Würfel, Gesundheit und Timer werden besser in einfachem Code gehandhabt. Wenn der Spieler etwas Riskantes versucht, wirf in Python und teile dem Erzähler das Ergebnis mit: "Der Versuch des Spielers schlägt fehl." Das Modell schreibt dann die Konsequenzen. Das hält das Modell davon ab, den Spieler stillschweigend jedes Mal gewinnen zu lassen, wozu Sprachmodelle neigen.

Speicher für Sitzungen, die stundenlang laufen

Das Kontextfenster hat 100.000 Token, gezählt Prompt und Antwort zusammen. Eine typische Antwort von 250 Token plus eine 30-Token-Nachricht des Spielers sind etwa 280 Token pro Zug, sodass der reine Verlauf Hunderte von Zügen fassen könnte. Du kannst alles senden. Viele Entwickler tun das dennoch nicht aus zwei Gründen: Die Kosten steigen mit jedem Zug, weil der gesamte Verlauf jedes Mal als Input abgerechnet wird, und alte Details können das Modell ablenken.

Ein vernünftiges Layout hält drei Schichten:

  1. Pinned: der System-Prompt und der Zustandsblock. Immer gesendet.
  2. Zusammenfassung: ein fortlaufender Absatz oder zwei darüber, was passiert ist, alle 10 bis 15 Züge neu geschrieben von einem kurzen Aufruf.
  3. Aktuelle Züge: die letzten 10 bis 20 Nachrichten wörtlich, damit die Prosa ihren Rhythmus behält.

Wenn das Fenster der letzten Züge voll ist, füge die ältesten zehn in die Zusammenfassung ein und entferne sie. Der Zusammenfassungsaufruf kann eine niedrige Temperatur und ein Limit von 300 Token verwenden. Spieler bemerken die Komprimierung selten, solange Namen, Versprechen und offene Bedrohungen in die Zusammenfassung einfließen. Schreibe das explizit in das Zusammenfassungs-Prompt: „Halte Namen, Schulden, Verletzungen, offene Fragen“.

Wenn du möchtest, dass ein Charakter eine stabile Stimme beibehält, gib ihm eine kurze Style-Karte in der gepinnten Schicht: drei Beispielzeilen seiner Sprache und zwei Dinge, die er niemals sagt. Kurze Beispiele formen den Ton besser als Adjektive.

Was eine Session kostet

Die Preise liegen bei $0,15 pro 1M Input-Token und $0,50 pro 1M Output-Token, mit Prepaid-Guthaben und ohne Abonnement. Arbeite eine lange Sitzung durch. Nimm 200 Züge an, mit der gepinnten Schicht, Zusammenfassung und aktuellen Zügen, die im Durchschnitt 6.000 Input-Token pro Anfrage und 400 Output-Token pro Antwort (die Geschichtenantwort plus ein kleiner Anteil für Status- und Zusammenfassungsaufrufe) ergeben.

Input: 200 x 6.000 = 1.200.000 Token, das ist 1,2 x $0,15 = $0,18. Output: 200 x 400 = 80.000 Token, das ist 0,08 x $0,50 = $0,04. Die Sitzung kostet $0,22.

Vergleiche die faule Version, die den gesamten Rohverlauf erneut sendet. Bis Zug 200 beträgt der Verlauf etwa 56.000 Token, und über die Sitzung hinweg beträgt der durchschnittliche Input etwa 28.000 Token pro Anfrage. Das sind 200 x 28.000 = 5.600.000 Input-Token, oder 5,6 x $0,15 = $0,84, plus die gleichen $0,04 für Output, was $0,88 ergibt. Immer noch erschwinglich, aber viermal die Rechnung für keinen Gewinn in der Qualität. Für eine App mit vielen Spielern entscheidet diese Lücke über deine Marge.

Fehlgeschlagene oder abgelehnte Anfragen werden nicht berechnet, und Guthaben verfällt nie, sodass ein nicht verwendeter Rest einfach wartet. Aufladungen laufen von $20 bis $500 in ganzen Dollar, mit einem 5-%-Bonus ab $50, 10 % ab $100 und 15 % ab $200. Siehe die Preisseite für die vollständige Liste der Optionen.

Viele Spieler gleichzeitig ausführen

Ein regulärer Schlüssel erlaubt 8 parallele Anfragen und 300 Anfragen pro Minute. Eine gestreamte Anfrage belegt ihren Slot, bis der Stream endet, also kannst du mit 8 Slots und Antworten, die etwa zehn Sekunden dauern, etwa 48 Antwort-Streams pro Minute bedienen, vorausgesetzt, sonst nichts läuft. Das ist eine grobe Regel, keine gemessene Zahl, da die Geschwindigkeit von Last und Antwortlänge variiert. Der Punkt ist, dafür zu planen: Wenn dein Spiel mehr gleichzeitige Spieler hat als Slots, stelle die Anfragen auf deinem Server in eine Warteschlange und zeige einen kurzen „Erzähler denkt nach“-Zustand an, anstatt alles auf einmal abzufeuern.

Wenn du ein Limit überschreitest, gibt die API HTTP 429 zurück. Der Code ist rate_limited für Anfragen pro Minute und concurrency für zu viele gleichzeitig. Temporäre Überlastung gibt auch 429, mit einem Retry-After: 5-Header. Eine einfache Wiederholung, die diese fünf Sekunden wartet, reicht für ein Gelegenheitsspiel. Lies auch die Antwort-Header: X-RateLimit-Limit-Requests und X-RateLimit-Limit-Concurrency zeigen deine Limits, und X-Balance-USD zeigt, was übrig ist, was du in einem Admin-Bildschirm anzeigen kannst.

Führe den Status-Extraktionsaufruf nicht parallel zum nächsten Spielerzug aus. Warte darauf, oder der Erzähler wird auf Basis veralteter Fakten schreiben. Ein guter Kompromiss ist, die Extraktion zu starten, sobald der Stream endet, und den Spieler lesen zu lassen, während es läuft.

Szenen mit deiner Bildpipeline verbinden

Diese Seite ist für Menschen gebaut, die Textmodelle in Stable-Diffusion-Workflows nutzen, und Szenarien passen gut dazu. Die API selbst ist nur Text: Sie generiert keine Bilder, Audio oder Video. Aber der Narrator kann eine Szenenbeschreibung schreiben, die deine Pipeline in einen Bild-Prompt umwandelt. Füge nach einer großen Szenenänderung einen weiteren JSON-Aufruf hinzu, der nach einer kurzen visuellen Beschreibung der Umgebung mit Beleuchtung, Stimmung und Kamerawinkel fragt, und übergebe dann diese Zeichenkette an das Bild-Tool, das du ausführst. Die Dokumentationsseite zeigt die Anfragestruktur für Prompt-Schreib-Aufrufe, und der Artikel zum SD-Prompt-Generator geht darauf tiefer ein.

Um die Engine zu testen, erstelle ein Konto und beanspruche das Testguthaben. Neue Nutzer können mit einem Klick auf der API-Schlüssel-Seite $0,50 Testguthaben erhalten, gültig für 7 Tage ohne Kartenangabe. Nach den oben genannten Zahlen reicht das für ein paar vollständige Sitzungen. Test-Schlüssel sind auf 2 parallele Anfragen und 60 pro Minute begrenzt, was für einzelne Tests ausreicht.

Halte Fiktion in der Fiktion. Szenarien mit Erwachsenenthemen sind rechtmäßige Nutzung, wenn alle Beteiligten erwachsen sind; die eine Regel, die sich nie beugt, ist kein sexueller Inhalt mit Minderjährigen. Prompts, die an die API gesendet werden, werden nicht zum Training verwendet.

Fragen und Antworten

Kann das Modell jede Figur oder jedes Genre spielen?

Es ist ein allgemeines Textmodell, das darauf abgestimmt ist, rechtmäßige Erwachsenenanfragen nicht abzulehnen, daher sind Horror, Krimi, Drama und Komödie alle in Ordnung. Die einzige harte Grenze ist sexueller Inhalt mit Minderjährigen.

Wie lang kann eine Sitzung sein, bevor das Kontextfenster voll ist?

Das Kontextfenster umfasst 100.000 Token für Prompt und Completion. Mit einer Zusammenfassungsschicht kannst du in der Praxis weit darüber hinausgehen, da alte Nachrichten komprimiert werden.

Sollte ich die Antworten streamen?

Ja, für alles, was eine Person live liest. Streaming gibt dir die Nutzung am Ende, und ein Stream belegt einen deiner parallelen Slots, bis er abgeschlossen ist.

Gibt es eine Möglichkeit, eine Szene exakt zu wiederholen?

Übergebe denselben Seed mit identischen Nachrichten und Einstellungen. Es ist ein Testhilfsmittel und garantiert keine byte-identische Ausgabe.

Dein Schlüssel ist nur ein Formular entfernt

Erstelle ein Konto, kopiere den Schlüssel, ändere die Basis-URL. Das ist die gesamte Einrichtung.

API-Schlüssel erhalten