Как я добавил файловый доступ в локального AI-агента и что из этого вышло

Как я добавил файловый доступ в локального AI-агента и что из этого вышло

Пару недель назад я выпустил Доку — локального AI-агента для Windows и macOS. Статья попала в топ-5 Хабра за сутки, поступило 22 баг-репорта в первые 48 часов и 154 комментария за неделю.

Самый частый запрос: «Когда будет работа с файлами?». Это логично — агент, который умеет искать в интернете, но не может взаимодействовать с диском, похож на браузер без закладок.

В этой статье — технические детали реализации файлового доступа: permission gate, agent timeline, укрепление pipeline. А также три бага, которые я обнаружил по ходу и которые стоит знать, если вы строите похожие системы.

Баг №1: убийца KV-кэша, которого я не замечал месяц

После первого релиза я заметил, что многошаговые задачи работают медленнее ожидаемого. Модель не прогрета? Проблемы с железом? Медленная генерация?

Нет. Я сам всё сломал.

В системный промпт была вставлена строка с текущим временем:

Session started: ${new Date().toISOString()}

Она находилась в начале промпта — и это убивало весь KV-кэш node-llama-cpp на каждой итерации агентного цикла.

Как работает KV-кэш в llama.cpp: модель кэширует обработанные токены промпта. Если промпт не изменился — платишь только за новые токены. Если изменился хотя бы один символ в начале — весь кэш становится невалидным, и обработка начинается с нуля.

Время меняется каждую секунду. Агентный цикл делает 6–12 итераций. На каждой — полный пересчёт системного промпта. При системнике в 2000 токенов и 10 итерациях это 20 000 лишних токенов за один запрос.

Фикс — перенести timestamp в конец промпта или в user-тёрн. Этот паттерн описан в гайдах Anthropic для Claude, но я не подумал, что он актуален и для локальных моделей.

После исправления многошаговые задачи ускорились в 2–3 раза на тех, где агент делал 8+ итераций.

Как я подошёл к файловому доступу

Дать агенту инструменты для чтения файлов — несложно. Дать инструменты для записи — уже вопрос ответственности.

Сначала я хотел просто добавить file tools в тулбокс. Но чем больше думал, тем яснее понимал: это неправильно.

Агент с неограниченным доступом к файлам может случайно перезаписать рабочий документ, удалить важное или записать не туда. Не из злого умысла — просто из-за ошибки модели в пути или интерпретации.

Я изучил подходы других: MCP-протокол использует read/write-разрешения на уровне сервера, Claude Desktop показывает доступные инструменты, но скрывает параметры вызова.

Мне нужно было нечто более явное: не «пользователь включил write tools», а «прямо сейчас агент хочет записать вот этот конкретный файл — подтверждаешь?»

Tool Permission Gate: как это работает

Каждый инструмент в тулбоксе помечен флагом dangerous: boolean.

Когда агент вызывает dangerous-инструмент, выполнение останавливается. Пользователю показывается диалог с точными аргументами вызова — не «агент хочет что-то записать», а конкретно:

  • Какой файл будет изменён
  • Какие данные будут записаны
  • Какой инструмент используется

Пользователь видит именно то, что агент собирается сделать. Кнопка «Разрешить» не является default — чтобы исключить случайный клик.

Если пользователь нажимает «Отклонить», инструмент возвращает агенту структурированную ошибку. Агент может её обработать и сообщить, что действие не удалось и почему.

Все шесть файловых инструментов

Вот ключевые из них:

  • edit_file — принимает инструкцию, а не весь файл. Агент говорит: «добавь в конец такую-то секцию» — система применяет патч. Это критично для больших файлов: не нужно гонять 10 КБ туда-обратно через контекст.

Под капотом edit_file выполняет: read → apply patch → write → verify.

Баг №2: агент редактирует не тот файл

Первые тесты вскрыли интересный класс ошибок.

Задача: «добавь раздел в файл notes.md». Агент делает list_dir, находит notes.md, вызывает edit_file. Кажется, всё ок.

