Локальный ИИ-агент: почему модель не должна решать сама

Локальный ИИ-агент: почему модель не должна решать сама

Gemma 3 поставила файлу AC-X200_manual.pdf дату 27 октября 2024 года. В самом документе нет ни одной даты. Модель ответила с уверенностью 0.95, потому что ей нужно было что-то ответить, а выдуманная дата в имени файла хуже, чем её отсутствие.

Этот случай перевернул архитектуру Dropzone Zero, локального агента, который раскладывает папку загрузок по местам сразу после скачивания. Его написал разработчик под ником scs0209 для Hacktoberfest Open-Source AI Challenge, и кроме рабочего приложения он выложил замеры: 63 синтетических теста, сравнение трёх моделей, эксперимент с LoRA и честный список того, что сломалось при ежедневном использовании. Ценность здесь не в том, что Gemma умеет читать квитанции, а в том, как устроена страховка от её ошибок.

Что такое Dropzone Zero

Dropzone Zero это локальный агент для macOS, который раскладывает каждый новый файл в папке загрузок по заранее заданным правилам: определяет тип документа, достаёт напечатанную в нём дату, придумывает имя и перемещает файл в нужную папку. Модель здесь только читает и предлагает, а решение принимает обычный код. Работает через Ollama с моделью gemma4:e4b, без обращения к облаку.

Маленькая модель уверенно врёт про даты

Первая версия просила у Gemma весь ответ целиком: тип, имя, папку и дату. Автор прогнал Gemma 3 и Gemma 4 на одних и тех же 24 демо-файлах и прочитал каждый ответ вручную. С типами документов всё было более или менее нормально. С датами начался цирк.

В AC-X200_manual.pdf Gemma 3 нашла 27 октября 2024 года при уверенности 0.95, хотя внутри нет ни одной даты. Файлам notes.txt и router_quick_start.pdf она приписала 1 января 2024 года, тоже с уверенностью 0.95. Gemma 4 датировала скриншоты IMG_2034.png, IMG_2035.png и IMG_2036.png годами 2034, 2035 и 2036, явно вытащив цифры из имени. Итого пять выдуманных дат на 24 файла.

Вывод, который сделал автор, звучит как принцип проектирования: маленькая модель не знает, чего она не знает, поэтому нельзя позволять ей заполнять поля, которых в документе нет. Теперь Gemma возвращает строгий JSON по схеме Zod: тип из фиксированного списка, дата, напечатанная в документе, короткий заголовок и уверенность. Папку и имя собирает код, а дата проходит дальше только если она реально встречается в тексте в любом человеческом формате, будь то 2026-10-14, Oct 14, 2026 или 2026년 10월 14일.

Отдельная находка касается режима размышлений. Gemma 4 по умолчанию рассуждает перед ответом, и прогон 30 файлов занимал пять с половиной минут. С флагом think: false тот же прогон уложился в 52 секунды и дал те же ответы. Для задачи «прочитай документ и назови его тип» цепочка рассуждений оказалась чистыми накладными расходами.

Модель предлагает, решает код

Второй слой защиты автор называет guard. Это детерминированный модуль, который выносит по каждому перемещению один из трёх вердиктов: разрешить, спросить, заблокировать. Разрешение выдаётся, если файл остаётся внутри папки, уверенность классификации не ниже 0.5 и это не программа и не установщик. Вопрос ждёт, если классификация не удалась после повтора, уверенность ниже 0.5, файл похож на установщик или исполняемый файл, либо за один проход набирается больше 20 перемещений (тогда спрашивают один раз за всю пачку). Блокировка срабатывает жёстко: недокачанный файл, цель за пределами папки, источник не является обычным файлом, смена расширения при перемещении, скрытая папка в качестве цели, символическая ссылка где-либо на пути. Такое не выполняется даже с разрешения пользователя.

Третий слой это harness, то есть код, который реально трогает диск. Он не доверяет ничему, что пришло до него, включая собственный план: заново проверяет каждый путь, перемещает файл только если его хеш всё ещё совпадает с превью, никогда не перезаписывает существующее и пишет в журнал запись до и после переименования. Журнал нужен потому, что крышка ноутбука может закрыться посреди операции. Откат читает этот журнал и возвращает ровно то, что было перемещено, отказываясь трогать файл, который пользователь отредактировал после уборки.

Четвёртый слой оказался неожиданным. Приложению помогает TabPFN, маленькая табличная модель, которая предсказывает вероятность того, что конкретное перемещение будет отменено. Обучается она на истории самого пользователя: отмена одного файла записывает отклонённое перемещение, а перемещения, которые никто не тронул, считаются принятыми. После десятка решений модель за один прямой проход оценивает новые перемещения и отправляет сомнительные в категорию «спросить». Работает это только на метаданных (расширение, тип, размер, возраст), имена и содержимое файлов в модель не попадают.

