-
Notifications
You must be signed in to change notification settings - Fork 0
Local LLM Limitation mitigations
When running narrative generation on local, mid-weight LLMs (such as Gemma 26B/12B or Qwen 30B), application code must actively compensate for foundational transformer architecture limits.
This document maps the Big 5 Local LLM Limitations directly to the design patterns implemented inside Storyteller.
| Limitation Class | Severity in Storytelling | Storyteller Status | Primary Mitigation Mechanism |
|---|---|---|---|
| 1. Entity Contagion & Drift | High | Structured canonical-state.yaml & synchronous validator evaluation (ALLOW / REPLACE). |
|
| 2. Lexical Corruption & Quantization Noise | Medium | ❌ Not Mitigated (Backend Dependent) | Left to model quantization choice (Q6_K / Q8_0) and backend decoding settings. |
| 3. Context Degradation ("Lost in the Middle") | High | ✅ Actively Mitigated | Multi-layer memory shaping (summary.md, recent-summary.md) & single-system-prompt stacking. |
| 4. Instruction Drift & Guardrail Collapse | Critical | ✅ Actively Mitigated | Periodic cache-busting resets (cacheBuster.interval), turn-level system injections, and JSON Schema constrained validation. |
| 5. Context Saturation & Overhead | Critical | ✅ Actively Mitigated | Fixed turn-window limits (chat.maxRecentTurns) & the sequential DerivedMemoryTaskQueue. |
In mid-weight local models, attributes associated with one character decay across narrative distance and accidentally bind to another active character in the scene ("entity bleeding").
-
Example: The prompt defines Chris as the guitarist and Mike as a childhood friend who does not play music. After a scene set at a music venue, the LLM outputs:
"Mike unstrapped his guitar and leaned it against the amplifier."
Storyteller limits entity bleeding through state boundary enforcement, but cannot alter the underlying transformer forward-pass logic:
-
Explicit Canon Context:
PromptAssemblyServiceinjectsfixed_protagonists.ymlandcanonical-state.yamldirectly into the system prompt to anchor character identities. -
Validator Guardrail: When enabled (
validation.enabled=true),ResponseGuardroutes the draft through a secondary validator pass. If the validator detects a breach of canonical character constraints, it issues aREPLACEdecision to override the invalid turn before display.
- Remaining Risk: Highly dense multi-character scenes in a single generation pass can still trigger momentary attribute bleeding if the validator misses subtle context cues.
Aggressive weight quantization (such as 4-bit or 6-bit K-quants) blurs the distance between subword embeddings. This causes token-level hallucinations or gibberish word completions.
-
Example: Instead of generating "She furrowed her eyebrows in frustration," a quantized 4-bit model might generate:
"She furrowed her highbrows in frustration" or "She furrowed her eyebrows wish."
Storyteller currently relies on raw text output for the primary story generation pipeline:
-
Current Architecture:
ResponseSanitizerhandles formatting and escape cleanups, but does not perform dictionary parsing or spelling/grammar correction on story text. -
Recommended Setup Workaround: To avoid lexical noise, run larger models at Q6_K or Q8_0 precision on high-unified-memory machines (e.g., Apple Silicon 64 GB), or lower decoding temperature (
0.2–0.5) at the backend level.
Transformers exhibit a U-shaped attention curve: tokens placed in the middle of long prompts (e.g., tokens 10,000 to 25,000 in a 32K context) suffer from exponential retrieval decay (Needle in a Haystack).
- Example: Important rules or plot events established 15 turns ago get ignored because they sit in the low-attention center of the context window.
Storyteller completely avoids sending raw, unmanaged turn histories to the model:
-
Memory Hierarchy: Long-term events are continuously condensed into
summary.mdandrecent-summary.md. - Single System Message Stacking: All active state and background context are assembled into a single combined first system message. This ensures critical world constraints remain pinned at the very top of the context window where attention weights are highest.
During extended roleplay or storytelling, autoregressive momentum causes narrative prose to overwhelm static system rules. Negative constraints ("Never speak as the user") collapse after 10–20 turns.
- Example: The model gradually forgets its system rules and begins outputting dialogue for the user's protagonist or breaking character formatting.
graph TD
A["Main System Prompt + Fixed Protagonists + Canonical State + Long-Term Summary + Recent Summary"] --> B["Last N Raw Turns (chat.maxRecentTurns)"]
B --> C["Latest User Prompt"]
Storyteller employs a multi-tiered defense against instruction drift:
-
Periodic Cache-Busting Resets (
cacheBuster.interval): Every$N$ turns (default5), Storyteller sends a transient request with a unique token prepended to the prompt. This deliberate prefix change forces prefix-caching backends (like LM Studio or llama.cpp) to re-evaluate system prompt rules rather than relying on stale cached attention states. -
User-Triggered Manual Resets: Shortcuts like
Ctrl-WandCtrl-Utrigger an immediate transient reset-with-cache-buster to re-anchor model attention without corruptinghistory.json. -
JSON Schema Validator Enforcement: The validator pass (
ValidationClient) enforces structured output viaJSON Schema(validation.outputMode=auto). Constraining the decoding sampling layer physically prevents the model from drifting into invalid decision formats.
As prompt size approaches the maximum context limit (e.g., 32K–65K tokens), Key-Value (KV) cache memory consumption explodes. This causes severe Time-To-First-Token (TTFT) latency spikes and hardware swapping.
- Example: Running simultaneous background summarization requests alongside user-facing generation causes local GPU/VRAM to thrash, dropping generation speed to under 1 token/sec.
Storyteller treats local compute constraints as a core design boundary:
-
Strict Context Window Bounding:
chat.maxRecentTurns(default2) restricts raw narrative history to a short, high-speed window. -
Sequential Derived-Memory Queue (
DerivedMemoryTaskQueue): When a story turn completes, three background memory updates are triggered (summary,recent-summary,canonical-state). To prevent three concurrent LLM inference calls from overwhelming local VRAM/CPU resources, Storyteller submits these jobs to a single-threaded daemon queue. - Non-Blocking Architecture: Background memory updates run asynchronously and sequentially without blocking the user interface or triggering parallel backend contention.