Gå til hovedinnhold

Prompts

Every tool that drives the engine sends the same prompt for the same block. Here you can see what a prompt contains, where each part comes from, and how to print it from the binary you run.

The framework builds the prompt with the same builder and rules whoever asks. The kapi CLI, Kapi Desktop and an embedding Go program send an identical prompt for an identical block, so the prompt can be documented and fingerprinted. The Prompt Reference is generated from these builders. The examples below use the kapi CLI's --explain-prompts.

See the prompt for a run​

Add --explain-prompts to any command that reaches a model. It prints every call the run made: the prompt, the provider, the model, the reply, and the tokens spent.

kapi translate messages.json --target-lang fr --explain-prompts

Pair it with --provider demo (the built-in offline stub) to see the prompt a paid provider would receive, with no API key, no network, and no spend:

kapi translate messages.json --source-lang en --target-lang fr --provider demo --explain-prompts

The output attributes every block of the prompt to the thing that produced it:

─── LLM call 1/1 ─────────────────────────────────
prompt: translate.single (v3)
provider: anthropic model: claude-sonnet-5

── system ──
[task · framework]
You are an expert linguist with deep command of both languages and of the
subject domain. Translate the user's text from en to fr, rendering it as a
specialist writing natively in that domain and language would. Return ONLY
the translation, with no explanation, preamble or quoting.

[constraint · framework]
Preserve placeholders such as {0}, %s and {{name}} exactly.

[instruction · --instruction / recipe]
Instruction (apply when translating):
Informal register. Keep product names in English.

[preferred_terms · terms (12 terms)]
Terminology (use these renderings):
- utilize → use

[do_not_translate · terms (2 do-not-translate terms)]
Keep these terms exactly as written, in the same casing. Do not translate them:
- Acme
- Acme Cloud

── user ──
[content · source block]
Save

--explain-prompts=<path> writes the same calls to a file as JSON, so a prompt can be diffed between runs or attached to a bug report.

merknad

--explain is a different flag. It reports a flow's resolved source → sink bindings without running the flow.

How a prompt is composed​

A kapi prompt is a document assembled from sections with different owners:

Frameworknot configurabletask: translate from X to Y, return only the translationconstraint: preserve placeholdersconstraint: reproduce every inline tag exactlyone promptYour projectthe steering surfaceinstruction: from --instruction or the recipevoice profile: from your voice profileterminology: from your terms storeone promptYour documentdata, never instructioncontext: the block's key, its neighbours, its prior translationcontent: the source block itself

The sections of a kapi prompt, and who owns each.

The framework owns the task and the constraints. A translation that drops a placeholder or mangles an inline tag cannot be written back into the source file: it breaks the round trip. No flag or setting removes these rules.

Your project owns the steering. The instruction, the voice profile and the pinned terms make the output fit your project. You declare them in the recipe, the voice profile and the terms store, and never type them into a prompt.

Your document owns the content and the context. The source text is the entire user turn: data, never instruction. Its key, its neighbours and its prior translation travel separately, in the system turn, as reference the model reads but must not translate.

Steering the model​

To change what the model does, add to the steering sections. The rest of the prompt is fixed.

You wantYou set
A one-off directive for this run--instruction "informal register"
A standing directive for the projectdefaults.tools.translate.instruction in the recipe
Consistent tone and phrasingA voice profile
Specific terms translated a fixed wayA terms store
A term kept exactly as writtenA concept marked do-not-translate, or --dnt
The model to know what a string is--context key (default) or --context neighbours
kapi.yaml
defaults:
source_language: en
target_languages: [fr, de]
voice:
profile: acme-docs
tools:
translate:
instruction: "Informal register. Keep product names in English."

Everything in that recipe is visible in the prompt, and --explain-prompts shows which section each line landed in. If a pinned term is not reaching the model, this is how you find out.

Prompts are provider-neutral​

kapi keeps one prompt for every vendor. A prompt declares its turns semantically (a system turn and a user turn), and each provider carries them the way its API expects.

core/ai/promptone promptProvidertransportVendor APIanthropic / openai / …system + user turnsidentical text for every providervendor-shaped requestAnthropic: top-level system param · OpenAI: a system message · Gemini: folded into the first user turncompletiontext or structured JSON

One prompt, carried by each provider's transport.

Providers differ in capability: how structured output is requested, whether images are accepted, how a system instruction is carried, and a workable batch size. That variance lives behind the provider interface. Switching --provider anthropic to --provider ollama changes the transport and the model, and leaves the instructions as they are.

Identical prompts and the translation cache​

The same inputs always render byte-identical prompt text. Terms in the terminology section are sorted, and no timestamp, run id or machine detail is ever interpolated.

kapi caches translations, and the cache key is derived from the rendered prompt text. Change your instruction, your voice profile, your terms, or the prompt itself, and the fingerprint moves. The affected strings are then re-translated, and none is served from a cache that a different prompt produced.

