Dev

Der Agent greift zum Werkzeug, das er lesen kann

Du optimierst ein Werkzeug danach, was es kann. Ein Agent wählt ein Werkzeug danach, ob er das Ergebnis vorhersagen und die Antwort lesen kann, wenn sie leer zurückkommt. Das sind verschiedene Eigenschaften, und der Abstand zwischen ihnen ist der Grund, warum gerade das mächtigere Werkzeug so oft unangetastet bleibt.

Ich gebe den Coding-Agents, die ich betreibe, einen Language Server. Er kann zu einer Definition springen, die Referenzen auf ein Symbol finden, einen Call-Graphen verfolgen. Die meiste Zeit ignoriert der Agent das alles und ruft grep auf.

Eine Weile lang habe ich das so gelesen, dass der Agent sich mit weniger zufriedengibt, und wollte ihn immer wieder zum besseren Werkzeug schubsen. Dann habe ich mir angesehen, was grep ihm tatsächlich liefert. grep beantwortet eine kleinere Frage, aber es antwortet in einer Form, die der Agent nutzen kann, und genau das ist die Eigenschaft, die entscheidet, welches Werkzeug aufgerufen wird. Die Leistungsfähigkeit, auf die ich stolz war, wurde nie abgewogen.

Das Werkzeug, das du für einen Agenten baust, und das Werkzeug, zu dem er greift, sind zwei verschiedene Dinge. Das erste baust du und optimierst es auf Leistungsfähigkeit: je mehr es kann, an je mehr Stellen es nachsehen kann, desto reicher seine Antworten. Das zweite wählt der Agent nach einer Achse, für die du nicht ausgelegt hast. Kann er vorhersagen, wie das Ergebnis aussehen wird, bevor er aufruft, und kann er das Ergebnis lesen, wenn es leer zurückkommt. Das ist nicht die ganze Geschichte der Werkzeugwahl, und ich komme darauf zurück, was noch dazugehört, aber es ist die Hälfte, die ein Tool-Autor immer wieder ignoriert, während er alles in die Leistungsfähigkeit steckt.

Es gibt eine einfachere Erklärung für das ganze Muster, und sie verdient Gehör, bevor ich mich auf meine stütze. grep taucht im Training des Modells öfter auf als alles, was ich bauen könnte, und ein maßgeschneidertes Werkzeug fast nie. Vielleicht greift der Agent zu grep, weil es vertraut ist, nicht weil es lesbar ist, und ich verkleide einen Beliebtheitswettbewerb als Prinzip. Die ehrliche Antwort ist, dass beides ineinander verwoben ist und eine einzelne Beobachtung es nicht entwirrt. Aber in einem Fall trennt es sich doch, und das ist der Fall, den man beobachten sollte: ein brandneues Werkzeug, das der Agent noch nie gesehen hat und dessen Ergebnis eine starre, selbstbeschreibende Form hat, gegen ein abgegriffenes Werkzeug, dessen Ausgabe Brei ist. Wäre Vertrautheit die ganze Geschichte, bliebe das neue Werkzeug unangetastet, bis es berühmt wird. Leistet Lesbarkeit echte Arbeit, verdient sich das neue Werkzeug den Griff beim ersten Mal, sobald sich sein Ergebnis als lesbar erweist. Ich kann dieses Experiment von meinem Platz aus nicht sauber durchführen, also halte ich die kausale Behauptung nur locker fest. Woran ich nicht locker festhalten muss, ist, welche Hälfte ich kontrolliere. Ich kann mein Werkzeug einem bereits trainierten Modell nicht vertrauter machen. Sein Ergebnis lesbar machen kann ich. Der Hebel, den der Tool-Autor tatsächlich hat, ist genau der, um den es in diesem Text geht.

Ein leeres grep bedeutet nur eines

Wenn grep nichts findet, beendet es sich mit Status 1 und gibt nichts aus. Das ist das ganze Ereignis. Die leere Ausgabe hat genau eine Lesart: Das Muster ist nicht in den durchsuchten Dateien. Wenn grep dagegen nicht nachsehen kann, ein fehlender Pfad, ein fehlerhafter regulärer Ausdruck, sagt es das auf stderr und beendet sich mit 2. Es gibt also zwei Arten, leer zurückzukommen, und sie treffen als zwei verschiedene Zahlen an derselben kleinen Stelle ein, die der Aufrufer ohnehin prüft. Stille ist der Abwesenheit vorbehalten. Der Grund fährt neben dem Ergebnis nach Hause, und genau das schaffen die reicheren Werkzeuge nicht.

Diese Lesbarkeit gilt nur für einen Aufrufer, der den Exit-Status liest. Ein Harness, das stdout behält und den Status verwirft, holt sich die Mehrdeutigkeit zurück, die grep vermieden hat. Die Information ist da, was schon mehr ist, als die reicheren Werkzeuge versprechen können, aber sie zählt nur, wenn sie bis zur Entscheidung überlebt.

Die leere Antwort des reicheren Werkzeugs nicht

