Skip to main content
Ein einzelner Funktionsaufruf ist einfach. Der interessante Teil ist die Schleife darum herum, denn ein Modell bekommt selten beim ersten Aufruf, was es braucht. Es schlägt etwas nach, sieht das Ergebnis und entscheidet, wonach es als Nächstes fragen soll. Dieses Tutorial baut einen Kommandozeilen-Agenten, der Fragen zu einer SQLite-Datenbank beantwortet, die er noch nie gesehen hat. Er hat kein Schema in seinem Prompt. Er bekommt drei schreibgeschützte Tools und findet den Rest selbst heraus:
Dabei werden wir:
  1. Dem Modell eine Datenbank und drei Tools geben, die sie lesen
  2. Diese Tools beschreiben, damit das Modell weiß, wann es zu welchem greifen soll
  3. Die Schleife ausführen, die Tool-Aufrufe in Tool-Ergebnisse verwandelt
  4. Beobachten, wie es mehrere Tools gleichzeitig anfordert
  5. Fehler an das Modell zurückgeben, statt sie zu werfen
  6. Die Grenze zwischen dem ziehen, was das Modell nicht tun wird, und dem, was es nicht tun kann
Der Function-Calling-Leitfaden behandelt die Form der Anfrage für sich allein. Auf dieser Seite geht es darum, was passiert, nachdem die erste Antwort zurückkommt.

Setup

Du brauchst Python 3.9 oder neuer, das requests-Paket und einen Venice-API-Schlüssel. Siehe API-Schlüssel erzeugen, falls du noch keinen hast. Alles andere ist in der Standardbibliothek.
Erstelle agent.py mit den Imports und dem Header-Block, den jeder Aufruf wiederverwendet:
Nicht jedes Modell kann Tools aufrufen, und die Modell-IDs ändern sich, also frag die API, welches verwendet werden soll, statt einen Namen festzupinnen, der veralten wird:
GET /models/traits bildet stabile Trait-Namen auf das Modell ab, das gerade diese Rolle ausfüllt. Wenn du function_calling_default beim Start ausliest, funktioniert dein Agent weiter, wenn das zugrunde liegende Modell ersetzt wird. Siehe Models für die vollständige Trait-Liste.

1. Eine Datenbank, die es wert ist, befragt zu werden

Jede SQLite-Datei tut es. Diese hier ist ein kleiner Shop mit Kunden, Produkten und den Bestellungen, die sie verbinden — genug, dass eine echte Frage einen Join und eine Aggregation braucht:

2. Drei Tools, nach denen das Modell greifen kann

Die Tools spiegeln wider, wie ein Mensch einer unbekannten Datenbank begegnet: herausfinden, was drin ist, sich eine Tabelle genauer ansehen und sie dann abfragen.
Jedes davon gibt einen JSON-String zurück, einschließlich der Fehlschläge. Das ist Absicht, und in Abschnitt 5 geht es darum, warum. Beschreibe sie nun für das Modell. Die description ist kein Kommentar. Sie ist das Einzige, was das Modell liest, wenn es entscheidet, welches Tool es aufruft und was es hineinlegt:

3. Die Schleife

Function Calling ist ein Gespräch, keine Anfrage. Das Modell antwortet mit Tool-Aufrufen, du führst sie aus, hängst die Ergebnisse an und fragst erneut. Es endet, wenn das Modell mit Inhalt statt mit Aufrufen antwortet.
Drei Details in dieser Schleife sind wichtiger, als sie aussehen. Die unveränderte Assistant-Nachricht wandert vor den Ergebnissen zurück in messages. Sie trägt die tool_calls, auf die die Ergebnisse antworten, und bei einem Reasoning-Modell trägt sie zusätzlich ein Feld reasoning_content. Die Nachricht von Hand nachzubauen und Felder wegzulassen, mit denen du nicht gerechnet hast, ist der häufigste Weg, die zweite Runde kaputtzumachen. Jedes Ergebnis wird über tool_call_id seinem Aufruf zugeordnet. Sonst identifiziert es nichts. max_rounds ist eine echte Grenze, keine Formalität. Ein Modell, das immer weiter abfragt, ohne zu einem Schluss zu kommen, würde sonst schleifen, bis dir die Geduld oder das Guthaben ausgeht.
Tool-Aufrufe tragen außerdem ein index-Feld, und es ist verlockend, damit Ergebnisse zu Aufrufen zuzuordnen. Tu es nicht. Wenn das Modell drei Tools gleichzeitig anfordert, können alle drei mit demselben index ankommen, weil dieser den Assistant-Turn nummeriert, nicht den Aufruf darin. Nur id ist eindeutig.

4. Was er tatsächlich tut

