Клієнт надсилає повідомлення: «На сайті написано, що мою посилку доставлено вчора, але її тут немає. Перевірте, будь ласка. Якщо вона втрачена, я хочу повернення коштів».
Це повідомлення пролежить до ранку. Клієнт засмучений, команда підтримки починає день із купи непрочитаних звернень, і ніхто не знає, які з них термінові.
Довгий час команди Laravel, які хотіли додати AI у продукт, мали два основні варіанти: створити окремий сервіс на Python або викликати API моделі через сирі HTTP-запити. Laravel 13 пропонує третій шлях. До нього входить Laravel AI SDK - офіційний пакет, який встановлюється через Composer і працює в тому самому стилі, що й решта фреймворку.
У цій статті ми розглянемо п'ять розумних функцій застосунку, які можна створити за його допомогою. Кожна з них пов'язана з однією й тією ж повсякденною проблемою в TokoKita.
Перед початком: швидке налаштування
Згідно з офіційною документацією, Laravel 13 потребує PHP 8.3 або новішої версії. Встановлення SDK складається з трьох команд:
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
Міграція створює таблиці, у яких SDK зберігає розмови агентів. Після цього додайте API-ключ вашого провайдера до .env, наприклад OPENAI_API_KEY або ANTHROPIC_API_KEY. SDK підтримує багато провайдерів через єдиний інтерфейс, тож перемикання між ними не означає переписування логіки застосунку.
Один термін, який ви зустрічатимете часто, - агент. У цьому SDK агент - це звичайний PHP-клас, що поєднує інструкції, контекст розмови, інструменти та формат виводу. Уявіть його як нового члена команди з чітко описаними обов'язками. Створити агента можна командою php artisan make:agent.
1. Автоматичне сортування звернень зі структурованим виводом
Проблема: усі скарги потрапляють в одну чергу. Клієнт, який вказав неправильну адресу, і клієнт, з якого двічі списали гроші, обробляються в порядку надходження.
Ідея: нехай AI прочитає повідомлення і поверне дані, а не абзац тексту. За допомогою структурованого виводу ми описуємо форму відповіді через схему, і наш код може використовувати результат напряму, без розбору вільного тексту.
Згенеруйте агента командою php artisan make:agent TicketTriage --structured, а потім заповніть його:
<?php
namespace App\Ai\Agents;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Attributes\UseCheapestModel;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Promptable;
#[UseCheapestModel]
class TicketTriage implements Agent, HasStructuredOutput
{
use Promptable;
public function instructions(): string
{
return 'You read customer messages for a marketplace and classify them for the support team.';
}
public function schema(JsonSchema $schema): array
{
return [
'category' => $schema->string()
->enum(['shipping', 'payment', 'product', 'account', 'other'])->required(),
'priority' => $schema->string()->enum(['low', 'medium', 'high'])->required(),
'sentiment' => $schema->string()->enum(['calm', 'upset', 'angry'])->required(),
'summary' => $schema->string()->required(),
];
}
}
Користуватися результатом так само просто, як читати масив:
$result = TicketTriage::make()->prompt($request->input('message'));
Ticket::create([
'category' => $result['category'],
'priority' => $result['priority'],
'summary' => $result['summary'],
]);
Атрибут #[UseCheapestModel] обирає найдешевшу текстову модель зі скоєфігурованого провайдера. Для класифікації рідко потрібна найпотужніша модель. Документація попереджає, що модель, яку обирає цей атрибут, може змінюватися між релізами SDK, тож якщо вам потрібна стабільна поведінка та передбачувана ціна, вкажіть модель явно через атрибут Model.
Чому це важливо для продукту: звернення з позначкою «подвійне списання, розлючений, високий пріоритет» може опинитися на початку черги ще до того, як його відкриє людина.
2. Асистент, що відповідає на основі реальних даних (інструменти)
Проблема: мовна модель не знає, який статус у замовлення вашого клієнта. Якщо залишити її саму, вона може просто вигадати відповідь.
Ідея: дайте агентові інструмент - функцію, яку він може викликати, коли потребує даних. Це як надати новому співробітникові доступ до екрана пошуку замовлень. Йому не доведеться вгадувати, він зможе просто подивитися.
Створіть інструмент командою php artisan make:tool LookupOrder:
<?php
namespace App\Ai\Tools;
use App\Models\Order;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class LookupOrder implements Tool
{
public function __construct(private User $user) {}
public function description(): Stringable|string
{
return 'Fetches the status of an order that belongs to the current customer, by order number.';
}
public function handle(Request $request): Stringable|string
{
$order = Order::where('user_id', $this->user->id)
->where('number', $request['order_number'])
->first();
if (! $order) {
return 'Order not found.';
}
return "Status: {$order->status}. Courier: {$order->courier}. "
. "Last update: {$order->updated_at}.";
}
public function schema(JsonSchema $schema): array
{
return [
'order_number' => $schema->string()->required(),
];
}
}
Зверніть увагу, що запит завжди фільтрується за user_id. Цей інструмент бачить лише ті замовлення, які належать клієнтові з поточної розмови. Така межа має жити в коді, а не в інструкціях, яким, сподіваємося, підкорятиметься модель.
Тепер підключимо інструмент до агента:
class SupportAssistant implements Agent, Conversational, HasTools
{
use Promptable, RemembersConversations;
public function __construct(public User $user) {}
public function instructions(): string
{
return 'You are the TokoKita support assistant. Be friendly and concise. '
. 'For order status, always use the tool. Never guess.';
}
public function tools(): iterable
{
return [new LookupOrder($this->user)];
}
}
Трейт RemembersConversations автоматично зберігає історію чату в базі даних, тож асистент зберігає контекст, коли клієнт уточнює: «а що з тим, що було вчора?»
$response = (new SupportAssistant($user))
->forUser($user)
->prompt('Where is order TK-1029 right now?');
return (string) $response;
3. Пошук, що розуміє намір (ембединги та пошук за схожістю)
Проблема: клієнт пише «товар прийшов зламаний, чи можна його повернути?», а ваша стаття довідки називається «Політика повернення товарів». Пошук за ключовими словами не зв'яже одне з одним.
Ідея: перетворіть текст на ембединги - списки чисел, що відображають зміст. Тексти зі схожим змістом опиняються поруч, навіть якщо слова різні.
Одна важлива примітка з документації: векторні запити наразі підтримуються лише на PostgreSQL з розширенням pgvector. Якщо ваш продукт працює на MySQL, враховуйте це.
Міграція:
Schema::ensureVectorExtensionExists();
Schema::create('help_articles', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->boolean('published')->default(false);
$table->vector('embedding', dimensions: 1536)->index();
$table->timestamps();
});
У моделі приведіть колонку embedding до типу array. Потім, коли статтю збережено, згенеруйте для неї ембединг:
use Laravel\Ai\Embeddings;
$response = Embeddings::for([$article->title . "\n" . $article->content])
->dimensions(1536)
->generate();
$article->update(['embedding' => $response->embeddings[0]]);
Для звичайного поля пошуку достатньо одного запиту:
$articles = HelpArticle::query()
->where('published', true)
->whereVectorSimilarTo('embedding', 'item arrived broken, can I send it back?', minSimilarity: 0.4)
->limit(5)
->get();
Якщо передати звичайний рядок замість масиву чисел, Laravel сам згенерує ембединг.
Ми також можемо віддати цю можливість агентові як інструмент. Це патерн, відомий як RAG (retrieval-augmented generation): агент спочатку шукає в базі знань, а потім відповідає.
use Laravel\Ai\Tools\SimilaritySearch;
public function tools(): iterable
{
return [
new LookupOrder($this->user),
SimilaritySearch::usingModel(
model: HelpArticle::class,
column: 'embedding',
minSimilarity: 0.7,
limit: 5,
query: fn ($query) => $query->where('published', true),
)->withDescription('Search help articles relevant to the customer question.'),
];
}
Тепер асистент відповідає на основі реальних правил TokoKita, а не загальних уявлень моделі.
4. Чутливі дії, що чекають на людину (підтвердження інструментів)
Проблема: асистент тепер уміє читати замовлення. Наступна спокуса - дозволити йому оформлювати повернення коштів. Але гроші - не те місце, де варто експериментувати.
Ідея: SDK містить підтвердження інструментів людиною. Чутливий інструмент може змусити агента зупинитися перед виконанням і чекати на рішення: схвалити, відхилити або відредагувати аргументи.
Потрібен агент із розмовами, історія яких зберігається, а це вже забезпечує трейт RemembersConversations. Ось інструмент:
<?php
namespace App\Ai\Tools;
use App\Models\Order;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Approvals\Approval;
use Laravel\Ai\Concerns\InteractsWithApprovals;
use Laravel\Ai\Contracts\Approvable;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;
class RefundOrder implements Approvable, Tool
{
use InteractsWithApprovals;
public function __construct(private User $user) {}
public function description(): Stringable|string
{
return 'Processes a refund for a customer order.';
}
protected function needsApproval(Request $request): Approval|bool
{
$total = Order::where('user_id', $this->user->id)
->where('number', $request['order_number'])
->value('total');
return $total > 500_000
? Approval::required('Refunds above the threshold need finance review.')
: false;
}
public function handle(Request $request): Stringable|string
{
app(RefundService::class)->process($this->user, $request['order_number']);
return "Refund for order {$request['order_number']} has been processed.";
}
public function schema(JsonSchema $schema): array
{
return [
'order_number' => $schema->string()->required(),
];
}
}
Тут зроблено два свідомі вибори. По-перше, сума повернення береться з бази даних і ніколи не з числа, яке називає модель. По-друге, невеликі повернення виконуються автоматично, а великі зупиняються й чекають на людину.
Потік на стороні застосунку:
$response = (new SupportAssistant($user))
->forUser($user)
->prompt('My package TK-1029 is lost, please refund it.');
if ($response->hasPendingApprovals()) {
foreach ($response->pendingApprovals as $approval) {
}
}
Коли фінансовий відділ ухвалить рішення, ми продовжуємо розмову з цим рішенням:
use Laravel\Ai\Approvals\Decision;
use Laravel\Ai\Approvals\Decisions;
$response = (new SupportAssistant($user))
->continue($conversationId, as: $user)
->prompt(Decisions::from([
$approvalId => Decision::approve(),
// or Decision::reject('Delivery proof is valid.'),
]));
Одне попередження з документації: метод continue не перевіряє, чи справді учасник є власником розмови. Авторизація, наприклад через Gate, залишається нашою відповідальністю.
5. Голосові повідомлення в текст і короткі підсумки (транскрибація)
Проблема: не кожен клієнт любить друкувати. Дехто надсилає довгі голосові повідомлення, а слухати кожне ні в кого немає часу.
Ідея: перетворити мовлення на текст, а потім стиснути його до підсумку.
use Illuminate\Support\Str;
use Laravel\Ai\Transcription;
$transcript = Transcription::fromUpload($request->file('voice_note'))
->diarize()
->generate();
$text = (string) $transcript;
$summary = Str::of($text)->summarize(sentences: 2);
Метод diarize() розділяє транскрипт за мовцями, що корисно, коли ви записуєте телефонну розмову двох людей. Метод summarize() використовує найдешевшу текстову модель вашого налаштованого провайдера, якщо ви не вкажете інше.
Аудіофайли важкі. Якщо ви не хочете, щоб користувачі чекали на відповідь запиту, виконайте обробку в черзі:
use Laravel\Ai\Responses\TranscriptionResponse;
Transcription::fromStorage('voice/complaint-1029.mp3')
->queue()
->then(function (TranscriptionResponse $transcript) {
});
Зверніть увагу, як це поєднується з першою функцією: голосове повідомлення надходить, стає текстом і автоматично проходить сортування. Усі п'ять функцій можна об'єднувати в ланцюжок таким чином.
Одне, що варто перевірити самостійно, - це підтримка мов. Документація Laravel перелічує провайдерів, які пропонують транскрибацію, але якість для вашої цільової мови залежить від провайдера, тож протестуйте на реальних записах і перегляньте документацію провайдера.
Бонус: дрібниці, що виручають у production
Розумні функції все одно потребують надійного фундаменту. SDK уже покриває кілька таких речей:
- Резервування провайдерів. Передайте список провайдерів, наприклад
provider: [Lab::OpenAI, Lab::Anthropic]. Якщо основний досягає ліміту запитів або перевантажений, SDK переходить на резервний.
- Тестування з фейками. Можна підміняти агентів (
TicketTriage::fake([...])), ембединги, транскрибації та інше. Для агентів зі структурованим виводом SDK навіть може згенерувати фейкові дані, що відповідають вашій схемі.
- Стримінг і черги. Відповіді можна передавати в браузер потоком або обробляти у фоні, щоб інтерфейс залишався чутливим.
- Middleware та події. Корисні для логування промптів або відстеження використання, наприклад, щоб оцінити вартість кожної функції.
З чого почати
Якщо ви засновник або tech lead і вирішуєте, з чого почати, ось порядок, який видається нам найлогічнішим:
- Сортування звернень. Низький ризик, негайна користь, і воно ніколи не змінює жодних даних.
- Асистент з інструментами лише для читання. Почніть із функцій, які тільки читають.
- Семантичний пошук. Потребує певної підготовки бази даних, але ефект великий.
- Чутливі дії з підтвердженням. Лише після того, як перші три довели свою стабільність.
- Голосові повідомлення. Додайте це, коли ваші користувачі справді цього потребуватимуть.
AI все ще може помилятися, тому не віддавайте в його руки незворотні рішення без страхувальної сітки. Гарна новина в тому, що SDK дає вам таку сітку з першого дня.
Яку функцію ви спробували б першою? Напишіть у коментарях.