Плюс отдельный файл .dropzoneignore держит избранные файлы вне всей системы: их не читают, не показывают модели, не индексируют и не перемещают. Для папки, где лежат выписки из банка, сканы паспорта и договоры, это дешевле любого шифрования.

Дообучение проверили и не стали внедрять

Самый интересный кусок работы начинается там, где большинство проектов заканчивают: автор решил выяснить, даст ли дообучение лучше необученной локальной модели. Для этого он собрал 180 синтетических размеченных файлов, 117 на обучение и 63 на тест: корейские документы, странные форматы дат, имена, вводящие в заблуждение, пары «квитанция против счёта», которые легко перепутать.

Первый прогон оценки нашёл настоящий баг. Имена файлов проходили через slug-функцию, которая оставляла только ASCII, поэтому у каждого корейского заголовка получалась пустая строка и тип падал в «другое». После починки точность Gemma выросла с 85.7% до 98.4%.

Дальше был эксперимент с Tinker: LoRA на Qwen/Qwen3.5-4B, ранг 16, три эпохи, батч 16, скорость обучения 2e-4, на тех же 117 промптах, собранных кодом самого приложения, с лоссом только на ответе. Лосс упал с 0.24 до 0.0005. Ответы на 63 тестовых промпта оценивал тот же пайплайн, что и в реальной работе.

Модель (63 теста)ТипДатаБитый выводp50
gemma4:e4b локально в Ollama98.4%100%0%3.6 с
Qwen3.5-4B на Tinker, без обучения100%92.1%0%7.8 с
Qwen3.5-4B на Tinker, после LoRA100%100%0%7.8 с

LoRA действительно вылечила все даты, которые базовая модель пропускала: на банковских выписках Qwen хватала дату транзакции вместо даты выписки. Заодно модель научилась замолкать после JSON, и длина ответа упала с 282 до 150 символов. У Gemma единственная ошибка была в том, что оплаченный счёт она назвала счётом, а не квитанцией.

И всё равно приложение осталось на необученной Gemma. Причина не в результате, а в постановке задачи: прочитать один документ и назвать его тип с датой это короткая, хорошо определённая работа, где улучшать особо нечего. Дообученная модель забирала бы пару процентных пунктов ценой выхода файлов с ноутбука. Автор отдельно оговаривает, что гонка по задержке была нечестной: Gemma работала на его Mac, а Qwen отвечала по сети.

Честный вывод из таблицы касается тестового набора, а не моделей. Когда три варианта укладываются между 98% и 100%, набор слишком простой, чтобы их различить. Это редкий случай, когда автор эксперимента признаёт, что его измерительный инструмент слабее вопроса, на который он отвечает.

Про Tinker стоит добавить отдельно, потому что это меняет представление о пороге входа. Весь эксперимент уложился в один Python-скрипт на 108 строк: создать клиента, вызывать forward_backward и optim_step на каждом батче, сохранить веса, сэмплировать из обученной модели. GPU автор не трогал вообще, а написание оценки заняло больше времени, чем написание обучения.

Что сломалось в реальной эксплуатации

К моменту, когда автор начал пользоваться приложением каждый день, у него было 142 юнит-теста, и все они проходили. Большинство багов при этом оставались живыми, потому что тесты проверяли код, а не поведение. Первый прогон на собственной папке показал 39 файлов из 55 в категории «другое»: список типов был слишком узким, и после добавления документов, таблиц, презентаций и изображений «другое» сжалось до 8.

Дальше пошло по мелочи, и каждая мелочь была неприятной. Два наблюдателя за папкой гоняли друг друга: забытый в терминале dz watch успевал переместить файл первым, а демон приложения падал с «файл исчез» и молчал. Накопившийся архив блокировал всё: при более чем 20 старых файлах режим автоуборки упирался в лимит пачки на каждом проходе и не трогал даже свежую загрузку. Кнопка отката не работала, потому что спрашивала подтверждение через window.confirm(), а WebView, который использует Tauri на macOS, такой диалог не показывает и тихо возвращает false. Откат умел отменять сам себя: возвращённый в загрузки файл выглядел как новая загрузка, и наблюдатель через пару секунд убирал его обратно.

Правила жили в таблице внутри TypeScript, поэтому перенос папки для квитанций означал правку кода. Теперь есть .dropzonerules.json, но правило задаёт только путь: guard всё равно проверяет каждое перемещение, а правило, ведущее за пределы папки, останавливает прогон с сообщением вместо тихого игнорирования. Напоминания об оплате счетов сломались на удалении события: Calendar выдаёт импортированному событию новый UID, и удаление возвращало removed: 0, пока автор не начал хранить свой идентификатор в заметках события.

