---
title: 'Format and Length Control'
source: 'https://academia.sh/en/courses/prompt-engineering/format-and-length-control'
course: 'Prompt Engineering'
language: en
updated: '2026-10-21T05:02:33+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes the difference that a format declaration adds to the parseable response share and to the label hit rate next to the token cost.'
    - 'Compares in the same chart the tokens gained by narrowing the maximum output length and the parseability lost.'
    - 'Compares the difference that a prompt change adds with the difference between two runs of the same prompt and decides whether it is measurable.'
---

# Format and Length Control

The response to a clear request is still free text, and a program cannot read it. On the same 120 fictional support tickets a three-field record is requested: the share that the strict field parser reads in the free response is 0.0000, it becomes 0.9750 when a format declaration is added, and the label hit rate rises from 0.3083 to 0.6833. The cost is a rise from 6140 to 8780 in input tokens and from 1392 to 2026 in output tokens. When the maximum output length is lowered from 24 to 10, the output tokens fall from 2026 to 1170 but the parseable share drops from 0.9750 to 0.0000; at the intermediate values the curve stays tied exactly to the number of cut responses.

The previous lesson counted in how many different forms an ambiguous request is answered and showed
how far making the request measurable narrows this distribution. The request is now clear: the label
set to choose from is written in the prompt, and what is asked stands in a single sentence. Even so,
one decision is still left to the model. The model knows **what to say** but does not know **in which
form** to write it.

The cost of this distinction depends on who will read the response. A human reads the sentence "your
record has been received, we will review the matter and get back to you" without trouble. A program
that will write the same sentence into a record field cannot read it: it contains no label field, no
fault code and no separate rationale. This lesson treats the form of the response as a design
decision and measures two things: the difference that a format declaration adds to **machine
parseability**, and the share that a length limit takes back from the same measure.

- **PD41.** The model is a **simulator**; no real endpoint is called. It is written with the standard
  library, its seed is **20260218**, and the same prompt gives the same response with the same seed.
- **PD42.** The support tickets are **fictional**: 120 short records on five topics, with a
  misleading sentence added to some of them.
- **PD43.** The task asks for a three-field **record**: label, fault code and short rationale. The
  fault codes are fictional too.
- **PD44.** Two parsers are used. The **strict field parser** looks for all three fields to be
  complete and consistent; the **loose reading** looks for a label word anywhere in the response.
- **PD45.** In all three prompts the label set is the same and the request is equally clear. **The
  only thing that changes is the format declaration**; the length limit is measured separately.
- **PD46.** The length limit is the **maximum output length** of the simulator. When the limit is
  exceeded the stop condition fires and the response is cut in the middle.
- **PD47.** The cost is written in **tokens**: input tokens, output tokens and number of calls.

## A Free Response Is a Response the Program Cannot Read

The first block sets up the simulator, defines the three-field record and writes the two parsers. Three
prompts are run side by side: one without a format declaration, one with a format declaration, and
one with a format declaration whose output length is limited to twelve tokens.

