← Writing

Why the More You Write a Persona, the Less Human It Sounds

5 min read#Prompt#Persona#LLM

The persona grew from 800 to 8,000 characters, and replies started sounding like the character reciting its own spec. Most of the problems turned out to be in the code that assembles the prompt, not in the persona text itself.

Table of Contents

For a while the character spoke strangely, and the user’s words for it were that it reported what it was doing in every sentence: one line for each thing it did, another tagged on the moment the last one landed. I counted: that instruction was active for 89% of the day.

The user guessed the persona had grown too long and was crowding the model, and asked me to trim it. My first instinct was the same. But I printed it and measured: 7,468 characters across 28 blocks, with the static persona taking 5,855 characters, or 78%, while the agenda and tone injection I had recently added came to 314 characters, four percent. Scanning the whole prompt for imperative words gave sixty-six hits. The suspect wasn’t even in the room.

Length had bitten me before, though, just not this time. I once wrote a persona over three thousand characters long, filling it with everything I could think of: speech habits, backstory, likes, dislikes, a stance on every category of topic. After shipping it the character was indeed on spec, but it felt less like chatting and more like listening to someone read their own bio. Cutting it to seventeen hundred characters made it sound natural instead.

01Keep statistics out of the prompt

Analyzing real chat logs yields beautiful numbers: median sentence length five characters, P90 fourteen, some filler word appearing 316 times. They look like the most objective evidence available, so they end up in the voice layer as a matter of course. The model then writes toward them: every sentence aimed at fourteen characters, fillers sprinkled according to frequency.

Those numbers are useful to a person; handed to a model, it just aims for them. What a model needs is rules it can follow directly:

  • If they send 1–6 characters, reply with 1–10;
  • One action per turn;
  • When elaborating, give 3–5 sentences rather than dribbling them out one at a time.

These lines came out of the same analysis as the percentiles, just rewritten as something the model can follow. The percentiles all went back into the analysis file.

02Fields written for humans

A persona file carries a great deal written for whoever maintains it: where this corpus came from, why it was designed this way, what the storage layout is, which lines are audit notes. Put any of that in an ordinary sub-field and it gets serialized into the prompt wholesale. Once I counted one character’s authoring notes at 45% of the system prompt, over four thousand characters, every one of them amounting to “this material comes from the chat logs of such-and-such month”. None of it helps the model understand the character; the replies just got wordier.

Then there is a subtler one: a field like inject_into_prompt: false looks like a switch, but is only a label for humans, and the loader never reads it. I was fooled by that once and assumed writing it meant turning something off.

The fix is to move everything with that reviewing tone into meta, or into a separate analysis directory, and strip it at assembly time.

03It can’t tell whose state it is

During context assembly the user profile and the character’s own state get merged into one environment block: what the user has been up to sits right next to what the character is doing now. The model can only tell whose is whose from the headings.

One class of character has user state and none of its own. Drop the word user from that block and the model reads the user’s state as its own and describes, in the first person, something the user just did.

When hunting this I don’t open the code first. The character’s own context comprehension record spells out word for word how it understood things, so I read that, align the timestamps, and only then trace back to the renderer. The rule itself is simple: any injected user data must carry user in its heading. Don’t expect the model to guess ownership from position.

04A persona shouldn’t carry identity

In one character’s static isolation test it answered I don’t know you. Chasing it down showed the persona wasn’t written wrong: the test bypassed the speaker identity the dispatch layer was supposed to supply, while the persona had been saddled with deciding who is talking to me right now.

Which account this is and what the relationship is happen to be runtime facts, and belong in the prompt assembled by code. A persona should only describe how we get along once identity is settled. Hardcode an account id into it and everything breaks the moment the account changes.

After that change the expanded persona fell from 7,878 characters to 1,700, all twelve isolation generation tests passed, and the longest reply came to twelve characters. Everything deleted was something the model had never once used.

Since then I stick to three things when editing a persona. Each rule goes in one place: say it under reply length, under closing style, and under hard constraints, and the model treats them as three separate rules and follows all three. Storage layout, host responsibilities, corpus provenance, design rationale — anything with that reviewing tone goes to meta or a separate analysis directory. And a fact that surfaced exactly once inside a single topic belongs in retrieval, not in the resident layer.

After writing a passage of persona I ask myself whether it tells the model what to do or describes what kind of person it is. Write a lot of description and the model starts acting the description out.

Related

Filtering kwargs with inspect.signature: a function that takes **kwargs looks like it accepts nothing, and a retry lost its model_path2026-11-08inspect.signature(f).parameters lists only declared parameters; **kwargs shows up as a single entry named kwargs. Checking by name whether a callee accepts an argument lets nothing through to the most permissive function. Autarkia’s empty-reply retry lost its model_path this way and fell back to the default model; the resulting 400s landed among upstream 503s, and that day’s error triage filed them as gateway noise.#Python#LLM#DebuggingGenerator Exceptions Surface on the First next(): try Around create_chat_completion(stream=True) Never Sees the Context Window Error2026-11-08Calling a generator function only builds a generator; the body, including any raise in it, runs on the first next(). llama-cpp-python checks the context window inside such a body, so the try around a streaming call, and the shrink-and-retry under it, never fire with the real library. The test double raised at call time, so the tests passed with 100% coverage.#Python#LLM#Error Handling#Testing#AutarkiaLLM Request Observability: Logs Count Messages, and the Native Tools Schema Shows Up Nowhere2026-11-07With native function calling, tool definitions go in the tools parameter, not in messages. A log that measures message length can’t see them, and streaming responses omit usage by default. Autarkia sent 82 tools and 47,898 characters of schema every turn while the prompt log showed about 2,200 characters; the API counted 21k–23k tokens.#LLM#Python#Observability#Autarkia