Laravel: Odgovori podrške koje je izradila umjetna inteligencija uz tijek odobravanja od strane čovjeka
Ulazna pošta za kontakte postaje mnogo korisnija kada može predložiti promišljen odgovor, ali automatsko slanje generiranog teksta pogrešan je zadani izbor. Imena mogu biti pogrešno napisana, obećanja mogu premašiti pravila, a naizgled jednostavno pitanje može sadržavati kontekst koji model ne može vidjeti. Sigurniji obrazac je tijek rada s nacrtima: AI radi početno pisanje, dok autentificirana osoba pregledava, uređuje i odobrava svaki odgovor.
Ovaj vodič izrađuje taj tijek rada u Laravelu na PHP-u 8.3 ili novijem. Posao u redu čekanja poziva Smart Routing AI Model putem Laravelova ugrađenog HTTP klijenta, preslikava odgovor u eksplicitna stanja domene i pohranjuje samo nacrt. Odobrenje ostaje zasebna ljudska radnja.
Pribavite pristup prije pisanja integracijskog koda
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Smart Routing AI Model.
- Odaberite dostupni Free, Plus ili Pro paket i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte ondje prikazan token ograničen na uslugu.
Ova usluga zahtijeva token. Njegovim ponovnim generiranjem opoziva se prethodno aktivni token, stoga uskladite rotaciju s implementacijom: ažurirajte tajnu aplikacije i odmah ponovno pokrenite radnike. Nikada nemojte predati token u repozitorij ni ga stavljati u zapisnike, fiksture, snimke zaslona ili JavaScript na strani klijenta.
Točna API operacija je POST https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions, autentificirana s Authorization: Bearer {serviceToken}. Prihvaća chat zahtjev kompatibilan s OpenAI-jem i vraća standardan odgovor u stilu OpenAI-ja. Usluga provodi usmjeravanje modela prema paketu i praćenje kvota.
Pokrenite jedan minimalni zahtjev prije uključivanja Laravela. Zamijenite oba rezervirana mjesta. Budući da ugovor ne propisuje doslovni identifikator modela, identifikator modela za usmjeravanje pribavite iz dokumentacije za aktivirani paket umjesto da ga pogađate.
curl --request POST \
--url https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions \
--header "Authorization: Bearer YOUR_SERVICE_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"model": "YOUR_PLAN_MODEL",
"messages": [
{
"role": "user",
"content": "Draft a brief reply confirming that we received the enquiry."
}
]
}'
Uspješan standardni odgovor sadrži generirani tekst na choices[0].message.content. Produkcijski kod i dalje mora tu putanju tretirati kao nepouzdanu: uspješan HTTP status ne jamči potpuno ni ispravno oblikovano tijelo.
Pohranite vjerodajnicu i odabrani model za usmjeravanje u okruženje implementacije. U lokalnom razvoju upotrijebite Laravelovu nepredanu datoteku .env:
MIHAJLO_AI_TOKEN=YOUR_SERVICE_TOKEN
MIHAJLO_AI_MODEL=YOUR_PLAN_MODEL
QUEUE_CONNECTION=database
Izložite te vrijednosti putem config/services.php. Čitanje env() samo iz konfiguracijskih datoteka održava aplikaciju kompatibilnom s config:cache.
'mihajlo_ai' => [
'token' => env('MIHAJLO_AI_TOKEN'),
'model' => env('MIHAJLO_AI_MODEL'),
'endpoint' => 'https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions',
],
Arhitektura: asinkrono sastavljanje nacrta, sinkrono odobravanje
Preglednik ne bi trebao čekati na poziv vanjskog modela. Stvaranje nacrta zato šalje posao u red čekanja i odmah vraća prihvaćeni odgovor. Posao poziva API, provjerava odgovor na granici sustava i pomiče poruku u stanje draft_ready ili draft_failed. Odobrenje je zaseban autentificirani zahtjev koji dostavlja konačni uređeni tekst recenzenta.
Relevantna struktura projekta namjerno je mala:
app/
Data/AiDraftResult.php
Http/Controllers/InboxDraftController.php
Jobs/GenerateReplyDraft.php
Models/ContactMessage.php
Services/SmartRoutingClient.php
config/services.php
database/migrations/..._create_contact_messages_table.php
routes/web.php
tests/Feature/InboxDraftControllerTest.php
tests/Unit/SmartRoutingClientTest.php
Red čekanja dodaje operativnu odgovornost, ali sprječava spore uzvodne odgovore da zauzimaju web radnike i operaterima daje jasno mjesto za pregled neuspjeha. HTTP klijent upravlja kratkim, ograničenim ponovnim pokušajima prijenosa; sam Laravelov posao ne pokreće opetovano dvosmislen zahtjev.
Stvorite model stanja ulazne pošte
Stvorite model, migraciju, posao i kontroler Laravelovim generatorima. Osigurajte da su prisutne i tablice reda čekanja pomoću generatora tablice reda čekanja koji pruža vaša verzija Laravela, a zatim pokrenite migracije.
php artisan make:model ContactMessage -m
php artisan make:job GenerateReplyDraft
php artisan make:controller InboxDraftController
php artisan migrate
Migracija poruka za kontakt bilježi i strojni nacrt i verziju koju je odobrila osoba. Njihovo odvajanje čuva povijest pregleda i sprječava da se generirani nacrt predstavlja kao odobren sadržaj.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('contact_messages', function (Blueprint $table): void {
$table->id();
$table->string('email');
$table->text('body');
$table->string('status')->default('pending');
$table->text('draft_reply')->nullable();
$table->text('final_reply')->nullable();
$table->string('ai_failure_code')->nullable();
$table->timestamp('approved_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('contact_messages');
}
};
Učinite ta polja dodjeljivima u ContactMessage i pretvorite approved_at u datetime. U većoj ulaznoj pošti zamijenite slobodne nizove statusa podržanim PHP enumom i dodajte stupce vlasništva organizacije.
Izgradite obrambenu API granicu
Mali objekt rezultata sprječava kontrolere i poslove da moraju razumjeti uzvodni JSON. Predstavlja ili upotrebljiv sadržaj ili strukturirani kod neuspjeha.
<?php
namespace App\Data;
final readonly class AiDraftResult
{
private function __construct(
public bool $succeeded,
public ?string $content,
public ?string $failureCode,
) {}
public static function success(string $content): self
{
return new self(true, $content, null);
}
public static function failure(string $code): self
{
return new self(false, null, $code);
}
}
Klijent upotrebljava ograničena vremenska ograničenja za povezivanje i ukupno trajanje. Ponovno pokušava samo kod neuspjeha povezivanja, HTTP 429 odgovora i pogrešaka poslužitelja, s najviše tri pokušaja. Neuspjesi autentifikacije i validacije deterministički su i nikada se ne ponavljaju naslijepo.
<?php
namespace App\Services;
use App\Data\AiDraftResult;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
final class SmartRoutingClient
{
public function draft(string $customerMessage): AiDraftResult
{
$token = config('services.mihajlo_ai.token');
$model = config('services.mihajlo_ai.model');
$endpoint = config('services.mihajlo_ai.endpoint');
if (!is_string($token) || $token === '' ||
!is_string($model) || $model === '') {
return AiDraftResult::failure('configuration_error');
}
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = Http::withToken($token)
->acceptJson()
->connectTimeout(3)
->timeout(20)
->post($endpoint, [
'model' => $model,
'messages' => [
[
'role' => 'system',
'content' => 'Draft a concise, courteous support reply. Do not promise refunds, deadlines, or actions not stated by the business. Return only the proposed reply.',
],
[
'role' => 'user',
'content' => $customerMessage,
],
],
]);
} catch (ConnectionException) {
if ($attempt === 3) {
return AiDraftResult::failure('transport_error');
}
usleep(200000 * $attempt);
continue;
}
if ($response->successful()) {
$content = $response->json('choices.0.message.content');
if (!is_string($content) || trim($content) === '') {
return AiDraftResult::failure('invalid_response');
}
return AiDraftResult::success(trim($content));
}
if (in_array($response->status(), [401, 403], true)) {
return AiDraftResult::failure('authentication_error');
}
if (in_array($response->status(), [400, 422], true)) {
return AiDraftResult::failure('request_rejected');
}
$retryable = $response->status() === 429 ||
$response->serverError();
if (!$retryable) {
return AiDraftResult::failure('upstream_error');
}
if ($attempt < 3) {
sleep($attempt);
continue;
}
return AiDraftResult::failure(
$response->status() === 429
? 'quota_or_rate_limited'
: 'provider_unavailable'
);
}
return AiDraftResult::failure('upstream_error');
}
}
Uputa namjerno ograničava ovlasti, ali upute nisu sigurnosne kontrole. Ljudski pregled jest kontrola. Klijent također izbjegava slanje e-adrese kontakta; samo tijelo poruke prelazi API granicu.
Generirajte nacrte u poslu reda čekanja
Posao provjerava trenutačni status prije pozivanja usluge, čime zastarjeli duplicirani poslovi postaju bezopasni. Bilježi identifikatore i kategorije neuspjeha, nikada tijela poruka, generirani tekst ni vjerodajnice.
<?php
namespace App\Jobs;
use App\Models\ContactMessage;
use App\Services\SmartRoutingClient;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
final class GenerateReplyDraft implements ShouldQueue
{
use Queueable;
public int $tries = 1;
public function __construct(public readonly int $messageId) {}
public function handle(SmartRoutingClient $client): void
{
$message = ContactMessage::find($this->messageId);
if (!$message || $message->status !== 'drafting') {
return;
}
$result = $client->draft($message->body);
if (!$result->succeeded) {
$message->update([
'status' => 'draft_failed',
'ai_failure_code' => $result->failureCode,
]);
Log::warning('ai_draft_failed', [
'message_id' => $message->id,
'failure_code' => $result->failureCode,
]);
return;
}
$message->update([
'status' => 'draft_ready',
'draft_reply' => $result->content,
'ai_failure_code' => null,
]);
Log::info('ai_draft_ready', [
'message_id' => $message->id,
]);
}
}
Zadržite odobrenje izričito ljudskim
Kontroler upotrebljava transakciju i zaključavanje retka kako bi spriječio utrkivanje dvaju zahtjeva za nacrt. Za generiranje prihvaća samo poruke na čekanju ili neuspjele poruke. Radnja odobravanja zahtijeva novi tekst koji dostavlja recenzent umjesto da neprimjetno kopira nacrt.
<?php
namespace App\Http\Controllers;
use App\Jobs\GenerateReplyDraft;
use App\Models\ContactMessage;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
final class InboxDraftController extends Controller
{
public function generate(ContactMessage $message): RedirectResponse
{
DB::transaction(function () use ($message): void {
$locked = ContactMessage::query()
->lockForUpdate()
->findOrFail($message->id);
abort_unless(
in_array($locked->status, ['pending', 'draft_failed'], true),
409
);
$locked->update([
'status' => 'drafting',
'ai_failure_code' => null,
]);
GenerateReplyDraft::dispatch($locked->id)->afterCommit();
});
return back()->with('status', 'Draft generation started.');
}
public function approve(
Request $request,
ContactMessage $message
): RedirectResponse {
abort_unless($message->status === 'draft_ready', 409);
$validated = $request->validate([
'reply' => ['required', 'string', 'max:10000'],
]);
$message->update([
'final_reply' => $validated['reply'],
'status' => 'approved',
'approved_at' => now(),
]);
return back()->with('status', 'Reply approved.');
}
}
Zaštitite obje rute Laravelovom autentifikacijom. U aplikaciji s više zakupaca dodajte pravilo ili vezanje rute s opsegom kako bi korisnici mogli pristupiti samo porukama svoje organizacije.
use App\Http\Controllers\InboxDraftController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function (): void {
Route::post('/inbox/messages/{message}/drafts',
[InboxDraftController::class, 'generate']);
Route::patch('/inbox/messages/{message}/approval',
[InboxDraftController::class, 'approve']);
});
Odobreni zapis i dalje se ne šalje automatski. Povežite isporuku sa zasebno ovlaštenom radnjom slanja pošte ako je ulaznoj pošti potrebna. To odvajanje čini „odobravanje” provjerljivim i sprječava da se vodič za sastavljanje nacrta pretvori u slučajnog autonomnog pošiljatelja.
Testirajte uspjeh, ponovne pokušaje i ljudsku kontrolu
Laravelov Http::fake() pruža deterministički prijenos. Spriječite neželjene zahtjeve kako tipfeler ne bi pozvao stvarnu uslugu tijekom paketa testova.
<?php
namespace Tests\Unit;
use App\Services\SmartRoutingClient;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
final class SmartRoutingClientTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config()->set('services.mihajlo_ai', [
'token' => 'test-token',
'model' => 'test-routing-model',
'endpoint' => 'https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions',
]);
Http::preventStrayRequests();
}
public function test_it_maps_a_valid_draft(): void
{
Http::fake([
'*' => Http::response([
'choices' => [[
'message' => ['content' => 'Thanks for contacting us.'],
]],
], 200),
]);
$result = app(SmartRoutingClient::class)->draft('Are you open?');
$this->assertTrue($result->succeeded);
$this->assertSame('Thanks for contacting us.', $result->content);
}
public function test_it_does_not_retry_authentication_failure(): void
{
Http::fake(['*' => Http::response([], 401)]);
$result = app(SmartRoutingClient::class)->draft('Hello');
$this->assertFalse($result->succeeded);
$this->assertSame('authentication_error', $result->failureCode);
Http::assertSentCount(1);
}
}
Funkcionalni testovi trebali bi dodatno lažirati red čekanja, potvrditi da generiranje pomiče poruku u drafting i potvrditi da je poslan jedan posao. Za odobravanje stvorite poruku draft_ready, autentificirajte korisnika, pošaljite uređeni tekst i provjerite final_reply, approved_at i approved. Također testirajte da se nacrti na čekanju i neuspjeli nacrti ne mogu odobriti.
Sigurnost, vidljivost i implementacija
- Tajne: umetnite token putem spremišta tajni hosting platforme. Nakon rotacije ponovno izgradite predmemorije konfiguracije i ponovno pokrenite dugotrajne radnike.
- Minimizacija podataka: šaljite samo tekst potreban za sastavljanje nacrta. Definirajte pravila zadržavanja i otkrivanja informacija primjerena obvezama privatnosti obrasca za kontakt.
- Umetanje uputa: tretirajte tekst korisnika kao neprijateljski unos. Generirani sadržaj nema pristup alatima ni ovlasti za slanje pošte ili promjenu zapisa.
- Obrada izlaza: izbjegnite nacrt i završni tekst u Bladeu s
{{ }}. Nemojte prikazivati generirani tekst putem neizbjegnutih HTML direktiva. - Metrike: brojite uspjehe i kodove neuspjeha, bilježite latenciju oko poziva klijenta i upozorite na trajne neuspjehe autentifikacije, kvote, prijenosa ili neispravno oblikovanih odgovora.
- Zdravlje radnika: pokrenite nadzirani radnik reda čekanja, dodijelite mu vremensko ograničenje dulje od ograničenog prozora zahtjeva klijenta i ponovno ga pokrenite tijekom implementacija.
Implementirajte kod aplikacije, navedite MIHAJLO_AI_TOKEN i MIHAJLO_AI_MODEL, pokrenite php artisan migrate --force, ponovno izgradite konfiguraciju s php artisan config:cache i ponovno pokrenite radnike reda čekanja s php artisan queue:restart. Potvrdite da je veza reda čekanja u produkciji asinkrona i da nadzornik procesa održava radnike aktivnima.
Uobičajeni obrasci neuspjeha
authentication_errorobično znači da token nedostaje, neispravno je oblikovan, opozvan je ili je zastario u predmemoriranoj konfiguraciji ili procesu radnika.quota_or_rate_limitedzahtijeva provjeru upotrebe paketa i volumena zahtjeva. Više ponovnih pokušaja može pogoršati zasićenje.request_rejectedukazuje na to da poslani JSON ili konfigurirani identifikator modela ne odgovara dokumentiranom ugovoru.invalid_responseznači da je HTTP uspio, ali očekivana putanja sadržaja nije postojala ili je bila prazna. Sačuvajte kategoriju neuspjeha i pregledajte pročišćene uzvodne metapodatke.- Poruka zaglavljena u
draftingobično upućuje na nedostupnog radnika ili prekinuti posao. Dodajte operativnu naredbu za usklađivanje ako su prekinuti poslovi česti.
Završni kontrolni popis za provjeru
- Aktivni token postoji samo u tajnoj konfiguraciji podržanoj okruženjem.
- Minimalni API zahtjev uspijeva s dokumentiranim identifikatorom modela aktiviranog paketa.
- Autentificirani zahtjev za nacrt vraća se brzo i stavlja točno jedan posao u red čekanja.
- Uspješan izlaz pohranjuje se samo u
draft_replysa statusomdraft_ready. - Neuspjesi autentifikacije, validacije, kvote, prijenosa, poslužitelja i neispravno oblikovanog odgovora ostaju razlikovni.
- Zapisnici sadrže ID-jeve poruka i kodove neuspjeha, ali ne i tekst korisnika, tekst odgovora ili token.
- Osoba može urediti nacrt i samo taj poslani tekst postaje
final_reply. - Nijedna ruta u ovom tijeku rada ne šalje odgovor automatski.
Najvažniji izbor dizajna nije uputa, pa čak ni usmjerivač modela. To je granica između prijedloga i ovlasti. Neka model ukloni teret prazne stranice, neka red čekanja apsorbira nepouzdano mrežno vrijeme, a neka osoba ima posljednju riječ. To skromno odvajanje pretvara impresivnu demonstraciju u funkciju podrške kojom malo poduzeće može odgovorno upravljati.