Если дать агенту проект как есть, он читает всё подряд: node_modules, .env, файлы целиком, по второму разу. Я год делал инструмент, который отдаёт нейросети только нужное — сначала для чата, потом как MCP-сервер для Claude Code и Codex, — и весь сентябрь замерял, что из моих представлений о «нужном» вообще верно. Подтвердилось немногое. Привычное ASCII-дерево дороже JSON. Список мусорных папок ломает чужие проекты. Подсказки в описаниях инструментов агент не читает. А слепое сравнение с Repomix, самым известным инструментом в этой нише, показало, что победитель зависит от типа вопроса, а не от инструмента. Ниже цифры и что из них следует.

Копипаста

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

Все цифры ниже — с DevProjex, это мой инструмент: окно с деревом и галочками, терминальная версия, CLI и MCP-сервер на одном движке. Замеры лежат в репозитории вместе с коммитами, на которых они сделаны, ссылка в конце.

Терминальная версия: то же дерево, те же фильтры, тот же предпросмотр
Терминальная версия: то же дерево, те же фильтры, тот же предпросмотр

Самый дорогой формат

Дерево проекта — первое, что уходит модели, и я был уверен, что оно ничего не стоит: пара экранов с именами файлов. Форматов у меня со временем стало четыре — ASCII, JSON, XML и Markdown — и выбирал я между ними на глаз. Пока не прогнал дерево Flask через токенизатор. Токенизатор Claude не опубликован, поэтому считал открытым o200k_base от OpenAI.

Формат дерева Flask

Токенов

ASCII

2 785

JSON

1 802

Markdown

1 663

ASCII-дерево, с которого всё начиналось, оказалось самым дорогим, даже дороже JSON со всеми его кавычками и скобками. Причина в псевдографике. Чтобы показать вложенность, ASCII-дерево на каждой строке повторяет │ столько раз, сколько уровней над файлом, и токенизатор берёт плату за каждую палочку. Четыре строки одного и того же поддерева, в ASCII это 38 токенов:

│       ├── json
│       │   ├── provider.py
│       │   ├── tag.py
│       │   └── __init__.py

В Markdown те же строки стоят 24 токена. Обратные слэши — не опечатка, это настоящий вывод, экранирование подчёркиваний:

    - json/
      - provider.py
      - tag.py
      - \_\_init\_\_.py

Теперь сервер отдаёт дерево в Markdown, а ASCII я оставил для людей — глазами его читать удобнее. Тысяча токенов на одном дереве звучит как мелочь, но дерево агент просит в начале почти каждой сессии, так что мелочь повторяется каждый раз.

Папка build, которая не build (Smart Ignore)

Дальше вопрос, что модели не отдавать совсем. Первое решение простое: список bin, obj, node_modules, dist, что совпало — пропускаем. Работает ровно до первого чужого проекта. В JS-монорепозитории папка packages — это и есть весь исходный код. vendor в Go хранит зависимости, а в интернет-магазине так может называться модуль работы с поставщиками. build бывает и результатом сборки, и пакетом с кодом сборщика. Пропусти такую папку по имени — модель не увидит исходники, не узнает, что они вообще были, и будет уверенно рассуждать о проекте без половины кода.

Поэтому у меня имя папки само по себе ничего не решает. Сначала фильтр ищет маркеры стека: package.json, .csproj, go.mod, pom.xml, Cargo.toml и десятки других. Ближайший маркер отвечает за свою часть дерева, и правила его стека действуют только внутри неё. В монорепозитории, где рядом лежат бэкенд на .NET, фронтенд на React и скрипты на Python, у каждой части свои правила, и node_modules фронтенда никак не влияет на соседнюю папку с тем же именем. В DevProjex этот фильтр называется Smart Ignore.

Для спорных имён вроде build, bin или vendor фильтр заглядывает внутрь и ищет признаки сборки: метаданные компилятора или пакетного менеджера, структуру кэша, заголовки скомпилированных бинарников, сгенерированные манифесты. Смотрит ограниченно, по симлинкам не ходит. Если признаков нет — папка остаётся видимой. Так в .NET-проекте прячется bin с результатами сборки, но не папка bin с исходниками. Лишний файл модели не страшен, пропавший модуль — страшен.

С .gitignore похожая история. Простой парсер ломается на вложенных .gitignore, отрицаниях через !, вложенных репозиториях и разной чувствительности к регистру. В итоге правило одно: видно то, что сам Git считает неигнорируемым. Парсер при этом свой, а глобальный core.excludesFile я не читаю, чтобы результат не зависел от настроек конкретной машины.