Но на третьем-четвёртом тесте агент взял notes.md из другой директории — ту, что была ближе к корню проекта, а не ту, что просили. Путь был относительным, и агент выбрал «по смыслу» — то, что ему казалось правильным.

Это не баг модели — это моя ошибка. Я не указал агенту явный рабочий контекст.

Решение:

  • Задать рабочую директорию по умолчанию в системном промпте
  • Валидировать, что все пути находятся внутри неё
  • Если агент пытается выйти за пределы — ошибка с пояснением

Дополнительно добавил в промпт инструкцию: если путь к файлу неоднозначен — спроси пользователя, не угадывай.

Auto-verify после записи

После write_file или edit_file агент автоматически вызывает read_file, чтобы проверить, что данные записались корректно.

Зачем? Без этого агент может «успешно» записать файл, получить подтверждение — но на диске окажутся неполные данные или файл вообще не обновится (из-за прав, блокировки или полного диска).

Auto-verify добавляет один вызов инструмента на каждую запись, но устраняет целый класс молчаливых ошибок.

Именно поэтому я увеличил MAX_TOOL_STEPS с 6 до 12: типичная файловая задача — plan (1) + read (1–2) + write (1) + verify (1) — уже занимает 5 из 6 шагов, без запаса на ошибки.

Agent Timeline: зачем нужна прозрачность

Первая версия Доки показывала только статус: «Ищу в сети...» → «Читаю страницу...» → «Генерирую ответ...».

Несколько пользователей жаловались: «запустил задачу, ждал три минуты, ничего не понял, что происходит». Это обоснованно.

С файловым доступом прозрачность стала критичной. Если агент читает файлы, пользователь должен видеть какие именно. Не для красоты — а чтобы понимать, что происходит с его данными.

Я добавил боковую панель — Agent Timeline. Это лог всех шагов: аргументы инструмента, результат, время.

Технически — это stream событий от агентного движка. Каждый вызов инструмента эмитит tool_call_start и tool_call_end, которые рендерятся в React в реальном времени.

Баг №3: структурированные ошибки, которых не было

Когда read_file падал с ошибкой доступа или файл не существовал, агент получал голый стектрейс Node.js. Что он с ним делал? Чаще всего — либо бесконечно пытался повторить, либо галлюцинировал ответ.

Теперь все ошибки инструментов форматируются в структуру с полем canRetry — подсказкой для агента, стоит ли повторять попытку.

  • Для «файл не найден» — canRetry: false
  • Для «таймаут сети» — canRetry: true

Агент использует это в логике принятия решений. Количество бессмысленных повторов заметно снизилось.

Системный промпт: четыре секции вместо одной

Это не связано напрямую с файлами, но стало ключом к стабильности v1.5.0.

Я переписал system.md по структуре:

  • Роль и границы — кто агент, что делает, чего не делает
  • Правила с приоритетами — безопасность > точность > скорость
  • Формат ответа — как структурировать разные типы ответов
  • Few-shot пример — один пример правильного рассуждения на граничном случае

Раньше промпт был единым текстом. Это работало для простых задач, но при длинных цепочках агент «забывал» правила.

Чёткая структура устранила несколько классов галлюцинаций — особенно когда правильный ответ — отказать и объяснить почему.

Think → Plan → Act

Ещё один важный паттерн.

Раньше агент сразу начинал вызывать инструменты. Для простых задач — нормально. Но в многошаговых он часто «застревал» — не строил план заранее.

Я добавил в промпт обязательную фазу планирования перед действиями:

Сначала подумай. Потом составь план. Только потом действуй.

На практике это сократило количество ситуаций, когда агент три раза подряд вызывал один и тот же инструмент с одинаковыми аргументами, примерно вдвое. Планирование заставляет агента явно представить цель перед действием.

Что дальше?

Планирую:

  • Работу с форматами: Excel/CSV/DOCX не как «прочитать как текст», а с нормальным парсингом и сохранением структуры
  • History compaction при переполнении контекста — сейчас при длинном чате модель теряет начало
Читать оригинал