7.5 KiB
Laravel (laravel/ai) mit dem zentralen LocalAI-Server
Das Laravel AI SDK spricht den Server wie einen OpenAI-kompatiblen Anbieter an. Damit gehen Agenten mit Tools, Streaming, Embeddings, Transkription (Spracheingabe) und Bildgenerierung.
Der Server-Teil ist getestet (Messwerte unten). Der Laravel-Code hier folgt der SDK-Doku, ist aber noch nicht in einer echten App gelaufen.
Was der Server bietet
| Modell-ID (Aliase) | Endpoint | SDK-Funktion | Dauer (gemessen) |
|---|---|---|---|
qwen3.5-35b-a3b, glm-4.7-flash, qwen3.5-9b |
/v1/chat/completions |
Agent, Text, Tools, Streaming | ~850 t/s Prompt, ~47 t/s Antwort |
nomic-embed |
/v1/embeddings |
Embeddings (768 Dimensionen) |
|
whisper (whisper-1) |
/v1/audio/transcriptions |
Transcription |
~0,5 s für 7 s Sprache |
z-image (gpt-image-1, dall-e-3, dall-e-2) |
/v1/images/generations |
Image |
512²: ~11 s, 1024²: ~40 s, 1536×1024: ~65 s |
Bilder verdrängen das Chat-Modell. Beide passen nicht gleichzeitig in die 16 GB VRAM. Der erste Bild-Request lädt das Bildmodell (+ einige Sekunden), der nächste Chat-Request lädt das Chat-Modell zurück (~5 s). Whisper, Autocomplete und Embeddings bleiben immer geladen.
1. Netz und Zugang
- Der Rechner, auf dem Laravel läuft, muss in
ALLOWED_NETS(Server-.env) liegen: localhost, LAN192.168.178.0/24oder NetBird100.73.0.0/16. Ein öffentlicher Server braucht also einen NetBird-Peer. - Der API-Key ist
LLM_API_KEYaus der Server-.env(./scripts/start.sh --show-key).
2. Konfiguration
.env der Laravel-App:
LOCAL_AI_URL=http://100.73.82.219:8800/v1
LOCAL_AI_API_KEY=<KEY>
config/ai.php:
'providers' => [
// Text, Agent, Embeddings, Transkription
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'),
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'text' => ['default' => 'qwen3.5-35b-a3b'],
'embeddings' => ['default' => 'nomic-embed', 'dimensions' => 768],
'transcription' => ['default' => 'whisper'],
],
],
// Bilder: openai-compatible kann keine Bilder, der OpenAI-Treiber schon.
// Er schickt "gpt-image-1" & Co. – der Server kennt diese Namen als Aliase.
'local-images' => [
'driver' => 'openai',
'key' => env('LOCAL_AI_API_KEY'),
'url' => env('LOCAL_AI_URL'),
],
],
local in config/ai.php als Standard für Text, Embeddings und Transkription eintragen und local-images als Standard für Bilder. Die genauen Schlüssel für die Standard-Provider stehen in der veröffentlichten config/ai.php.
3. Agent
php artisan make:agent Assistant
$response = (new Assistant)->prompt('Fasse die offenen Rechnungen zusammen', timeout: 300);
// im Controller als Stream (SSE) an den Browser
return (new Assistant)->stream($text);
- Timeouts großzügig setzen. Das Laden eines Modells kann einige Sekunden bis Minuten dauern. Längere Agent-Läufe besser als Queue-Job.
- Tool-Calling klappt mit
qwen3.5-35b-a3bundglm-4.7-flashzuverlässig, mitqwen3.5-9bbei mehrstufigen Aufgaben schlechter. - Alle Anwendungen sollten dasselbe Chat-Modell nutzen (opencode, Continue, Laravel). Ein anderes Modell löst bei jedem Wechsel einen Modellwechsel aus.
4. Spracheingabe (Aufnahmetaste)
Ablauf: Browser nimmt auf → POST /voice/transcribe → Text anzeigen und bestätigen → POST /voice/ask → Agent streamt die Antwort.
use App\Ai\Agents\Assistant;
use Illuminate\Http\Request;
use Laravel\Ai\Transcription;
Route::post('/voice/transcribe', function (Request $request) {
$request->validate(['audio' => 'required|file|max:20480']);
return ['text' => trim((string) Transcription::fromUpload($request->file('audio'))->generate())];
});
Route::post('/voice/ask', function (Request $request) {
$request->validate(['text' => 'required|string|max:4000']);
return (new Assistant)->stream($request->string('text'));
});
let rec, chunks = [];
const mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/mp4';
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
rec = new MediaRecorder(stream, { mimeType: mime });
chunks = [];
rec.ondataavailable = e => chunks.push(e.data);
rec.onstop = async () => {
stream.getTracks().forEach(t => t.stop());
const fd = new FormData();
fd.append('audio', new Blob(chunks, { type: mime }), mime.includes('webm') ? 'voice.webm' : 'voice.m4a');
const res = await fetch('/voice/transcribe', {
method: 'POST', body: fd,
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
});
const { text } = await res.json();
showForConfirmation(text); // eigenes UI: Text anzeigen/bearbeiten, dann an /voice/ask senden
};
rec.start();
}
const stopRecording = () => rec?.stop();
// Taste: pointerdown → startRecording(), pointerup → stopRecording()
- HTTPS ist Pflicht. Browser geben das Mikrofon nur auf
https://oderlocalhostfrei. Das betrifft die Laravel-App, nicht die Verbindung von Laravel zum KI-Server. - Formate: Chrome/Firefox liefern WebM/Opus, Safari MP4/AAC. Der Server wandelt alles per ffmpeg selbst um, Laravel muss nichts konvertieren (getestet: WAV, WebM, M4A).
- Sprache: Der Server erkennt die Sprache automatisch.
- Bestätigung vor dem Absenden: Whisper verhört sich manchmal bei Namen und Fachbegriffen. Bei Agenten, deren Tools etwas ausführen, den Text deshalb erst anzeigen.
5. Bilder
use Laravel\Ai\Image;
$path = Image::of('Ein rotes Fahrrad vor einer gelben Hauswand')
->landscape()
->timeout(300)
->generate() // mit 'local-images' als Standard-Provider für Bilder
->store();
square()= 1024×1024 (~40 s),landscape()/portrait()= 1536×1024 (~65 s).quality()wird ignoriert.- Bilder am besten in einem Queue-Job erzeugen (
->queue()->then(...)), nicht im Web-Request. - Als Agent-Tool:
php artisan make:tool GenerateImage. Inhandle()das Bild erzeugen, speichern und die URL zurückgeben. Bedenken: Danach muss das Chat-Modell neu geladen werden (~5 s), bevor der Agent weiterantwortet. - Falls der OpenAI-Treiber in einer neueren SDK-Version Parameter schickt, die der Server ablehnt, geht es auch direkt:
$b64 = Http::withToken(config('ai.providers.local.key'))->timeout(300) ->post(config('ai.providers.local.url').'/images/generations', [ 'model' => 'z-image', 'prompt' => $prompt, 'size' => '1024x1024', ])->json('data.0.b64_json'); Storage::put($path, base64_decode($b64));
6. Fehlersuche
| Symptom | Ursache |
|---|---|
| 401 | API-Key falsch |
| 403 | Laravel-Server nicht in ALLOWED_NETS |
| Timeout beim ersten Request | Modell lädt noch → Timeout erhöhen |
| Bild dauert > 1 min | normal bei 1536×1024 und nach einem Modellwechsel |
| Mikrofon-Abfrage erscheint nicht | Seite läuft nicht über HTTPS |
| Leeres Transkript | Aufnahme zu kurz/stumm. Mindestlänge im Frontend prüfen |
Schnelltest vom Laravel-Server aus:
curl -H "Authorization: Bearer $KEY" http://<server>:8800/v1/audio/transcriptions -F model=whisper -F file=@test.webm
curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' http://<server>:8800/v1/images/generations \
-d '{"model":"z-image","prompt":"a red bicycle","size":"512x512"}' | head -c 200