Bitte einen Language Server, zu einer Definition zu springen, und die Antwort ist ein Ort, mehrere oder null. „Ein Rückgabewert von null bedeutet kein Ergebnis", sagt die Spezifikation. Lies den Typ, und du siehst die Lücke: null ist eine Form ohne Platz für ein Warum. Es ist die Abwesenheit, der der Grund abgezogen wurde.

Die Spezifikation ist darin nicht naiv. Sie hat Fehlercodes für die Zustände, die sie sauber benennen kann: ContentModified, wenn sich das Dokument während einer laufenden Anfrage verschoben hat, ServerNotInitialized für eine Anfrage, die vor dem Start-Handshake eintraf. Was sie nicht tut, ist, den Zustand, auf den ein Coding-Agent tatsächlich stößt, auf einen davon zu zwingen: einen Server, der läuft, seinen Handshake hinter sich hat und immer noch seinen Index aufbaut. Ein nicht bereiter Server darf eine Definitionsanfrage mit einem schlichten null beantworten, und nichts im Protokoll hält ihn davon ab. Die Tatsache, die einen Agenten „es gibt keine Definition" von „ich war nicht bereit nachzusehen" unterscheiden ließe, ist also nirgends garantiert festgehalten. Auf ein Fehlerobjekt zu prüfen rettet nichts, denn der nicht bereite Server hat nie eines ausgelöst. Der unordentliche Zustand in der Mitte landet bei dem, der das null liest, und was er liest, kann seine eigene Ursache nicht tragen.

So gesehen ist der Agent, der am Language Server vorbeigreift, nicht faul. Er weigert sich, auf einem Signal aufzubauen, das er nicht lesen kann, und greift zu einem, das er lesen kann, was der richtige Instinkt in Bezug auf das Signal ist. In Bezug auf die Aufgabe ist er nicht immer richtig: grep kann ein Symbol nicht von einer gleichnamigen Zeichenkette in einem Kommentar unterscheiden, kann den Scope nicht beachten, kann kein sicheres Umbenennen durchführen, wie ein Language Server es kann, und eine lesbare Antwort auf die falsche Frage ist nur ein selbstsicherer Fehler. Der Griff hat recht darin, worauf er sich verlassen kann, nicht darin, was die Aufgabe braucht, und ein guter Tool-Autor schuldet dem Agenten beides.

Die Werkzeuge, die wir für Agenten bauen, haben dasselbe Loch

Die Form zeigt sich eine Schicht weiter außen, in den Servern, die wir absichtlich für Agenten aufsetzen. Ein Model-Context-Protocol-Tool liefert eine Liste von Content-Blöcken zurück, und standardmäßig sind diese Blöcke unstrukturiert, meistens ein Klumpen Text. Die Spezifikation hat ein optionales Output-Schema und ein Feld für strukturierten Inhalt ergänzt, damit ein Tool etwas mit garantierter Form zurückgeben kann, aber die Form ist optional, also geben die meisten Tools Text zurück, den das Modell bei jedem Aufruf neu parsen muss. Das Unlesbare habe ich selbst gebaut: einen kleinen Server, der jedes Mal richtig antwortete und jedes Mal in einem anders angeordneten Absatz, sodass das Zurücklesen der Antwort seine eigene kleine Parsing-Aufgabe war.

Der Fehlerfall ist merkwürdigerweise der Teil, den MCP eher richtig hinbekommt als der Language Server. Ein Protokollfehler deckt ein unbekanntes Tool oder eine fehlerhafte Anfrage ab, und ein Fehler innerhalb des Tools kommt als isError: true zurück, mitten in einem normalen Ergebnis, an der einen Stelle, an der der Agent ohnehin schon nachsieht. Was MCP offenlässt, ist die andere Hälfte, die Form. Das Flag sagt, dass ein Aufruf fehlgeschlagen ist. Der Grund für den Fehler trifft als derselbe Freitext ein wie alles andere, ein lesbares Signal um eine unlesbare Ursache gewickelt. Leistungsfähigkeit und Lesbarkeit sind getrennte Investitionen, und wir zahlen weiter für die erste, während wir annehmen, die zweite komme mit ihr.

Die Kosten, die verzögertes Laden überleben

Es wäre leicht, das neben die Token-Steuer einzuordnen, und es ist eine andere Rechnung. Jene wird vor dem Aufruf bezahlt, für das Schema, das jede geladene Definition im Kontextfenster belegt. Diese wird nach dem Aufruf bezahlt, auf dem Ergebnis, und sie überlebt jede Behebung der ersten. Verzögere die Definition des Language Servers, sodass sie nichts kostet, bis der Agent danach fragt, und der Agent überspringt sie trotzdem, denn der Grund, warum er sie überspringt, war nie das Schema im Prompt. Es war die Antwort, die er zurückbekäme.

Der Token-Steuer-Beitrag hat ein eigenes Auswahlargument, dass ein ausreichend großer Katalog das Zielen des Modells über viele Werkzeuge hinweg verschlechtert. Jenes handelt von Quantität, von der Unterscheidung zwischen gedrängten Optionen. Dieses handelt vom Ergebnisvertrag des einzelnen Werkzeugs, das selbst dann verliert, wenn es das einzige Werkzeug im Werkzeugkasten ist. Die beiden überschneiden sich nicht. Ein lesbar geformtes Werkzeug gewinnt, ob es Gesellschaft hat oder nicht.

