Die Capability Card
Ein Artefakt ist gleichzeitig Registrierung, Doku, Routing-Index und Testfall.
Meilenstein Drei handgeschriebene Cards laufen durch den Validator, eine absichtlich kaputte wird mit lesbarer Meldung abgewiesen.
Jetzt könnte man anfangen, einen Router zu bauen. Man würde ihn auf Beschreibungen von Agenten loslassen, die es noch nicht gibt, und hätte den schwierigsten Teil des Systems auf Vermutungen gebaut.
Deshalb kommt zuerst das Artefakt. Eine Capability Card ist eine YAML-Datei pro Fähigkeit, und sie erfüllt vier Aufgaben auf einmal:
- Registrierung - was es gibt
- Dokumentation - was es tut, und ausdrücklich, was es nicht tut
- Routing-Index - die Beispiele darin werden eingebettet
- Testfall - dieselben Beispiele sind die Eval-Fälle des Routers
Vier Aufgaben, eine Datei. Das ist der Grund, warum das Format so aussieht, wie es aussieht.
Das Ziel
Ein Artefakt ist gleichzeitig Registrierung, Doku, Routing-Index und Testfall. Wer die Beispiele ändert, ändert damit zwangsläufig auch die Testfälle - eine Card kann nicht heimlich veralten, ohne dass die Messung es merkt.
Das Format
Leg registry/web-reader.card.yaml an:
id: web-reader
version: 1.0.0
summary: >
Ruft eine oder mehrere URLs ab, extrahiert den Textinhalt und beantwortet
eine konkrete Frage dazu. Liefert Zusammenfassung plus Quellenbelege.
# --- Routing: Positiv-Index ---
examples:
- "was steht auf der seite https://..."
- "lies mir mal die seite durch und sag was drinsteht"
- "hol dir die preise von der website von firma x"
- "auf der landingpage steht irgendwo die telefonnummer, finde die"
- "fass mir den blogartikel zusammen"
- "welche produkte listet die seite auf"
- "steht da was zu lieferzeiten drin"
- "check mal ob die noch offene stellen haben"
- "was kostet das bei denen laut website"
- "gibt es auf der seite ein impressum mit adresse"
# --- Routing: Veto-Index, SEPARAT ---
counter_examples:
- "suche im web nach informationen ueber"
- "google mal wer der geschaeftsfuehrer ist"
- "trag den kontakt ins crm ein"
- "schick eine mail an"
does_not:
- "Sucht nicht selbst im Web, braucht eine konkrete URL"
- "Fuellt keine Formulare aus, klickt nichts"
- "Umgeht keine Paywalls und keine Logins"
# --- Vertrag ---
inputs:
required: [urls, question]
returns:
required: [answer, sources, found]
# --- Betrieb ---
budget:
max_output_tokens: 800
max_tool_calls: 8
max_wall_seconds: 60
tools: [http_get, html_to_text]
side_effects: none # none | writes | irreversible
Vier Felder verdienen eine Begründung, weil man sie sonst falsch ausfüllt.
examples sind Nutzerformulierungen, nicht Beschreibungen. Das ist die
wichtigste Regel des ganzen Formats. Der Grund steht in Kapitel 03 ausführlich;
kurz: Der Router vergleicht die Anfrage des Nutzers mit diesen Zeilen. „Ich
brauch die Rechnung von Meier nochmal“ ist semantisch weit weg von „Agent für
Dokumentenabruf aus dem DMS mit Volltextsuche“ und ganz nah an „hol mir nochmal
die rechnung von“. Schreib die Beispiele deshalb schlampig: klein
geschrieben, umgangssprachlich, mit den Auslassungen, die echte Leute machen.
counter_examples liegen in einem eigenen Index. „Ich mache keine
Web-Suche“ enthält das Wort Web-Suche und matcht damit prächtig auf genau die
Anfrage, die es ausschließen soll. Im selben Index wäre das ein Eigentor. In
Kapitel 04 wirken sie als Veto: binär, nicht als Punktabzug.
does_not wird nicht eingebettet. Es ist Prosa, sie geht in den
Systemprompt des Agenten und in die Antwort auf die Frage „kannst du das?“.
Menschen lesen sie, der Index nicht.
budget ist keine Empfehlung. In Kapitel 06 setzt der Runner es durch,
notfalls mit der Schere. Eine Card ohne Budget ist eine Card ohne
Kompressionsgrenze, und dann kannst du dir das ganze System sparen.
Zwei weitere Cards
Ein Router mit einem einzigen Agenten routet immer richtig. Für alles ab Kapitel 03 brauchst du mindestens drei, und sie sollten benachbart genug sein, dass Verwechslung möglich ist. Leg an:
registry/crm-writer.card.yaml - schreibt ins CRM (Kontakte, Notizen). Wichtig:
side_effects: writes. Beispiele wie „trag den neuen kontakt ein“, „notier bei
meier dass er zurueckruft“, „leg eine firma an fuer“. Counter-Examples: „wer ist
der ansprechpartner bei“, „such mir die telefonnummer von“ - das ist Lesen, und
Lesen macht ein anderer.
registry/docs-finder.card.yaml - durchsucht die interne Dokumentation.
Beispiele: „wie war nochmal der ablauf fuer“, „wo steht was zur urlaubsregelung“,
„gibt es eine anleitung fuer“. Counter-Examples: „was steht auf der seite
https://...“, „google mal“ - das erste ist der web-reader, das zweite kann
niemand.
Nimm dir dafür die zehn Minuten. Die Qualität deines Routers hängt ab Kapitel 03 an nichts anderem als an diesen Zeilen.
Der Validator
Eine Card, die still falsch ist, vergiftet den Index. Also prüft ein Validator sie beim Laden. Er ist kurz, weil er nur die Regeln durchsetzt, die man tatsächlich verletzt:
# tiny/cards.py
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
import yaml
SIDE_EFFECTS = {"none", "writes", "irreversible"}
class CardError(ValueError):
"""Eine Card verletzt das Format. Die Meldung nennt Datei und Feld."""
@dataclass
class Budget:
max_output_tokens: int = 800
max_tool_calls: int = 8
max_wall_seconds: int = 60
@dataclass
class Card:
id: str
summary: str
examples: list[str]
counter_examples: list[str]
does_not: list[str]
returns_required: list[str]
tools: list[str]
budget: Budget
side_effects: str = "none"
version: str = "0.0.0"
quelle: Path | None = field(default=None, repr=False)
def _liste(daten: dict, feld: str, datei: Path, mindestens: int = 0) -> list[str]:
wert = daten.get(feld, [])
if not isinstance(wert, list) or any(not isinstance(x, str) for x in wert):
raise CardError(f"{datei.name}: '{feld}' muss eine Liste aus Strings sein.")
if len(wert) < mindestens:
raise CardError(
f"{datei.name}: '{feld}' hat {len(wert)} Eintraege, "
f"mindestens {mindestens} noetig."
)
return wert
def card_laden(datei: Path) -> Card:
daten = yaml.safe_load(datei.read_text(encoding="utf-8")) or {}
for pflicht in ("id", "summary", "returns", "tools"):
if pflicht not in daten:
raise CardError(f"{datei.name}: Pflichtfeld '{pflicht}' fehlt.")
# Die Faustregel aus dem Konzept: unter 8 Positivbeispielen wird das
# Routing bruechig, unter 3 Counter-Examples hat das Veto keine Wirkung.
examples = _liste(daten, "examples", datei, mindestens=8)
counter = _liste(daten, "counter_examples", datei, mindestens=3)
ueberschneidung = set(examples) & set(counter)
if ueberschneidung:
raise CardError(
f"{datei.name}: {sorted(ueberschneidung)} steht in beiden Indizes. "
"Ein Beispiel kann nicht gleichzeitig Treffer und Veto sein."
)
seiteneffekt = daten.get("side_effects", "none")
if seiteneffekt not in SIDE_EFFECTS:
raise CardError(
f"{datei.name}: side_effects '{seiteneffekt}' unbekannt, "
f"erlaubt sind {sorted(SIDE_EFFECTS)}."
)
budget = Budget(**(daten.get("budget") or {}))
if budget.max_output_tokens > 2000:
raise CardError(
f"{datei.name}: max_output_tokens={budget.max_output_tokens}. "
"Ueber 2000 ist das keine Kompressionsgrenze mehr."
)
return Card(
id=str(daten["id"]),
summary=str(daten["summary"]).strip(),
examples=examples,
counter_examples=counter,
does_not=_liste(daten, "does_not", datei),
returns_required=list((daten.get("returns") or {}).get("required", [])),
tools=[str(t) for t in daten["tools"]],
budget=budget,
side_effects=seiteneffekt,
version=str(daten.get("version", "0.0.0")),
quelle=datei,
)
def registry_laden(ordner: str | Path = "registry") -> dict[str, Card]:
cards: dict[str, Card] = {}
for datei in sorted(Path(ordner).glob("*.card.yaml")):
card = card_laden(datei)
if card.id in cards:
raise CardError(f"{datei.name}: id '{card.id}' gibt es schon.")
cards[card.id] = card
if not cards:
raise CardError(f"In '{ordner}' liegt keine einzige Card.")
return cards
if __name__ == "__main__":
import sys
ordner = sys.argv[1] if len(sys.argv) > 1 else "registry"
try:
cards = registry_laden(ordner)
except CardError as fehler:
print(f"FEHLER {fehler}")
raise SystemExit(1)
for card in cards.values():
print(
f"OK {card.id:<14} v{card.version:<8} "
f"{len(card.examples):>2} Beispiele "
f"{len(card.counter_examples):>2} Counter "
f"{len(card.tools)} Werkzeuge "
f"side_effects={card.side_effects}"
)
Zwei Prüfungen darin sind mehr als Formalie.
Die Überschneidungsprüfung fängt den Fehler ab, den jeder einmal macht: dieselbe Zeile aus Bequemlichkeit in beide Listen kopieren. Das Ergebnis wäre ein Agent, der sich selbst per Veto ausschließt, und die Fehlersuche dauert eine Stunde.
Die Budget-Obergrenze ist eine Meinung, und sie steht mit Absicht im Code: Wer 4.000 Ausgabe-Token erlaubt, hat keine Kompressionsgrenze, sondern eine Höflichkeitsfloskel. Wenn du anderer Meinung bist, ändere die Zahl - aber bewusst.
Der Meilenstein
python -m tiny.cards registry/
OK crm-writer v1.0.0 9 Beispiele 4 Counter 2 Werkzeuge side_effects=writes
OK docs-finder v1.0.0 10 Beispiele 3 Counter 1 Werkzeuge side_effects=none
OK web-reader v1.0.0 10 Beispiele 4 Counter 2 Werkzeuge side_effects=none
Und jetzt die zweite Hälfte des Meilensteins: Mach eine Card kaputt. Lösch drei
Beispiele aus docs-finder, oder setz max_output_tokens: 5000, oder kopier
eine Zeile aus examples nach counter_examples. Lauf noch mal.
FEHLER docs-finder.card.yaml: 'examples' hat 7 Eintraege, mindestens 8 noetig.
Wenn die Meldung Datei und Feld nennt, ist der Schritt fertig. Ein Validator, der nur „ungültig“ sagt, ist ein Validator, den man nach der dritten Card abschaltet.