Поиск по файлам тоже пришлось приземлять. На демо-данных правильные ответы начинались с оценки 0.39, а на реальной папке поиск выдавал пять результатов на любой запрос и подсовывал посторонние фотографии с 0.31. Порог 0.35 отсечки решил проблему.

Приватность как архитектурное решение

Вход этого агента это папка загрузок, где лежат банковские выписки, квитанции с номерами карт, документы, договоры и фотографии. Отправлять это в закрытый API каждый день автор не захотел, и открытые веса здесь не идеология, а условие работы: файлы не покидают ноутбук, приложение работает без интернета в самолёте и не берёт денег за файл. Побочный эффект оказался важнее: каждый элемент цепочки можно проверить, включая то, какие именно даты выдумывает каждая версия Gemma.

Согласования при этом доходят до телефона, и дверь в Mac для этого не открывается. Через CIBA от Auth0 вопросы guard приходят пушем в Guardian: «разрешить» отправляет установщик на место, «запретить» оставляет его лежать. Что ответят первым, Zee или телефон, то и решает. Пуш содержит только текст вида «Dropzone Zero: move 1 installer», без имени файла, демон делает исключительно исходящие запросы, а согласие освобождает ровно одно удержанное перемещение через тот же guard. В тесте удержанный установщик уехал через 13 секунд после нажатия «разрешить».

Ограничения автор перечисляет сам. Сканы без текстового слоя классифицируются только по имени и размеру, а фотография счёта не получает напоминания об оплате, потому что срок берётся из текста. Мелочь вроде «файл ещё качается» решается блокировкой, а вот «файл выглядит как счёт, но это не счёт» остаётся вероятностной зоной, где помогает только вопрос пользователю.

Что забрать в своего агента

Главный урок Dropzone Zero не про файлы, а про распределение ответственности. Модель здесь отвечает за понимание содержимого, а всё, что можно проверить детерминированно (путь, расширение, хеш, лимиты), проверяет код. Если перенести это на агента, который ходит в базу или правит конфиги, картина та же: модель предлагает операцию, отдельный слой решает, допустима ли она, а исполнитель сверяет реальность с планом перед каждым шагом.

Второй урок про журнал. Возможность отката должна проектироваться вместе с операцией, а не добавляться после инцидента: запись до и после изменения, сверка хеша, отказ трогать объект, который изменился после операции. Именно журнал превращает ошибку модели из потери данных в неправильную папку, которую возвращают одним нажатием.

Третий урок про измерение. Автор сначала собрал набор для оценки, потом нашёл на нём баг в собственном коде, потом проверил гипотезу о дообучении и отказался от неё, потому что разница не окупала потерю локальности. Порядок здесь важнее результата: сначала измерительный инструмент, потом улучшение. И четвёртый урок бытовой: 142 зелёных юнит-теста не значат, что приложение работает, потому что диалог, который не показывается в WebView, тестом не поймать.

Часто задаваемые вопросы

Нужно ли дообучать модель, чтобы агент аккуратно раскладывал файлы?

В этом проекте нет. Необученная Gemma 4 дала 98.4% по типу документа и 100% по дате на 63 тестах, а LoRA на Qwen3.5-4B подняла точность дат с 92.1% до 100%, но потребовала бы отправлять файлы в сеть. Дообучение имеет смысл, когда задача требует специфического формата ответа или домена, а не когда базовой модели не хватает пары процентов.

Как работает откат, если агент уже переместил файлы?

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

Можно ли построить такого же агента на облачной модели?

Технически можно, архитектура guard и harness от модели не зависит. Но весь смысл конструкции в том, что чувствительные файлы не покидают машину: в загрузках лежат выписки и документы. Если данные не чувствительные, облачная модель даст более высокое качество понимания, а журнал и вердикты останутся такими же.

Итог

Dropzone Zero это пример того, как локальный ИИ-агент становится надёжным не за счёт размера модели, а за счёт разделения ролей: Gemma читает и предлагает, guard выносит вердикт, harness сверяет реальность с планом и пишет журнал. Автор не поленился измерить альтернативу, получил красивую таблицу с LoRA и всё равно оставил необученную модель, потому что задача оказалась слишком простой для усложнений.

Если вы делаете своего агента, который трогает файлы, базы или инфраструктуру, начните с вопроса: что произойдёт, когда модель ошибётся, и сколько будет стоить отмена. Ответ на него важнее выбора модели.

← Все записи