Verschachtelte Tool-Aufrufe
Ein Tool ruft ein anderes Tool auf — wann sinnvoll und wie sicher.
Ein Handler darf über den Kontext $ctx andere Tools aufrufen. Das ist wie ein normaler Funktionsaufruf: Auth, Mapping und Logging laufen automatisch mit.
Wann lohnt sich das?
- Ein Tool braucht Daten, die schon ein anderes Tool liefert (Code-Dedup)
- Du willst mehrere kleine Tools zu einem „Workflow-Tool" bündeln, das das Modell mit einem einzigen Call aufrufen kann
- Für komplexe Berechnungen, die zu umständlich wären, das Modell selbst orchestrieren zu lassen
Beispiel
<?php
return [
'tools' => [
'invoiceWithCustomer' => [
'description' => 'Rechnung und zugehörigen Kunden in einem Rutsch holen.',
'parameters' => [
'type' => 'object',
'properties' => ['invoiceId' => ['type' => 'string']],
'required' => ['invoiceId'],
],
'handler' => function (array $results, array $args, array $ctx): array {
$invoice = $ctx->runTool('getInvoice', ['id' => $args['invoiceId']]);
$customer = $ctx->runTool('getCustomer', ['id' => $invoice['customer_id']]);
return [
'invoice' => $invoice,
'customer' => $customer,
];
},
],
],
];
Das Modell ruft invoiceWithCustomer mit einer ID auf — und bekommt beide Objekte zurück. Ohne Nested Calls müsste es selbst zwei Tool-Aufrufe hintereinander orchestrieren, was langsamer und fehleranfälliger ist.
Empfehlung zur Verschachtelungs-Tiefe
Es gibt aktuell kein hartes Limit — aber halte dich praktisch an 5–10 Ebenen. Tiefere Ketten sind schwer zu debuggen, langsamer, und bei einem Logikfehler in deinem Handler entsteht schnell eine Endlos-Rekursion. Wenn dein Handler runTool in einer Schleife aufruft, baue eine eigene Abbruchbedingung ein.
Jeder innere Aufruf zählt als eigener Tool-Run im Log — du siehst später genau, was passiert ist.
Worauf achten?
Fehlertoleranz
Ein innerer Tool-Aufruf kann fehlschlagen. Fang das ab, wenn du eine teil-erfolgreiche Antwort liefern willst:
'handler' => function (array $results, array $args, array $ctx): array {
$invoice = $ctx->runTool('getInvoice', ['id' => $args['invoiceId']]);
try {
$customer = $ctx->runTool('getCustomer', ['id' => $invoice['customer_id']]);
} catch (\Throwable $e) {
$customer = ['error' => 'Kunde nicht gefunden: ' . $e->getMessage()];
}
return compact('invoice', 'customer');
},
Performance
Jeder verschachtelte API-Aufruf kostet Latenz. 2–3 aneinandergereihte Calls sind okay. Wenn du 10+ Dinge parallel brauchst, überlege:
- Gibt es einen API-Endpunkt, der das in einem Request macht?
- Kannst du parallelisieren (
Promise::all-Stil)? — aktuell nicht eingebaut, aber machbar via eigenem HTTP-Client.
Allowlist gilt weiter
Auch verschachtelte Calls respektieren setAllowedTools(). Das bedeutet: ein Task, der Tool A erlaubt aber B nicht, kann A nicht benutzen, um B indirekt aufzurufen. Bewusst so — verhindert, dass der Modell-„Umgehungs-Trick" wird.
Test-Modus
Wenn der äußere Runner mit testMode=true läuft, gilt das für alle inneren Aufrufe. Schreibende Requests werden durchgängig gestubbt.