Einhaltung der Agentenregeln im Code: PreToolUse, PostToolUse und Stop Hooks
Erfahren Sie, warum die Autorisierung für LLM-Agenten in deterministischen Tool-Call-Hooks untergebracht werden sollte, wie Anrufe sicher abgelehnt werden können und wie ein Dispatcher ohne Rekursion umschlossen wird.
Quellen: LLM Agents, The Way Nobody Told You — Part 8: Hooks
Ein Systemhinweis, der besagt „Nur Premium-Nutzer dürfen Rückerstattungen beantragen“, wirkt wie eine Regel, ist für das Modell jedoch nur ein starker Rat. Er wird bei fast jeder Eingabe befolgt, doch bei gelegentlichen Anfragen, die nur gerade ungewöhnlich formuliert sind, wird er ignoriert. Dieser Artikel zeigt, wie man wirklich wichtige Regeln aus dem Prompt in Hooks verlagert: kleine Stücke deterministischen Codes, die bei jeder Tool-Aufrufung ausgeführt werden, den vertrauenswürdigen Serverseitig-Kontext einsehen können und eine Aktion verhindern, bevor sie stattfindet. Am Ende wissen Sie, welchen Hook-Typ man für welche Aufgabe verwenden sollte, wie man einen Aufruf ablehnen kann, ohne das Gespräch zu stören, und wie man Hooks sicher an einen bestehenden Tool-Dispatcher anhängt.
Wie Prompt-Regeln in der Praxis versagen
Stellen Sie sich einen Kundenservice-Mitarbeiter zusammen mit einem Spezialisten für Rückerstattungen vor. In den Anweisungen steht, dass nur Premium-Kunden Rückerstattungsanfragen stellen dürfen und dass Free-Tier-Kunden höflich darauf hingewiesen werden sollen, dass Rückerstattungen nicht zu ihrem Plan gehören. Die Tests laufen gut: Free-Nutzer werden abgewiesen, Premium-Nutzer erhalten ein Antragsdokument.
Dann sendet ein Free-Tier-Nutzer eine Beschwerde in ungewöhnlicher Formulierung. Sie klingt wie eine Rückerstattungsanfrage, bezieht sich jedoch tatsächlich auf eine andere Art von Konto-Gutschrift. Das Modell, das darauf ausgelegt ist, hilfreich zu sein, ist nun unsicher bezüglich der Formulierung und entscheidet dennoch, dass die Situation ausreicht, um ein Antragsdokument zu erstellen. Für einen Menschen war nichts in den Regeln zweideutig, doch ein Kunde, dem es nie erlaubt wurde, Anfragen zu stellen, besitzt nun eine.
Dies ist die Kategorie der Fehlerhooks, die beseitigt werden sollen. Das Modell ist nicht schlecht darin, Anweisungen zu befolgen; es arbeitet probabilistisch. Eine Regel, die für 99 % der Eingaben gilt, wird bei einem Teil des verbleibenden 1 % versagen – und in der Produktion tritt genau dieser Teil auf. Wenn das Verletzen einer Regel echte Kosten verursacht, muss die Regel durch Code durchgesetzt werden, der jedes Mal ausgeführt wird und mit dem das Modell nicht argumentieren kann.
Drei Hook-Points im Lebenszyklus einer Tool-Aufrufung
Hooks werden zu bestimmten Zeitpunkten im Lebenszyklus einer Tool-Aufrufung angehängt. Es gibt drei davon, und jeder hat eine eigene Aufgabe:
- PreToolUse wird vor der Ausführung des Tools ausgeführt. Es kann den Aufruf ablehnen, die Eingaben umschreiben oder ihn einfach beobachten.
- PostToolUse wird nach der Ausführung des Tools ausgeführt. Es kann nur beobachten: protokollieren, auditen oder benachrichtigen. Es kann nicht rückgängig machen, was bereits geschehen ist.
Eine kompakte Art, sich daran zu erinnern, ist: PreToolUse fragt ab, ob etwas geschehen soll, PostToolUse protokolliert, dass es tatsächlich geschah, und Stop markiert das Ende der Sitzung.
Diese Rollen sind nicht austauschbar. Die Autorisierung gehört zu PreToolUse, denn eine Überprüfung in PostToolUse erfolgt erst nachdem die Aktion bereits Wirkung gezeigt hat. Das Audit-Logging gehört zu PostToolUse, denn eine in PreToolUse geschriebene Eintragung dokumentiert lediglich die Absicht; der Aufruf könnte dennoch abgelehnt oder fehlschlagen, wodurch das Log Dinge beschreiben würde, die nie stattgefunden haben. Ordnen Sie jede Aufgabe dem Hook zu, dessen Zeitpunkt am besten passt.
Fähigkeiten, Autorisierung und Richtlinien sind unterschiedliche Überprüfungen
Nehmen wir an, Sie möchten drei neue Regeln zum Fallbearbeitungstool hinzufügen:
- Regel A: Eine Falldatei benötigt eine nicht leere Liste mit Beweismitteln.
- Regel B: Kunden im kostenlosen Tarif dürfen keine Falldateien erstellen.
- Regel C: Die Erstellung von Fällen ist von 2 Uhr morgens bis 4 Uhr morgens während des wöchentlichen Deploys blockiert.
Jede dieser Regeln lehnt bestimmte Tool-Aufrufe ab, befindet sich jedoch an drei verschiedenen Stellen. Es hilft, jede Regel in eine von vier Kategorien einzuordnen:
- Funktionalität, also ob die Eingaben intern konsistent sind, liegt in der Tool-Implementierung. „Die Liste der Beweismittel muss nicht leer sein“ ist eine Überprüfung der Funktionalität.
- Berechtigung, also ob der Aufrufer dies gerade tun darf, liegt in einem PreToolUse-Hook. „Kostenlose Nutzer dürfen keine Falldateien erstellen“ ist eine Berechtigungsprüfung.
Ein nützlicher Shortcut: Das Tool fragt, ob es die Operation mit den erhaltenen Argumenten ausführen kann, während der Hook prüft, ob dem Aufrufer zu diesem Zeitpunkt die Ausführung gestattet ist.
Warum die Trennung aus dem hervorgeht, was jede Schicht sehen kann
Der Unterschied ist nicht stilistischer Natur; er ergibt sich aus den Eingaben, die jede Schicht hat. Ein Tool erhält nur die von dem Modell generierten Argumente. Es kann diese Argumente miteinander vergleichen, um zu überprüfen, ob eine Liste nicht leer ist oder eine Menge positiv ist, doch es hat nichts anderes, womit es sie vergleichen könnte.
Ein Hook erhält dieselben Argumente plus den vom Server zusammengestellten vertrauenswürdigen Kontext: die authentifizierte Benutzer-ID, die tatsächliche Stufe des Benutzers, die aktuelle Zeit sowie die Sitzungs-ID. Dieser Kontext befindet sich in einem HookContext-Objekt, das zu Beginn der Sitzung erstellt und danach unverändert bleibt.
Mit diesen Informationen kann ein Hook wie folgt urteilen: Das Modell möchte lookup_user mit user_id='u_999' aufrufen, doch die Sitzung gehört zu u_004, weshalb der Aufruf abgelehnt wird. Das Tool könnte niemals eine solche Entscheidung treffen, da es nie erfährt, wem die Sitzung gehört. Autorisierung bedeutet stets den Vergleich von unvertrauenswürdigen Modellausgaben mit dem vertrauenswürdigen Serverzustand – und nur Hooks besitzen beides.
Trennung von vertrauenswürdigem Kontext und unvertrauenswürdigen Eingaben
HookContext ist ein Server-Seitenzustand. Ihr eigener Code erstellt ihn zu Beginn der Sitzung aus von Ihnen kontrollierten Quellen wie der Authentifizierungsstufe, dem Sitzungsmanager und der Datenbank. Er bleibt während der gesamten Dauer der Sitzung unveränderlich, und weder der Benutzer noch das Modell können ihn beeinflussen. Die untenstehende gefrorene Dataclass enthält das Minimum, was Sie benötigen:
@dataclass(frozen=True)
class HookContext:
user_id: str # set from your auth layer
tier: Literal["free", "premium"]
session_id: str
frozen=True erfüllt hier eine wichtige Funktion: Sobald das Objekt erstellt wurde, kann es durch keinen Codepfad mehr verändert werden – auch nicht durch einen fehlerhaften Hook.
tool_input hingegen ist das Dictionary, das das Modell in seinem tool_use-Block platziert. Behandeln Sie es genauso wie den Inhalt einer eingehenden HTTP-Anfrage. Ein Benutzer kann das Modell dazu bringen, dort fast alles zu erzeugen – sei es durch Prompt-Injection oder einfach durch Ausdauer.
Eine Autorisierungsprüfung vergleicht die beiden Werte. Ist der user_id, den das Modell liefert, gleich HookContext.user_id? Schließt HookContext.tier == „free“ dieses Schreibwerkzeug aus? Das Prinzip, das das System sicher hält, ist einfach: Die Sicherheitsentscheidung muss auf HookContext beruhen, niemals auf irgendetwas, was das Modell bereitstellt.
Eine Anfrage ablehnen, ohne das Gespräch zu stören
Die offensichtliche Art, eine Anfrage abzulehnen, besteht darin, sie einfach zu überspringen. Bei API-Anfragen zur Aufrufung von Werkzeugen, die dem Anthropic-Nachrichtenformat folgen, funktioniert das jedoch nicht. Jeder tool_use-Block, den der Assistent sendet, muss durch einen tool_result-Block mit derselben tool_use_id in der unmittelbar nächsten Benachrichtigung des Nutzers beantwortet werden. Lässt man einen unbeantwortet, scheitert die folgende API-Anfrage mit einem 400-Fehler.
Daher muss eine Ablehnung von PreToolUse ein synthetisches Ergebnis erzeugen, das die Ausgabe des Tools ersetzt. Das untenstehende Beispiel ist ein Python-Dictionary (beachten Sie True) mit dem Status Fehler, zusammen mit einer Nachricht, die erklärt, warum der Aufruf abgelehnt wurde und was stattdessen zu tun ist:
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC",
"is_error": True,
"content": "BLOCKED: free-tier users cannot call prepare_case_file. Suggest upgrade or route to general FAQ."
}
Das erfüllt den API-Vertrag, und das Modell behandelt es wie jede andere Beobachtung. Es erkennt, dass der Antrag aus einem bestimmten Grund abgelehnt wurde, entschuldigt sich und bietet einen anderen Weg an, wodurch das Gespräch normal weitergehen kann.
Zwei Aspekte sorgen dafür, dass dies gut funktioniert.
Ein konsistentes Präfix verwandelt Ablehnungen in ein Signal
Das Präfix BLOCKED: ist keine Anforderung der API – es handelt sich um eine Konvention. Ein konsistenter Marker hilft dem Modell, zwischen einer bewussten Ablehnung und einem vorübergehenden Fehler zu unterscheiden, sodass es seine Vorgehensweise ändert anstatt denselben Aufruf erneut zu versuchen. Derselbe Marker ermöglicht es späteren Bewertungen, absichtliche Ablehnungen von echten Tool-Fehlern zu trennen. Eine Konvention im reinen Textformat wird somit zu einem maschinenlesbaren Signal.
Die Ablehnungsnachricht ist eine Anweisung
Das Modell erstellt seine nächste Antwort auf der Grundlage des von Ihnen zurückgegebenen Textes – schreiben Sie ihn daher mit derselben Sorgfalt wie eine Anweisung. Ein einfaches „Denied“ liefert ihm keine Grundlage für die Arbeit. Eine Nachricht, die den Grund nennt (zum Beispiel: Die kostenlose Version kann dieses Tool nicht verwenden) und Alternativen vorschlägt (Upgrade oder Verweis auf die allgemeine FAQ), teilt dem Modell mit, warum der Aufruf fehlgeschlagen ist und was stattdessen angeboten werden kann.
Hooks hinzufügen, indem der Dispatcher umschlossen wird
Die meisten Agenten verfügen bereits über einen Dispatcher mit einer call_tool-Funktion, die einen tool_use-Block entgegennimmt und die entsprechende Python-Funktion aufruft. Die am wenigsten invasive Methode, Hooks hinzuzufügen, besteht darin, diese Funktion zu umhüllen, anstatt den Dispatcher neu zu schreiben. Der erste Schritt ist es, vor jeder Änderung einen Referenz auf die ursprüngliche Funktion beizubehalten:
original_call_tool = dispatcher.call_tool # save the reference FIRST
Dann wird ein Wrapper definiert, der die Hook-Kette um die gespeicherte ursprüngliche Funktion ausführt, und dieser wird anstelle der alten Funktion installiert. Im veröffentlichten Auszug wurden die Zeilen return result und die Neuzuweisung dispatcher.call_tool = hooked_call_tool zu einer Zeile zusammengefasst; es handelt sich dabei um zwei getrennte Anweisungen, wobei die Neuzuweisung außerhalb des Funktionskörpers steht.
def hooked_call_tool(name, args):
event = build_event(name, args)
if denial := registry.run_pre(event):
return synthetic_error_result(denial) # PreToolUse denied
result = original_call_tool(name, args) # call the saved original
registry.run_post(event, result)
return resultdispatcher.call_tool = hooked_call_tool # NOW replace it
Der Wrapper führt fünf Schritte aus:
- Die Tool-Aufrufung in ein Hook-Event umwandeln.
- Jedes PreToolUse-Hook ausführen.
Speichern vor Ersetzen
Das Muster, das befolgt werden sollte, besteht darin, das Original vor dem Ersetzen zu speichern – und das gilt gleichermaßen für Decorator, Middleware und Interceptor. Überlegen Sie, was passiert, wenn man die gespeicherte Referenz überspringt und dispatcher.call_tool = lambda *a, **kw: dispatcher.call_tool(*a, **kw) schreibt. Die Lambda sucht nach dispatcher.call_tool erst beim Ausführen, und zu diesem Zeitpunkt verweist dieses Attribut bereits auf die Lambda selbst. Der Wrapper ruft sich selbst auf, was wiederum zu einer weiteren Selbstaufrufung führt, bis der erste Toolaufruf in endloser Rekursion und einem Stack Overflow endet.
Durch die Zuweisung des Originals zunächst an eine Variable bedeutet das, dass die Closure einen Verweis auf das eigentliche Funktionselement enthält und nicht auf einen später neu gebundenen Namen. Der Wrapper erreicht somit stets die tatsächliche Implementierung.
Warum Hooks den Prompt-Regeln und statischen Tool-Listen überlegen sind
Es gibt drei gängige Wege, einem Agenten zu verbieten, etwas zu tun – wobei jede Methode auf unterschiedliche Weise versagt:
- Eine Prompt-Anweisung wie „Tu nicht X“ kann einfach ignoriert werden, wenn das Modell unsicher ist.
- Das Filtern von Tools, also das Auslassen eines Tools aus der Liste ganz, ist statisch und kann nicht auf den Laufzeitzustand reagieren.
- Hooks sind deterministisch und kennen den Laufzeitzustand.
Tool filtering is still valuable. If the free-tier specialist never needs create_jira_ticket, do not give it that tool. A model cannot emit a tool_use block for a tool it has never been told about, and an ability the model lacks is more reliable than an instruction it might disregard.
Filtering falls short for rules that depend on the session. Whether the user is free-tier depends on who is logged in: the agent code is identical, but different sessions are allowed different actions. A static tool list cannot express that; a hook can.
The robust arrangement layers all three:
- Tool filtering at the specialist level, for tools a specialist never needs.
- Prompt guidance for soft preferences and general nudges.
- Hooks for runtime rules that must hold without exception.
Für einen vergleichbaren Ansatz innerhalb eines Frameworks sehen Sie sich an, wie LangChain Guardrails und Middleware dasselbe Problem bewältigen.
Eine einzige Tool-Oberfläche bedeutet eine einzige Durchsetzungsstufe
Hooks sind am nützlichsten, wenn jede Aktion, die der Agent ausführen kann, als Toolaufruf modelliert wird: Delegation an Unteragenten, Anfragen an externe APIs, Datenbankabfragen sowie Suchvorgänge in Wissensbasen. Der Wrapper hooked_call_tool erfasst dann alle diese Aktionen, einschließlich später hinzugefügter Tools, wobei Autorisierungsregeln, Richtlinien und Audits einheitlich auf der gesamten Aktionsoberfläche angewendet werden.
Falls die Delegation an einen Unteragenten stattdessen als direkter Funktionsaufruf umgesetzt würde, würde die Hook-Schicht dies niemals wahrnehmen. Man bräuchte ein zweites Autorisierungsmechanismus nur für die Routierung, was bedeutet, dass zwei Systeme gewartet werden müssen und zwei Orte vorhanden sind, an denen sich Fehler verstecken können. Wenn von Anfang an eine einheitliche Tool-Oberfläche entworfen wird, stellen Hooks nur einen kleinen Zusatz dar; eine Nachrüstung später verwandelt sie hingegen in einen umfangreichen Refaktorierungsprozess.
Wie eine Kette von PreToolUse-Hooks funktioniert
Reale Systeme sammeln mehrere PreToolUse-Hooks, beispielsweise eine Ebene-Prüfung, eine Deploy-Fenster-Prüfung, einen Abgleich der Benutzer-ID sowie eine Ausgabegrenze. Sie laufen in dieser Reihenfolge mit folgender Semantik ab:
- Die erste Ablehnung hat Vorrang. Sie unterbricht die Kette, sodass spätere Hooks sowie das Tool selbst nicht ausgeführt werden.
- Eine Genehmigung unterbricht die Kette nicht. Jeder Hook muss bestanden werden, damit der Aufruf fortgesetzt werden kann.
Ordnen Sie die Kette nach Kosten und Dringlichkeit, wobei günstige, entscheidende Überprüfungen zuerst durchgeführt werden sollten. Die Überprüfung, ob der Benutzer im kostenlosen Tarif ist, erfordert die Lektüre eines Feldes; die Überprüfung einer wöchentlichen Ausgabengrenze der Organisation kann hingegen eine Datenbankabfrage erfordern. Durch die Durchführung der günstigen Überprüfung zuerst wird unnötige Arbeit bei Anfragen vermieden, die ohnehin abgelehnt werden würden – ähnlich wie bei der Platzierung der selektivsten Bedingungen zuerst in einer SQL WHERE-Klausel.
Was soll passieren, wenn ein Hook selbst abstürzt
Jeder Hook-Typ benötigt eine andere Fehlerbehandlungsstrategie, und deren Fehlinterpretation führt zu echten Fehlern statt nur zu Stilproblemen:
- Fehler bei PreToolUse sollten weitergeleitet werden. Ein abstürzender Policy-Hook ist ein Defekt. Zu tun, als wäre die Überprüfung abgeschlossen, obwohl dies nicht der Fall ist, ist schlimmer als ein Abbruch – daher sollte der Agent bis zur Behebung der Strategie gestoppt werden.
- Fehler bei PostToolUse müssen ignoriert werden. Ein fehlerhafter Audit-Logger sollte die Anfrage des Benutzers nicht stören, da dies im Moment wichtiger ist als der Eintrag im Log. Der Fehler sollte auf einem separaten Kanal erfasst und die Arbeit fortgesetzt werden.
- Stop-Fehler müssen ignoriert werden. Die Sitzung endet ohnehin, daher ist die beste Option, den Fehler bei der Bereinigung zu protokollieren und anschließend abzuschließen.
Ein PreToolUse-Hook, der seine eigenen Ausnahmen stillschweigend abfängt, stellt ein Sicherheitsloch dar: Die Überprüfung wurde nicht durchgeführt, der Aufruf erfolgte dennoch, und die Audit-Spur liefert keine Erklärung. Ein PostToolUse-Hook, der seine Ausnahmen durchlassen lässt, verwandelt jeden Logging-Ausfall in einen Ausfall des Agents.
Wo die Audit-Spur gespeichert wird
Bei einer einfachen Konfiguration werden zwei nur zum Hinzufügen geeignete Dateien erstellt:
data/audit_log.jsonl, von PostToolUse bei jedem Toolaufruf erstellt, mit dem Toolnamen, den Eingaben, dem Ergebnis, einem Fehlerflag und einem Zeitstempel.data/session_summaries.jsonl, einmal pro Sitzung durch den Stop-Hook erstellt, enthält eine Zeile mit hochrangigen Statistiken pro Konversation.
Das Prüfprotokoll wird schnell zur wertvollsten Datei im Projekt. Da es von Anfang an integriert ist, liefert es aus einer einzigen Quelle Informationen für Compliance-Anforderungen, Bewertungstools, Kostenanalysen, Fehlerbehebung und Dashboards. In der Produktion würden in der Regel dieselben Datensätze in einen dauerhaften Speicher übertragen, doch die Struktur der Daten bleibt unverändert.
Haupterkenntnisse
- Anweisungen für Prompts sind nur empfehlend; jede Regel, deren Verletzung tatsächliche Folgen hat, muss im Code durchgesetzt werden.
tool_result mit klarer Präfixierung zurückgeben, damit das Gespräch gültig bleibt und das Modell sich erholen kann.