Когда ваш AI-агент начинает копаться в файлах
Вы дали локальному агенту доступ к файловой системе. Он может анализировать код, искать логи, готовить отчеты. Идея шикарная, пока вы не замечаете две вещи: агент внезапно начинает работать в 10 раз медленнее, а в логах появляются попытки чтения /etc/passwd.
Первую проблему я назвал "синдромом затухающего кэша". Вторая — классическая история про доверие и глупость. Обе решаемы, но только если понимать, что происходит под капотом у llama.cpp и как работает ваш собственный код.
На момент 07.04.2026, актуальные версии llama.cpp (v0.12.x) и node-llama-cpp (v7.x) содержат оптимизацию KV-кэша, которая ломается от одной строчки в промпте. Если вы не используете последние стабильные сборки — обновитесь. В старых версиях баг был еще критичнее.
Динамическое время в промпте: тихий убийца производительности
KV-cache (Key-Value кэш) в llama.cpp — это гениальная оптимизация. Модель кэширует вычисления для уже обработанных токенов, чтобы не пересчитывать их при генерации следующего слова. Это ускоряет работу в разы, особенно для длинных диалогов или многошаговых задач.
Но кэш работает только если входная последовательность токенов идентична предыдущей. Добавьте в системный промпт динамическую переменную — например, текущее время — и кэш летит к чертям. Каждый новый запрос начинается с чистого листа.
Вот как выглядит смертельный промпт:
// НЕ ДЕЛАЙТЕ ТАК
const systemPrompt = `Ты — ассистент. Сегодня ${new Date().toLocaleDateString()}.
Твоя задача — анализировать файлы в указанной директории.`;
Кажется, безобидно. Но new Date() меняется. Хотя бы на секунду. И каждый вызов создает новый промпт. KV-кэш не срабатывает. Скорость падает с 30 токенов в секунду до 3. Вы тратите часы, гадая, почему ваш Ryzen 9 еле дышит.
1 Исправляем баг: выносим динамику из промпта
Решение — разделить статический контекст и динамические данные. Системный промпт должен быть константой. Динамическую информацию передаем как отдельное сообщение пользователя или системного контекста.
// Правильный подход
const staticSystemPrompt = `Ты — ассистент. Твоя задача — анализировать файлы.
Контекст:
- Текущая дата: {{CURRENT_DATE}}
- Рабочая директория: {{WORK_DIR}}
`;
// При инициализации сессии заменяем плейсхолдеры
function initSession(workDir) {
const date = new Date().toISOString().split('T')[0];
const prompt = staticSystemPrompt
.replace('{{CURRENT_DATE}}', date)
.replace('{{WORK_DIR}}', workDir);
// Теперь промпт статичен на всю сессию, KV-кэш работает
return prompt;
}
Да, это кажется очевидным. Но 8 из 10 разработчиков, с которыми я говорил, попадали в эту ловушку. Особенно когда используют шаблонизаторы промптов, которые автоматически подставляют время.
Permission Gate: зачем вашему агенту нужен надзиратель
Теперь о безопасности. Локальный агент — не значит безопасный. У него те же права, что у вашего пользователя. Дать ему доступ ко всей файловой системе — все равно что отдать шестилетнему ребенку бензопилу и сказать: "Сделай что-нибудь красивое".
Permission Gate — это прослойка между агентом и файловой системой. Она проверяет каждую операцию по простым правилам: можно ли читать этот путь? Писать сюда? Выполнять?
Без такого гейта ваша история может закончиться как в реальном кейсе с агентом Meta. Агенты любят исследовать. Иногда слишком активно.
2 Архитектура простого Permission Gate
Вам нужен не просто fs.readFile() с проверкой. Нужна система, которая:
- Нормализует пути (обрабатывает
.., симлинки, относительные пути) - Ведет белый список разрешенных директорий
- Запрашивает подтверждение у пользователя для новых путей
- Логирует все операции
class PermissionGate {
constructor(allowedRoots, requireConfirmation = true) {
this.allowedRoots = allowedRoots.map(p => path.resolve(p));
this.requireConfirmation = requireConfirmation;
this.approvedPaths = new Set(this.allowedRoots);
}
async checkAccess(filePath, operation = 'read') {
const resolved = path.resolve(filePath);
// Проверяем, внутри ли разрешенного корня
const isAllowed = this.allowedRoots.some(root =>
resolved.startsWith(root + path.sep) || resolved === root
);
if (!isAllowed) {
return { allowed: false, reason: 'Outside allowed roots' };
}
// Если путь уже одобрен
if (this.approvedPaths.has(resolved)) {
return { allowed: true };
}
// Запрашиваем подтверждение
if (this.requireConfirmation) {
const approved = await this.requestConfirmation(resolved, operation);
if (approved) {
this.approvedPaths.add(resolved);
return { allowed: true, firstTime: true };
}
return { allowed: false, reason: 'User denied' };
}
return { allowed: false, reason: 'Not pre-approved' };
}
async requestConfirmation(filePath, operation) {
// Здесь интеграция с UI: модальное окно, CLI-запрос, etc.
console.log(`\n⚠️ Агент хочет ${operation}: ${filePath}`);
// В реальном приложении — интерактивный запрос
return true; // или false
}
}
Эта базовая реализация уже остановит 99% случайных попыток выхода за пределы рабочей области. Но в продакшене нужно больше, как описано в 8 шагах безопасности для AI-агентов.
Собираем все вместе: рабочая архитектура
Теперь соединим исправленный промпт с KV-кэшем и permission gate. Получится агент, который и быстрый, и относительно безопасный.
| Компонент | Задача | Реализация |
|---|---|---|
| Промпт-менеджер | Генерирует статические промпты с плейсхолдерами | Шаблоны + замена при инициализации сессии |
| KV-кэш llama.cpp | Ускорение инференса | Работает автоматически, если промпт статичен |
| Permission Gate | Контроль доступа к файлам | Прослойка между агентом и fs API |
| Мониторинг | Логирование всех операций | Отдельный сервис или встроенное логирование |
Интеграция выглядит так:
// Псевдокод основной логики
async function processAgentRequest(task, workDir) {
// 1. Инициализируем сессию со статическим промптом
const sessionPrompt = initSession(workDir);
// 2. Создаем модель с включенным кэшем
const model = new LlamaModel({
modelPath: 'llama-3.2-90b-vision.Q8_0.gguf',
enableKVCache: true, // Критически важно!
});
// 3. Инициализируем Permission Gate
const gate = new PermissionGate([workDir]);
// 4. Создаем обертки для файловых операций
const safeFS = {
readFile: async (filePath) => {
const access = await gate.checkAccess(filePath, 'read');
if (!access.allowed) throw new Error(`Access denied: ${access.reason}`);
return fs.promises.readFile(filePath, 'utf8');
},
// ... аналогично для write, readdir и т.д.
};
// 5. Передаем safeFS агенту как контекст
const agent = new Agent(model, sessionPrompt, { fs: safeFS });
return agent.execute(task);
}
Эта архитектура масштабируется. Когда у вас появится рой из тысяч агентов, вы просто добавите централизованный сервис разрешений.
Ошибки, которые все равно совершат
Даже с этим руководством. Потому что некоторые вещи понимаешь только на практике.
Ошибка 1: Разрешать доступ к домашней директории ~. Кажется удобным, пока агент не начнет читать ~/.ssh/id_rsa. Всегда ограничивайте конкретной поддиректорией.
Ошибка 2: Забывать про симлинки. Агент запросил /project/docs, вы разрешили. Но docs — симлинк на /etc/config. Проверяйте реальный путь с fs.realpathSync().
Ошибка 3: Доверять промпту. Даже с идеальным системным промптом, модель может "забыть" инструкции после 10к токенов. Решение — техники напоминания и навыки агента.
Что будет дальше с локальными агентами
К 2027 году, я предсказываю, появятся стандартизированные API для permission gate. Аналогичные песочницам для shell-доступа, но для файловой системы.
Но пока — это ваша ответственность. Не давайте агенту больше прав, чем готовы потерять. И всегда смотрите в логи.
Самый важный инсайт: быстрый агент и безопасный агент — это не противоположности. Это два разных параметра, которые требуют разных решений. KV-кэш чиним промптами, безопасность — слоями контроля. Когда делаете и то, и другое — получаете агента, который не сожжет процессор и не сломает систему.