Fact Grounding for AI Characters: Classify Hallucinations by Supply Chain, Not as "the Model Being Bad"
Background: a character's hallucination is a product incident, not a model defect
In an AI character-raising product, characters have personas, memories and their own schedules. A typical production incident: on a request at 16:08, the character answered as if it were 14:30 — and migrated the lunch break from its own schedule into advising the user to go take a nap. Attributed to "model hallucination", this class of problem is unfixable — the real lesion is that the prompt contains several competing copies of the facts: current time, calendar anchors and the schedule's absolute time windows are injected independently, and the model merely picked the wrong authority among several. The distilled lesson: fact bugs must be classified by supply chain into four kinds, each with its own fix — freshness (when the fact was taken), authority (which of several copies rules), ownership (whose fact it is), and exit (what the model is allowed to write).
Turn-level fact capsule: same turn, same source, same snapshot
The fix for authority problems is consolidation: each request compiles one fact capsule with owner boundaries (world / character / user), injected once near the latest user turn, while the clock expansions and standalone schedule comments scattered elsewhere get deleted — no second place is left that can say "what time it is now". Schedule start times render as relative durations rather than absolute instants, so a salient number like "14:30" cannot be promoted by the model into the current time. Outside the capsule only one thin deterministic backstop remains: if the reply states the current time or weekday explicitly wrong, retry once; the same predicate is reused on the fail-closed paths of streaming, persistence and proactive messages. The judgment sentence: all facts within one turn must share one source and one snapshot, and "who owns this fact" belongs in the structure, not in the wording.
Send-time grounding: shrink generation into selection
On the path where the character proactively messages the user, the time gap is a hallucination hotbed: scheduling and moderation sit between the decision moment and the send moment. Even with an authoritative clock injected, a guard that only catches "explicitly wrong time" cannot stop implicit temporal claims — "binged that show till midnight", "about to be late" carry no checkable number yet are pure time assertions. The fix is to withdraw the pen: the model no longer writes message text freely; it selects a candidate_id from server-held candidates. Neutral candidates contain no clock or activity claims; schedule candidates carry a schedule-snapshot fingerprint and a two-minute lease; after moderation and before persistence, the time window, lease and snapshot are re-validated at the send moment. On the concurrency side, a database-atomic claim locks each user-character opportunity slot into a single decision, and the record stores observation time, actual send time and grounding source. The judgment sentence: for implicit claims validation cannot catch, deny the model the chance to write them — shrink generation into selection, and the fact constraint turns from "prompt" into "structure".
Semantic recall: the model outputs references, code verifies references
"Do you remember that doctor who just moved here?" fails to match the archived "new doctor" — literal topic matching misses paraphrase, producing a false "no record" — worse than a hallucination, because the character does remember yet claims to forget. The backstop is a bounded semantic evidence selector: it activates only after literal matching comes up empty; the model may only output high-confidence message IDs from the already-filtered messages of the current session; the code side re-validates every ID and expands only to adjacent context; if everything fails it closes and falls back to the original "not found" answer. When the candidate set is incomplete due to truncation, the answer is "unavailable" rather than a flat denial. The judgment sentence: let the model participate in retrieval, but have it output references, not content — references can be verified by code, content cannot.
The PE sandbox: every fix reproduces first, ships second
Every step above leans on the same piece of infrastructure: a side-effect-free prompt A/B sandbox. It uses the real production prompt-assembly logic and current character context, but produces no messages, threads, intimacy events or memory writes: drafts have normalized hashes, snapshots are immutable and shared, tools run through a read-only allowlist, each variant keeps a trace, with response budgets and cancellation, open only on staging. The 16:08 incident's fix first ran old and new injection styles against the same question in the sandbox, reproduced the old style answering "it's a bit past noon — nap time", and only shipped after the difference was confirmed. The judgment sentence: without a side-effect-free comparison sandbox, every fact-grounding fix ships on faith.
Transferable judgments
Most character-consistency problems are not "the model being bad" but the fact supply chain being bad. Four lesions map to four fixes: authority is unified by the turn-level capsule, freshness by re-grounding at the send moment, implicit claims by shrinking the generation space rather than validating after the fact, and memory retrieval by having the model output verifiable references. The value of this classification is stopping the reflex of "add another prompt rule" — rules cannot fix a supply chain; they only postpone the next hallucination into phrasing the rules don't cover.