```python
# SIMULATOR -- FICTIONAL. This code does not call a real model endpoint; it is a
# language model simulator written with the standard library. The tickets are fictional too.
import math

SEED, M32 = 20260218, 0xFFFFFFFF
WINDOW, MAX_OUT = 240, 24           # context window and maximum output, in tokens


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


def tokenize(m):                    # FICTIONAL tokenizer: pieces of at most five characters
    b = []
    for s in m.lower().replace(",", " , ").split():
        while len(s) > 5:
            b.append(s[:5])
            s = s[5:]
        b.append(s)
    return b


def prompt_trace(m):                # trace that depends on the prompt content, independent of the run
    h = 2166136261
    for c in m:
        h = ((h ^ ord(c)) * 16777619) & M32
    return h


TOPICS = ["fatura", "ariza", "sayac", "tarife", "kesinti"]
CODES = {"fatura": "ft-21", "ariza": "ar-14", "sayac": "sy-32", "tarife": "tr-05",
         "kesinti": "ks-40", "odeme": "od-11", "sikayet": "sk-09", "bilgi": "bg-02",
         "genel": "gn-00"}
TEMPLATES = {"fatura": ["bu ayki fatura tutari gecen aya gore cok yuksek geldi",
                         "faturami odedim ama sistemde borc gorunuyor",
                         "fatura ayrintisini nereden gorebilirim"],
             "ariza": ["evde su akmiyor bir ariza var galiba",
                       "baglanti bozuk oldu ve hic calismiyor",
                       "ariza kaydi actim ama ekip gelmedi"],
             "sayac": ["sayac okumasi yanlis girilmis endeks tutmuyor",
                       "sayac degisimi icin randevu istiyorum",
                       "sayac uzerindeki endeks ile fatura ayni degil"],
             "tarife": ["hangi tarife basamagindayim ogrenmek istiyorum",
                        "tarife degisikligi icin ne yapmam gerekiyor",
                        "birim ucret abone tipine gore degisiyor mu"],
             "kesinti": ["mahallede kesinti var ne zaman gelecek",
                        "planli kesinti duyurusu nereden takip edilir",
                        "aksamdan beri kesinti yasiyoruz"]}
EXTRAS = ["ayrica gecen ay da ayni sorun olmustu", "bu konuda memnun degilim",
          "acil bilgi almam gerekiyor", "kayit numaram elimde yok"]

TICKETS = []
for i in range(120):
    r = rng(SEED + i)
    k = TOPICS[pick(r(), [0.24, 0.22, 0.18, 0.20, 0.16])]
    m = TEMPLATES[k][int(r() * 3)]
    if r() < 0.34:
        m += " " + EXTRAS[int(r() * 4)]
    TICKETS.append({"id": 4001 + i, "text": m, "topic": k})

CONCEPTS = TOPICS + ["odeme", "sikayet", "bilgi", "genel"]
CUES = {"fatur": "fatura", "tutar": "fatura", "ayrin": "fatura",
        "ariza": "ariza", "bozuk": "ariza", "calis": "ariza", "akmiy": "ariza",
        "sayac": "sayac", "endek": "sayac", "okuma": "sayac",
        "tarif": "tarife", "birim": "tarife", "abone": "tarife", "basam": "tarife",
        "kesin": "kesinti", "duyur": "kesinti", "mahal": "kesinti",
        "borc": "odeme", "odedi": "odeme", "ucret": "odeme",
        "memnu": "sikayet", "sorun": "sikayet", "acil": "sikayet",
        "bilgi": "bilgi", "ogren": "bilgi", "nered": "bilgi"}
TRAPS = {"degis": "tarife", "kayit": "ariza", "yukse": "tarife", "gorun": "fatura",
         "ekip": "kesinti", "takip": "tarife", "giril": "fatura"}   # misleading cues
FREE_FORM = ["talebiniz {k} konusuyla ilgili gorunuyor",
             "bu kayit {k} basligina girer , ilgili ekibe yonlendirildi",
             "kaydiniz alindi , konuyu inceleyip donus yapacagiz"]
CALL_COUNT = [0]


def call(prompt, summary, seed=SEED, temperature=0.6, max_out=MAX_OUT):
    CALL_COUNT[0] += 1
    tok = tokenize(prompt)
    empty = {"tokens_in": len(tok), "tokens_out": 0, "response": "", "cut": 0}
    if len(tok) > WINDOW:
        return dict(empty, status="context_overflow")
    if CALL_COUNT[0] % 37 == 0:
        return dict(empty, status="rate_limit")
    r = rng(seed ^ prompt_trace(prompt))
    if len(tok) > 90 and r() < 0.04:
        return dict(empty, status="timeout")
    cand = [k for k in CONCEPTS if k[:5] in tok]    # concepts named in the prompt
    if len(cand) < 3:
        cand = CONCEPTS
    p = {k: 0.15 for k in cand}
    for t in tok:
        for table, w in ((CUES, 1.0), (TRAPS, 1.3)):
            if table.get(t) in p:
                p[table[t]] += w
    if temperature <= 0.0:
        k = max(sorted(p), key=lambda a: p[a])       # most likely choice
    else:
        e = [math.exp(p[a] / temperature) for a in sorted(p)]
        k = sorted(p)[pick(r(), [v / sum(e) for v in e])]
    if "bicim" in tok:                               # the prompt contains a format declaration
        y = f"etiket : {k} ; kod : {CODES[k]} ; gerekce : {summary}"
    else:
        y = FREE_FORM[pick(r(), [0.42, 0.26, 0.32])].format(k=k)
    ot, cut = tokenize(y), 0
    if len(ot) > max_out:                            # stop condition
        ot, cut, y = ot[:max_out], 1, " ".join(ot[:max_out])
    return {"status": "ok", "tokens_in": len(tok), "tokens_out": len(ot),
            "response": y, "cut": cut}


def parse_strict(y):                # STRICT field parser: all three fields must be complete
    a = {}
    for part in y.split(";"):
        kv = part.split(":")
        if len(kv) == 2:
            a[kv[0].strip()] = kv[1].strip()
    complete = a.get("etiket") in TOPICS and a.get("kod") == CODES.get(a.get("etiket"))
    return a["etiket"] if complete and a.get("gerekce") else None


def read_label(y):                  # LOOSE reading: the first label word in the response
    for s in y.lower().split():
        if s in TOPICS:
            return s
    return None


BASE = ("Talebi su etiketlerden birine ata : fatura , ariza , sayac , tarife , "
        "kesinti . Ilgili ariza kodunu ve kisa gerekcesini de bildir . Talep : ")
FORMAT_DECL = "Yanitin bicimi : etiket : <deger> ; kod : <deger> ; gerekce : <deger> . "


def measure(with_format, max_out=MAX_OUT, seed=SEED):
    CALL_COUNT[0] = 0
    d = {"strict": 0, "loose": 0, "hit_rate": 0, "tokens_in": 0, "tokens_out": 0, "cut": 0}
    for x in TICKETS:
        s = call((FORMAT_DECL if with_format else "") + BASE + x["text"],
                 " ".join(x["text"].split()[:3]), seed=seed, max_out=max_out)
        for a in ("tokens_in", "tokens_out", "cut"):
            d[a] += s[a]
        d["strict"] += parse_strict(s["response"]) is not None
        d["loose"] += read_label(s["response"]) is not None
        d["hit_rate"] += read_label(s["response"]) == x["topic"]
    for a in ("strict", "loose", "hit_rate"):
        d[a] /= len(TICKETS)
    d["calls"] = CALL_COUNT[0]
    return d


A, B, C = measure(False), measure(True), measure(True, max_out=12)
sample = call(FORMAT_DECL + BASE + TICKETS[3]["text"], " ".join(TICKETS[3]["text"].split()[:3]))
print(f"FICTIONAL: {len(TICKETS)} support tickets. formatted response sample ({sample['tokens_out']} tokens)")
print(f"  {sample['response']}")
print(f"\n{'measure':<26}{'free':>10}{'format':>14}{'format + 12':>12}")
for name, k, b in (("parseable share", "strict", "{:.4f}"),
                   ("label-read share", "loose", "{:.4f}"),
                   ("label hit rate", "hit_rate", "{:.4f}"),
                   ("input tokens", "tokens_in", "{:d}"),
                   ("output tokens", "tokens_out", "{:d}"),
                   ("cut responses", "cut", "{:d}")):
    print(f"{name:<26}{b.format(A[k]):>10}{b.format(B[k]):>14}{b.format(C[k]):>12}")
```

