Пару недель назад я выпустил Доку — локального 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 при переполнении контекста — сейчас при длинном чате модель теряет начало