Как скормить нейросети большой проект и не скормить лишнего: что я узнал, пока агенты читали мой код. claude code.. claude code. llm.. claude code. llm. mcp.. claude code. llm. mcp. Open source.. claude code. llm. mcp. Open source. Repomix.. claude code. llm. mcp. Open source. Repomix. агенты.. claude code. llm. mcp. Open source. Repomix. агенты. искусственный интеллект.. claude code. llm. mcp. Open source. Repomix. агенты. искусственный интеллект. контекст.. claude code. llm. mcp. Open source. Repomix. агенты. искусственный интеллект. контекст. нейросети.. claude code. llm. mcp. Open source. Repomix. агенты. искусственный интеллект. контекст. нейросети. Ненормальное программирование.. claude code. llm. mcp. Open source. Repomix. агенты. искусственный интеллект. контекст. нейросети. Ненормальное программирование. Управление разработкой.

Год назад я копировал файлы в чат с нейросетью по одному и каждый раз забывал какой‑нибудь интерфейс. Сейчас Claude Code и Codex читают проект сами, через MCP‑сервер, который я написал и выложил в открытый доступ. Эта статья не про сам инструмент, а про то, что выяснилось по дороге: почему текстовое дерево проекта обходится модели дороже JSON, почему папку build нельзя пропускать по имени, как спрятать пароль и не сломать код вокруг него, и что агент на самом деле делает с контекстом, когда его никто не контролирует. Последнее удивило меня сильнее всего.

Как скормить нейросети большой проект и не скормить лишнего: что я узнал, пока агенты читали мой код - 1

Копипаста

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

Сначала я написал скрипт для своих проектов на C#, потом окно с деревом и галочками, потом это разрослось. Дальше в тексте будут цифры из этого инструмента и из его сравнения с Repomix, самым известным упаковщиком репозиториев. Все замеры лежат в репозитории вместе с коммитами, на которых они сделаны, ссылка в конце.

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

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

Любимое ASCII‑дерево, с которого всё начиналось, обошлось в 2 785 токенов. Markdown‑список с той же информацией уложился в 1 663. Даже JSON со всеми его кавычками и скобками вышел дешевле: 1 802. Всё дело в псевдографике: чтобы показать вложенность, ASCII‑дерево на каждой строке повторяет │ столько раз, сколько уровней над файлом, и токенизатор берёт плату за каждую палочку. Вот четыре строки одного и того же поддерева, в ASCII это 38 токенов:

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

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

    - json/
      - provider.py
      - tag.py
      - __init__.py

С тех пор MCP‑сервер отдаёт дерево в Markdown, а ASCII я оставил для людей: глазами его читать всё‑таки приятнее. Разница в тысячу токенов на одном дереве кажется мелочью, но дерево агент запрашивает в начале почти каждой сессии, и мелочь превращается в постоянный налог.

Папка build, которая не build

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

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

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

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

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

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, тела именованных методов вычищаются, остаются объявления, сигнатуры, поля, свойства и константы вместе со значениями. Вот обычный класс из моего проекта после сжатия:

namespace DevProjex.Application.Services;

public readonly record struct TopFileMetric(string Path, long Tokens);

public sealed class TopFileRanking
{
    private readonly int _capacity;
    private readonly List<TopFileMetric> _items;

    public TopFileRanking(int capacity)
    { }

    public IReadOnlyList<TopFileMetric> Items => _items;

    public TResult[] Project<TResult>(Func<TopFileMetric, TResult> projection)
    { }

    public void Add(string path, long tokens)
    { }

    private sealed class TopFileMetricComparer : IComparer<TopFileMetric>
    {
        public static readonly TopFileMetricComparer Instance = new();

        public int Compare(TopFileMetric left, TopFileMetric right)
        { }
    }
}
Тот же файл до сжатия
namespace DevProjex.Application.Services;

public readonly record struct TopFileMetric(string Path, long Tokens);

public sealed class TopFileRanking
{
    private readonly int _capacity;
    private readonly List<TopFileMetric> _items;

    public TopFileRanking(int capacity)
    {
        if (capacity <= 0)
            throw new ArgumentOutOfRangeException(nameof(capacity));

        _capacity = capacity;
        _items = new List<TopFileMetric>(capacity);
    }

    public IReadOnlyList<TopFileMetric> Items => _items;

