BLOG

🌱 digital garden

Basé à Nice, France - CET (UTC+1)

BLOG

🌱 digital garden

Basé à Nice, France - CET (UTC+1)

BLOG

🌱 digital garden

Basé à Nice, France - CET (UTC+1)

A reuse rate case study for an LLM workflow

Notes from reviewing a German to French translation pipeline for regulated documents. One setting nobody sees moves the headline number from 14% to 91%.

I was asked to review the architecture of a machine translation pipeline for regulated technical directives, German to French. The documents are the kind where one term has one approved translation and a reviewer signs the result.

To review it I needed something to measure, so I wrote a small demonstrator: 6,600 lines of Python, standard library plus a PDF reader, runs offline in one command and prints a table. It translates badly on purpose. By default the model is a local lookup table. Its job is to measure three mechanisms, and to label everything it does not measure.

All figures below come from a public document: Swissgrid's minimum technical requirements for connecting converter-based energy storage systems, version 1.1, 36 pages, published in German and in French. No client document appears in this post.

The design

Three decisions carry the rest.

Segment per sentence, not per paragraph. A paragraph almost never repeats between two versions of a directive. A sentence does. Everything about reuse follows from this.

Look up the memory by edit distance on the source string, not by embeddings. A sentence and its negation share a topic and a vocabulary, so a vector index tends to call them near-identical. Edit distance does not put them far apart either, but it never calls them the same: the match comes back fuzzy, below 100%, and a fuzzy match is always opened by a reviewer. In a regulated text, serving the translation of "requires approval" for "does not require approval" is the worst available failure, and the test suite has a case for exactly that pair. I did not run an embedding model to compare, so the first half of that argument is reasoning and not a measurement.

Label every segment. Each sentence leaves with a status and a reason, so the reviewer opens only what is flagged.

Status

Meaning

Reviewer opens it

MEMOIRE_100

exact memory match, no model call

no

MEMOIRE_FLOUE

fuzzy match reused as is

yes

MACHINE

model output, conforms to the glossary

no

MACHINE_SIGNALEE

violation still there after one retry

yes

LANGUE_SOURCE_RESIDUELLE

sentence left in German

yes

LANGUE_INDETERMINEE

too few clues to decide

yes

What reading the PDF costs

Measure

Value

Pages

36

Segments (sentences)

750

End-of-line hyphenation breaks repaired

208

Repeated header and footer lines removed

35

Median segment length

86 characters

Longest segment

1,490 characters

Segments with undetermined language

159 (21%)

The longest segment is a table that PDF extraction poured into running text. That one row is the argument for working from the original Word file through a proper filter and not rebuilding structure from a PDF.

The number that depends on a setting nobody sees

To measure reuse on a real document I aligned the German with the published French, sentence by sentence, and loaded the pairs into the memory. Then I translated the German again and counted how many characters never reached the model.

The aligner gives each pair a confidence score. A threshold decides which pairs enter the memory. Same document, same memory, same code, only the threshold moves:

Threshold

Pairs kept

Reuse rate

Characters sent to the model

0.0

711

91%

6,642

0.5

655

80%

15,305

0.7 (default)

313

30%

53,675

0.8

286

25%

57,790

0.9

151

14%

65,845

From 14% to 91% on one setting. Anyone quoting a reuse rate without the threshold behind it is quoting nothing.

At 0.0 the pipeline reuses 91% by accepting wrong pairs, and a wrong pair loaded into a memory comes back later as a wrong translation with a displayed score of 100%. The high number is the dangerous one.

The cliff between 0.5 and 0.7 has a dull cause.

202 of the 716 pairs score exactly 0.600. The aligner cannot rank them against each other, so it gives them all the same value. A threshold at 0.7 drops all of them at once and a threshold at 0.6 keeps all of them. My default landed just above the plateau by accident. The tool now prints its plateaus so the cliff is visible before someone picks a number.

The score is noisy in both directions. • Normen IEC; and • les normes de la CEI ; are a correct pair scored 0.32, because the acronym itself is translated and the surfaces diverge. A plainly wrong pair in the same document also scores 0.32. The score has never been calibrated against a human judgement on this corpus. That calibration is a concrete task with a cost, and it comes before any claim about reuse.

Reuse between two versions

The other reuse case is the pleasant one: version 2.3 of a document after version 2.2 has been translated. I built a synthetic pair where the second version differs from the first by six declared string replacements, listed in a changelog next to the files.


Pass 1 (v2.2)

Pass 2 (v2.3)

Segments

64

65

Model calls

91

5

Characters sent

6,331

372

Reuse rate

0%

92%

92% is a laboratory ceiling, since I wrote the second version myself. The number to present is the one from the real document, with its threshold.

A word search does not hold in German

The usual glossary check is a regular expression with word boundaries. German breaks it twice. It declines: \bPlangenehmigungsverfahren\b does not find Plangenehmigungsverfahrens, because the genitive s removes the boundary. And it compounds: the term can sit inside Plangenehmigungsverfahrensantrag, where no boundary will ever be.

I ran both checkers on the same segments, the naive regex and a morphological one that splits compounds and folds declensions.


Synthetic German sample

Public Swissgrid document

Violations caught by morphology

56

2

Violations caught by regex

52

2

Violations missed by the regex

5 of 56 (9%)

0%

Fixed by one retry with the violation named

56

2

The regex reports 52 on the synthetic sample, and one of those is a finding the morphological checker does not confirm. That is why it misses 5 and not 4. The second column is the more useful one. The glossary was written for a directive about plan approval. On a document about energy storage it recognises one compound and one term. A glossary does not transfer between documents, and neither does a hand-written compound lexicon. The 9% is measured on that text with that glossary. It is not a statement about German.

Measuring quality, with the caveats attached

Quality is measured in exactly one place: translate cold with a real model, then score against the published human translation.


First run

Second run

Model

claude-haiku-5-5, low effort

same

Calls

703

703

Input tokens, uncached

432,938

153,847

Input tokens read from cache

0

435,157

Output tokens

45,972

49,270

Duration

897 s

823 s

chrF

0.737

0.752

BLEU

0.565

0.596

Mean edit similarity

0.799

0.817

Two changes separate the runs. The glossary and instructions moved into a cached system prompt, with only the per-segment terms in the user turn. And the first run leaked. On short headings the model sometimes returned the German unchanged followed by its own hesitation: 6.2.1.5 Validierung, then "Hmm, the segment is". A scorer sees that as a bad translation. A reviewer sees a document with model commentary inside it. The second run forbids commentary in the prompt and filters what still gets through.

It did not fix everything. Segment 648, a sentence fragment cut at a page break, still comes back in German in the second run. It is flagged, not hidden.

What these scores do not say is printed under them every time:

  • 311 of 750 segments are scored, 41% of the document. A segment without a reference is not counted as a zero and enters no average.

  • The reference is paired by an automatic aligner, so alignment error is inside the score.

  • chrF and BLEU measure surface overlap with a single reference. A different correct translation scores low and a close wrong one scores high.

  • Nobody has read the output. The numbers are automatic end to end.

What I would keep

A probabilistic component is easier to trust inside a deterministic frame. The model is one interface with three implementations: a local stub, an API, a self-hosted server. Nothing else in the pipeline knows which one is running, so the question of where the data may go stays a deployment decision.

Every number the tool prints is either measured by the tool or labelled as a demonstration, and the table always ends with what it does not say. That costs a few lines of code and it is the part of the review people remember.

Sources

The demonstrator behind these measurements was built with AI coding tools. The text was proofread by AI.