Building a local AI storyteller - Part II - When the model is not enough
In the first part of this series, I described the most important lesson I learned while building a local AI storyteller:
A good LLM application is not a clever prompt. It is software architecture around a probabilistic component.
That sounds reassuringly architectural.
It also leaves one small question:
What exactly are we building around?
Local models can write surprisingly good prose. They can create atmosphere, remember characters, and occasionally produce a sentence that makes me wonder why I spent so much time choosing variable names.
They can also give Mike somebody else’s guitar, lose the most important fact in the middle of a prompt, quietly forget the rules, and invent a word that sounds confident enough to be real.
The model is not broken when that happens. At least, not necessarily.
These are failure modes you can encounter when you run a model locally, especially during long-form storytelling. Their severity depends on the model, quantization, inference backend, prompt structure, decoding settings, and available hardware.
This post looks at five of them. For each one, I will cover:
-
what the problem looks like
-
what may help
-
what I implemented in Storyteller
Spoiler: I did not solve all five.
That is not a cliffhanger. It is engineering.
Before we continue, meet two recurring characters in the examples below.
Chris is a guitarist. The guitar belongs to him, and playing it is part of his character. Mike is Chris’s childhood friend. He is not a musician; he is simply there to support his friend and, apparently, to become the victim of several model-related misunderstandings.
Their roles are intentionally simple. If a model cannot keep one guitar attached to the correct person, imagine what can happen when a story contains a complete cast, several relationships, and a few chapters of history.
The explanations below are intentionally practical rather than exhaustive. For the more technical discussion of these failure modes, and a closer look at how Storyteller tries to mitigate them, I have documented the details separately in the project wiki.
Local LLM Limitations — the technical background behind the failure modes discussed here.
Local LLM Limitation mitigations — the implementation details and trade-offs behind Storyteller’s responses to them.
1. Entity contagion and feature bleeding
What is it?
Entity contagion happens when a model transfers an attribute, possession, relationship, action, or role from one character to another.
Imagine the story canon says:
-
Chris is the guitarist.
-
Mike is Chris’s childhood friend.
-
Mike does not play music.
A few pages later, the story reaches a music venue and the model writes:
Mike unstrapped his guitar and leaned it against the amplifier.
Congratulations, Mike. Your musical career started without consulting either the author or the canon.
The useful mental model is not that a guitar-shaped fact literally floated from one character to another inside the neural network. Entity contagion is a name for the visible result.
An LLM has no transactional character database. It generates the next token from the context it receives and the patterns stored in its weights. If facts are distant, ambiguous, or surrounded by competing details, the wrong continuation can still look locally plausible.
What may help?
Keep entity definitions compact and explicit. Store important character facts outside the model. Send only the canon relevant to the current scene, prune unrelated history, and validate generated text against that canon before accepting it.
This is one of those cases where more prose is not always more clarity. A short, explicit fact such as "Chris is the guitarist" may anchor a character better than three paragraphs about how Mike never really understood Chris’s fascination with guitars.
Negative instructions can also have an awkward side effect. A sentence such as "Mike does not play guitar" still places Mike and guitar close together in the context. Instead of removing the association, it may make that combination more salient during generation. This does not prove that the model literally assigns more internal "weight" to those words, but the practical result can be wonderfully unhelpful:
MIKE + NOT + GUITAR
|
v
MIKE + GUITAR
When possible, it is therefore useful to describe what is true, not only what must not happen: Chris owns and plays the guitar; Mike is attending as his friend. Explicit exclusions can still be valuable, but they should not be the only thing defining the characters.
What did I do in Storyteller?
Storyteller partially mitigates the problem in two ways.
First, it keeps important character facts in a separate source of truth. Before writing a new scene, the storyteller is reminded who the characters are, what belongs to them, and which established facts matter. Their identities therefore do not depend only on the model finding the right sentence somewhere in an old conversation.
Second, the generated scene can be read by a separate reviewer before it is shown. That reviewer checks the draft against the explicit story rules and fixed character definitions. If it spots a clear violation, it can replace the offending response.
This is deliberately only a partial solution. The reviewer does not yet see every piece of accumulated story knowledge, and subtle feature bleeding in a crowded scene can still escape detection.
There is also an uncomfortable practical problem: a good reviewer needs to understand the story almost as well as the storyteller does.
Running a second large model would provide a more independent opinion, but on local hardware it may require more memory than is available and make every response considerably slower. Using a smaller model is cheaper, but subtle character mistakes are exactly the kind of details a less capable reviewer may miss. In the worst case, the reviewer creates a new continuity error while trying to repair the first one.
Storyteller therefore reuses the same model with a different, much narrower task. That helps because reviewing a draft is different from inventing one. However, it is not a truly independent pair of eyes. The reviewer may share the storyteller’s blind spots and confidently approve the same plausible-looking mistake.
The model may make the mistake. The application gets a chance to catch it. Sometimes it does.
2. Lexical corruption and quantization noise
What is it?
Local models are often quantized so they fit into available memory and run at a usable speed. Quantization stores model weights at lower numerical precision. That trade-off can increase generation errors, depending on the model, quantization method, backend, and decoding configuration.
In prose, this can appear as odd word choices, repetitions, awkward phrases, or sentences that are almost correct:
She furrowed her highbrows in frustration.
Or, more ambitiously:
I'm feeling very problembilistic today.
It is tempting to describe this as nearby words bumping into each other inside the model. That is a nice cartoon, but not a measured explanation of what happened. What we can observe is degraded output after numerical approximation and decoding. The precise path to one strange word is usually not available to us.
What may help?
Test quantizations with prompts that resemble your actual workload. A benchmark score will not tell you whether your chosen model suddenly develops an experimental relationship with adjectives in chapter twelve.
Higher precision may improve quality, at the cost of memory and speed. Decoding settings such as temperature, Top-P, Top-K, Min-P, and repetition controls can improve stability, but they cannot reconstruct information lost through quantization.
Spelling or grammar post-processing is another option. It also carries a risk: in fiction, the strange word might be intentional. A correction pass that fixes a character’s dialect is not necessarily correcting the story.
What did I do in Storyteller?
Nothing.
More precisely: Storyteller does not try to detect or repair lexical corruption.
The application cleans up technical clutter around a response, but it is not a spell checker or a prose editor. The reviewer checks story rules and character constraints, not whether highbrows should have been eyebrows.
For now, this remains a model-selection and inference-configuration concern. I prefer that honest boundary over a correction layer that quietly rewrites intentional prose.
So yes, problembilistic may survive all the way to the screen.
3. Context degradation: Lost in the Middle
What is it?
A large context window tells you how much text a model can accept. It does not guarantee that the model will use every part of that text equally well.
Models may retrieve facts less reliably when those facts sit deep inside a long prompt, surrounded by unrelated information. This position-dependent degradation is commonly called Lost in the Middle. Related Needle in a Haystack tests hide one fact at different positions in a large context and ask the model to retrieve it.
In a story, the lost needle may be a rule, a character detail, or the apparently minor fact that the key to the basement is already in Valerie’s pocket.
The model remembers page one. It remembers the most recent exchange. The key in the middle has entered a witness protection program.
What may help?
Do not fill the context window merely because it exists.
Prune irrelevant turns. Keep a short window of recent raw dialogue. Summarize older events into task-relevant memory. Store durable facts in canonical state, and place current instructions and relevant facts in stable, clearly marked prompt sections.
Repeating an important rule can help, but duplication is not free. Contradictory summaries or five slightly different versions of the same fact create a new retrieval problem with more paperwork.
What did I do in Storyteller?
Storyteller does not replay the complete conversation. It builds every story request from several kinds of memory:
the current writing instructions
fixed character facts
the established state of the story
a summary of older events
a more detailed summary of recent events
the latest conversation turns
Older events are condensed into summaries, while durable facts are kept separately as the story’s source of truth. Only a small number of recent turns are included word for word. Together, these layers give the model a compact and consistently organised reminder instead of an ever-growing transcript.
This actively mitigates the problem, but it does not make retrieval perfect. Derived memory is updated asynchronously and may briefly lag behind the latest turn. A fact can also be summarized badly. Compression saves context; it can compress away nuance too.
4. Instruction drift and guardrail degradation
What is it?
During a long session, a model may follow its original instructions less consistently. Narrative patterns accumulate, the current scene becomes more influential, and an earlier constraint starts losing the argument.
For example, the system prompt may say:
Never write dialogue for the user's protagonist.
Twenty turns later, the model writes an excellent, emotionally layered speech for exactly that protagonist.
The speech is moving. The guardrail is less so.
System instructions influence generation; they are not compiled rules. There is no universal turn number at which they collapse, and drift is not explained simply by story text becoming "stronger" than the system prompt. The visible fact is that adherence can degrade as the assembled context grows and changes.
What may help?
Reassemble active instructions on every stateless request. Keep rules concise and consistent. Validate semantic constraints separately, and use constrained output only for requirements it can actually enforce.
For example, software can require the reviewer to answer with a simple accept or replace decision. It cannot guarantee that the original story respects character ownership merely because the answer arrived in the correct format.
A retry, reset, or undo workflow is also valuable. Probabilistic systems need recovery paths, not just optimistic prompts.
What did I do in Storyteller?
Storyteller uses several best-effort safeguards.
Every request is rebuilt from the active rules and selected memories, rather than relying on the model to maintain an invisible state between calls. At intervals, Storyteller also sends a disposable request with a deliberately different beginning. This may prevent some local inference software from reusing an exact earlier prompt, but it is not a magic reset button and cannot promise better behaviour on the next scene. I kept it as an experiment rather than a proven mitigation. I do not yet know whether it helps, and finding that out is part of the project.
The user gets recovery options too. A reset starts again from a freshly assembled prompt. Undo removes the latest turn and puts the original input back, ready to edit and retry. That may sound less sophisticated than an advanced AI safety mechanism, but it is wonderfully effective when a fictional character suddenly starts speaking on your behalf.
Finally, the separate reviewer must return a simple decision: accept the draft or replace it. A predictable decision makes the review easier for the application to use, although it still cannot force the storyteller itself to obey every rule.
These measures limit the impact of drift. They do not eliminate it. Sometimes the correct interface for an imperfect model is still an undo key.
5. Context saturation and performance issues
What is it?
Context has a computational cost.
As a prompt grows, the model must read and temporarily remember more input before it can write the next word. On a local machine, that consumes more memory and processing time. Eventually this can mean a longer wait before generation starts, slower writing, or the computer moving work to slower parts of the system.
There is also competition. On a local machine, memory and processing power are limited. Some inference backends can queue or batch multiple requests efficiently, but they cannot create additional GPU capacity. Every active context needs its own working memory, while background summarization and foreground storytelling still share the same compute resources. Batching may improve overall throughput, but on consumer hardware it can also make the story response noticeably slower.
The laptop now understands the complete emotional history of six fictional people. Unfortunately, it needs a moment to recover emotionally.
Long context can also worsen retrieval, but not every repetition, ramble, or abrupt ending is context saturation. Output-token limits, stop sequences, sampling settings, the model itself, and the inference backend can produce similar symptoms.
What may help?
Bound raw history and summarize older content. Choose a context size that fits both the model and the hardware. Measure instead of assuming that the maximum advertised window is the best configuration.
On constrained hardware, run heavy background jobs one at a time instead of letting them compete for memory and processing power. Some local inference tools offer additional memory-saving options, but those introduce their own trade-offs in speed and sometimes quality.
What did I do in Storyteller?
Storyteller includes only a small, fixed number of recent turns word for word. This controls one source of growth, but it is not a hard limit on the complete prompt. Instructions, character definitions, established facts, and summaries can still become larger over time.
Storyteller handles this by placing heavy background operations in an application-level queue. Summary and memory updates are processed one at a time instead of competing with each other for the local model. They run in the background, so the current story response does not have to wait for the bookkeeping to finish.
There is still a remaining risk: a background update can overlap with the user’s next story request. Both then ask the same local model for attention at once. On limited hardware, careful scheduling remains more predictable than simply sending every task to the inference backend at once.
This is an active mitigation, not infinite capacity disguised as a queue.
What the five problems have in common
These limitations come from different places.
-
Entity bleeding is a consistency problem.
-
Lexical corruption can be a model and quantization problem.
-
Lost in the Middle is a context retrieval problem.
-
Instruction drift is an adherence problem.
-
Context saturation is also a resource problem.
They should not be collapsed into one diagnosis called "the model got confused."
But they do share one architectural lesson:
The context window is not memory, the system prompt is not a rule engine, and the model is not the whole application.
The practical response is not to distrust the model. It is to give each responsibility to the component best suited for it:
-
the model generates candidate prose
-
structured state preserves durable facts
-
context assembly selects what matters now
-
validation checks explicit constraints
-
queues and limits protect local resources
-
the interface gives the user a way to recover
An intentionally unfinished ending
Storyteller now handles several of these failure modes better than its first version did. More importantly, it achieved exactly what I built it for.
|
This project was set up to help me learn more about LLM application design, prompting, memory shaping, and validation behaviour. The goal was not to spend most of the project building framework infrastructure, nor was it to create a storyteller that could hide every limitation of a local model. The first results became the first blog post in this series. This second post exists because the remaining imperfections turned out to be at least as educational as the things that worked. |
But Storyteller is not merely an experiment that produced a useful list of limitations. It is a complete, working application that already mitigates several of them and demonstrates how capable a local LLM can be when the surrounding software gives it structure.
That separation between model and application is deliberate. Storyteller is not tied to one particular model or runtime. It can run GGUF and MLX models using managed local services, connect to a larger model through a compatible API when more capacity is needed, and use vision-capable models for image-aware interactions.
The model can change. The application around it remains.
So the story is not that Storyteller fails to catch every swapped attribute, repair every strange word, or guarantee perfect instruction adherence. The story is that a useful application already exists, several difficult failure modes are reduced by the software around the model, and the remaining boundaries are now visible enough to investigate deliberately.
Finding those boundaries was part of the goal. That is useful information, not an embarrassing footnote.
It also makes the next experiments fairly clear:
-
give the reviewer more established story facts without overwhelming it
-
test character consistency with repeatable multi-character scenes
-
compare models and quantizations (compression levels) using representative story passages
-
make the size and cost of every assembled prompt visible
-
prevent background memory work from slowing down the next story request
-
check whether summaries preserve the facts that later scenes actually need
Some of those changes belong in application code. Some belong in configuration. Some may simply lead to choosing a different model.
And that is probably the real open ending of a local AI project.
You do not eventually discover the perfect prompt and stop engineering. You keep deciding which uncertainty belongs to the model, which belongs to the software around it, and which one you are willing to live with.
Preferably before Mike books his first guitar solo.