И тут выяснилось, что сам Git не так безобиден. В чужом репозитории даже git status может запустить программу из его настроек — например, через core.fsmonitor. Поэтому инструмент вызывает Git с отключёнными хуками, fsmonitor и внешними diff-программами, а git.exe, подложенный в папку проекта, не запустит. Про это напишу отдельно, пока только общая часть каждого вызова:

git --no-pager --no-optional-locks
    -c core.fsmonitor=false
    -c core.quotepath=false
    -c core.hooksPath=<пустая папка приложения>
    -c credential.helper=
    -c core.askPass=
    -c log.showSignature=false
    -c submodule.recurse=false
    -c core.attributesFile=<пустой файл приложения>
    -c core.excludesFile=<пустой файл приложения>

Карта вместо кода

Даже после фильтра большой проект в контекст часто не влезает. При этом для половины вопросов модели не нужны тела методов — нужна карта: какие есть классы, что принимают методы, что возвращают, какие константы заданы. Так я пришёл к сжатию. Файл разбирается грамматикой Tree-sitter, тела именованных методов вычищаются, остаются объявления, сигнатуры, поля, свойства и константы со значениями. На C# это выглядит так. До:

public class PriceCalculator
{
    public const decimal TaxRate = 0.21m;
    private readonly List<Order> _orders = new();
    public int MaxRetries { get; set; } = 3;

    public decimal Subtotal => _orders.Sum(o => o.Total);

    public decimal CalculateTotal(bool applyTax)
    {
        var subtotal = Subtotal;
        if (applyTax)
            subtotal *= 1 + TaxRate;
        return Math.Round(subtotal, 2);
    }
}

После:

public class PriceCalculator
{
    public const decimal TaxRate = 0.21m;
    private readonly List<Order> _orders = new();
    public int MaxRetries { get; set; } = 3;

    public decimal Subtotal => _orders.Sum(o => o.Total);

    public decimal CalculateTotal(bool applyTax)
    { }
}

Константа сохранила значение, поле — инициализатор, свойство — значение по умолчанию, однострочный Subtotal остался как был: одна строка — это ещё сигнатура, а не реализация. В Python свои правила. __init__ остаётся целиком, потому что там объявляется состояние объекта, докстринг сохраняется даже над пустым телом, а заглушка — ..., это валидный Python:

class Repository:
    """Stores and finds orders."""

    def __init__(self, connection):
        self.connection = connection
        self.cache = {}

    def find_by_id(self, order_id):
        """Return one order or None."""
        ...

На одном классе из моего проекта было 387 токенов, осталось 181. Пустые тела агент видит не вслепую: в ответе сервера указано, какой уровень детализации применён к каждому файлу, так что он знает, что тело вырезано, а не отсутствует. Безымянные лямбды сжатие не трогает — если опустошить тело без имени, от него ничего полезного не останется. А файл, который не получается безопасно разобрать, уходит целиком.

На целых проектах результат зависит от того, как написан код. Прикладной слой моего проекта на C# сжимается примерно втрое, с 427 до 143 тысяч токенов. Исходники Flask теряют меньше трети: докстринги в Python я сохраняю, для модели докстринг часто полезнее самого кода. Если выкинуть заодно и комментарии, Flask ужимается с 87 до 26 тысяч. А Hono почти не поддаётся — теряет чуть больше пятой части. Причина нашлась быстро: 114 его файлов из 294 — тесты, а тест выглядит как test('...', () => { ... }), то есть та самая безымянная лямбда. Так что цифру «сжимаем в N раз» я никому не обещаю: на тестах она не сходится.

Секреты: значение, а не файл

Любой такой инструмент рано или поздно отдаёт модели .env, и пароль от базы уезжает на чужой сервер вместе с кодом. Самое простое решение — выкинуть файл, в котором нашёлся секрет. Так делает Repomix, и логика в этом есть: рядом с найденным секретом может лежать второй, которого правила не поймали. Но вместе с секретом пропадает и код вокруг, например целый класс настроек, и модели остаётся гадать. Я решил заменять только само значение. Вот что получает агент из файла настроек с паролем от базы и токеном GitHub (ключи, понятно, ненастоящие):

import os


class Settings:
    """Runtime configuration for the shop service."""

    DATABASE_URL = "postgresql://shop_app:DEVPROJEX_REDACTED[credential-uri-password#1]@db.internal:5432/shop"
    GITHUB_TOKEN = "DEVPROJEX_REDACTED[config-secret#1]"
    CACHE_TTL_SECONDS = 300
    SMTP_HOST = "smtp.internal"

    def database_pool_size(self) -> int:
        return int(os.environ.get("DB_POOL_SIZE", "10"))

