Dialogue · Product · AI tone
← Design notes

AI tone — the written-not-said problem

The panel's meaning is good. Its register is an essay. This is the measurement of that gap against the humans sitting in the same rooms, the three things that actually move it, and the one that does not. Status: shipped — the meter, the net, and the prompt work. Written 2026-08-16.

A panel reply lands in a bubble on a phone, next to messages the guest typed with their thumbs. The complaint「AI 味」is not about what the hosts say. It is about punctuation and sentence shape that belong to print, arriving in a chat app. Every symptom below is meaning-free, which is what makes it fixable without touching the content.

The measurement

Intuition does not survive a frequency problem, so the first thing built was a ruler, not a fix: exam/aitone.py counts fifteen named tells and reports each as a rate per 1,000 characters. The control is not zero — it is the human column of the same corpus. Real people use colons and the odd metaphor, and a turn scrubbed to 0 reads as a telegram, which is its own tell.

The corpus: 40 real production rooms off the SG box, long human-with-panel chats, no game cartridges and no test rooms — 7,563 panel turns against 5,620 human turns (scripts/sweep_prod_chats.py).

rate per 1,000 characters, same 40 rooms human panel em dash 10.81 0.86 markdown 3.95 0.42 不是X,而是Y 2.37 0.28 rule of three 2.05 0.82 colon to explain 2.96 2.09 word echo 8.19 5.77 the last two are close to human and are reported, never driven to zero
Six of the fifteen tells. The em dash is not one symptom among many — by weight it is roughly half the entire gap.
the headline88.6% of panel turns contain an em dash. 1.6% of human turns do. A 55× gap on a mark that carries no meaning at all. Alongside it: the panel's median turn is 283 characters against the humans' 34, and 27.5% of panel turns arrive carrying a bullet list or a bold run.

Where it comes from

Not from the model being stubborn. Running the same meter over the prompts and profiles the panel reads every turn found them saturated with exactly the mark they were producing:

what the model readsem dashes / 1k
the human turns it is answering (the target)0.86
the megaprompt (system-prompt.md)5.78
a typical persona profile5.3 – 8.3
what it then writes10.81

The model reads roughly 27,000 characters of analyst prose about a person, then writes as that person, and carries the register across. It is not disobeying an instruction; it is obeying the most recent demonstration. A second cause sat in the code: the FORMATTING note told every host its words render as Markdown and to "use it naturally when it helps (a list, a table, emphasis)" — read as a licence to publish into a chat bubble.

What actually moved it

Three arms were tried and measured. Only two earned their place.

1 · the net — deterministic, and it does the heavy lifting

run_room.de_tell() runs on on-stage speech only, at the single seam where a <speak> becomes a bubble. It spends every em dash for the mark a thumb would have typed. Fenced blocks are left alone (a dash inside a diagram is syntax), and artifacts are never touched — the side pane is where writing is supposed to look written.

before
你说得对——这让我想起一件事。 核心不再是"我有没有能力"——核心变成了"我怕失败之后不知道路在哪里"。
after
你说得对,这让我想起一件事。 核心不再是"我有没有能力",核心变成了"我怕失败之后不知道路在哪里"。

Replayed over all 7,563 production turns it removes 64% of the whole excess over human, at zero cost and zero latency, because a punctuation mark carries no meaning and so nothing is lost by spending it.

2 · the prompt — real, but it plateaus at about a tenth

A How a message looks section in the megaprompt names the tells outright, and a closing chat_register_note() sits after the profiles, telling the model in as many words that the essay prose it has just read is the analyst's and not the host's. Measured by exam/tone_ab.py — 20 real guest messages replayed against the live megaprompt, base arm in a git worktree, both arms scored raw:

symptom before after Δ em_dash 7.24 5.51 -1.72 colon_expound 4.90 2.95 -1.95 negation_reframe 3.01 2.44 -0.57 parallel_frame 1.22 0.51 -0.71 INDEX 62.66 56.23 -6.43 mean len 449 411 -39
the honest readNaming the tell helps, and it is not enough on its own. Adding the terminal register note on top landed inside the noise. A model asked not to reach for a token still reaches for it about three quarters as often. Prompting sets the intent; the net sets the floor. Anything meaning-free should be spent deterministically rather than argued for.

2b · two more prompt levers, taken from what other people do

The whole「去 AI 味」ecosystem on GitHub — speak-human-tw (38 patterns), shuorenhua, ai-flavor-remover, de-ai-flavor-skill — is one design: a symptom checklist handed to a second LLM pass over finished text. No code, no measurement, and no human control. Their lists nonetheless independently confirm the meter: 破折號過度使用, 否定平行結構「不是A,而是B」, 排比三段式, 粗體過度, 編號切碎, 句長過勻, 說教式深度腔, 刻意換詞循環, and — for Vibes — 幻覺引用「提升 47.3%」. Fifteen detectors built from this room's own data landed on the same list.

