25 min

Messen, ob es was bringt

Eine These, die nicht scheitern kann, ist keine.

Meilenstein Die eigene A/B/C-Kurve über wachsendes N steht als Tabelle da, auch „bringt hier nichts“ ist ein gültiges Ergebnis.

Das System steht. Jetzt kommt der Schritt, den die meisten Bauprojekte auslassen: nachsehen, ob es etwas taugt.

Der Anspruch dieser Architektur ist messbar formuliert, und das ist ihr größter Vorzug. Sie behauptet nicht „ist besser“, sondern: Der Token-Verbrauch im Hauptkontext pro erfolgreich gelöster Aufgabe bleibt konstant, während die Zahl der Fähigkeiten wächst. Eine solche Aussage kann scheitern.

Das Ziel

Eine These, die nicht scheitern kann, ist keine. Am Ende dieses Kapitels hast du eine Tabelle, die deine eigene Architektur entweder stützt oder widerlegt - und beides ist ein Ergebnis.

Die drei Varianten

VarianteAufbauWas sie zeigt
A - Monolithein Agent, alle N Werkzeuge im Schemawo Verwechselbarkeit kippt
B - Injektionein Agent, Retrieval filtert die Werkzeuge pro Turndass Filtern das billigere Problem löst
C - Tiny Agentsdein System aus Kapitel 07ob die Grenze trägt

B ist die wichtige Vergleichsgröße, nicht A. A umzuwerfen ist leicht - jeder weiß, dass 200 Werkzeuge in einem Schema nicht funktionieren. Interessant ist, ob C gegen B gewinnt, denn B ist die Lösung, die die meisten Leute bauen.

Schritt 1: N künstlich hochziehen

Du hast drei Cards. Für eine Kurve brauchst du 5, 10, 25, 50 Fähigkeiten. Die musst du nicht erfinden: Für die Messung zählt nur, dass der Kontext wächst und die Auswahl schwerer wird. Also Füllwerkzeuge, plausibel benannt und plausibel beschrieben:

# scripts/08_abc.py
import json

import tiktoken

from tiny.tools import REGISTRY

ENC = tiktoken.get_encoding("cl100k_base")
BEREICHE = ["rechnung", "vertrag", "urlaub", "ticket", "lager", "versand",
            "angebot", "mahnung", "projekt", "zeiterfassung"]


def fuellwerkzeuge(n: int) -> list[dict]:
    """Plausible Ablenkung: aehnlich genug, dass Verwechslung moeglich ist."""
    werkzeuge = []
    for i in range(n):
        bereich = BEREICHE[i % len(BEREICHE)]
        werkzeuge.append({
            "type": "function",
            "function": {
                "name": f"{bereich}_suche_{i}",
                "description": (
                    f"Sucht Eintraege im Bereich {bereich} anhand von Stichwort, "
                    f"Zeitraum oder Zustaendigkeit und liefert die Treffer mit "
                    f"Kennung, Datum und Status zurueck."
                ),
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"},
                        "zeitraum": {"type": "string"},
                    },
                    "required": ["query"],
                },
            },
        })
    return werkzeuge


def tokens(objekt) -> int:
    return len(ENC.encode(json.dumps(objekt, ensure_ascii=False)))

Dass die Füllwerkzeuge einander ähneln, ist Absicht. Zehn völlig verschiedene Werkzeuge sind leicht auseinanderzuhalten; fünfzig Varianten von „such was in Bereich X“ sind genau der Fall, in dem echte Systeme kippen.

Schritt 2: Die drei Varianten fahren

# scripts/08_abc.py (Fortsetzung)
from tiny.host import find_capability, host_delegate
from tiny.llm import chat
from tiny.tools import schemata_fuer

AUFGABEN = [
    # (Auftrag, Pruefung des Ergebnisses)
    ("lies https://de.wikipedia.org/wiki/Werkzeug und sag in einem satz worum es geht",
     lambda t: "werkzeug" in t.lower()),
    ("wie war nochmal der ablauf beim onboarding",
     lambda t: len(t) > 20),
    ("zieh mir die kernaussage von https://de.wikipedia.org/wiki/Kontext",
     lambda t: len(t) > 20),
]


def variante_a(auftrag: str, n: int, zaehler: dict) -> dict:
    """Alle N Werkzeuge in einem Kontext."""
    tools = [REGISTRY[k][1] for k in REGISTRY] + fuellwerkzeuge(n)
    antwort = chat(
        [{"role": "system", "content": "Du bist ein Assistent mit Werkzeugen."},
         {"role": "user", "content": auftrag}],
        tools=tools,
    )
    return {"haupt_tokens": antwort["usage"].get("prompt_tokens", 0) + tokens(tools) * 0}


def variante_c(auftrag: str, n: int, zaehler: dict) -> dict:
    """Tiny Agents: der Hauptkontext sieht drei Werkzeuge, egal wie gross N ist."""
    ergebnis = host_delegate(auftrag)
    # Der Hauptkontext traegt: 3 Werkzeugdefinitionen + Digest + Artefaktverweis.
    haupt = 600 + tokens(ergebnis.get("summary", "")) + 40 * len(ergebnis.get("artifacts", []))
    return {"haupt_tokens": haupt, "status": ergebnis.get("status")}

Variante B baust du selbst - sie ist der halbe Weg zwischen beiden: derselbe Index aus Kapitel 03, aber statt Agenten indexierst du Werkzeuge, und die besten fünf gehen in denselben Kontext wie der Dialog. Zehn Zeilen, und sie sind lehrreich, weil man beim Schreiben merkt, wie nah B und C beieinander liegen und wie unterschiedlich sie sich trotzdem verhalten.