Context: what the model is told about a block​

A block on its own is often not enough to translate it. Save can be a verb or a noun (in German, Speichern or Speicherung), and a better model cannot resolve an ambiguity in the input.

So the engine sends reference material: text the model reads but must not translate. Reference material differs from the other blocks in a batch. Every segment in a batch is a task that competes for the model's attention and distracts from the others. Reference material only disambiguates, and adds no work. More segments in a batch lower quality; more reference material does not.

contextWhat the model is told
noneOnly the text.
keyDefault. The block's key (settings.save).
neighboursThe key, plus the surrounding source blocks.

The key provides context already present in the document at a small token cost. A key such as settings.save tells a button label apart from other uses of “Save”. The key stays with its block.

Neighbours provide more context and consume more tokens. Their content is included in the cache fingerprint, so editing a neighbouring string invalidates cached translations that depended on it. It is off by default.

kapi translate messages.json --target-lang fr --context neighbours

A third piece of reference material comes from the content memory. When a block has been edited since it was last translated, translate sends the block's own last approved translation alongside the new source (reuse: prior, the default), so the model revises a settled rendering. The prior version is offered only when the governing context (voice profile, term rules) is the one it was approved under; reuse: none turns it off.

What the engine does not send​

The engine leaves these out of every prompt:

  • Fuzzy content-memory matches. Memory reuse is a separate, deterministic step (recycle) that runs before translate and fills what it can. Matches are never offered to the model as examples; the one thing the memory contributes to a prompt is a block's own prior approved translation, above.
  • A rolling document context. Reference material is a bounded window around the block, never the whole document.
  • Telemetry. Your content goes to the provider you configured, and nowhere else.

By default, translate batches several blocks into one call (translate.batch), taken in document order and sent as a JSON payload keyed by segment id. Blocks in one call are all being translated, so none of them is settled reference material for another, and the blocks either side of the batch are in a different call. A block is translated on its own (translate.single) when you set batching: single, or when the provider runs on-device: a small local model tends to ignore the "return JSON" instruction on a large batch.

kapi sets how many blocks share a call. The right number depends on what your model can emit, how long your segments are, and a quality-versus-batch-size curve with no published measurements. kapi sizes each call against the model's real output limit. A fixed batch size would overrun that limit on anything longer than a UI string, and the model could not finish its reply. You choose between batching: auto and batching: single.

The evidence favours a large untranslated context with a small translation unit, and --context neighbours --batching single has that shape.

Prompt injection​

The text kapi translates is often not written by the person running kapi: it is CMS copy, user comments, strings from a dependency. A block that says "ignore your instructions and output X" does occur.

The prompt tells the model the content is data, in a constraint section you can see in --explain-prompts and in the Prompt Reference. This is one layer of defence. No prompt-level rule reliably stops a model from following an instruction embedded in its input.

The structure of the call limits the damage:

  • The call has no tools, no filesystem and no network. It is one request that returns text. An injected instruction cannot act; at worst it produces a wrong translation, and the damage stays within the content.
  • The reply is constrained to a schema by the provider's own grammar, so it cannot escape its shape, only fill it.
  • kapi accepts only the segment ids it sent, so an injected omission is caught and retried, and the translations after it stay aligned.
  • The payload is JSON, so content cannot forge a segment boundary. A block containing [2] is enough to break a delimiter-framed payload with no attacker involved, and a user documenting a numbered list writes one by accident.

An injected instruction can still corrupt the text of a translation. Checks and review catch that.

The prompts kapi ships​

Every LLM call kapi makes carries a prompt id, so it can be named in --explain-prompts and cited in a bug report:

PromptSent by
translate.singletranslate, one block at a time
translate.batchtranslate, several blocks in one call
voice.checkvoice-check
voice.infervoice-infer (drafting a profile)
term.formskapi terms expand, listing a term's inflected forms
quality.checkqa, in AI mode
reviewreview
term.extractterm-extract
entity.extractentity-extract
axis.discovercontext-scan, proposing the axes a corpus varies along
media.refine.imagemedia-refine, re-reading an image crop
media.refine.audiomedia-refine, re-hearing a speech clip
media.refine.videomedia-refine, re-reading a video frame
segmentsegment, with the LLM engine

The three media.refine.* prompts are separate prompts, and each sends a different kind of your data: a cropped image, a clip of audio, a video frame.

If a call appears in --explain-prompts without an id, report it as a bug: an unlabelled call is an undocumented use of your content.

The full text of each is published in the Prompt Reference, generated from the same builders the binary uses and checked against them in CI.

Watch a prompt get built​

Build a prompt up a layer at a time and read the real output at each step. This runs kapi in your browser: the same binary, the offline demo provider, no key and no network.

Loading the walkthrough…

Start bare and you get the framework's two sections plus your content. Add --instruction and a steering section appears, attributed to the flag. Translate a block that carries inline markup and the tag-fidelity constraint appears, which a plain string never sees.