Laravel: Одговори за поддршка составени со ВИ со тек на одобрување од човек
Сандачето за дојдовна пошта за контакти станува многу покорисно кога може да предложи промислен одговор, но автоматското испраќање генериран текст е погрешна стандардна поставка. Имињата може да бидат погрешно напишани, ветувањата може да ја надминат политиката, а навидум едноставно прашање може да носи контекст што моделот не може да го види. Побезбедниот образец е процес со нацрти: ВИ го прави првичното пишување, додека автентицирано лице го прегледува, уредува и одобрува секој одговор.
Овој туторијал го гради тој процес во Laravel на PHP 8.3 или понова верзија. Задача во редица го повикува Smart Routing AI Model преку вградениот HTTP клиент на Laravel, го мапира одговорот во експлицитни доменски состојби и складира само нацрт. Одобрувањето останува посебна човечка акција.
Обезбедете пристап пред да пишувате интеграциски код
- Регистрирајте се на https://ai.mihajlo.mk/register, или користете https://ai.mihajlo.mk/login ако веќе имате сметка.
- Отворете ја страницата на услугата Smart Routing AI Model.
- Изберете достапен Free, Plus или Pro план и завршете ја неговата активација.
- Отворете ја официјалната документација за услугата.
- Најдете го панелот Service token и копирајте го токенот ограничен на услугата што е прикажан таму.
Оваа услуга бара токен. Неговото регенерирање го поништува претходно активниот токен, затоа координирајте ја ротацијата со распоредувањето: ажурирајте ја тајната на апликацијата и навремено рестартирајте ги worker-процесите. Никогаш не го внесувајте токенот во commit, ниту не го ставајте во логови, fixtures, слики од екранот или JavaScript на клиентската страна.
Точната API операција е POST https://ai.mihajlo.mk/api/smart-routing-ai-model/v1/chat/completions, автентицирана со Authorization: Bearer {serviceToken}. Таа прифаќа OpenAI-компатибилно chat барање и враќа стандарден одговор во OpenAI-стил. Услугата врши рутирање на модели според планот и следење на квотата.
Извршете едно минимално барање пред да го вклучите Laravel. Заменете ги двата placeholders. Бидејќи договорот не пропишува буквален идентификатор на модел, земете го идентификаторот на routing моделот од документацијата за вашиот активиран план наместо да погодувате.
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."
}
]
}'
Успешен стандарден одговор содржи генериран текст во choices[0].message.content. Продукцискиот код сè уште мора да ја третира таа патека како недоверлива: успешен HTTP статус не гарантира целосно или правилно обликувано тело.
Складирајте ги акредитивот и избраниот routing модел во средината за распоредување. При локален развој, користете ја Laravel-датотеката .env што не е внесена во commit:
MIHAJLO_AI_TOKEN=YOUR_SERVICE_TOKEN
MIHAJLO_AI_MODEL=YOUR_PLAN_MODEL
QUEUE_CONNECTION=database
Изложете ги тие вредности преку config/services.php. Читањето на env() само од конфигурациски датотеки ја одржува апликацијата компатибилна со 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',
],
Архитектура: асинхроно изготвување нацрти, синхроно одобрување
Прелистувачот не треба да чека надворешен повик до модел. Затоа создавањето нацрт испраќа задача во редица и веднаш враќа прифатен одговор. Задачата го повикува API-то, го валидаира граничниот одговор и ја преместува пораката или во draft_ready или во draft_failed. Одобрувањето е посебно автентицирано барање што го доставува конечно уредениот текст на проверувачот.
Релевантната структура на проектот е намерно мала:
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
Редица додава оперативна одговорност, но спречува бавните upstream одговори да ги трошат web worker-процесите и им дава на операторите јасно место за проверка на неуспесите. HTTP клиентот управува со кратки, ограничени повторни транспортни обиди; самата Laravel задача не извршува постојано повторување на двосмислено барање.
Создадете го моделот на состојби за сандачето
Создадете ги моделот, миграцијата, задачата и контролерот со генераторите на Laravel. Осигурете се дека се присутни и табелите за database редицата со генераторот за queue-табели што го обезбедува вашата верзија на Laravel, а потоа извршете ги миграциите.
php artisan make:model ContactMessage -m
php artisan make:job GenerateReplyDraft
php artisan make:controller InboxDraftController
php artisan migrate
Миграцијата за контакт-пораки ги бележи и машинскиот нацрт и верзијата одобрена од човек. Нивното раздвојување ја зачувува историјата на прегледот и спречува генерираниот нацрт да се претставува како одобрена содржина.
<?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');
}
};
Направете ги тие полиња доделливи во ContactMessage и кастирајте го approved_at во datetime. Во поголемо сандаче, заменете ги слободно внесените status-низи со backed PHP enum и додадете колони за сопственост на организацијата.
Изградете дефанзивна API граница
Мал result-објект спречува контролерите и задачите да мора да го разбираат upstream JSON. Тој претставува или употреблива содржина или структуриран код за неуспех.
<?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);
}
}
Клиентот користи ограничени timeout-вредности за поврзување и вкупно време. Повторува само неуспеси при поврзување, HTTP 429 одговори и серверски грешки, со најмногу три обиди. Неуспесите при автентикација и валидација се детерминистички и никогаш не се повторуваат слепо.
<?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');
}
}
Промптот намерно го ограничува овластувањето, но промптовите не се безбедносни контроли. Човечкиот преглед е контролата. Клиентот исто така избегнува да ја испраќа е-поштата на контактот; само телото на пораката ја преминува API границата.
Генерирајте нацрти во задача од редицата
Задачата ја проверува тековната состојба пред да ја повика услугата, со што застарените дупликат-задачи стануваат безопасни. Таа евидентира идентификатори и категории на неуспех, но никогаш тела на пораки, генериран текст или акредитиви.
<?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,
]);
}
}
Одобрувањето нека биде изречно човечко
Контролерот користи трансакција и заклучување на ред за да спречи две барања за нацрт да се натпреваруваат. Тој прифаќа само pending или failed пораки за генерирање. Акцијата за одобрување бара свеж текст доставен од проверувачот, наместо тивко да го копира нацртот.
<?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.');
}
}
Заштитете ги двете рути со Laravel автентикација. Во мултитенантна апликација, додадете policy или scoped route binding за корисниците да имаат пристап само до пораките на нивната организација.
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']);
});
Одобрен запис сè уште не се испраќа автоматски. Поврзете ја испораката со посебно овластена mail-акција ако сандачето има потреба од тоа. Тоа раздвојување го прави „одобри“ проверливо и спречува туторијалот за изготвување нацрти да се претвори во случаен автономен испраќач.
Тестирајте успех, повторни обиди и човечка контрола
Http::fake() на Laravel обезбедува детерминистички транспорт. Спречете случајни барања, за печатна грешка да не може да ја повика услугата во живо за време на тест-пакетот.
<?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);
}
}
Feature-тестовите дополнително треба да ја лажираат редицата, да потврдат дека генерирањето преместува порака во drafting и да потврдат дека е испратена една задача. За одобрување, создадете порака draft_ready, автентицирајте корисник, доставете уреден текст и проверете ги final_reply, approved_at и approved. Исто така тестирајте дека pending и failed нацрти не може да се одобрат.
Безбедност, набљудливост и распоредување
- Тајни: внесете го токенот преку складиштето за тајни на хостинг-платформата. По ротација, повторно изградете ги конфигурациските кешови и рестартирајте ги долготрајните worker-процеси.
- Минимизирање на податоци: испраќајте само текст потребен за изготвување нацрт. Дефинирајте правила за задржување и откривање соодветни на обврските за приватност на контакт-формата.
- Prompt injection: третирајте го текстот на клиентот како непријателски влез. Генерираната содржина нема пристап до алатки и нема овластување да испраќа пошта или да менува записи.
- Обработка на излез: ескепирајте го нацртот и конечниот текст во Blade со
{{ }}. Не прикажувајте генериран текст преку неескепирани HTML директиви. - Метрики: броете успеси и кодови за неуспех, бележете латентност околу повикот до клиентот и алармирајте при трајни неуспеси со автентикација, квота, транспорт или неправилно обликуван одговор.
- Здравје на worker-процесите: извршувајте надгледуван worker за редицата, задајте му timeout подолг од ограничениот прозорец за барање на клиентот и рестартирајте го при распоредувања.
Распоредете го кодот на апликацијата, обезбедете MIHAJLO_AI_TOKEN и MIHAJLO_AI_MODEL, извршете php artisan migrate --force, повторно изградете ја конфигурацијата со php artisan config:cache и рестартирајте ги worker-процесите на редицата со php artisan queue:restart. Потврдете дека продукциската врска со редицата е асинхрона и дека процесен надгледувач ги одржува worker-процесите активни.
Вообичаени обрасци на неуспех
authentication_errorобично значи дека токенот недостига, е неправилно обликуван, поништен или застарен во кеширана конфигурација или worker-процес.quota_or_rate_limitedбара проверка на користењето на планот и обемот на барања. Повеќе повторни обиди може да ја влошат заситеноста.request_rejectedпокажува дека доставениот JSON или конфигурираниот идентификатор на модел не одговара на документираниот договор.invalid_responseзначи дека HTTP успеал, но очекуваната патека за содржина недостасувала или била празна. Зачувајте ја категоријата на неуспех и проверете ги санираните upstream метаподатоци.- Порака заглавена во
draftingобично укажува на недостапен worker или прекината задача. Додадете оперативна команда за усогласување ако прекинатите задачи се чести.
Контролна листа за финална проверка
- Активниот токен постои само во конфигурација на тајни поддржана од средината.
- Минималното API барање успева со документираниот идентификатор на модел од активираниот план.
- Автентицирано барање за нацрт се враќа брзо и става точно една задача во редица.
- Успешниот излез се складира само во
draft_replyсо статусdraft_ready. - Неуспесите при автентикација, валидација, квота, транспорт, сервер и неправилно обликуван одговор остануваат разликувачки.
- Логовите содржат ID на пораки и кодови за неуспех, но не и текст на клиентот, текст на одговор или токен.
- Лице може да го уреди нацртот и само тој доставен текст станува
final_reply. - Ниту една рута во овој процес не испраќа одговор автоматски.
Најважниот дизајнерски избор не е промптот, ниту дури router-от на моделот. Тоа е границата меѓу предлог и овластување. Нека моделот го отстрани товарот од празната страница, нека редицата го апсорбира ненадежното мрежно време и нека лице ги поседува конечните зборови. Тоа скромно раздвојување претвора импресивно демо во функција за поддршка што мал бизнис може одговорно да ја користи.