20 min

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.