    public TResult[] Project<TResult>(Func<TopFileMetric, TResult> projection)
    {
        ArgumentNullException.ThrowIfNull(projection);
        var result = new TResult[_items.Count];
        for (var index = 0; index < _items.Count; index++)
            result[index] = projection(_items[index]);
        return result;
    }

    public void Add(string path, long tokens)
    {
        var candidate = new TopFileMetric(path, tokens);
        var index = _items.BinarySearch(candidate, TopFileMetricComparer.Instance);
        if (index < 0)
            index = ~index;
        if (index >= _capacity)
            return;
        _items.Insert(index, candidate);
        if (_items.Count > _capacity)
            _items.RemoveAt(_capacity);
    }

    private sealed class TopFileMetricComparer : IComparer<TopFileMetric>
    {
        public static readonly TopFileMetricComparer Instance = new();

        public int Compare(TopFileMetric left, TopFileMetric right)
        {
            var tokenOrder = right.Tokens.CompareTo(left.Tokens);
            return tokenOrder != 0
                ? tokenOrder
                : StringComparer.Ordinal.Compare(left.Path, right.Path);
        }
    }
}

Было 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 это не даёт, но агенту труднее принять комментарий вроде «игнорируй предыдущие указания» в чужом репозитории за команду.

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

Отдельный вопрос, нужен ли вообще такой сервер, если у агента есть свои Read, Grep и Glob. Я проверил на двух репозиториях одним и тем же набором задач. На маленьком httpx, около девяти тысяч строк, встроенные инструменты оказались дешевле и ничуть не хуже: 26 600 токенов и 14 вызовов против 32 900 и 16 через сервер, ответы те же. На hono, это 43 тысячи строк, встроенные инструменты нашли 0 из 4 ключевых файлов и в трёх сессиях из четырёх упёрлись в лимит ходов, а через сервер агент нашёл 4 из 4 за 49 300 токенов. Честная формулировка получается такой: на маленьком репозитории каталог инструментов MCP это чистые накладные расходы, на большом он удерживает агента в бюджете, когда встроенные средства до ответа не доходят.

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

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

Первое, что выяснилось: агенты обожают читать файлы целиком. Поиск уже показал нужные двадцать строк с именем объявления, а следующим вызовом агент всё равно просит весь файл. Я разобрал двенадцать таких чтений по одному. Семь оказались рефлекторными: имя объявления у агента уже было в руках, он просто не стал им пользоваться. Три случились из‑за того, что поиск обрезал результаты по алфавиту и нужный файл в выдачу не попал; агент знал только имя файла и прочитал его целиком «на пробу», это самый дорогой класс, около 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.

Четвёртое, и это главный вывод месяца: кто дешевле, решает не инструмент, а форма вопроса. Repomix упаковывает область в один документ и дальше ищет по нему, мой сервер ищет и читает ровно нужные фрагменты. На обзорных вопросах вида «как устроен механизм X» упаковка выигрывала: один раз заплатил, дальше читаешь дёшево. На вопросах с конкретным ответом выигрывало точечное чтение. В одной серии на одном и том же бинарнике это выглядело так: обзорные задачи 82 вызова и 70 825 токенов у меня против 47 и 50 199 у Repomix, точечные 33 вызова и 23 450 токенов против 29 и 50 393. Правильность ответов при этом совпадала на каждой задаче. С тех пор я перестал спрашивать «кто дешевле» и спрашиваю «дешевле на чём».

Финальная серия перед релизом, уже после всех исправлений: 11 задач на восьми репозиториях на шести языках, каждая по два раза с каждым сервером, 22 сессии на сторону. Через мой сервер агент израсходовал 973 020 токенов, через Repomix 1 174 907; обязательных файлов нашёл 85 из 90 против 83; вызовов сделал больше, 490 против 461, и почти весь перебор пришёлся на те самые обзорные задачи. Судья предпочёл ответы через мой сервер в 16 парах из 22. Тут нужны две оговорки. Задачи составлял я сам, и тот же набор потом проверял исправления, так что это бенчмарк разработки, а не независимый. И одинаковые прогоны у меня расходились на 20–25 процентов: одна и та же задача на одном и том же сервере стоила 45 824 токена в одной сессии и 23 299 в следующей. Разница меньше чем в два раза на отдельной задаче ничего не значит, значат только суммы.

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

Фокус задаёт человек

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

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

[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 сессий дешевле, чем неверное значение по умолчанию в релизе.

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

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

  • Одинаковые сессии различаются на четверть. Без размера выборки любая цифра в этой области ничего не стоит, включая мои.

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

Автор: Avazbek22

Источник