```
FICTIONAL: 120 support tickets. formatted response sample (15 tokens)
  etiket : ariza ; kod : ar-14 ; gerekce : ariza kaydi actim

measure                         free        format format + 12
parseable share               0.0000        0.9750      0.0000
label-read share              0.4667        0.9750      0.4833
label hit rate                0.3083        0.6833      0.2667
input tokens                    6140          8780        8780
output tokens                   1392          2026        1404
cut responses                      0             0         117
```

The first row is the real number of this lesson. In the free response the number of records the strict
field parser reads is **zero**: not one of the one hundred twenty responses is a three-field record.
The request was clear, the label set was written in the prompt, and the fault code and the rationale
were asked for separately; even so, what came out is a sentence. The second row shows that the
sentences are not unreadable: in **0.4667** of the responses the loose reading finds a label word. The
gap between the two is the subject of this lesson. A free response is an output that a human reads and
a program cannot.

When a format declaration is added the parseable share becomes **0.9750**. The remaining share comes
not from the model breaking the format but from three calls caught by the rate limit; the simulator
returns an empty response on every thirty-seventh call, and no record is read from an empty response.
At the same time the label hit rate rises **from 0.3083 to 0.6833**, but the source of this rise is
not the model deciding better. More than half of the free responses carried no label, and every record
whose label could not be read was counted wrong. The format declaration does not correct the decision;
it **makes the decision visible**.