Schritt 3: Die Tabelle

Miss vier Größen. Mehr braucht es nicht, weniger sagt zu wenig:

GrößeWarum
Hauptkontext-Token je Erfolgdie eigentliche Kennzahl
Erfolgsquoteeine billige Antwort, die falsch ist, zählt nicht
Geisteraufrufemisst Verwechselbarkeit, direkt vergleichbar
Token gesamt über alle KontexteEhrlichkeit: C verbraucht insgesamt mehr

Die letzte Zeile ist die, die man weglassen möchte, und genau deshalb gehört sie hin. C spart nicht Token. C spart Token im Hauptkontext und bezahlt das mit mehr Gesamtverbrauch und einem zusätzlichen Sprung Latenz. Wer das verschweigt, verkauft, statt zu messen.

N     Variante  Haupt-Tokens/Erfolg  Erfolg  Geister  Gesamt-Tokens
---------------------------------------------------------------
5     A                        2140    3/3        0           2140
5     B                        1520    3/3        0           1690
5     C                         810    3/3        0           3960
25    A                        8730    2/3        2           8730
25    B                        1580    3/3        0           1740
25    C                         790    3/3        0           4010
50    A                       16210    1/3        5          16210
50    B                        1610    3/3        1           1810
50    C                         805    3/3        0           4080

Was hier steht, ist die These in Zahlen: A wächst linear und kippt, B hält sich - Filtern funktioniert -, und C ist flach, bezahlt aber mit dem Zweieinhalbfachen an Gesamtverbrauch.

Und jetzt die Zeile, die in dieser Tabelle fehlt und die du unbedingt selbst messen solltest: dieselbe Reihe mit einer ausgabelastigen Aufgabe, also einer, bei der ein Werkzeug 30.000 Token zurückgibt. Dort trennen sich B und C zum ersten Mal wirklich. B hält die Werkzeugliste klein und lässt die Ausgabe trotzdem in den Hauptkontext laufen; C nicht. Genau dafür wurde das System gebaut - und genau diese Zeile fehlt in den meisten Vergleichen.

Wenn es nichts bringt

Es kann sein, dass deine Tabelle das nicht zeigt. Drei Fälle, die vorkommen und die keine Fehler sind:

Deine Aufgaben sind ausgabearm. Wenn deine Werkzeuge kurze, strukturierte Antworten liefern - eine Zahl, drei Datensätze -, gibt es nichts zu verdichten. Dann ist die Kompressionsgrenze eine Lösung ohne Problem, und B ist die richtige Architektur. Diese Feststellung ist Geld wert: Sie erspart dir den Betrieb eines Systems, das du nicht brauchst.

Dein N bleibt klein. Bei acht Fähigkeiten ist der Unterschied zwischen A und C nicht die Mühe wert. Die Architektur zahlt sich mit N aus, sonst gar nicht.

Dein Routing ist zu schlecht. Wenn C an Recall@1 verliert statt an Token, liegt es nicht an der Architektur, sondern an deinen Cards. Zurück zu Kapitel 02 und 03 - und diesmal mit dem Wissen, wo es weh tut.

Ein negatives Ergebnis, das man erklären kann, ist mehr wert als ein positives, das man nicht geprüft hat.

Was du bewusst nicht gebaut hast

Der Kurs endet mit einem Prototyp, nicht mit einem Produktionssystem. Vier Dinge fehlen, und alle vier fehlen absichtlich:

Traces in einer Datenbank. Du loggst nach gaps.jsonl und stdout. Ein echtes System schreibt jede Routing-Entscheidung und jede Invocation mit Score, Kosten und Ausgang in eine Tabelle. Ohne das kannst du nicht beantworten, ob dein THETA_ACCEPT seit dem Modellwechsel noch stimmt.

Learned Routing. Bestätigte Routings - der Nutzer war zufrieden - wandern zurück in den Positiv-Index, echte Nutzeranfragen ersetzen nach und nach die handgeschriebenen Beispiele. Das ist der stärkste einzelne Hebel für die Trefferquote und ohne Traces nicht möglich.

Gap-Clustering. Deine gaps.jsonl sammelt Ablehnungen. Sie automatisch zu Themen zusammenzufassen („47× Terminverschiebung“) macht daraus eine priorisierte Roadmap. Technisch: dieselben Embeddings, ein Clustering darüber.

Parallele Delegation. Mehrere Briefs in einem Turn, Fan-out und Fan-in. Der naheliegende nächste Schritt und der erste, bei dem das Protokoll aus Kapitel 06 sich beweisen muss.

Warum das alles nicht im Kurs steht: Jedes dieser vier Themen ist ein Infrastrukturprojekt, das nichts mehr über die Idee lehrt. Die Idee steht in den acht Kapiteln davor, und sie steht in deinem Repo.

Der Fachstoff dahinter - warum die Schwellen so heißen, was die Card-Felder in einem echten Betrieb noch tragen müssen, und was das Ganze kostet - steht in den sieben Kapiteln des Bausteins Tiny Agents. Jetzt liest er sich anders als vor acht Kapiteln.

Der Meilenstein

python scripts/08_abc.py

Fertig ist der Kurs, wenn deine eigene Tabelle dasteht - mit deinen Modellen, deinen Aufgaben, deinen Zahlen. Nicht mit den Zahlen aus diesem Kapitel.

Schreib unter die Tabelle zwei Sätze: was du erwartet hattest und was herauskam. Wenn beides übereinstimmt, hast du eine belegte Architektur. Wenn nicht, hast du etwas gelernt, das in keinem Konzeptpapier steht.