
Продолжение первой статьи
Пару недель назад я выложил Доку — локального AI-агента для Windows и macOS. Статья попала в топ-5 Хабра за сутки, пришло 22 баг-репорта в первые 48 часов и 154 комментария за неделю.
Самый частый запрос в комментариях: «когда будет работа с файлами?». Это логично — агент который умеет искать в интернете, но не может взаимодействовать с твоим диском, это как браузер без закладок.
В этой статье — технические детали того что я сделал: файловый доступ для агента, permission gate, agent timeline, pipeline hardening. Плюс три баги которые я поймал по дороге и которые стоит знать если вы строите что-то похожее.
Баг №1: убийца KV-кэша которого я не замечал месяц
Начну с самого обидного.
После первого релиза я видел что многошаговые агентские задачи работают медленнее чем должны. Модель не прогрета? Железо? Просто медленная генерация?
Нет. Я сам всё ломал.
В системный промпт у меня была вставлена строка с текущим временем:
Session started: ${new Date().toISOString()}
Вставлена в начало промпта. И это убивало весь KV-кэш node-llama-cpp на каждой итерации агентного цикла.
Как работает KV-кэш в llama.cpp: модель кэширует обработанные токены промпта. Если промпт не изменился — платишь только за новые токены. Если изменился хоть один символ в начале — весь кэш невалиден, обрабатываешь заново.
Время меняется каждую секунду. Agent loop делает 6–12 итераций. На каждой итерации — полный пересчёт системного промпта. При системнике в 2000 токенов и 10 итерациях это 20 000 лишних токенов за один запрос.
Фикс — перенести timestamp в конец промпта или в user-тёрн. Это описано как критический паттерн в гайдах Anthropic для Claude, но я читал это применительно к облачным API и не подумал что это так же работает локально.
После фикса: многошаговые задачи ускорились в 2–3 раза на задачах где agent loop делал 8+ итераций.
Как я думал про файловый доступ
Дать агенту инструменты для чтения файлов — не сложно. Дать инструменты для записи — это уже ответственность.
Сначала я думал просто сделать file tools и добавить их в тулбокс агента. Но чем больше я думал, тем больше понимал что это неправильно.
Агент с файловым доступом без каких-либо ограничений — это программа которая может перезаписать твой рабочий документ, удалить что-то важное, или просто записать не туда. Не злонамеренно — просто потому что модель ошиблась в пути к файлу или неправильно интерпретировала инструкцию.
Я посмотрел как другие решают это. MCP-протокол предлагает разграничение через read / write permissions на уровне сервера. Claude Desktop показывает какие инструменты доступны агенту, но не показывает конкретные параметры вызова.
Мне нужно было что-то более явное. Не «пользователь включил write tools», а «прямо сейчас агент хочет записать вот этот конкретный файл — подтверждаешь?»
Tool Permission Gate: как это устроено
Каждый инструмент в тулбоксе помечен флагом dangerous: boolean.
const fileTools: Tool[] = [ { name: 'read_file', dangerous: false, // ... }, { name: 'write_file', dangerous: true, // ... }, { name: 'edit_file', dangerous: true, // ... }, { name: 'delete_file', dangerous: true, // ... }, ];
Когда агент вызывает dangerous-инструмент, выполнение останавливается. Пользователю показывается диалог с точными аргументами вызова — не «агент хочет что-то записать», а конкретно:
Агент хочет выполнить: write_file Путь: /Users/ilya/Documents/report.md Содержимое: [показывает первые 500 символов] [Разрешить] [Отклонить]
Пользователь видит именно то что агент собирается сделать. Кнопку «Разрешить» я сделал не default — чтобы не кликнуть случайно.
Если пользователь нажимает «Отклонить» — инструмент возвращает агенту структурированную ошибку:
{ "error": "User denied permission for write_file", "canRetry": false, "suggestion": "Ask the user to confirm the intended file path" }
Агент может это обработать и сообщить пользователю что именно не получилось и почему.
Все шесть файловых инструментов
read_file(path: string): string write_file(path: string, content: string): void // dangerous edit_file(path: string, instruction: string): void // dangerous list_dir(path: string): FileEntry[] move_file(from: string, to: string): void // dangerous delete_file(path: string): void // dangerous
edit_file — принимает инструкцию, не весь файл. Агент говорит «добавь в конец файла такую-то секцию» — я применяю патч. Это важно для больших файлов: не гонять 10KB туда-обратно через контекст.
Под капотом edit_file делает read → apply patch → write → verify (об этом ниже).
Баг №2: агент редактирует не тот файл
Первые тесты file tools вскрыли интересный класс ошибок.
Даёшь задачу: «добавь раздел в файл notes.md». Агент делает list_dir, находит notes.md, делает edit_file. Окей.
Но на третьем-четвёртом тесте агент взял notes.md из другой директории — та что была ближе к корню проекта, не та что просили. Путь был относительным, и агент выбрал «по смыслу» — что ему казалось правильным.
Это не баг модели, это моя недоработка. Я не указывал агенту работать в явном рабочем контексте.
Решение — рабочая директория по умолчанию, явно прописанная в системном промпте, и валидация что все пути находятся внутри неё. Если агент пытается пойти за её пределы — ошибка с объяснением.
Дополнительно добавил в системный промпт явную инструкцию: если путь к файлу неоднозначен — спроси пользователя, не угадывай.
Auto-verify после записи
После write_file или edit_file агент автоматически вызывает read_file чтобы убедиться что записалось правильно.
Зачем? Потому что без этого агент может «записать» файл, получить success, двигаться дальше — но файл при этом содержит неполные данные или вообще не обновился (мало ли, права, блокировка, диск заполнен).
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 падал с permission error или файл не существовал — агент получал голый стектрейс Node.js. Что он с ним делал? Обычно — ничего хорошего. Либо бесконечный retry, либо галлюцинировал ответ.
Теперь все ошибки инструментов форматируются в структуру:
{ "error": "File not found: /path/to/file.md", "canRetry": false, "suggestion": "Check if the path is correct with list_dir first" }
canRetry — подсказка агенту: есть ли смысл пробовать снова. Для «файл не найден» — нет, для «таймаут сети» — да. Агент использует это в логике принятия решений, и количество бессмысленных retry заметно упало.
Системный промпт: четыре секции вместо одной
Это не файловые инструменты, но важная часть того почему v1.5.0 стал стабильнее.
Я реструктурировал system.md по паттерну:
1. Роль и границы — кто агент, что он делает, чего не делает
2. Правила с приоритетами — явный порядок: безопасность > точность > скорость
3. Формат ответа — как структурировать разные типы ответов
4. Few-shot пример — один пример правильного reasoning на граничном кейсе
До этого системник был одним большим текстом. Это работало для простых задач, но агент регулярно «забывал» правила при длинных цепочках рассуждений.
Структура с явными секциями убрала несколько классов галлюцинаций — особенно на кейсах вида «пользователь просит сделать X, но правильный ответ — отказать и объяснить почему».
Think → Plan → Act
Ещё один паттерн который стоит документировать.
Раньше агент сразу начинал вызывать инструменты. Это нормально для простых задач, но для многошаговых — агент иногда «застревал» в середине, потому что не строил план заранее.
Добавил в системный промпт обязательную фазу планирования перед выполнением:
Before using tools, write a short plan: THINK: [what information do I need?] PLAN: [step 1 → step 2 → step 3] ACT: [execute plan]
На практике это снизило количество ситуаций «агент три раза вызвал один и тот же инструмент с одними и теми же аргументами» примерно в два раза. Планирование форсирует явное представление цели перед действием.
Что дальше?
Работа с форматами: Excel/CSV/DOCX не как «прочитать как текст», а нормальный парсинг с сохранением структуры. Плюс history compaction при переполнении контекста — сейчас при длинном чате модель просто начинает терять начало.
Cкачать можно здесь бесплатно и без VPN - https://dokaai.ru
Вопросы по архитектуре и баги — в комментарии.