The cost moves in two directions at once. The input tokens rise **from 6140 to 8780**: the format
declaration is twenty-two tokens and is added to the front of each of the one hundred twenty requests.
The output tokens also XX, because the three-field record is longer than the
free sentence. The lesson here is exactly this: the format constraint is a decision that **enlarges
both token items** in this task. The third column shows the cost of taking this growth back. When the
output length is limited to twelve tokens the output tokens fall to 1404, but the parseable share
becomes **0.0000**: the number of cut responses is 117, and a cut record is a missing record.

## The Length Limit Is a Threshold, Not a Slope

The second block sweeps the limit over a wide range and shows how the number of cut responses and the
parseable share are tied together.

```python
print("max out      cut         parseable  label hit rate    output tokens")
for n in (24, 20, 18, 16, 14, 12, 10):
    d = measure(True, max_out=n)
    print(f"{n:>7}  {d['cut']:>7}  {d['strict']:>16.4f}  {d['hit_rate']:>14.4f}"
          f"  {d['tokens_out']:>15}")

readable = {}
for x in TICKETS:
    s = call(FORMAT_DECL + BASE + x["text"], " ".join(x["text"].split()[:3]), max_out=12)
    e = read_label(s["response"])
    readable[e] = readable.get(e, 0) + 1
print(f"\nmax out 12 , readable labels in cut responses: {sorted(k for k in readable if k)}")
print(f"labels over five characters: {[k for k in TOPICS if len(k) > 5]}")

B2, A2 = measure(True, seed=SEED + 1), measure(False, seed=SEED + 1)
print(f"\nformat constraint gain : parseable {B['strict'] - A['strict']:+.4f} ,"
      f" hit rate {B['hit_rate'] - A['hit_rate']:+.4f}")
print(f"two-run difference     : parseable {B2['strict'] - B['strict']:+.4f} ,"
      f" hit rate {B2['hit_rate'] - B['hit_rate']:+.4f}")
print(f"free prompt two runs   : hit rate {A['hit_rate']:.4f} / {A2['hit_rate']:.4f}")
```

```
max out      cut         parseable  label hit rate    output tokens
     24        0            0.9750          0.6833             2026
     20        0            0.9750          0.6833             2026
     18       30            0.7250          0.4417             1992
     16       78            0.3250          0.3500             1865
     14      117            0.0000          0.2917             1638
     12      117            0.0000          0.2667             1404
     10      117            0.0000          0.2667             1170

max out 12 , readable labels in cut responses: ['ariza', 'sayac']
labels over five characters: ['fatura', 'tarife', 'kesinti']

format constraint gain : parseable +0.9750 , hit rate +0.3750
two-run difference     : parseable +0.0000 , hit rate +0.1167
free prompt two runs   : hit rate 0.3083 / 0.5000
```

