Хардкод промптов парализует разработку ИИ-фичей. На старте проекта конкатенация строк кажется оправданной. Разработчик пишет $prompt = "Сделай саммари: " . $text;, коммитит код и деплоит сервис. Через неделю продакт-менеджер требует добавить обращение к пользователю по имени. Еще через неделю — изменить тон ответа для премиум-сегмента. Затем появляется задача подмешивать в запрос историю последних транзакций.
Контроллер обрастает каскадами if/else. Код деградирует в месиво из бизнес-логики и фрагментов текста на естественном языке. Промпт-инженеры лишаются возможности тестировать гипотезы без привлечения бэкендера. Любое изменение текста требует полного цикла CI/CD.
Промпт — это динамический интерфейс между состоянием системы и языковой моделью. Для управления этим интерфейсом бэкенд выстраивает конвейер сборки контекста (Context Pipeline).
Архитектура конвейера сборки контекста
Сборка payload для LLM представляет собой задачу ETL (Extract, Transform, Load), выполняемую в реальном времени. Процесс разбит на четыре изолированных этапа.
1. Извлечение данных (Data Fetching) Система агрегирует сырые данные из множества источников. Профиль пользователя и настройки извлекаются из реляционной базы (MySQL). Текущая сессия и история диалога поднимаются из кэша (Redis). Релевантные фрагменты базы знаний подтягиваются через векторный поиск (RAG). На этом этапе текстовое представление отсутствует, система оперирует исключительно типизированными DTO (Data Transfer Objects).
2. Приоритизация и усечение (Context Window Management) Языковые модели ограничены размером контекстного окна. Отправка избыточных объемов данных увеличивает latency и стоимость запроса, особенно при тарификации за токены через RouterAPI. Бэкенд рассчитывает объем данных до их рендеринга. Если история диалога и найденные документы превышают лимит, применяется стратегия деградации. Сначала отбрасываются старые сообщения диалога. Затем усекается выборка документов из RAG. Системные инструкции и профиль пользователя имеют наивысший приоритет и остаются неизменными.
3. Рендеринг шаблона (Templating) Отфильтрованные DTO передаются в шаблонизатор. Использование движков вроде Blade, Twig или Jinja решает проблему ветвления логики внутри промпта. Шаблонизатор изолирует текст от PHP-кода.
Пример реализации на Blade демонстрирует внедрение условной логики в системный промпт:
{{-- resources/views/prompts/system/assistant.blade.php --}}
Ты — финансовый ассистент сервиса.
Текущее время сервера: {{ $system->currentTime }}.
Контекст пользователя:
Имя: {{ $user->name }}
Тип аккаунта: {{ $user->isPremium ? 'Premium' : 'Basic' }}
@if($user->isPremium)
Твоя задача — давать глубокую аналитику. Используй профессиональную терминологию.
Обязательно приводи расчеты доходности.
@else
Твоя задача — давать базовые советы. Пиши простым языком, избегай сложных терминов.
В конце ответа ненавязчиво предложи перейти на Premium тариф для получения детальной аналитики.
@endif
Последние транзакции пользователя:
@forelse($transactions as $tx)
- [{{ $tx->date }}] {{ $tx->type }}: {{ $tx->amount }} {{ $tx->currency }}
@empty
Транзакции отсутствуют.
@endforelse
Правила форматирования:
Ответ должен быть в формате Markdown. Не используй HTML-теги.
Вынос шаблонов в отдельную директорию позволяет промпт-инженерам редактировать .blade.php файлы напрямую. В высоконагруженных системах шаблоны хранятся в базе данных, кэшируются в Redis и редактируются через админ-панель. Time-to-market для новых промптов сокращается до минут.
4. Сборка Payload для RouterAPI Отрендеренный текст требует упаковки. API провайдеров (и унифицированный RouterAPI) ожидают структурированный массив сообщений. Бэкенд формирует финальный payload, четко разделяя системные инструкции, историю и текущий запрос пользователя.
namespace App\Services\AI;
use App\Models\User;
use Illuminate\Support\Facades\View;
readonly class PromptPipeline
{
public function __construct(
private TransactionRepository $transactions,
private TokenEstimator $estimator
) {}
public function buildPayload(User $user, string $userMessage, array $history): array
{
$recentTransactions = $this->transactions->getRecent($user->id, limit: 5);
$systemContent = View::make('prompts.system.assistant', [
'user' => $user,
'transactions' => $recentTransactions,
'system' => ['currentTime' => now->toIso8601String]
])->render;
$messages = [
['role' => 'system', 'content' => $systemContent]
];
$availableTokens = 4000 - $this->estimator->estimate($systemContent) - $this->estimator->estimate($userMessage);
foreach (array_reverse($history) as $msg) {
$msgTokens = $this->estimator->estimate($msg['content']);
if ($availableTokens - $msgTokens < 0) {
break;
}
array_splice($messages, 1, 0, [$msg]); // Вставляем после системного промпта
$availableTokens -= $msgTokens;
}
$messages[] = ['role' => 'user', 'content' => $userMessage];
return $messages;
}
}
Интеграция с RouterAPI
Сформированный массив $messages передается в HTTP-клиент. RouterAPI берет на себя маршрутизацию запросов между провайдерами (резервный провайдер, резервный канал RouterAPI) и обработку специфичных форматов. Клиент отправляет стандартный OpenAI-совместимый payload.
namespace App\Services\AI;
use Illuminate\Support\Facades\Http;
use App\Exceptions\AIProviderException;
readonly class RouterApiClient
{
public function __construct(
private string $baseUrl,
private string $apiKey
) {}
public function generateResponse(array $messages, string $model = 'anthropic/claude-3.5-sonnet'): string
{
$response = Http::withToken($this->apiKey)
->timeout(30)
->retry(3, 100)
->post("{$this->baseUrl}/v1/chat/completions", [
'model' => $model,
'messages' => $messages,
'temperature' => 0.3,
'max_tokens' => 1000,
]);
if ($response->failed) {
throw new AIProviderException('RouterAPI request failed: ' . $response->body);
}
return $response->json('choices.0.message.content');
}
}
Такое разделение изолирует бизнес-логику от инфраструктуры. PromptPipeline отвечает за бизнес-контекст и сборку промпта. RouterApiClient обеспечивает доставку payload до нейросети и обработку сетевых ошибок.
Композиция промптов и управление состоянием
С ростом кодовой базы монолитные шаблоны дублируют логику. Если десять независимых ИИ-агентов должны соблюдать единые правила безопасности и возвращать данные в JSON, дублирование этих инструкций приводит к рассинхронизации.
Бэкенд решает эту проблему через композицию шаблонов (Partial Templates). Системный промпт собирается из независимых блоков:
{{-- resources/views/prompts/system/base.blade.php --}}
@include('prompts.partials.persona', ['role' => $role])
@include('prompts.partials.safety_rules')
@yield('specific_instructions')
@include('prompts.partials.output_format', ['format' => $expectedFormat])
Модульность позволяет обновлять правила безопасности глобально одним коммитом.
Динамическая сборка контекста требует жесткого контроля над состоянием. Промпт, сгенерированный сегодня, отличается от промпта, сгенерированного завтра для того же пользователя из-за изменения времени сервера или появления новых транзакций. Для аудита и отладки финальный payload, отправленный в RouterAPI, логируется целиком. Хранение сырых массивов $messages позволяет точно воспроизвести условия запроса и определить причину сбоя: галлюцинацию модели или ошибку в логике сборки контекста, при которой шаблонизатор получил некорректные DTO.