Verdrahte einen Main-Block und führ ihn aus:
Die Tool-Aufrufe werden auf stderr ausgegeben, während sie passieren, sodass du bei der Arbeit zusehen kannst:
Das hat fünf Runden gedauert. Deren Form lohnt es sich genau anzusehen, denn sie ist das gesamte Argument für die Schleife: Runde 4 ist der Teil, den ein einzelner Funktionsaufruf nicht leisten kann. Das Modell konnte diese Abfrage nicht schreiben, bevor es die Antwort auf die davor gesehen hatte. Dein Lauf wird nicht Aufruf für Aufruf zu diesem passen. Das Modell beschreibt manchmal alle drei Tabellen auf einmal und manchmal eine nach der anderen, und gelegentlich überspringt es list_tables und rät einen Namen. Die Zahlen sind stabil, weil sie aus der Datenbank kommen; der Weg zu ihnen ist es nicht.
Runde 2 lieferte drei Tool-Aufrufe in einer Antwort, und die obige Schleife führt sie nacheinander aus. Sie sind unabhängig, sodass sich ein ThreadPoolExecutor hier lohnt, sobald deine Tools echtes I/O machen. Halte die tool-Nachrichten in derselben Reihenfolge wie die Aufrufe, die sie erzeugt haben.
Jede Runde sendet das gesamte Gespräch erneut, sodass der Prompt wächst, während der Agent arbeitet. Venice cacht das stabile Präfix automatisch, und der usage-Block zeigt, wie sich das auszahlt:
In der letzten Runde wurden 960 von 1020 Prompt-Tokens aus dem Cache bedient. Prompt Caching behandelt, wie du dieses Präfix stabil hältst.

5. Lass Fehler das Modell erreichen

Der Instinkt ist, bei einer fehlerhaften Abfrage eine Exception zu werfen. Widersteh ihm. Ein Fehler ist Information, und das Modell kann darauf reagieren. Frag nach einer Tabelle, die nicht existiert:
Die erste Abfrage schlug fehl. Weil run_query {"error": "OperationalError: no such table: purchases"} als gewöhnliches Tool-Ergebnis zurückgab, statt eine Exception zu werfen, hat das Modell es gelesen, list_tables aufgerufen, um herauszufinden, was tatsächlich existiert, und sich korrigiert. Hätte sich die Exception fortgepflanzt, wäre das Skript an einem Tippfehler gestorben. Deshalb gibt jedes Tool auch im Fehlerfall JSON zurück. Die Regel ist einfach: Wenn ein Mensch, der dein Tool debuggt, die Nachricht sehen möchte, dann möchte das Modell sie auch.

6. Was er nicht tun wird und was er nicht tun kann

Bitte den Agenten, etwas zu zerstören:
Führ das zweimal aus, und du bekommst möglicherweise zwei unterschiedliche Verhaltensweisen. Einmal hat er abgelehnt, bevor er ein Tool angerührt hat:
Ein anderes Mal hat er zuerst nachgesehen, einen SELECT für spanische Kunden ausgeführt, keine gefunden, weil die Spalte ES statt Spain speichert, und stattdessen das gemeldet:
Beides ist vernünftig. Keines ist eine Sicherheitskontrolle. Das Modell hat das Wort „read-only” in einer Tool-Beschreibung gelesen und entschieden, es zu respektieren, und ein anderes Modell, ein längeres Gespräch oder ein hartnäckigerer Benutzer kann eine andere Entscheidung erzeugen. Der Guard in run_query ist der Teil, der nicht von einer Entscheidung abhängt:
Schreib die Beschreibung so, dass das Modell es selten versucht. Schreib den Guard so, dass es keine Rolle spielt, wenn es das tut.
Diese zweite Zeile ist der Grund, warum run_query neben sqlite3.Error auch sqlite3.Warning abfängt. Pythons Treiber lehnt gestapelte Anweisungen ab, wirft für sie aber Warning, und Warning ist keine Unterklasse von Error. Nur sqlite3.Error abzufangen lässt eine gestapelte Anweisung dem Handler entkommen und tötet die Schleife, statt eine Nachricht zurückzugeben, die das Modell lesen kann.
Eine Präfix-Prüfung stoppt Schreibvorgänge, sagt aber nichts über Lesevorgänge aus. Jeder SELECT, den das Modell schreibt, kann jede Tabelle in der Datei erreichen, auch solche, die du nie freigeben wolltest. Zwei Änderungen lohnen sich, bevor das echte Daten anfasst: Öffne die Datenbank schreibgeschützt mit sqlite3.connect("file:shop.db?mode=ro", uri=True), was Schreibvorgänge mit attempt to write a readonly database fehlschlagen lässt, egal was der String-Check übersieht, und richte den Agenten auf eine Datenbank oder eine Menge von Views, die nur die Spalten enthalten, die er sehen darf.

Steuern, wann Tools benutzt werden

tool_choice entscheidet, wie viel Mitsprache das Modell hat: "required" ist plumper, als es aussieht. Frag diesen Agenten What is 2 + 2? mit tool_choice auf "required", und er ruft list_tables auf, sieht sich eine Datenbank an, für die er keine Verwendung hat, und antwortet dann in der nächsten Runde 4. Mit "auto" antwortet er sofort mit 4 und ruft nichts auf. Greif zu "required", wenn ein Tool wirklich ausgeführt werden muss, etwa um eine Anfrage zu protokollieren, und lass es ansonsten sein.

Den Agenten justieren

Nächste Schritte

Die Schleife, die du jetzt hast, ist dieselbe, die hinter den meisten Agenten steckt. Nur die Tools ändern sich.

Function Calling

Referenz für das tools-Array und tool_choice.

Strukturierte Antworten

Beschränke die finale Antwort auf ein JSON-Schema.

Prompt Caching

Halte das wachsende Gespräch günstig.

Private Research Agent

Dieselbe Schleife mit Web-Tools und einem Planer.