Модель видит, что база есть, на каком хосте и под каким пользователем, что где-то нужен токен GitHub, и может писать код, который всем этим пользуется. Самих значений у неё нет. Repomix на этом же файле пишет «1 suspicious file(s) detected and excluded», и от файла в упаковке остаётся только имя.

В заглушке есть тип секрета и номер. Одно значение под одним правилом получает один номер во всех файлах, так что модель видит, где используется один и тот же ключ, хотя самого ключа не видела. Замена знает форматы: в строке подключения меняется только пароль, кавычки и соседние поля остаются на месте, и так же в .env, JSON, YAML, XML, Dockerfile, .netrc и .npmrc. Внутри — 221 правило из Gitleaks, перенесённое на .NET, плюс свои правила для .env, строк подключения и Dockerfile. Регулярные выражения без бэктрекинга, у каждого таймаут, всё считается локально, без модели и без отправки кода куда-либо.

Ошибается оно тоже. В первой версии правило для строк подключения приняло за пароль обычное password=auth[1] в коде httpx, заменило его заглушкой и обрезало остаток строки — агент получил синтаксически сломанный файл. Заметил это не я: двое агентов в тестовых прогонах прямо написали, что из-за этого не доверяют отданному коду. Правило я переписал в разборщик, который понимает, где вообще может начинаться значение. Но «безопасным» результат инструмент не называет никогда. Находка значит, что сработало правило. Нет находок — значит, правила ничего не нашли, не больше.

Зачем это, если агент читает сам

Пока я возился с форматами и масками, агенты научились читать проект без меня: Claude Code и Codex сами ходят по папкам, ищут и открывают файлы. Логичный вопрос — зачем тогда сервер. Затем, что агент читает всё, до чего дотянется, включая тот же .env, и часто читает файлы целиком. Через MCP — это протокол, которым агенты подключают внешние инструменты — ему можно отдать тот же движок: фильтр, маскирование, сжатие и поиск, и всё только на чтение. Маскирование секретов в этом режиме выключить нельзя: ни флагом сервера, ни из схемы инструмента. Агент может сузить себе выборку путями, масками или бюджетом токенов, но выйти за выданную ему папку не может, а содержимое файлов приходит в обёртке «это данные, а не инструкции». Полной защиты от prompt injection это не даёт, но комментарий «игнорируй предыдущие указания» в чужом репозитории агенту уже труднее принять за команду.

Подключение агента из окна DevProjex: Live context или обычный режим, потом выбор клиента
Подключение агента из окна DevProjex: Live context или обычный режим, потом выбор клиента

Встроенные инструменты агента сервер не отключает: обычным cat .env агент пароль увидит. Сам .env стоит закрыть в настройках агента, в Claude Code для этого есть permissions.deny. Только секрет редко живёт в одном месте. Он сидит в appsettings.Development.json, в тестовом конфиге, в строке подключения посреди кода. Запрет по имени файла такое пропустит, маскирование значений — найдёт.

И нужен ли сервер вообще, если у агента есть свои Read, Grep и Glob? Я проверил на двух репозиториях одним набором задач:

Репозиторий

Встроенные инструменты

Через DevProjex

httpx, около девяти тысяч строк

26 600 токенов, 14 вызовов, ответы верные

32 900 токенов, 16 вызовов, ответы те же

hono, 43 тысячи строк

нашли 0 из 4 ключевых файлов, в трёх сессиях из четырёх упёрлись в лимит ходов

нашёл 4 из 4 за 49 300 токенов

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

Слепой бенчмарк против Repomix

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

Взял чужие открытые репозитории на разных языках: httpx, hono, Serilog, потом gin, gson, axum, jsoup, click, fmt, MediatR, chi. Написал задачи четырёх видов: найти баг по симптомам, найти точное значение, оценить, что сломается при изменении сигнатуры, разобраться в механизме. Агент получал задачу и два MCP-сервера под именами server_a и server_b, не зная, где чей. Ответы сравнивал отдельный судья, тоже модель, и ей названия тоже не показывали. Модель везде одна, Claude Haiku, чтобы серии были сопоставимы по цене.

Первый результат, который поменял мне картину: что дешевле, зависит от вопроса, а не от инструмента. Repomix упаковывает область в один документ и дальше ищет по нему, DevProjex читает только нужные фрагменты. В одной серии на одном бинарнике:

Тип задач

DevProjex

Repomix

