Das Problem messen
Werkzeugliste und Werkzeugausgabe sind zwei Probleme mit zwei Größenordnungen.
Meilenstein Ein Skript stellt die Token einer Werkzeugliste und einer einzigen gescrapten Seite nebeneinander, der Faktor steht als Zahl da.
Der übliche Einstieg in dieses Thema ist eine Behauptung: „Zu viele Werkzeuge sprengen den Kontext.“ Der Kurs fängt eine Stufe davor an, weil die Behauptung in dieser Form falsch ist. Werkzeuge sprengen den Kontext nicht. Ihre Ausgaben tun das.
Das ist keine Wortklauberei, sondern der Unterschied zwischen zwei Lösungen. Wer das erste Problem sieht, baut einen Filter. Wer das zweite sieht, baut eine Grenze. Und wer nur den Filter baut, hat das billigere von beiden gelöst.
Also zählen wir zuerst. Am Ende dieses Kapitels steht eine Ausgabe, die du später mehrfach zitieren wirst.
Das Ziel
Zwei Probleme, zwei Größenordnungen. Eine Werkzeug-Definition kostet ungefähr 200 Token. Eine Werkzeug-Ausgabe kostet je nach Werkzeug das Hundert- bis Tausendfache. Solange man beides „Kontextproblem“ nennt, sucht man die Lösung an der falschen Stelle.
Das Gerüst
Leg dein Repo an. Zwei Ebenen, weil tiny-agents gleichzeitig das Projekt und
das Paket darin heißt - dasselbe Layout wie bei jedem Python-Paket:
tiny-agents/ ← dein Repo
├── tiny/ ← das Paket
│ └── __init__.py (leer)
└── scripts/
└── 01_token_vergleich.py
mkdir -p tiny-agents/tiny tiny-agents/scripts
cd tiny-agents
touch tiny/__init__.py
python -m venv .venv && source .venv/bin/activate
pip install httpx pyyaml numpy tiktoken
tiktoken ist der Zähler. Er ist auf OpenAI-Modelle geeicht, und dein Modell
ist wahrscheinlich ein anderes - die Zahlen stimmen also auf ein paar Prozent
genau, nicht exakt. Für einen Faktor von 100 ist das mehr als genug, und die
Alternative wäre, für jedes Modell den passenden Tokenizer zu besorgen. Sag es
in der Ausgabe dazu, dann lügt die Zahl nicht.
Schritt 1: Was eine Werkzeug-Definition wirklich kostet
Eine Definition ist das JSON-Schema, das im tools-Feld des Requests landet.
Nicht der Aufruf, nicht die Antwort - die reine Beschreibung, die in jedem
einzelnen Turn mitfährt.
# scripts/01_token_vergleich.py
import json
import tiktoken
ENC = tiktoken.get_encoding("cl100k_base")
def tokens(text: str) -> int:
return len(ENC.encode(text))
def werkzeug_definition(i: int) -> dict:
"""Eine realistisch ausführliche Definition, wie sie ein echtes Werkzeug hat."""
return {
"type": "function",
"function": {
"name": f"crm_lookup_contact_{i}",
"description": (
"Sucht einen Kontakt im CRM anhand von Name, E-Mail-Adresse oder "
"Kundennummer und gibt Stammdaten, zugeordnete Firma und die "
"letzten Aktivitaeten zurueck. Verwende dieses Werkzeug, wenn nach "
"einer bestimmten Person gefragt wird. Nutze es nicht fuer "
"Firmensuchen und nicht zum Anlegen neuer Kontakte."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Suchbegriff: Name, E-Mail oder Kundennummer.",
},
"limit": {
"type": "integer",
"description": "Maximale Trefferzahl, Standard 5.",
},
"include_activities": {
"type": "boolean",
"description": "Wenn true, werden die letzten Aktivitaeten mitgeliefert.",
},
},
"required": ["query"],
},
},
}
def kosten_werkzeugliste(n: int) -> int:
schema = [werkzeug_definition(i) for i in range(n)]
return tokens(json.dumps(schema, ensure_ascii=False))
Ruf das für ein einzelnes Werkzeug auf und schau dir die Zahl an, bevor du weiterliest. Sie liegt irgendwo zwischen 150 und 250. Das ist der Preis dafür, dass das Modell weiß, dass es dieses Werkzeug gibt - unabhängig davon, ob es je benutzt wird.
Schritt 2: Was eine einzige Werkzeug-Ausgabe kostet
Jetzt die andere Seite. Wir holen eine echte Seite und rechnen ihren Textinhalt
in Token um. Keine Bibliothek dafür, nur die Standardbibliothek plus httpx:
# scripts/01_token_vergleich.py (Fortsetzung)
import re
import httpx
TAGS_WEG = re.compile(r"<(script|style)[^>]*>.*?</\1>", re.S | re.I)
MARKUP_WEG = re.compile(r"<[^>]+>")
LEERRAUM = re.compile(r"\n{3,}")
def seite_als_text(url: str) -> str:
html = httpx.get(url, timeout=30, follow_redirects=True).text
ohne_skripte = TAGS_WEG.sub(" ", html)
text = MARKUP_WEG.sub(" ", ohne_skripte)
return LEERRAUM.sub("\n\n", text).strip()
def kosten_seite(url: str) -> tuple[int, int]:
roh = httpx.get(url, timeout=30, follow_redirects=True).text
return tokens(roh), tokens(seite_als_text(url))
Zwei Zahlen, weil beide vorkommen: Manche Werkzeuge liefern rohes HTML in den Kontext (der schlimme Fall), gute liefern extrahierten Text (der normale Fall). Der Unterschied zwischen beiden ist die erste, billigste Kompression, die es gibt - und sie reicht trotzdem nicht.
Schritt 3: Nebeneinanderstellen
# scripts/01_token_vergleich.py (Schluss)
URL = "https://de.wikipedia.org/wiki/Werkzeug"
def main() -> None:
roh, text = kosten_seite(URL)
print(f"Zaehler: tiktoken/cl100k_base (Naeherung, modellabhaengig +/- wenige Prozent)\n")
print(f"{'Was':<44}{'Token':>10}")
print("-" * 54)
for n in (1, 5, 25, 50, 100, 200):
print(f"{n:>3} Werkzeug-Definitionen im Schema{'':<10}{kosten_werkzeugliste(n):>10}")
print("-" * 54)
print(f"{'EINE Seite, rohes HTML':<44}{roh:>10}")
print(f"{'EINE Seite, Text extrahiert':<44}{text:>10}")
print("-" * 54)
print(f"Faktor: eine Seite entspricht {text / (kosten_werkzeugliste(1)):.0f} Werkzeug-Definitionen.")
if __name__ == "__main__":
main()
python scripts/01_token_vergleich.py
Was du dabei siehst
Die Ausgabe sieht ungefähr so aus (deine Zahlen weichen ab, das ist in Ordnung):
Was Token
------------------------------------------------------
1 Werkzeug-Definitionen im Schema 193
5 Werkzeug-Definitionen im Schema 957
25 Werkzeug-Definitionen im Schema 4772
50 Werkzeug-Definitionen im Schema 9542
100 Werkzeug-Definitionen im Schema 19082
200 Werkzeug-Definitionen im Schema 38162
------------------------------------------------------
EINE Seite, rohes HTML 104233
EINE Seite, Text extrahiert 21804
------------------------------------------------------
Faktor: eine Seite entspricht 113 Werkzeug-Definitionen.
Drei Dinge stehen jetzt fest, und sie tragen den ganzen Rest des Kurses:
Erstens: Zweihundert Werkzeuge sind teuer, aber nicht absurd. 38.000 Token sind bei modernen Kontextfenstern ärgerlich, kein Weltuntergang. Genau deshalb funktioniert dynamisches Filtern als Lösung für dieses Problem - es macht aus 38.000 rund 1.000.
Zweitens: Eine einzige Seite kostet so viel wie hundert Werkzeuge. Und anders als die Werkzeugliste kommt sie pro Aufruf dazu. Nach drei Scrapes ist ein 128k-Fenster voll, und zwar unabhängig davon, wie viele Werkzeuge registriert waren.
Drittens: Filtern hilft gegen die erste Zeile und ändert an der zweiten nichts. Die gefilterten fünf Werkzeuge laufen anschließend in demselben Kontext, in dem auch der Dialog steht, und schütten ihre Ausgaben genau dort aus.
Wenn es bei dir anders aussieht
Wenn deine Seite nur 2.000 Token hat, hast du eine kleine Seite erwischt. Probier eine Produktseite mit Preistabelle, ein PDF-zu-Text-Ergebnis oder eine API-Antwort mit hundert Datensätzen. Der Punkt ist nicht die konkrete Zahl, sondern dass die Verteilung schief ist: Definitionen sind gleichmäßig teuer, Ausgaben sind unvorhersehbar teuer. Der Ausreißer ist das Problem, nicht der Mittelwert.
Heb das Skript auf. In Kapitel 06 misst du damit deinen Digest, und in Kapitel 08 ist es die Grundlage der ganzen Messreihe.