Ablauf eines Tool-Aufrufs
Was zwischen "Modell wählt Tool" und "Ergebnis geht zurück" im Detail passiert.
Wenn das LLM entscheidet, ein Tool aufzurufen, übernimmt der ToolRunner. Er ist der Leim zwischen deklarativer Tool-Config und tatsächlichem Request.
Die Pipeline auf einen Blick
args ──▶ preprocess ──▶ API-Call ──▶ mapping ──▶ postprocess ──▶ handler ──▶ result
Jeder Schritt ist optional (außer mindestens einem von API-Call und handler). Du kannst also ein Tool bauen, das nur aus handler besteht, oder eines, das nur API-Call + Mapping ist.
Schritt für Schritt
1. Args entgegennehmen
Das Modell liefert ein JSON-Objekt, das gegen das parameters-Schema validiert wird. Fehlt ein required Feld, erhält das Modell eine Fehlermeldung zurück und versucht es erneut.
2. Placeholder auflösen
Vor dem API-Call ersetzt der Runner bestimmte Platzhalter in den Argumenten:
| Platzhalter | Ersetzt durch |
|---|---|
<PDF:foo.pdf> |
Volltext-Inhalt der hochgeladenen Datei |
<PDF_CONTENT_AVAILABLE> |
true/false — gibt es überhaupt PDFs im Chat? |
{{user_message_count}} |
Anzahl Nutzer-Nachrichten in der Session |
Siehe Platzhalter-Referenz.
3. preprocess
Eine optionale PHP-Funktion, die die Args umschreiben kann, bevor sie in den Request wandern. Signatur wie handler und postprocess: bekommt $args und den vollen $ctx (siehe unten).
'preprocess' => function (array $args, array $ctx): array {
// $ctx enthält u.a.: tenant, chat_id, user_id, session, http, logger,
// tool (Closure für Sub-Tool-Calls), api_config
$ctx['logger']->info('preprocess läuft', ['input' => $args]);
$args['date'] = (new \DateTime($args['date']))->format('Y-m-d');
return $args;
},
Nützlich für Formatkonvertierungen, Default-Werte oder um zusätzliche Header aus der Session zu ziehen.
Special-Key _query: wenn preprocess ein _query Array zurückgibt, wird der Runner es als URL-Query-String an den Endpoint anhängen. Siehe api-features.md.
4. API-Call
Wenn api gesetzt ist, baut der Runner den Request aus:
apis.php→ Basis-URL, Auth-Header, Timeoutsmethod→GET/POST/PUT/PATCH/DELETEpath→ Pfad, Platzhalter in{curly}werden aus den Args gefülltbody/query→ Mapping der restlichen Args auf Payload oder Query-String
Test-Modus: Mit tm test-tool ... --dry-run überspringt der Runner alle schreibenden Requests (POST, PUT, PATCH, DELETE) und liefert einen Stub zurück. So kannst du Tools gefahrlos im Trockenlauf ausführen.
5. Mapping / Normalize
Das Response-JSON wird gegen mapping durchgeschoben:
'mapping' => [
'id' => 'invoice_id',
'total' => 'amounts.gross',
],
Links steht der Schlüssel, den das Modell sieht. Rechts der Pfad (dot-notation) im API-Response. Felder die im Mapping nicht auftauchen, werden weggelassen — das reduziert Token-Verbrauch und versteckt interne Felder.
6. postprocess
Nach dem Mapping läuft optional eine weitere PHP-Funktion auf dem Ergebnis.
'postprocess' => function (array $result, array $args, array $ctx): array {
$ctx['logger']->debug('postprocess: rechne totalWithTax');
$result['totalWithTax'] = round($result['total'] * 1.19, 2);
return $result;
},
Unterschied zu preprocess: läuft nach dem API-Call und bekommt das Response-Objekt. Unterschied zu handler: darf keine eigenen API-Calls machen (dafür ist handler da, siehe unten).
7. handler (nur bei Handler-only oder hybrid)
Ein Handler ist die PHP-Funktion, die das Tool ausführt, wenn es keine API gibt — oder wenn du zusätzlich zur API weitere Calls (verschachtelte Tool-Aufrufe) brauchst.
Signatur: function (array $results, array $args, array $ctx): array
$results— Aggregat aller vorherigen Schritte (API-Response nach Mapping/postprocess, sonst leeres Array).$args— die Tool-Argumente (nach preprocess).$ctx— Runtime-Kontext (Array, KEIN Objekt).
'handler' => function (array $results, array $args, array $ctx): array {
$ctx['logger']->info('handler startet', ['tool' => 'compositeExample']);
$invoice = $ctx['tool']('getInvoice', ['id' => $args['id']]);
$customer = $ctx['tool']('getCustomer', ['id' => $invoice['customer_id']]);
$ctx['logger']->success('handler fertig', ['count' => 2]);
return ['invoice' => $invoice, 'customer' => $customer];
},
$ctx-Keys (verbindliche Referenz):
| Key | Typ | Wofür |
|---|---|---|
tenant |
string | Tenant-Code (bloomify, evolvet, …) |
chat_id |
string | Aktueller Chat |
user_id |
int|string | Angemeldeter User (0 bei public/anon) |
session |
\Cake\Http\Session |
Cake-Session |
http |
\Cake\Http\Client |
Vorkonfigurierter HTTP-Client (SSL-relaxed) |
logger |
\App\Tooling\TaskLogger |
Pflicht nutzen! ->info()/->debug()/->warning()/->error()/->success(). Zeilen landen in task_executions.log_output und im Terminal-Debug-Block der Kommandozentrale |
log_path |
string | Tenant-Log-Verzeichnis (falls du selbst File-Logs schreiben willst — meistens nicht nötig) |
tool |
\Closure |
Sub-Tool-Call: $ctx['tool']('otherTool', ['arg' => 1]). Result geht in den turn_results-Pool. Nicht $ctx->runTool() — das ist die alte falsche Doku |
api_config |
array | Bei API-Tools: der gemergte Endpoint-Config (base_url, auth, headers, …) |
$ctx ist ein Array, kein Objekt — Zugriff mit $ctx['logger'], nicht $ctx->logger.
Logger-Pflicht: Jeder Handler MUSS mindestens einmal $ctx['logger']->info() am Start und einmal am Ende (bzw. ->error() im Fail-Pfad) rufen. Ohne Logger-Aufrufe sieht der User im Terminal-Debug nur "Keine Debug-Zeilen" und muss ins Log-File wechseln — schlechte DX.
Limit: Maximal 10 verschachtelte Aufrufe pro Tool-Kette. Schützt vor Endlosschleifen.
8. Ergebnis zurück ans Modell
Das Endergebnis geht zurück in die Chat-Pipeline und wird dem LLM als tool_result serviert. Im ChatMessages-Log siehst du beides: Args, die reingingen, und das Ergebnis, das rauskam.
Logging und Debugging
Jeder Tool-Aufruf wird mit einem Eintrag in task_log_entries versehen:
tool_name,args,result,duration_ms- Bei API-Calls zusätzlich: HTTP-Status, Request-URL, Response-Bytes
entry_type→chatoderscheduled_task— so filterst du im Log nach Quelle
Zum Anschauen: /manage/tasks → Assistant öffnen → Reiter "Ausführungen".
Häufige Fehlerbilder
| Symptom | Ursache |
|---|---|
| Tool wird nie aufgerufen | Fehlt in der tools-Liste des Chats/Assistants |
args-Validation-Error |
Modell hat ein Pflichtfeld vergessen — Beschreibung schärfen |
| 401 beim API-Call | OAuth abgelaufen — Verbindung unter /manage/o-auth-connections erneuern |
| Leeres Ergebnis obwohl API 200 liefert | Mapping-Pfad passt nicht zum Response, mapping überprüfen |