Тема
Laravel AI SDK и семантический поиск — лучшие практики
Источники: laravel.com/docs/13.x/ai-sdk, официальный блог Laravel (multi-agent workflows, document search agent), production-разборы сообщества (сверено 2026-07). Обязательная база: specs/laravel_13/services — AI-вызовы только из Service/Job, ключи в config; specs/laravel_13/migrations — vector-колонки, HNSW-индекс. Здесь — расширенные паттерны поверх этой базы.
Главный принцип: start simple
Официальная позиция Laravel (по мотивам Anthropic «Building Effective Agents»):
«Start simple. A single
agent()call handles most tasks. Reach for these patterns only when the task genuinely needs them.»
Один агент с хорошим промптом покрывает большинство задач. Мультиагентные схемы — только когда задача реально этого требует.
Пять мультиагентных паттернов
Официальный блог Laravel реализует паттерны Anthropic поверх AI SDK:
| Паттерн | Когда применять | Механизм |
|---|---|---|
| Prompt Chaining | Фиксированная последовательность (draft → review → improve) | Pipeline, каждый шаг обогащает payload |
| Routing | Входы различаются по типу/сложности | Классификатор выбирает специалиста; #[UseCheapestModel] для простых запросов |
| Parallelization | Независимые под-задачи | Concurrency::run() (PHP-аналог Promise.all()) + агент-суммаризатор |
| Orchestrator-Workers | План шагов заранее неизвестен | Оркестратор вызывает суб-агентов как tools |
| Evaluator-Optimizer | Есть чёткий критерий качества | Цикл generate → evaluate (structured output со score/approved) → improve, ≤3 итераций |
Надёжность агентов
- Tools никогда не бросают исключения — возвращают понятную строку (
"Lookup failed: ..."). Модель должна прочитать ошибку и отреагировать сама; исключение убивает весь ReAct-цикл. #[MaxSteps(8)]— явный потолок ReAct-цикла, защита от зацикливания (каждый лишний шаг = деньги и латентность).- Structured output (
make:agent --structured) — схема гарантирует JSON, никакого ручного парсинга ответа модели. - Failover провайдеров — средствами SDK, не своим кодом:
provider: ['openai', 'anthropic']— SDK сам переключается при отказе/лимитах. - MCP-tools наружу — только через middleware с auth (Sanctum/OAuth), не «голые» actions.
Streaming и фон
Три примитива, из которых собирается любая архитектура ответа:
| Примитив | Сценарий |
|---|---|
->queue() | Фоновая обработка — HTTP-ответ не ждёт модель |
->stream() | SSE прямо из роута — браузер получает токены по мере генерации |
->broadcastOnQueue() | WebSocket-вариант через Reverb |
php
public function send(string $prompt): void
{
set_time_limit(0); // длинный ReAct-цикл переживёт лимит PHP
$response = ChatAgent::make()->forUser($this->user())->stream($prompt);
foreach ($response as $event) {
if ($event instanceof ToolCall) { /* показать статус «ищу...» */ }
elseif ($event instanceof TextDelta) { /* стримить markdown */ }
}
}- Queue-воркерам под AI поднимать таймаут (
--timeout=120) — вызовы моделей регулярно превышают дефолтные 60 секунд. - Embeddings — никогда в HTTP-запросе: chunk+embed 10-страничного PDF — это 3–5 секунд; только Job.
Память диалогов
php
class SalesCoach implements Agent, Conversational
{
use Promptable, RemembersConversations; // messages() вручную НЕ определять
}
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
(new SalesCoach)->continue($response->conversationId, as: $user)->prompt('Tell me more.');HasConversations на User даёт $user->conversations() — история из коробки, без своей таблицы сообщений.
Тестирование агентов — только fakes
Реальные вызовы провайдера в тестах = деньги, флейки и утечки промптов в CI:
php
SalesCoach::fake(); // авто-ответ
SalesCoach::fake(['First', 'Second']); // последовательность
SalesCoach::fake(fn (AgentPrompt $p) => 'For: '.$p->prompt); // динамический
SalesCoach::fake([['score' => 87]]); // structured — по схеме
SalesCoach::assertPrompted('Analyze this...');
SalesCoach::assertQueued('Analyze this...'); // для queued-вызовов
SalesCoach::fake()->preventStrayPrompts(); // упасть на незамоканном промптеpreventStrayPrompts() — аналог Http::preventStrayRequests(): обязателен, чтобы случайный реальный вызов не прошёл незамеченным.
Embeddings: стоимость и кеш
Каждый embedding — платный API-вызов. Иерархия экономии:
- Встроенный кеш SDK (ключ = provider+model+dimensions+content, TTL 30 дней):
php
// config/ai.php
'caching' => ['embeddings' => ['cache' => true, 'store' => env('CACHE_STORE')]],
// точечно
Str::of($text)->toEmbeddings(cache: true);
Embeddings::for([...])->cache(seconds: 3600)->generate();- Батчи при массовом ingest — по ~64 чанка на API-вызов, из Job.
- Хеш контента при повторном ingest — не пересчитывать embedding неизменённых чанков (на практике пропускает ~90% прогона).
- Чистка перед embedding — шаблонные блоки (шапки, футеры, copyright) тратят embedding signal впустую. Размер чанка — 600–1000 токенов по типу контента.
Vector-поиск: продвинутый API
База (vector(), ensureVectorExtensionExists(), HNSW) — в specs/laravel_13/migrations. Поверх неё:
php
Document::query()
->whereVectorSimilarTo('embedding', 'best wineries in Napa', minSimilarity: 0.4)
->limit(10)->get(); // строка → SDK сам сгенерирует embedding
Document::query()
->selectVectorDistance('embedding', $vec, as: 'distance')
->whereVectorDistanceLessThan('embedding', $vec, maxDistance: 0.3)
->orderByVectorDistance('embedding', $vec)
->limit(10)->get(); // низкоуровневый контроль
SimilaritySearch::usingModel(Document::class, 'embedding',
minSimilarity: 0.7, limit: 10,
query: fn ($q) => $q->where('published', true)); // готовый tool для агентаHNSW vs IVFFlat
| HNSW | IVFFlat | |
|---|---|---|
| Скорость/точность | Выше (sub-50ms на миллионах строк) | Ниже |
| Память | Больше | Меньше |
| Изменчивые данные | Адаптируется | Требует переиндексации |
| Вывод | Дефолт для production | Статичные датасеты при дефиците RAM |
Гибридный поиск: вектор + fulltext
Семантика ловит смысл, fulltext — точные термины/артикулы. Сбалансированный стартовый вес — 60/40:
sql
0.6 * (1 - (embedding <=> :query_vec)) +
0.4 * ts_rank_cd(search_vector, plainto_tsquery('russian', :query_text))tsvector + GIN-практика студии уже описана в specs/laravel_13/migrations — гибрид совмещает оба индекса.
RAG: контекст в LLM
После поиска отдавать в модель максимум ~6 чанков (после reranking — Reranking::of($documents)->rerank($query)). Больше контекста ≠ лучше ответ: дороже и шумнее.
Anti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
| Embeddings синхронно в HTTP-запросе | 3–5 с на документ, таймауты | Job + батчи по 64 |
| Свой fallback-код между провайдерами | Дублирует SDK, ломается первым | provider: ['openai', 'anthropic'] |
| Исключения из tools агента | Убивают ReAct-цикл | Возвращать строку ошибки |
| Тесты с реальными вызовами модели | Деньги, флейки CI | Agent::fake() + preventStrayPrompts() |
| Кеш embeddings выключен при ingest | Оплата пересчёта неизменённого | Кеш SDK + хеш контента |
Агент без #[MaxSteps] | Зацикливание = счёт за токены | Явный потолок шагов |
| Мультиагентная схема «на вырост» | Сложность без нужды | Один agent()-вызов, паттерны — по необходимости |
| Весь найденный контекст в промпт | Дорого, ответ шумнее | Reranking + ~6 чанков |