Обзорные: «как устроен механизм X»

82 вызова, 70 825 токенов

47 вызовов, 50 199 токенов

Точечные: конкретный ответ

33 вызова, 23 450 токенов

29 вызовов, 50 393 токена

На обзорных вопросах выигрывала упаковка: один раз заплатил, дальше читаешь дёшево. На вопросах с конкретным ответом — точечное чтение. Правильность ответов при этом совпадала на каждой задаче.

Что агент делает с контекстом на самом деле

Больше итоговых цифр дали логи сессий. Первое, что в них видно: агенты читают файлы целиком даже когда это не нужно. Поиск уже показал нужные двадцать строк с именем объявления, а следующим вызовом агент всё равно просит весь файл. Я разобрал двенадцать таких чтений. Семь — просто по привычке: имя объявления уже было в руках, агент им не воспользовался. Три — из-за того, что поиск обрезал выдачу по алфавиту и нужный файл в неё не попал; агент знал только имя и прочитал файл целиком «на пробу», это самый дорогой случай, около 46 килобайт на три чтения. Ещё два — угадывание по названию папки из дерева. И во всех двенадцати решение принималось сразу после результата предыдущего вызова, без единой строки рассуждений.

Отсюда вывод, до которого я бы сам не додумался: описание инструмента агент читает один раз, при старте сессии, и в момент выбора оно уже ни на что не влияет. Я объяснял в описаниях, что объявление можно прочитать по имени, что несколько диапазонов можно забрать одним вызовом, что не надо читать файл целиком. Эффект нулевой: за весь обзорный прогон чтение по имени объявления использовалось 0 раз из 30 вызовов. А подсказка внутри самого результата, рядом с блоком «в каких объявлениях лежат совпадения», работала. Ещё лучше работал отказ с готовым вызовом: агент по ошибке передал вместо файла папку, сервер вернул ему правильный вызов для этой папки, и агент тут же его выполнил. Теперь все подсказки у меня живут в ответах, а не в описаниях.

Заодно агенты оказались хорошими тестировщиками. Первый же слепой прогон показал, что Claude Code молча не видит один из восьми инструментов. В схему get_file я добавил not, чтобы запретить два взаимоисключающих параметра, — и клиент на такой схеме тихо выбросил инструмент из списка, а агенты в отчётах жаловались на «фантомный get_file», о котором пишут инструкции. Теперь контрактный тест запрещает в схемах not, allOf, anyOf, if, then, else, $ref и const. Там же всплыл и баг маскировщика с password=auth[1].

Со статистикой я тоже промахнулся. Поиск возвращает тело объявления с лучшим совпадением, и встал вопрос, сколько символов отдавать. По распределению размеров 70 процентов объявлений укладываются в 1 800 символов, и логика подсказывала поднять лимит до 3 000, чтобы реже обрезать. Потом я измерил на 36 сессиях — один бинарник, три значения лимита, четыре задачи по три повтора:

Лимит тела

Ходов

Стоимость

Полных ответов

Пропущено обязательных файлов

выключено

173

$1,27

2 из 12

22 из 54

1 800 символов

161

$1,21

5 из 12

14 из 54

3 000 символов

179

$1,38

2 из 12

30 из 54

Вариант 3 000, который на тот момент стоял по умолчанию, проиграл по всем столбцам, включая единственный ответ, который судья счёл неверным. Лишний объём стоил дороже, чем обрезка с подсказкой, какие строки дочитать. Лимит остался 1 800.

Финальная серия

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

DevProjex

Repomix

Токенов за 22 сессии

973 020

1 174 907

Стоимость

$4,07

$4,25

Обязательных файлов найдено

85 из 90

83 из 90

Вызовов инструментов

490

461

Судья: чей ответ точнее

16 пар

3 пары

Вызовов у DevProjex больше, и почти весь перебор пришёлся на те самые обзорные задачи. Остальные три пары судья оставил без вердикта.

Две оговорки. Задачи составлял я сам, и тот же набор потом проверял исправления, так что это бенчмарк разработки, а не независимый. И одинаковые прогоны у меня расходились на 20–25 процентов: одна и та же задача на одном и том же сервере стоила 45 824 токена в одной сессии и 23 299 в следующей. Разница меньше чем в два раза на отдельной задаче ничего не значит, значат только суммы.

