
Продолжение первой статьи
Пару недель назад я выложил Доку — локального 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 по паттерну:
Роль и границы — кто агент, что он делает, чего не делает
Правила с приоритетами — явный порядок: безопасность > точность > скорость
Формат ответа — как структурировать разные типы ответов
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
Вопросы по архитектуре и баги — в комментарии.
Комментарии (10)

babikov Автор
07.04.2026 05:33Код закрыт не из-за идеологии. Из-за ресурсов.
Поддерживать открытый репозиторий — это работа. Это не «выложил и забыл». Это issues которые надо разбирать, pull request'ы которые надо ревьюить, документация которую надо писать, публичная архитектура которую не стыдно показывать.
Дока это инди-проект. Один разработчик. Выходные. У меня нет команды которая будет отвечать на вопросы в issues и объяснять почему я выбрал именно такую структуру промпта.
Когда я открою код — я хочу сделать это правильно. Не «вот каша из 800-строчных файлов, разбирайтесь сами», а нормальная документация, нормальная архитектура, нормальный onboarding для контрибьюторов. Пока у меня нет на это времени — я не буду делать вид что есть.

Zailox
07.04.2026 05:33Тоже делал подобный проект, но cli-only.
Хотел спросить, как:
Реализовали "прогон" файлов? (Да и "переключение роли"). Я пытался дать модели системник "ты можешь использовать тег \<search\>, после чего тебе от имени пользователя вернётся содержимое сайта/поиска" (и то же самое с "agent-to-agent". Но получилось плохо.
Как работает tools-calling? Просто перехват в начале сообщения тега, потом внедрение как то в контекст "ответа" от тула, и продолжение ответа?
Будет ли на Linux? На андроид? (Как pocketpal)

babikov Автор
07.04.2026 05:331. Теги галлюцинирует модель, использую вместо них THINK→PLAN→ACT планирование и структурированный JSON для tool calls, на граничных кейсах structured errors с подсказками, переключение ролей не делаю.
2. Qwen не имеет native function calling, парсим JSON который пишет модель, выполняем инструмент, возвращаем результат, по 6–12 итераций на задачу, медленнее чем native calling но работает, сложность не в самих calls а в обработке кейсов когда модель галлюцинирует.
3. Linux технически возможен но GPU поддержка вариативна и один разработчик, 2-3% рынка, не стоит — Android совсем нет, 262K контекст это 500MB+ на мобиле и батарея, может быть лёгкая версия с маленькой моделью но это другой продукт.

Zailox
07.04.2026 05:33Ну, с учётом того, на каком уровне нынешние флагманы (24 оперативы, так ещё и NPU) реализовать возможно (но согласен, морочиться с sdk от qualcomm такое себе удовольствие)
А на Линукс как по мне с драйверами всё даже лучше. Мне было легче прописать "apt install cuda-tools", чем чистить десятки гигов и ставить куду на диск D:.

alex_bobylkin
07.04.2026 05:33Скачалась модель и всё зависло на проверке совместимости. Windows 11, Intel U9-285H, A140T, NPU, ОЗУ 32Gb. Не судьба?
x89377
Спасибо за статью,
есть возможность в настройках присоединиться к llama на другом компьютере через http (c моделью Gemini например) ?
babikov Автор
В следующем обновлении это уже запланировано.