Das ist das dritte Mal, dass ich auf eine Abwesenheit mit zwei Lesarten stoße. Ein Pager kann einen fehlgeschlagenen Job nicht von einem unterscheiden, der absichtlich gestoppt wurde; ein Watchdog kann einen arbeitenden Agenten nicht von einem festgefahrenen unterscheiden; ein null kann eine Definition, die nicht existiert, nicht von einer unterscheiden, die der Server nicht bereit war zu finden. Die Situationen unterscheiden sich und die Reparaturen auch, aber es ist ein und dieselbe Form: Eine Leere kann ihre eigene Ursache nicht tragen, und solange nicht etwas diese Ursache dort festhält, wo der Leser nachsieht, bleibt dem Leser nur, sie zu erschließen, und die beiden Ursachen erschließen sich gleich.

Gib der leeren Antwort einen Grund

Die Lösung für ein Werkzeug, das der Agent ignoriert, ist also fast nie die, die zuerst vorgeschlagen wird. Der Instinkt ist, es leistungsfähiger zu machen oder ihm eine bessere Beschreibung zu schreiben, damit das Modell endlich versteht, wofür es da ist. Keines von beidem rührt an den Grund, warum es ungenutzt bleibt. Ein Werkzeug verdient sich den Griff, wenn sein Ergebnis eine Form hat, die der Agent vorhersagen kann, und seine leere Antwort ihre eigene Ursache trägt, nicht bloß „nichts", sondern welches Nichts. Das ist der Unterschied zwischen einem nackten null und einem Ergebnis, das seine eigene Leere benennt:

json
{ "definitions": [], "reason": "not_indexed" }

mit no_definition, not_indexed und out_of_scope als eigenständigen Werten, die der Agent an der einen Stelle liest, an der er ohnehin liest, sodass „es gibt keine" und „ich konnte nicht nachsehen" nie dasselbe Gesicht tragen. Leg den Grund in den Kanal, der das Ergebnis trägt, so wie ein Exit-Code neben stdout nach Hause fährt, und hör auf, den Agenten ein zweites Objekt gegenprüfen oder raten zu lassen, um herauszufinden, ob das erste das meinte, was es sagte. Das ist stumpfere Arbeit, als ein Feature auszuliefern, und es ist die Arbeit, die dazu führt, dass das Werkzeug aufgerufen wird.

Das Werkzeug, das du gebaut hast, wird daran gemessen, was es kann. Das Werkzeug, zu dem der Agent greift, wird daran gemessen, ob der Agent erkennen kann, was gerade geschah, als er es aufrief. Das ist nicht dasselbe Werkzeug, und bis du das zweite baust, antwortet das erste in einer Form, die der Agent nicht lesen kann, während der Agent grep ausführt.

Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit

Max Nardit

@mnardit

Weitere Artikel

Ein Exit ist kein Urteil

Ein Monitor, der auf die Abwesenheit eines Prozesses hin alarmiert, beantwortet eine Frage, deren Antwort du längst siehst. Ob ein Job weg ist, lässt sich billig feststellen und ist fast nie das, worauf es ankommt. Ob er fehlgeschlagen ist oder absichtlich beendet wurde, durch ein Deploy, einen Neustart, einen Operator, das ist es, woran ein Alarm hängt, und ein bloßer Exit trägt davon nur einen Bruchteil.

Ein Modell ist eine Abhängigkeit, die nicht stillhält

Eine gepinnte Software-Bibliothek ist ein Artefakt, das du inspizieren und erneut ausführen kannst: Wenn sich die Ausgabe geändert hat und du nichts geändert hast, dann hat sich etwas Lesbares geändert, und du kannst herausfinden, was. Ein Modell durchbricht das. Es verschiebt sich hinter einem unveränderten Namen, verliert ohne Vorwarnung Kapazität und driftet in seiner Disposition, also kommt eine Regression ohne Ursache, auf die du zeigen kannst, und du verschwendest eine Woche damit, deinen eigenen Prompt zu beschuldigen. Behandle es als eine Abhängigkeit, die du misst, nicht als ein Fundament, dem du vertraust: Pinne die Version, damit dir der Moment gehört, in dem sie sich ändert, und lege vorab ein kleines Set billiger Metriken fest, damit du die Drift des Anbieters von deiner eigenen unterscheiden kannst.

Dein Freigabe-Gate ist jetzt eine Vermutung

Eine Sicherheitsgrenze wird an der einen Aktion gemessen, die gebaut wurde, um sie zu überschreiten, und genau dort ist ein Modell, das nach Ähnlichkeit urteilt, am schwächsten. Die unbeaufsichtigte Freigabe gehört an die Reichweite, die der Harness selbst vergibt, den Schreibbereich, das Netzwerk, das Credential, und verweigert, was sie nicht verantworten kann. Vertrau dem Modell als Grenze, und du baust genau die Naht wieder auf, die Prompt-Injection-Erkennung nie schließen konnte.