Судье я тоже не сразу поверил. Каждую пару ответов он читал в обоих порядках, и вердикт засчитывался только при совпадении. С правильностью он стабилен. А на вопрос «какой ответ лучше» на половине пар отвечал по-разному в зависимости от порядка. К тому же заметная часть его доверия к ответам через DevProjex объяснялась тем, что ответы через Repomix ссылались на номера строк внутри упакованного файла, а не исходного, и судья считал такие ссылки неверными. Это особенность их режима упаковки, которую легко поправить, поэтому счёт судьи я нигде не выношу вперёд.

Фокус задаёт человек (Live Context)

Оставался вопрос, который замерами не решить. Агент не знает, над каким куском проекта я сейчас работаю, и каждую задачу начинает с поиска по всему репозиторию, а я пишу ему словами: «смотри в sessions.py, остальное не трогай». После месяца логов решение показалось очевидным: фокус задаёт человек галочками, а инструмент сообщает агенту границы. В DevProjex это называется Live Context и работает так.

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

Live Context: галочки задают фокус агента, звёздочки показывают, какие файлы он уже получил
Live Context: галочки задают фокус агента, звёздочки показывают, какие файлы он уже получил

В неведении агента не держат, каждый ответ говорит, сколько осталось снаружи. Та же идея — подсказка в результате:

[Live context] focus: 1 of 9 selectable files; 8 outside the focus were not searched; read any of them by name with get_file.

Когда я меняю галочки, агент на следующем же вызове узнаёт, что изменилось с прошлого раза, без перезапуска сессии:

[Live context] changed since revision 14: +2 folders, -1 file

Обратная связь нужна и в другую сторону. Файлы, которые агент получил, помечаются в дереве, а журнал сессии хранит клиента, число вызовов, примерную цену в токенах, список полученных файлов и количество замаскированных значений. Содержимое файлов и сами секреты в журнал не попадают. Это чек: что именно ушло модели.

Журнал агента: сессии и вызовы внутри сессии, с токенами и числом замаскированных значений
Журнал агента: сессии и вызовы внутри сессии, с токенами и числом замаскированных значений

Что я вынес из этого месяца

Короткий список. Ни один пункт я бы заранее не угадал, и они же объясняют, почему инструмент устроен именно так.

  • Что дешевле, упаковка или точечное чтение, зависит от вопроса: обзорные задачи дешевле через упаковку, задачи с конкретным ответом — через точечное чтение.

  • Агент решает читать файл целиком сразу после результата предыдущего вызова. Повлиять на это можно только из результата: описание инструмента он читает один раз.

  • Распределение размеров объявлений поведение модели не предсказало. Предсказали 36 сессий, и они обошлись дешевле, чем неверное значение по умолчанию в релизе.

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

  • На маленьком репозитории встроенных инструментов агента хватает, на большом они до ответа не доходят — там внешний сервер и окупается.

  • Один и тот же прогон на одном и том же сервере расходится по токенам на 20–25%, поэтому одиночные цифры здесь ничего не значат, включая мои. Сравнивать можно только суммы по сериям.

Инструмент, на котором всё это измерено, открыт под Apache-2.0. Полный журнал измерений по сериям — с коммитами, версиями Repomix и дословными вердиктами агентов и судьи — лежит в репозитории в Docs/Benchmark-History.md. Если вы прогоняете агентов на своих репозиториях иначе и получаете другие цифры, мне это интереснее, чем совпадающие.

https://github.com/Avazbek22/DevProjex

Комментарии (6)


  1. Glen5
    09.10.2026 13:03

    Deepseek (щас не знаю, это было пол года назад) ругался на файл и отказывался его загружать, если видел внутри пароли, токены и прочую подобную информацию.


    1. Avazbek22 Автор
      09.10.2026 13:03

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


  1. rrrrex
    09.10.2026 13:03

    Одинаковые сессии различаются на четверть.

    Какой-то поток сознания. Вся статья в том же духе. Как будто написал кривое эссе и раздул нейронкой до статьи.


    1. Avazbek22 Автор
      09.10.2026 13:03

      Имел в виду, что одна и та же задача от прогона к прогону гуляет по токенам примерно на 25%. Поэтому по одному запуску ничего сравнивать нельзя.


  1. stuxnetix
    09.10.2026 13:03

    Я уже давно разработал программу project-merger , даже залил в гитхаб , которая собирает весь проект в единый .md файл (тот который любит ИИ) с опцией Исключить все секреты в проекте. Пользуюсь часто достаточно. Но дорабатывать еще нужно конечно .. времени не хватает.


  1. a1oleg
    09.10.2026 13:03

    Написал концепт по укладке кода в графовую БД и генерацию накопительного контекста с переиспользованием.

    Рекомендую ознакомиться: https://habr.com/ru/articles/1087078/