We shipped a persona for our startup-registration assistant and it passed every acceptance scenario we had written. Warm without being saccharine. Catches the user’s mood before handing them a plan. Doesn’t announce its own personality. On real hardware, six out of six.
Then a user filled in a form field wrong and the assistant said:
字段取值有误:手机号「138」需为 11 位大陆手机号
Field value error. A judgment delivered at the exact moment the user was already stuck. The persona never touched it, because that string is hardcoded in a validator, and the persona only governs what the model writes.
That is the whole lesson, and it took me embarrassingly long to see it. A persona is not character engineering. It’s one layer of it. The user doesn’t experience your prompt; they experience every character on the screen. Ours was maybe half prompt-generated and half hardcoded, and only the first half had an owner.
The six layers
Once I started counting layers instead of patching strings, the shape fell out fast:
| Layer | What it governs | Where it lives |
|---|---|---|
| 1 · Persona | Who the assistant is — identity, temperament, boundaries | System prompt |
| 2 · Expression | Every character the user reads, including the strings that never touch the model | Copy registry |
| 3 · Perception | What state the user is in this turn | Per-turn signal |
| 4 · Memory | What we remember about them across visits | Profile store |
| 5 · Initiative | When we reach out unprompted | Event hooks |
| 6 · Guardrails | The edges of warmth — no sycophancy, no jokes in serious moments, no sensitive data retention | Rules plus verification |
We had layer 1. We had fragments of 4 and 5 as plumbing with nothing plugged in. Layer 2 didn’t exist as a concept, which is precisely why it kept breaking: an unnamed layer has no owner, no checklist, and no acceptance criteria, so its defects can only be discovered by accident on real hardware. Every time we found one, we fixed that one string and moved on, and the next one was already waiting.
It felt like whack-a-mole because we were playing whack-a-mole. There was no layer to fix, only individual moles.
Layer 2 is where the copy actually lives
The fix wasn’t better strings. It was giving the strings a home: a copy registry where the framework ships a neutral default for every user-visible message and each resident agent overrides it in its own voice. Mechanism in the platform, tone in the resident.
That single move converted a class of bug into a class of edit. The validation message became:
有一处得改一下就能往下走:手机号「138」对不上,需为 11 位大陆手机号
Same information, and the user is no longer being sentenced. But more importantly, when someone wants to retune the voice, there is now exactly one place to go — instead of a grep across a thousand lines.
The rewrite surfaced something I hadn’t expected: the coldest text in the product sat at the highest-emotion moments. Rejections, missing documents, validation failures, cancellations. Every one of them hardcoded, every one of them written in the register of a system log, every one of them arriving precisely when the user was most likely to give up. The persona said lead with empathy on bad news — and the persona was never in the room when bad news was delivered.
Failure direction decides where a rule belongs
One heuristic came to govern every layer boundary, and it is not “is this generic or domain-specific.” It is this:
When this gets it wrong, which way does it fall?
Same word list, three different verdicts:
- A keyword gate that decides whether to search the knowledge base — worst case, one wasted empty query. Keep the word list. It’s the right tool.
- A keyword gate that decides which progress card to render — worst case, we show a card about the wrong order. Annoying, visible, self-correcting. Borderline.
- A keyword gate that decides whether the user wants to cancel their service — worst case, we hand someone a termination confirmation while they were merely complaining that the process is tedious. Not acceptable.
That last one happened. A user typing “麻烦死了 看着就不想办” — this is such a hassle, I don’t even want to bother — matched the substring 不想办 in a cancellation list and got a “confirm service termination” card back in 51 milliseconds. Pure word list, no model involved.
The word list wasn’t wrong because word lists are bad. It was wrong because it conflated an instruction with a mood. 取消 (cancel) is a command. 不想办 (don’t want to bother) is a feeling. Both appeared in the same list with the same authority. The fix was to split them: commands still resolve deterministically and instantly; moods route to a judgment layer that gets to read the context. And when the judgment layer is unavailable, the degraded path unlocks only the tool that shows a choice card — never the one that executes.
The less certain the system is, the weaker the capability it should be handed. Not “block everything,” not “guess anyway” — a graded ladder.
Deterministic exits beat better guessing
The most valuable thing we built for layer 2 wasn’t a string. It was a button.
When the assistant said “you have an unfinished application, you can pick it up anytime,” the user’s only affordance was to type something the parser would recognize. Which meant every phrasing we hadn’t anticipated became a user-visible failure. We’d been patching that parser for weeks.
The actual fix was to render two buttons — continue filling and cancel application — next to the sentence. Clicking routes deterministically, no model, no parsing. Natural language became a fallback instead of the sole entrance. The parser still matters, but its failures stopped being fatal.
The same principle resolved a tension I’d been treating as a real tradeoff. Users who say “I want to register an e-commerce company” — do we open the form immediately, or do we talk first? Some users want each. We were guessing, and a wrong guess had no recovery.
It’s a false binary. Answer what they said, and attach a one-click entry to the form. Then the guess stops mattering. What’s left is a much smaller question — when to skip the conversation entirely — and that one is decided by asymmetry again: failing to open the form costs one extra sentence with a button right there; opening it wrongly buries what the user just told you about their business under a seven-step wizard.
For that residual question we needed to distinguish “I want to register a company” from “I want to register an e-commerce company.” We did not do it by enumerating phrasings, which is the mole game again. We did it by measuring leftover information. Strip the action words, the object words, and the pronouns, and see whether anything substantive remains. If nothing remains, the input is a command. If 「电商」 remains, the user is telling you about their business, which is the opening of a conversation rather than an order. New phrasings then require no new word list entry.
Rules you can’t verify aren’t rules
Layer 6 had rules — no sycophancy, drop the humor when things get serious — and no way to check any of them. They lived in a prompt and were enforced by hope.
Half of that is genuinely hard: whether the model got the tone right in a given reply is a human judgment. But the other half is mechanical. Once expression is a registry, you can point a test at it: take every copy key the resident has tagged as a bad-news moment, render it, and assert that no celebratory marker appears. Party emoji, “congratulations,” “great news” — in a rejection notice these are unambiguous defects that need no context to detect.
I deliberately kept the detector narrow. It only catches the zero-ambiguity cases. Whether a metaphor is too playful for the moment is left to human review, because a false positive there costs more than the miss. And ✅ is explicitly not on the list — it means “done,” not “hooray,” and it belongs in neutral notifications.
The point of the guardrail layer isn’t more rules. It’s turning the rules you already have into assertions.
The measured part
Two measurements mattered more than any design argument.
Deterministic paths run 20–60× faster than model paths. Measured across a real session: button clicks and shortcut routes finished in 33–369 ms; anything routed through the model took 6–25 seconds, median around 12. That gap is the entire case for deterministic exits — not elegance, latency. Every path you can resolve without the model is a path the user doesn’t wait on.
The emotion signal cost more than it was worth, until it didn’t have to. Adding a per-turn emotion classifier meant one extra serial model call in front of every reply. When I found that the assistant’s most important emotional moments — rejection, document return, license issued — are events the system itself generated, the answer got much cheaper. The case status already says what happened. Asking a model to infer “the user is probably frustrated” when a database column literally reads awaiting_new_names is paying for information you have.
So the signal became three tiers: infer from state where the state knows (free, and more accurate than a model guess); ride along on a classification call we were already making (free); and only pay for a dedicated call on the first turn of a conversation or on long messages, where genuine venting shows up. Cost dropped to roughly a quarter, and the highest-value cases became both cheaper and more reliable.
It’s still off by default. The persona passed all six emotion scenarios without it, which means the marginal value is real but unproven — and I’d rather ship the mechanism with a switch than ship an unproven cost on every turn.
Where the platform rules came from
I didn’t invent the tone baseline. I mined it.
Anthropic publishes Claude’s system prompts. Voice assistants like Sesame’s Maya have prompts that have been picked over publicly. These are the most iterated-on character instructions in existence — written by teams with more user feedback than we will see in years. Reading them cost an afternoon; rediscovering their contents through production incidents would cost quarters.
But you can’t paste someone else’s prompt into your platform layer. Three gates, and the middle one is the whole lesson:
Strip the domain. Every candidate rule had to still hold with no business noun in it. Lead with the feeling, then the plan, then the next step survives. Don’t pitch the registration package when the user is frustrated doesn’t — that’s the resident’s, not the platform’s. A unit test asserts the baseline contains zero business nouns; if one creeps in, that’s the signal it belongs a layer up.
A principle needs an enumeration. This one changed how I write prompts. “No formulaic customer-service pleasantries” is a principle, and every model interprets it differently — most of them decide their own pleasantries are the tasteful kind. “作为一个 AI” — as an AI — is an enumeration, and there is no room to interpret it. So the anti-service-desk rule ships as six literal strings the model can check itself against, not as a sentiment. Same for the memory taboos: not “reference memory naturally” but the two banned openers.
The general form: when you copy from a good prompt, the valuable part isn’t the text, it’s the resolution — the specific places where its authors decided a principle wasn’t enough and reached for a list. Those spots are where they lost an argument with reality.
De-pressurize the wording. This one was counterintuitive. Our existing prompts leaned on emphasis words such as must, never, and always, accumulated one production incident at a time. Testing on a newer model tier showed that it executes instructions more literally, and that the emphasis over-triggers. A rule marked must starts winning against rules that should have outranked it. The baseline is written flat, and a test asserts zero high-pressure emphasis markers in it.
I deliberately did not apply that to the resident’s existing rules. Those emphatic phrasings each commemorate a specific failure, and de-emphasizing them is a behavior change that needs its own testing. Platform layer flat, resident layer unchanged, revisit together.
The mining added one rule and completed another. The completed one matters most: our anti-sycophancy rule said don’t flatter, don’t agree without basis. The published prompts had the other half — when you’re challenged and you’re right, explain rather than fold. Sycophancy isn’t only excessive praise; it’s also capitulation under pressure. We had been guarding one door.
Why this is a platform concern
The last correction was the one I should have made first. I’d been building all of this inside one agent, as that agent’s humanization work.
But look at the six layers and ask which parts contain a domain noun. The ordering rule — catch the feeling, then give the plan, then name the next step — has none. Don’t narrate your own personality has none. Drop the humor when money or mistakes are involved has none; only the list of which scenes count as serious is domain knowledge. Same for the anti-AI-tone list, the conversational pacing rules, the memory-citation taboos.
Nearly all of the “how” is mechanism. Almost none of it is domain. That means it belongs in the platform, and every new agent should inherit it rather than rediscover it. This applies especially to the lessons that were paid for with production incidents. Never narrate your own persona exists because a model once introduced itself by reciting its own personality instructions to a user. That rule is a scar. Scars should be inherited, not re-earned by every agent.
The residents keep what’s genuinely theirs: who they are, what they say, which of their moments are serious, which events deserve a proactive message. Everything else moved down, and the new-resident checklist grew a section that names all six layers and what each one requires. That checklist previously said nothing about character at all — which is exactly why humanization looked like one agent’s feature instead of a property of the platform.
There are six layers. Four now have platform implementations, one proved unnecessary after investigation, and one is still ahead of us. But the reframing was worth more than any individual layer. the moment expression became a layer instead of a thousand string literals, we stopped finding those defects on real hardware and started finding them in CI.