Skip to the content.

Context

LLM-generated artifacts (example sentences, summaries, labels) are written to persistent storage and reused for a long time, while the models and prompts that produce them keep improving. The prompt changes more often than the model, and can matter as much to output quality, sometimes more.

Examples

When the system generates example sentences for a word, it stamps each stored sentence with a created_by value naming the model and prompt version that produced it (for example, claude-opus / examples-v3). When the example-generation prompt is improved to v4, the stale sentences are exactly those still stamped v3, so a single query finds them and they are regenerated, without touching the rest of the store.

The stamp is not confined to example sentences. Simplified articles and their per-level summaries, audio lessons, and meaning-frequency labels all carry it, and they carry it by pointing at a shared generator row rather than each repeating a model name in a string column of its own.1

That shared entity is what turns regeneration into a join. Asking which artifacts a given prompt version produced is then one lookup, answered across every kind of artifact at once. With the model name denormalized into each table it is four scans over four independently spelled strings, and each new kind of generated artifact adds a fifth.

Problem

When a prompt or model improves, how can exactly the stale artifacts be found and regenerated, without reprocessing the entire store?

Forces

Solution

Store the full provenance tuple alongside every LLM-generated artifact: model version, prompt version, generated output, timestamp. This turns regeneration into a targeted query, as broad as “re-run everything produced by prompt v2 with the improved v3” or as narrow as a single (model, prompt) pair, so when a prompt does well on one model and poorly on another, only that combination is redone.

Provenance as Zeeguu stores it. Each generated example sentence keeps its own timestamp and points at a shared `ai_generator` row naming the model and prompt version that produced it, rather than repeating them in every row. Finding what a given prompt produced is then a join on one table.
Provenance as Zeeguu stores it. Each generated example sentence keeps its own timestamp and points at a shared `ai_generator` row naming the model and prompt version that produced it, rather than repeating them in every row. Finding what a given prompt produced is then a join on one table.

Consequences

Known Uses

Notes

War Story: The Nine Hundred Lessons

Provenance has to capture the dimension that actually varies. Zeeguu’s audio_lesson_meaning rows carry a created_by field naming the model ("Claude-Opus-Prompt1"), but the prompt templates were edited in place without bumping that identifier, so one value spanned two materially different prompt eras. When the ~900 lessons generated under the earlier, ambiguous prompt had to be found, no query could find them. The only route was a content regex on the generated script itself, asking whether it contained the ambiguous phrasing. A provenance field that does not move when the prompt moves is decorative, and selective regeneration degrades into forensics on the output. If prompts evolve by in-place edits, the field naming them must bump on every edit, through a versioned filename or a content hash.

Finding them was half the problem. The rows were also a cache, and they were referenced from lessons learners had already listened to, so they could be neither regenerated in bulk (expensive, and wasted on content never requested again) nor deleted (old daily lessons resolve them by id). Each affected row was given a deprecated_at timestamp instead, and the cache lookup was gated to skip deprecated rows: new lessons regenerate under the improved prompt while existing ones keep playing what the learner heard before, and regeneration is paid lazily, one row at a time, on next demand.

That fix contained a second bug. Each lesson’s audio file was named after the vocabulary item it taught, which does not change when the lesson is regenerated, so the first regeneration wrote its audio over the recording the deprecated row still needed. Files had to be keyed on the row’s own id. Where old artifacts must stay resolvable, every stored by-product has to be keyed by the artifact that produced it, not by the subject it is about.

Both halves trace to one root: the stored artifact did not carry enough of its own history.


💬 Open an issue about this pattern

  1. The AIGenerator entity (model_name, prompt_version) and the tables referencing it in the zeeguu/api repository. ↩