Ein deutschsprachiger Tool-Calling-Benchmark für die lm-evaluation-harness, gebaut auf den zwölf MCP-Werkzeugen der InfraNode Open-Data-API.
Deutschsprachige Benchmarks für agentische Modelle gibt es praktisch nicht. Gemessen wird hier nicht Weltwissen, sondern das, woran Agenten in der Praxis scheitern: aus zwölf Werkzeugen das richtige wählen, Parameter mit Katalogschlüsseln füllen, die man erst nachschlagen muss, einen Städtenamen mit Umlaut auf den kanonischen Slug abbilden, und weiterarbeiten, wenn eine Quelle nicht liefert.
Die Umgebung dahinter ist echt und keylos: jede Aufgabe entstammt einem tatsächlichen API-Aufruf gegen 84 deutsche Städte, kein simuliertes Tool-Schema. Weil die API keine Zugangsdaten braucht, lässt sich derselbe Aufbau auch für RL-Environments oder zur Erzeugung von SFT-Daten nutzen, ohne Secrets auf Trainingsknoten zu verteilen.
| Datei | Zweck |
|---|---|
tools.json |
Die zwölf Werkzeuge als JSON-Schema, wie ein Modell sie sieht |
generate.py |
Erzeugt tasks.jsonl aus der Live-API und verifiziert jede Gold-Antwort |
tasks.jsonl |
Der Datensatz, aktuell 91 Aufgaben |
lm_eval_task/ |
Task-YAML plus Prompt-Bau und Metriken |
selftest.py |
Prüft Prompt und Bewertung, ohne lm-eval zu installieren |
run_eval.py |
Fährt den Benchmark direkt gegen eine API, ohne lm-eval |
report.py |
Fasst die Läufe in results/ zu einer Vergleichstabelle zusammen |
measure.py |
Misst die Promptgröße für die Kostenschätzung |
| Kategorie | Was geprüft wird | Aufgaben |
|---|---|---|
routing_broad |
Breite Frage zur Stadt führt zum Überblicks-Werkzeug, nicht zu einer geratenen Einzelabfrage | 12 |
routing_named |
Wetter- und Luftfragen führen zum jeweils spezialisierten Werkzeug | 12 |
resource_key |
Der richtige Katalogschlüssel im generischen Zugriff, etwa charging oder district-heating |
24 |
slug_resolution |
München wird zu muenchen, Köln zu koeln, ohne erfundene Slugs |
8 |
poi_type |
Der richtige POI-Typ aus der Allowlist | 12 |
compare_multi |
Ein Aufruf für mehrere Städte statt einer pro Stadt | 5 |
not_covered |
Die Datenart fehlt für diese Stadt. Der Aufruf bleibt trotzdem der richtige, das Modell darf nicht auf ein anderes Werkzeug ausweichen | 12 |
two_step |
Die EVA-Nummer ist unbekannt, also muss zuerst die Bahnhofsliste geholt werden, nicht direkt der Abfahrtsmonitor | 6 |
call_match, Werkzeug und alle Parameter korrekt. Die Kennzahl, auf die es ankommt.tool_match, nur das Werkzeug korrekt.args_match, nur die Parameter korrekt.parsed, es kam überhaupt ein lesbarer Werkzeugaufruf zurück.call_match_<kategorie>, dieselbe Kennzahl pro Kategorie, damit eine schwache Dimension sichtbar bleibt statt im Mittelwert zu verschwinden.
Die Auswertung ist absichtlich tolerant beim Format: Code-Fences, umgebende Prosa und die Form
{"name": ..., "parameters": ...} werden akzeptiert. Sonst würde der Benchmark Formatdisziplin messen
statt Werkzeugwahl. Die Reihenfolge der Städte in compare ist bedeutungslos und wird normalisiert.
uv run selftest.py # Prompt und Bewertung prüfen
uv run generate.py --out tasks.jsonl # Datensatz neu aus der Live-API bauen
lm_eval --model hf \
--model_args pretrained=<modell> \
--include_path lm_eval_task \
--tasks infranode_tools_de \
--batch_size 8run_eval.py braucht nur die Standardbibliothek und bewertet mit denselben Funktionen wie die
Task-YAML, die Zahlen sind also vergleichbar. Zwei Backends: gemini und alles, was
/v1/chat/completions spricht.
GEMINI_API_KEY=... uv run run_eval.py --backend gemini --model gemini-2.5-flash
HF_TOKEN=... uv run run_eval.py --backend openai \
--model Qwen/Qwen3-4B-Instruct-2507 \
--base-url https://router.huggingface.co/v1Ergebnisse landen als results/<modell>.json mit jeder einzelnen Antwort, dazu eine Tabelle auf der
Konsole. Nützlich bei knappen Kontingenten:
| Flag | Wirkung |
|---|---|
--rpm N |
Deckelt die Aufrufe pro Minute über alle Threads hinweg |
--resume |
Übernimmt beantwortete Aufgaben aus dem vorhandenen Ergebnis und wiederholt nur die fehlgeschlagenen |
--stop-after-errors N |
Bricht ab, wenn N Aufrufe scheitern, statt in ein leeres Tageskontingent zu laufen |
--limit N |
Nur die ersten N Aufgaben, für einen Rauchtest |
Ein unvollständiger Lauf wird als solcher ausgewiesen: complete: false im JSON und eine zweite
Kennzahl, die nur die beantworteten Aufgaben wertet. Sonst sähe ein aufgebrauchtes Kontingent wie ein
schlechtes Modell aus.
generate.py schreibt nur Aufgaben, deren erwartete Quelle beim Generieren tatsächlich geantwortet hat,
und legt Fälle mit not_covered bewusst als eigene Kategorie ab. Zur Eval-Zeit ist kein Netzzugriff
nötig.
Stand 27.07.2026, alle Läufe über die vollen 91 Aufgaben, ohne Fehler. Backends waren
der Router von Hugging Face, die Anthropic Messages API und die OpenAI-API.
Setup nennt die Abweichung von der Task-YAML: bei mehreren Modellen ließ sich das
Reasoning nicht abschalten, sie hätten mit dem regulären Budget von 160 Token nur
ihre eigenen Gedanken produziert und gar keine Antwort.
| Modell | Setup | call_match | tool_match | args_match |
|---|---|---|---|---|
| deepseek-ai/DeepSeek-V3.2 | Standard | 92,3 | 98,9 | 92,3 |
| Qwen/Qwen3.5-122B-A10B | Standard | 87,9 | 95,6 | 87,9 |
| Qwen/Qwen3.5-35B-A3B | Standard | 87,9 | 97,8 | 87,9 |
| Qwen/Qwen3.6-27B | Standard | 86,8 | 97,8 | 86,8 |
| gpt-4.1-mini | Standard | 85,7 | 95,6 | 85,7 |
| claude-opus-5 | 2048 Token | 84,6 | 90,1 | 84,6 |
| Qwen/Qwen3.6-35B-A3B | Standard | 84,6 | 97,8 | 84,6 |
| claude-opus-5 | Standard | 82,4 | 89,0 | 82,4 |
| gpt-4.1 | Standard | 78,0 | 82,4 | 78,0 |
| claude-sonnet-5 | Standard | 75,8 | 82,4 | 75,8 |
| openai/gpt-oss-20b | 1024 Token | 75,8 | 90,1 | 75,8 |
| nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B | Standard | 73,6 | 75,8 | 73,6 |
| gpt-5 | 2048 Token | 71,4 | 72,5 | 71,4 |
| Qwen/Qwen3.5-397B-A17B | 8192 Token | 71,4 | 74,7 | 71,4 |
| o4-mini | 2048 Token | 69,2 | 73,6 | 69,2 |
| gpt-5-mini | 2048 Token | 68,1 | 68,1 | 68,1 |
| claude-haiku-4-5 | Standard | 67,0 | 83,5 | 67,0 |
| gpt-5-nano | 2048 Token | 67,0 | 90,1 | 67,0 |
| openai/gpt-oss-120b | 1024 Token | 65,9 | 81,3 | 67,0 |
| Qwen/Qwen3-4B-Instruct-2507 | Standard | 63,7 | 94,5 | 63,7 |
| Qwen/Qwen3-Next-80B-A3B-Instruct | Standard | 63,7 | 74,7 | 63,7 |
Die Aufschlüsselung nach Kategorie liefert uv run report.py. Drei Beobachtungen:
- Der Katalogschlüssel ist die harte Kategorie. Kein Modell kommt über 75,0 in
resource_key. Das Werkzeug ist meist richtig, der Ressourcenname geraten. not_coveredmisst ein Verhalten, keine Fehlerquote. Fehlt eine Datenart für eine Stadt, treffen neun der zwanzig Modelle nie den direkten Aufruf: sie fragen stattdessenget_city_overviewab und sehen erst nach, was es überhaupt gibt. DeepSeek-V3.2 und Qwen3.5-35B-A3B rufen in allen zwölf Aufgaben direkt auf. Beides ist vertretbar, der Datensatz wertet den direkten Aufruf als Gold, weil die API darauf mitnot_coveredund einem Verweis auf die abgedeckten Städte antwortet. Wer das andere Verhalten bevorzugt, sollte diese Kategorie getrennt lesen.- Größe entscheidet nicht, Aktualität auch nicht. gpt-4.1-mini liegt vor gpt-5, und Qwen3.5-35B-A3B vor Qwen3.5-397B-A17B wie vor Nemotron-3-Ultra-550B.
- Bewertet wird der erste Werkzeugaufruf, nicht eine ganze Kette.
two_stepprüft deshalb nur, dass der richtige Nachschlage-Schritt zuerst kommt. Eine mehrschrittige Variante mit echter Ausführung gegen die API ist der nächste sinnvolle Ausbau. - Der Datensatz ist mit 91 Aufgaben klein. Er skaliert über
--max-per-category, die Grenze ist die Abdeckung der Datenarten pro Stadt. - Es gibt genau eine Gold-Antwort pro Frage. Bei sehr breiten Fragen wäre auch ein anderer Aufruf
vertretbar, deshalb ist
routing_broadbewusst auf Formulierungen beschränkt, bei denen der Überblick die klar richtige Antwort ist.
Code Apache 2.0. Die abgefragten Daten stehen unter den jeweiligen Quellenlizenzen, siehe
sources in der API und die Attributionspflicht dort.