Two levers came out of the literature rather than the repos, and both were measured against the shipped baseline (n=20, bench noise ~±0.5):

armindexvs baselineverdict
human control (the target)15.12
baseline — the shipped prompt work56.23
show the register — quote the guest's own last lines back as a style sample53.72−4%off by default
positive framing — "punctuate the way a thumb types" rather than "never use an em dash"50.81−10%shipped
the negative result worth keepingShowing the register did not transfer. The style-imitation literature reports zero-shot style instruction under 7% accuracy against 67–95% one-shot (Jemama et al.), which is the whole reason it was tried. It bought almost nothing here because the example was never missing — twelve turns of the guest's own messages are already in the context. The model was not lacking a demonstration; it simply was not imitating one. It also did not shorten anything: mean length went 411 → 425. Length is the floor producer's ceiling, not a thing imitation reaches.

Positive framing is the cheap one that worked, and it matches the one solid mechanism in the literature: a negative constraint has to suppress an already-activated path, while a positive one just names the path to take (constraint-compliance study). Note the popular「Pink Elephant」explanation for this is conceptual, not measured — the widely-cited article runs no experiment, so don't lean on it.

Stacked, the prompt arm reaches roughly −15%. The net is still doing four to five times as much work.

3 · de-dashing the prompts themselves — tried, reverted

The obvious follow-on was to strip the dashes out of the instruction files so the demonstration stops. It was done, measured by eye, and reverted: in instruction prose those dashes are structural, not stylistic. THE FIREWALL — never cross it. became THE FIREWALL, never cross it., and a comma splice in a rule the panel has to parse is a real risk to the thing this whole exercise is meant to protect. The 88 persona profiles were left alone for the same reason, and because the net already zeroes the output.

Layout is conditional

owner's ruling · 2026-08-16Markdown in a bubble is not banned outright. When a guest is being walked through something and asks for structured feedback, the structure is the answer and it stays. A casual chat gets it demoted.

The axis that decides this already exists and costs nothing: the floor producer's own length ceiling. It has tiered every turn since the flatness work — minimal ≤40字, tight ≤90, medium ≤140, generous ≤280, solo-deep 500–2000. keep_layout() reads the cap straight off the staging line: generous and up keeps its Markdown; below that it is demoted to the same lines without the furniture. No second judge, no extra call, and a missing staging fails open — a turn with no producer read never has a guest's structure stripped from it.

the staging line ≤ K 字 · ≤ K words keep_layout() cap ≥ 240字 / 100w ? generous · solo-deep structure survives — it is the answer minimal · tight · medium demoted to plain lines, every word kept
One axis, already computed. Demotion never deletes a word — only the bullet, the heading, the rule and the bold markers.

With demotion in play on conversational turns, the net removes up to 76% of the excess; the live figure sits between that and 64%, depending on how many of a room's turns are analysis turns.

The Vibes number

A separate surface, the same disease. Vibes posts kept landing invented precision — "47 minutes of paper jam", "it beeped 14 times", "37 times this week". The cause was literal: lib/vibes_write.py asked every post for "one concrete number", and the authoring kit's own worked example was "37 个拒绝". The model was quoting its brief.

the rule nowNumbers only where a person would actually have one. A count you could not miss (the third time, both socks), a clock time, a price: fine. A precise figure for something nobody was counting is the surest sign a machine wrote it. If you did not count it, you did not count it — again, twice, half the morning. Fixed in the megaprompt and in how-to-write-vibes-moments.md together, so the rule and its example agree.

Running it again

python scripts/sweep_prod_chats.py --pull # the corpus, off the box python scripts/sweep_prod_chats.py --local # …or the dev mirror, when :22 is blocked python exam/aitone.py exam/runs/sweep-prod.json # panel vs the human control python exam/aitone.py <corpus> --symptom em_dash --top 10 python exam/tone_ab.py --n 20 --base HEAD # a prompt change against its own BEFORE

The A/B builds the base arm in a git worktree, never a checkout in place — parallel sessions share this working copy. It scores raw, before the net, because the question it answers is whether the writing moved, not whether the net can mop up after it. --base-bubbles reuses a paid-for base arm when only the working tree changed.

still openLength. The panel's median turn is 283 characters against the humans' 34, and nothing here touched that — it is the floor producer's ladder, and moving it trades directly against substance. Worth its own study rather than a guess. The residue. After the net, the loudest things left are word_echo and the reframe reflex, neither of which is safe to rewrite mechanically.
AI tone · 2026-08-16 · the meter is exam/aitone.py, the net is run_room.de_tell, the bench is exam/tone_ab.py. Sits beside conversation quality (the flatness diagnosis) and the floor producer, whose length ceiling this page borrows.