Jemand hat einem KI-Assistenten mit Zugriff auf BugDump eine vernünftige Frage gestellt:
Gib mir einen Bericht über meine Aufgaben dieser Woche, aufgeteilt in „In Arbeit“ und „Fertig“.
Die Antwort war richtig. Sie hat aber etwa vierzig Sekunden und gut dreißig Tool-Aufrufe gekostet, denn der Assistent hat die Aufgaben aufgelistet und dann die Historie jeder Aufgabe einzeln geöffnet, um herauszufinden, wann sie verschoben wurde und von wem.
Das ist das N+1-Problem. Alle, die Backends bauen, kennen es vom ORM: Du lädst eine Liste und feuerst dann eine Abfrage pro Zeile ab, weil das, was du eigentlich brauchtest, nicht in der Liste stand. Hier hat es dieselbe Form, nur sitzt dort, wo früher die Schleife war, eine langsamere und teurere Query-Engine: ein Sprachmodell.
Der Agent war nicht dumm. Meine API ließ ihm keinen anderen Weg zur Antwort.
Ein Agent kann nur fragen, was du freigegeben hast
Ein Modell mit Tools macht etwas, das der Abfrageplanung sehr nahe kommt. Es liest deine Tool-Beschreibungen, überlegt, welche Aufrufe die Antwort liefern könnten, und führt sie aus. Wenn die Achse, die es braucht, kein Filter ist, rekonstruiert es diese Achse von Hand, indem es Zeilen holt und sie durchsieht.
Mein list_tasks konnte nach zuständiger Person filtern, nach Status, nach Spalte, nach Label, nach jeder benutzerdefinierten Eigenschaft und nach einem Zeitraum für das Erstellungsdatum. Was es nicht konnte: die Frage beantworten, wann das erledigt wurde. Der Zeitpunkt existierte, in einer Spalte namens resolvedAt, indiziert und bereit. Er war nur kein Filter und stand nicht in der Antwort.
Also zerfiel „was habe ich diese Woche erledigt“ aus Sicht des Modells in zwei Schritte: die geschlossenen Aufgaben auflisten, dann bei jeder die Historie öffnen und nach der Verschiebung suchen. Ein Aufruf pro Aufgabe. Dabei war die Information nur eine WHERE-Klausel entfernt.
Drei Fragen, die du jedem Tool stellen solltest
Seitdem habe ich meine Tools mit drei Fragen geprüft. Du kannst sie gern übernehmen.
1. Lässt sich nach jeder Achse filtern, die die Daten hergeben?
Nicht nach „jedem Feld“, sondern nach jeder Achse, nach der jemand fragen wird. Zeit ist fast immer eine davon, und fast immer mehr als eine: erstellt, aktualisiert, erledigt, zuletzt angefasst. Jede ist eine andere Frage. Ein Tool, das nur createdAt anbietet, zwingt alle, die nach den anderen drei fragen, Zeilen durchzublättern.
Mein Fix waren zwei Parameter, resolvedFrom und resolvedTo. Vier Zeilen Servercode. Damit verschwand eine ganze Klasse von N+1 aus jedem Bericht, den ein Agent schreibt.
2. Weiß das Tool, wer es aufruft?
„Meine Aufgaben“ sind die häufigsten Worte in solchen Anfragen, und lange ließ sich das für einen Agenten nicht beantworten, ohne zu raten. get_current_project gab die Mitglieder des Projekts zurück, mit Namen, Rollen und IDs, sagte aber nie, in wessen Namen dieses Token handelt.
Sieh dir an, was ein Agent damit macht. Er schaut in die Mitgliederliste, sieht „Alex Rivera“, erinnert sich, dass in der Frage „meine“ stand, und nimmt das Mitglied, dessen Name am ehesten zu etwas passt, das er vorher gesehen hat. Das funktioniert, bis ein Team zwei Leute namens Alex hat.
Jetzt gebe ich ein viewer-Objekt zurück: die User-ID, den Namen und die Rolle des Mitglieds, dem das Token gehört. Identität sollte keine Schlussfolgerung sein.
3. Liefert die Liste, was auch die Detailansicht liefert?
Wenn get_task ein Feld enthält und list_tasks es weglässt, hast du eine Fetch-Schleife gebaut. Der Agent bekommt zwanzig Zeilen, braucht dieses Feld für alle zwanzig und ruft den Detail-Endpunkt zwanzigmal auf.
Bei mir fehlten resolvedAt und updatedAt in den Listeneinträgen, während die Detailansicht beide hatte. Sie hinzuzufügen ist eine Änderung von zwei Zeilen, die zwanzig Roundtrips streicht.
Die Falle: ein Zeitstempel, der lügt
Beim Hinzufügen der Filter hätte ich fast einen vierten ausgeliefert, updatedFrom / updatedTo auf updatedAt, weil das der naheliegende Weg ist, nach „was hat sich diese Woche geändert“ zu fragen.
Er hätte aktiv geschadet, und der Grund gilt allgemein.
updatedAt wird in meiner Aufgabentabelle vom ORM gepflegt: Jedes UPDATE auf die Zeile setzt es neu. Aber Status, zuständige Person, Priorität, Labels und Fälligkeitsdatum stehen bei mir in einer separaten Wertetabelle. Also:
eine Aufgabe zuweisen verändert
updatedAtnicht;ihre Priorität ändern verändert es nicht;
sie zwischen zwei offenen Spalten verschieben verändert es nicht;
eine fremde Person, die in deinem öffentlichen Portal für sie abstimmt, verändert es schon.
Ein Filter auf dieser Spalte hätte „woran habe ich diese Woche gearbeitet“ mit einer Liste beantwortet, die die Arbeit ausließ und den Klick einer fremden Person enthielt. Schlimmer noch, sie hätte plausibel ausgesehen. Ein schlechtes Tool scheitert nicht mit einem Fehler, sondern mit einer selbstsicheren falschen Antwort.
Ich habe ihn weggelassen und das auch in die Tool-Beschreibung geschrieben: updatedAt ist ein Stempel für die letzte Berührung der Zeile, kein Aktivitätsstempel. Eine Tool-Beschreibung ist Dokumentation, die das Modell tatsächlich liest. Nutze sie, um Dinge auszuschließen, nicht nur, um sie zu erklären.
Was es kostet, das falsch zu machen
Die Agenten-Version eines langsamen Endpunkts ist keine langsame Seite. Sie besteht aus:
Wartezeit, die man spürt. Dreißig Roundtrips nacheinander, und bei jedem wartet man darauf, dass das Modell den nächsten wählt.
Verbranntem Kontextfenster. Jede Antwort besteht aus Tokens. Dreißig Aufgaben-Historien können das verdrängen, wonach eigentlich gefragt wurde.
Geld. Das sind abgerechnete Tokens, bei jeder einzelnen Anfrage.
Stillem Abschneiden. Ein Agent, dem der Platz ausgeht, hört früh auf und fasst zusammen, was er hat, und seine Antwort sagt nicht „Ich habe nur elf der vierzig Aufgaben gelesen“.
Wegen des letzten Punkts ist das ein Problem der Korrektheit, nicht der Performance.
Derselbe Bericht heute
Mit den drei Fixes sieht der Wochenbericht so aus:
get_current_project– wer bin ich, und welche Spalten gibt es;list_tasksmitassigneeId+ der Spalte „In Arbeit“;list_tasksmitassigneeId+status: "closed"+resolvedFrom.
Immer drei Aufrufe, egal wie viele Aufgaben zurückkommen. Jede Zeile trägt bereits resolvedAt, das Gruppieren nach Tag braucht also nichts weiter.
Einen Vorbehalt dokumentiere ich, statt ihn zu verstecken: resolvedAt hält fest, wann eine Aufgabe zuletzt in eine geschlossene Spalte kam. Stufst du eine Spalte von offen auf geschlossen um, bekommt jede Aufgabe darin diesen Zeitpunkt als Stempel, und ein Wochenbericht kann Arbeit aufnehmen, die eigentlich vor Monaten erledigt wurde. Solche Vorbehalte kann ein Agent nicht erschließen. Schreib sie dorthin, wo das Modell sie liest.
Wenn du einen MCP-Server baust
Stell einem echten Agenten eine echte Frage und schau dir das Aufruf-Log an. Nicht den Idealweg, den du entworfen hast, sondern die Frage, die jemand wirklich tippen würde, in eigenen Worten, an einem Freitagnachmittag.
Jede Schleife, die du siehst, ist eine Achse, die deine API nicht freigegeben hat. Die Lösung ist meist eine WHERE-Klausel, ein Feld in einer Listenantwort oder ein Satz in einer Beschreibung. Das ist viel billiger als der Umweg des Modells.
Der MCP-Server von BugDump ist in jedem Plan enthalten, auch im kostenlosen: Richte Claude Code auf dein Projekt ein und frag es nach deiner Woche, wie in BugDump mit Claude Code nutzen (MCP) beschrieben.
Was ich beim Bau eines MCP-Servers für ein Entwickler-Tool gelernt habe – weitere Entscheidungen dazu, wie Tools aussehen müssen, damit ein Agent sie schon beim ersten Versuch richtig benutzt.
MCP-Server-Authentifizierung: warum KI-Agenten persönliche Tokens brauchen – warum jeder Agent mit dem Token einer bestimmten Person arbeiten sollte und was das für Schreibzugriffe ändert.
Fehlerberichte mit Claude Code debuggen: der MCP-Server von BugDump – wie ein KI-Assistent Console, Netzwerk-Requests und Session-Replay aus einem Bericht liest und den Bug im Code findet.