Parallele Fragen
Führt einen 13-Fragen umfassenden regulatorischen Briefing über den Wikipedia-Artikel zur DSGVO durch und zeigt, dass die Bündelung aller Fragen in einem einzigen TypeSafe-Aufruf 12,2-mal günstiger und 10,0-mal schneller ist, ohne dass sich die Antworten ändern.
Maschinell aus en übersetzt, nicht lektoriert. Nur als Kurzreferenz geeignet.

Sie haben ein Dokument und N Fragen dazu. Sie können eine Anfrage mit allen N Fragen senden oder N Anfragen mit je einer Frage. Mit TypeSafe lauten die Antworten auf beiden Wegen gleich: Jede Frage wird separat gegen das Dokument bewertet, sodass ihre Antwort nicht davon abhängt, was sonst noch in der Anfrage enthalten ist.
Um das zu überprüfen, stellt das Kochbuch jede Frage mehrmals in beide Richtungen – alle N in einer Anfrage und eine Frage pro Anfrage – und vergleicht die Standardabweichung von Lauf zu Lauf: wie weit eine Antwort von einer Wiederholung zur nächsten abweicht. Jedes Rauschen, das eine Frage hat, hat sie unter beiden Batch-Verfahren. Batchen fügt keines hinzu. Die meisten Antworten blieben bei beiden Methoden über alle 5 Wiederholungen hinweg identisch, der gleiche Wert bei jedem Aufruf, die Standardabweichung genau 0,0.
Kosten und Geschwindigkeit ändern sich. Das Dokument dominiert jede Anfrage. N einzelne Aufrufe mit einer Frage bezahlen es N Mal, in N Roundtrips; der gebündelte Aufruf bezahlt einmal. Je größer das Dokument, desto näher kommt diese Ersparnis an einem vollen Nx heran.
Der Fall hier ist ein regulatorischer Briefing. Das Dokument ist der Wikipedia-Artikel über die DSGVO
(~54.000 Zeichen, eine dokumentdominierte Arbeitslast, bei der das Dokument den Großteil jeder
Anfrage ausmacht), und ein Compliance-Team möchte 13 Dinge überprüfen: 8 Noul
Fragen, 2 Choice
Fragen und 3 Score Fragen.
Einrichtung
pip install ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/
dann TYPESAFE_API_KEY festlegen.
import json
import os
import urllib.request
from pathlib import Path
from statistics import mean, stdev
from time import perf_counter
from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Choice, ChoiceAnswer, Noul, NoulAnswer, Score, TypeSafeClient
TYPESAFE_MODEL = "jev-1.12"
PRICE = (
0.042,
0.00,
) # $ per 1M tokens (input, output); TypeSafe jev-1.12 as of 2026-09, see README
RUNS = 5 # repeats per batching strategy, to estimate each answer's run-to-run std dev
client = TypeSafeClient(api_key=os.environ["TYPESAFE_API_KEY"], timeout=120.0)
json_cache = JsonCache(Path("json_cache.json"))
Das Dokument: der Wikipedia-Artikel zur DSGVO
Als Klartext abgerufen aus einer angehefteten Revision des Artikels und zwischengespeichert in json_cache.json neben den API-Aufrufen, sodass das Dokument und seine Zahlen fest bleiben, auch wenn der Live-Artikel bearbeitet wird.
WIKIPEDIA_REVISION = 1363040264 # "General Data Protection Regulation", as of 2026-07
@json_cache
def fetch_article(revision_id: int) -> str:
url = (
"https://en.wikipedia.org/w/api.php?action=query&format=json"
f"&prop=extracts&explaintext=1&revids={revision_id}"
)
request = urllib.request.Request(
url, headers={"User-Agent": "typesafe-cookbook/1.0"}
)
with urllib.request.urlopen(request) as response:
pages = json.loads(response.read())["query"]["pages"]
return next(iter(pages.values()))["extract"]
DOCUMENT = {
"source": f"https://en.wikipedia.org/?oldid={WIKIPEDIA_REVISION}",
"text": fetch_article(WIKIPEDIA_REVISION),
}
print(f"{len(DOCUMENT['text']):,} characters")
display(Markdown(f"📄 [Read the pinned Wikipedia revision]({DOCUMENT['source']})"))
53,777 characters
📄 Lies die angepinnte Wikipedia-Revision an
Die Fragen: 8 Noul + 2 Choices + 3 Scores
Eine Zahl pro Antwort, nach Typ:
Noul: die Wahrscheinlichkeit für „Ja“.Choice: die maximale Wahrscheinlichkeit, die Wahrscheinlichkeit für das ausgewählte Label.criteriaordnet jedem Label seine Bedeutung zu.Score: die auf 0–1 normierte Punktzahl, die Punktzahl geteilt durch das höchste Niveau.criterialistet die Niveaubeschreibungen auf, beginnend mit Niveau 0.
QUESTIONS = {
"breach_72h": Noul(
instructions="Must a personal data breach be reported to the supervisory authority within 72 hours?"
),
"applies_non_eu": Noul(
instructions="Does the regulation apply to organisations established outside the EU that offer goods or services to people in the EU?"
),
"dpo_all_orgs": Noul(
instructions="Must every organisation appoint a Data Protection Officer, regardless of what data it processes?"
),
"pre_ticked_consent": Noul(
instructions="Can valid consent be obtained through pre-ticked boxes or inactivity?"
),
"right_erasure": Noul(
instructions="Does the regulation grant individuals a right to erasure of their personal data?"
),
"data_portability": Noul(
instructions="Does the regulation include a right to data portability?"
),
"us_federal_law": Noul(instructions="Is the GDPR a United States federal law?"),
"criminal_penalties": Noul(
instructions="Does the GDPR itself impose criminal penalties such as imprisonment?"
),
"instrument_type": Choice(
instructions="What kind of EU legal instrument is the GDPR?",
criteria={
"Regulation": "Directly binding law in all member states, no national implementation needed.",
"Directive": "Sets goals that member states implement through national law.",
"Treaty": "An international treaty between states.",
"Recommendation": "Non-binding guidance.",
},
),
"max_fine": Choice(
instructions="What is the maximum administrative fine for the most serious infringements?",
criteria={
"TwentyM_or_4pct": "Up to EUR 20 million or 4% of annual worldwide turnover, whichever is greater.",
"TenM_or_2pct": "Up to EUR 10 million or 2% of annual worldwide turnover, whichever is greater.",
"FixedCap": "A fixed amount not tied to turnover.",
"NoFines": "The GDPR provides no administrative fines.",
},
),
"individual_rights": Score(
instructions="How strong are the rights the GDPR grants to individuals over their data?",
criteria=[
"None: individuals get no rights over their data.",
"Weak: a right to be informed, but little control.",
"Moderate: access and correction rights, but limited means to act on them.",
"Strong: access, erasure, portability, and objection rights, with enforcement behind them.",
],
),
"penalty_severity": Score(
instructions="How severe are the penalties the GDPR provides for non-compliance?",
criteria=[
"None: no penalties of any kind.",
"Symbolic: small fixed fines unlikely to change behavior.",
"Substantial: fines large enough to matter to most companies.",
"Severe: fines scaled to global revenue, material even to the largest companies.",
],
),
"compliance_burden": Score(
instructions="How heavy is the compliance burden the GDPR places on organisations?",
criteria=[
"Negligible: no meaningful obligations.",
"Light: a few notices and disclosures.",
"Moderate: documented processes and some dedicated roles for larger processors.",
"Heavy: records, impact assessments, officers, and breach procedures for many organisations.",
"Extreme: obligations so demanding that ordinary organisations cannot fully comply.",
],
),
}
N = len(QUESTIONS)
METRIC = { # question type -> the one number we track per answer
Noul: "p(yes)",
Choice: "max prob",
Score: "normalized score",
}
Frage auf zwei Arten, jeweils 5 Mal
ask() sendet jede beliebige Teilmenge der Fragen zusammen mit dem Dokument und reduziert jede Antwort auf ihre eine verfolgte Zahl. Das Dokument ist bei jedem Aufruf byte-identisch.
Beide Batch-Strategien werden RUNS = 5 Mal ausgeführt, wodurch jede Frage pro Strategie 5 Antworten erhält,
was ausreicht, um den Mittelwert (stimmen die beiden überein?) und die Standardabweichung (fügt das Batchen Rauschen hinzu?) zu vergleichen.
Aufrufe werden an json_cache.json zwischengespeichert, das mit dem Kochbuch ausgeliefert wird, sodass das erneute Rendern
kostenfrei ist; löschen Sie es, um live neu auszuführen.
@json_cache
def ask(keys: tuple[str, ...], run: int):
"""One TypeSafe call -> ({key: tracked metric}, input_tokens, output_tokens, latency_s);
``run`` only forces a distinct live call per repeat."""
started = perf_counter()
response = client.system_one(
state={"article": DOCUMENT},
questions={key: QUESTIONS[key] for key in keys},
model=TYPESAFE_MODEL,
)
values = {}
for key in keys:
answer = response.answers[key]
if isinstance(answer, NoulAnswer):
values[key] = answer.noul
elif isinstance(answer, ChoiceAnswer):
values[key] = max(answer.probabilities.values())
else:
values[key] = answer.score / (len(QUESTIONS[key].criteria) - 1)
return (
values,
response.usage.input_tokens,
response.usage.output_tokens,
perf_counter() - started,
)
def priced(result):
"""({key: metric}, in_tokens, out_tokens, latency) -> ({key: metric}, cost_usd, latency)."""
values, input_tokens, output_tokens, latency = result
return values, input_tokens / 1e6 * PRICE[0] + output_tokens / 1e6 * PRICE[1], latency
# Price after cache retrieval, so a price change needs no new calls.
batched = [
priced(ask(tuple(QUESTIONS), run)) for run in range(RUNS)
] # all N in one call, x RUNS
singles = [
{key: priced(ask((key,), run)) for key in QUESTIONS} for run in range(RUNS)
] # N x 1, x RUNS
Batching ändert die Antworten nicht
Pro Frage: der Mittelwert und die Standardabweichung der verfolgten Zahl über die 5 Durchläufe, unter jeder Batching-Strategie. Wenn das Batching die Antworten verändert hätte, würden sich die Batch-Spalten von den einzelnen Spalten unterscheiden. Eine verschobene Mittelwert ist Bias. Eine größere Standardabweichung ist Rauschen.
print(
f"{'question':<22}{'metric':<18}{'batched mean':>13}{'single mean':>12}"
f"{'batched std':>13}{'single std':>12}"
)
for key, question in QUESTIONS.items():
batched_values = [values[key] for values, _cost, _latency in batched]
single_values = [singles[run][key][0][key] for run in range(RUNS)]
print(
f"{key:<22}{METRIC[type(question)]:<18}{mean(batched_values):>13.3f}"
f"{mean(single_values):>12.3f}{stdev(batched_values):>13.4f}{stdev(single_values):>12.4f}"
)
question metric batched mean single mean batched std single std
breach_72h p(yes) 0.804 0.814 0.0055 0.0055
applies_non_eu p(yes) 0.990 0.990 0.0000 0.0000
dpo_all_orgs p(yes) 0.030 0.030 0.0000 0.0000
pre_ticked_consent p(yes) 0.040 0.040 0.0000 0.0000
right_erasure p(yes) 0.990 0.990 0.0000 0.0000
data_portability p(yes) 0.990 0.990 0.0000 0.0000
us_federal_law p(yes) 0.010 0.010 0.0000 0.0000
criminal_penalties p(yes) 0.108 0.108 0.0045 0.0084
instrument_type max prob 1.000 1.000 0.0000 0.0000
max_fine max prob 1.000 1.000 0.0000 0.0000
individual_rights normalized score 1.000 1.000 0.0000 0.0000
penalty_severity normalized score 1.000 1.000 0.0000 0.0000
compliance_burden normalized score 0.750 0.750 0.0000 0.0000
Die Tabelle nach Fragetypen lesen:
- Choices, Scores und sechs der acht Noul-Werte kommen in den 5 Wiederholungen identisch zurück: die Standardabweichung beträgt exakt 0,0 unter beiden Batch-Verfahren, wobei jeder gebatchte und einzelne Aufruf dieselbe Anzahl liefert. Ein Aufruf mit N Fragen liefert dieselben Antworten wie N Aufrufe mit je einer Frage.
breach_72hundcriminal_penaltiesweisen eine geringe, von Lauf zu Lauf schwankende Stichprobenvariabilität auf, und diese ist unter beiden Batch-Verfahren gleich groß, wobei die Mittelwerte innerhalb dieser Schwankung übereinstimmen. Die Schwankung ist eine Eigenschaft der Frage, nicht des Batchings: Das Batchen verschiebt die Antwort weder noch fügt es Varianz hinzu.
Auf jeden Fall gibt es keinen Batch-Effekt: Die Antwort auf keine Frage hängt von den 12 anderen Fragen ab, die dieselbe Anfrage teilen.
Der einzige Unterschied: Kosten und Geschwindigkeit
Gleiche Antworten, andere Rechnung. Der Artikel mit ~54.000 Zeichen dominiert jede Anfrage, daher:
- Kosten: Die 13 einzelnen Aufrufe senden den Artikel 13 Mal erneut; der gebündelte Aufruf sendet ihn einmal. Diese Einsparung gilt unabhängig davon, wie die Aufrufe getriggert werden.
- Geschwindigkeit: Die Zahl summiert die Latenzzeiten der 13 einzelnen Aufrufe, setzt also voraus, dass sie nacheinander ablaufen. Werden sie parallel getriggert, verkürzt sich die Zeitspanne, aber die 13-fache Token-Kosten bleiben bestehen.
Token-Anzahlen und Latenzzeiten werden zusammen mit den Antworten zwischengespeichert; die Kosten werden danach angewendet, und beide werden über die 5 Durchläufe gemittelt.
batched_cost = mean(cost for _values, cost, _latency in batched)
batched_latency = mean(latency for _values, _cost, latency in batched)
singles_cost = mean(
sum(singles[run][key][1] for key in QUESTIONS) for run in range(RUNS)
)
singles_latency = mean(
sum(singles[run][key][2] for key in QUESTIONS) for run in range(RUNS)
)
print(f"{'batching':<24}{'calls':>6}{'cost':>12}{'total time':>12}")
print(
f"{f'one call, all {N}':<24}{1:>6}{'$' + format(batched_cost, '.6f'):>12}{format(batched_latency, '.2f') + 's':>12}"
)
print(
f"{f'{N} calls, one each':<24}{N:>6}{'$' + format(singles_cost, '.6f'):>12}{format(singles_latency, '.2f') + 's':>12}"
)
print(
f"\nbatching: {singles_cost / batched_cost:.1f}x cheaper, {singles_latency / batched_latency:.1f}x faster"
)
batching calls cost total time
one call, all 13 1 $0.000497 0.27s
13 calls, one each 13 $0.006090 2.71s
batching: 12.2x cheaper, 10.0x faster
Öffnen Sie es im TypeSafe-Playground
Der gleiche Artikel und dieselben 13 Fragen, verpackt in einem Freigabelink. Öffnen Sie ihn, um das Briefing erneut live auszuführen; dieselben Zahlen kommen zurück.
playground_link = make_playground_link(
{"article": DOCUMENT}, QUESTIONS, models=[TYPESAFE_MODEL]
)
display(
Markdown(
f"🔗 [Open this article + questions in the TypeSafe playground]({playground_link})"
)
)