The second and the third column of the chart are locked to each other. With the limit at twenty there
is no cut response and the share is 0.9750; at eighteen thirty responses are cut and the share falls to
0.7250; at sixteen seventy-eight responses are cut and the share becomes 0.3250. In every row **the
number of parseable responses is exactly equal to the number of responses that were neither cut nor
returned as an error**. For a single record the length limit is a threshold: either the record is
complete or it does not exist at all; there is no in between. At the dataset level the curve looks soft
because response lengths change from record to record; the slope of the curve is a property not of the
model but of **the length distribution of the rationale field**.

The token column explains the appeal of this decision. Lowering the limit from twenty-four to ten
brings the output tokens down **from 2026 to 1170**, that is, roughly by half. The saving is real and
it follows from the decision; in return the number of records left in hand is zero. The length limit is
a cost setting and **its right place is just above the full length of the record**; every token of
saving below that deletes the whole record.

Which label can be read in a cut response shows a tokenizer detail. With the limit at twelve
the loose reading finds only the `fault` and `meter` labels; `billing`, `tariff` and
`outage` never appear. The reason is the tokenizer: words longer than five characters are
split into pieces, and when a cut output is joined back together these pieces remain separate
words. The same length limit
**behaves differently depending on the number of characters of the label**: the limit is a token
threshold but its effect depends on the spelling of the word.

The last three lines apply the rule of the course. The difference that the format declaration adds to
parseability is **+0.9750**, while the difference between two runs of the same prompt is **+0.0000**.
The measure is independent of the run and the gain is beyond dispute. The hit rate side is not as
comfortable: the gain is **+0.3750**, the two-run difference **+0.1167**. Since the gain is more than three times the
difference it counts as measured, but while there is a movement of 0.1167 between two
runs of the same prompt, a claim of a 0.05 improvement is an unmeasured guess in this scaffolding. **The
prompt is a design object; a prompt changed without measuring is not an improvement but a guess.**

## Summary

- A clear request does not determine the form of the response. In the free response the record share
  that the strict field parser reads is **0.0000** and the share in which the loose reading finds a
  label is **0.4667**: the response is readable for a human and unreadable for a program.
- The format declaration carries the parseable share to **0.9750** and the label hit rate **from 0.3083
  to 0.6833**. The remaining 0.0250 belongs not to the model but to the three calls caught by the rate
  limit.
- The cost is two-directional: the input tokens rise **from 6140 to 8780** and the output tokens **from
  1392 to 2026**. The format constraint enlarges both token items in this task.
- When the maximum output length is lowered from 24 to 10 the output tokens fall **from 2026 to 1170**
  and the parseable share becomes **0.0000**; in every row the number of parseable responses equals the
  number of responses that were neither cut nor returned as an error.
- The **+0.9750** that the format declaration adds to parseability has a two-run difference of
  **+0.0000**; the **+0.3750** it adds to the hit rate has a two-run difference of **+0.1167**. The
  second measure is noisier and sets the smallest hit rate difference this scaffolding can measure.

## Next Step

In this lesson what was written into the prompt was a **recipe**: the names, the order and the
separator of the fields. The recipe determined the container of the response, and determining the
container raised the hit rate indirectly. But the model's decision about **which ticket goes to which
label** was never supported; there is not even a single example of it in the prompt. The next lesson
puts **examples** in place of the recipe: a few solved tickets are written into the prompt and how the
hit rate changes is counted. The number of examples is raised from zero to eight, the hit rate and the
tokens are printed side by side at every step, and **the point where the gain saturates** is written.
The same lesson also shows something disturbing: when the same examples are written in a different
order the number changes, that is, the **ordering** of the examples is a design decision as much as the
example set.
