Привет, на связи Лука.

Знаете, есть такая поговорка: "тише едешь – дальше будешь". Работая с LLM, я пришёл к выводу, что аккуратность и точность в подаче контекста – это один из самых важных ключиков к хорошему результату. Иначе получится как в другой поговорке - про дурака и стеклянный орган.

Чего греха таить – все мы пользуемся LLM в различных ситуациях. От генерации бойлерплейта до неожиданного, но изящного решения сложной логики. Ничего такого – очередной инструмент, которым можно, как молотком, забить гвоздь, а можно и... ну, вы поняли.

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

А если как-то по-другому? Может, вручную отбирать файлы? Это мучительно долго, особенно если структура проекта ветвистая. К тому же, легко что-то упустить. Может, архивировать нужное и прикреплять архив? Тоже вариант, но не все LLM-интерфейсы это гладко переваривают, да и предварительная подготовка всё равно нужна.

Когда ты фулстек и горишь идеей быстренько поправить и контроллер, и репозиторий, и сервисный слой, и пару DTO, да ещё и на фронте композиты подправить под новый эндпоинт – вот тут-то и понимаешь, что существующие подходы к подаче контекста LLM напоминают попытку просунуть верблюда в игольное ушко.

Тогда у меня и родилась идея сборщика файлов, которая со временем, как снежный ком, начала обрастать новым функционалом.

Представляю вам projectson – утилиту на Go, которая собирает выбранные файлы вашего проекта в один аккуратный JSON-файл.

Почему именно JSON?

Да всё просто:

  • Компактность: JSON – это текстовый формат, но при должной обработке (о чём ниже) он может быть весьма лаконичным.

  • Удобство для LLM: Большинство современных LLM отлично понимают JSON. Его можно скормить модели как единый файл, что гораздо удобнее, чем вставлять километровые листинги кода в текстфилд, в результате чего весь интерфейс пыхтит и кряхтит в процессе рендера сего действа.

  • Структурированность: JSON сам по себе структурирован, что помогает модели лучше разделять и понимать отдельные фрагменты кода или текста.

Политика максимального сохранения контекстного окна

Одна из главных фишек – это стремление максимально эффективно использовать ограниченное контекстное окно LLM. Для этого предусмотрены:

  • Опции для вырезки "мусора": Можно настроить правила для удаления из файлов определённых участков кода или текста (например, комментарии, ненужные в контексте текущего вопроса блоки).

  • Удаление лишних пробелов и переносов строк (по умолчанию): Автоматически сжимаем содержимое файлов, удаляя множественные пробелы и лишние переносы строк, заменяя их на одиночные пробелы. Это сокращает объём текста без потери семантики (для большинства языков программирования - привет, python). В будущем планируется добавить возможность более тонкой настройки этого процесса.

Таким образом, мы не просто собираем файлы, а предварительно причёсываем их, чтобы LLM получила только самое важное.

Философия и формат конфигурации

Сборка JSON-файла конфигурируется простым YAML файлом, который описывает, что и как собирать.

  1. root (Корневой путь):

    • Тип: String

    • Обязательно: Да

    • Описание: Абсолютный путь к корневой директории вашего проекта.

    • Пример:

      root: "/path/to/your/project" 
      # На Windows:
      # root: "C:\\Users\\YourName\\Projects\\MyProject"
      
  2. include (Что включаем):

    • Тип: List of Strings

    • Обязательно: Нет (По умолчанию сканируется вся root директория, с учётом formats и exclude_patterns)

    • Описание: Список файлов или директорий для включения. Пути указываются относительно root. Каждый элемент может определять путь и режим сбора:

      • "path/to/item": Собирает и путь, и содержимое (режим по умолчанию both). Если item – директория, сканируется рекурсивно.

      • "path/to/item:path": Собирает только путь.

      • "path/to/item:content": Собирает только содержимое.

      • "path/to/directory/*": Собирает файлы непосредственно внутри path/to/directory (нерекурсивно), режим по умолчанию both.

      • "path/to/directory/*:mode": Аналогично предыдущему, но с указанным режимом (path или content).

    • Пример:

      include:
        - "src"                   # Включить всё из src/, рекурсивно, путь и контент
        - "README.md:content"     # Только контент из README.md
        - "assets/*:path"         # Только пути файлов из assets/ (нерекурсивно)
        - "docs/api.md"           # Путь и контент docs/api.md
      
  3. formats (Форматы файлов):

    • Тип: List of Strings

    • Обязательно: Да

    • Описание: Список расширений файлов (без точки), которые нужно включать.

    • Пример:

      formats:
        - "go"
        - "vue"
        - "ts"
        - "js"
        - "py"
        - "html"
      
  4. output (Выходной файл):

    • Тип: String

    • Обязательно: Да

    • Описание: Полный путь к выходному JSON-файлу.

    • Пример:

      output: "project_data_output.json"
      # output: "/tmp/my_project_collection.json"
      
  5. exclude_patterns (Паттерны исключения по имени):

    • Тип: List of Strings

    • Обязательно: Нет

    • Описание: Список паттернов для исключения файлов или директорий. Применяются после правил include и formats.

      • Glob-паттерны: Стандартные (например, node_modules, .log, dist/). Метчатся по имени файла/директории или по относительному пути.

      • Регулярные выражения: Go-совместимые, должны быть заключены в слеши (например, /\.git/, /private_.*\.key$/).

    • Пример:

      exclude_patterns:
        - "node_modules"
        - ".git"
        - "*.min.js"
        - "dist"
        - "/test_data/" # Regex для исключения папок с именем test_data
        - "/^\\.(svn|hg|DS_Store)/" # Regex для служебных файлов
      
  6. content_exclusions (Исключения содержимого из файлов):

    • Тип: List of Objects

    • Обязательно: Нет

    • Описание: Список правил для удаления определённых секций внутри файлов перед их добавлением в JSON. Это главное отличие от exclude_patterns, которые работают с именами файлов/папок.

      • type (String, Обязательно): delimiters (по начальному и конечному тегам) или regexp (по регулярному выражению).

      • file_pattern (String, Обязательно): Glob-паттерн, на какие файлы (по расширению или имени файла, если оно уникально как расширение) применять правило. * – для всех файлов, подходящих под formats. Метчится с расширениями (vue) или расширениями с точкой (.vue).

      • start (String, Опционально): Для type: "delimiters". Начальный тег.

      • end (String, Опционально): Для type: "delimiters". Конечный тег.

      • pattern (String, Опционально): Для type: "regexp". Регулярное выражение.

    • Пример:

      content_exclusions:
        - type: "delimiters"
          file_pattern: "*.vue"  # или просто "vue"
          start: "<style>"
          end: "</style>"
        - type: "regexp"
          file_pattern: "*.js"  # или "js"
          pattern: "(//.*)|(/\\*[\\s\\S]*?\\*/)" # Удалить JS комментарии
        - type: "regexp"
          file_pattern: "*" # Для всех типов файлов
          pattern: "SECRET_API_KEY = '.*?'" # Удалить строку с секретным ключом
      

Пример конфигурационного файла projectson_config.yaml:

root: "/Users/username/goprojects/my-awesome-app"
include:
  - "cmd"
  - "internal/services"
  - "internal/models:content" # Только контент моделей
  - "pkg/*:path"              # Только пути из pkg
  - "README.md"
  - "go.mod"
  - "main.go"
formats:
  - "go"
  - "md"
  - "mod"
output: "./collected_project_context.json"
exclude_patterns:
  - "internal/services/legacy_service.go" # Исключить конкретный файл
  - "/.*_test\\.go$/"                      # Исключить все тестовые файлы (regex)
  - "vendor"
  - "*.tmp"
content_exclusions:
  - type: "delimiters"
    file_pattern: "go" # Применить ко всем .go файлам
    start: "// @exclude-start"
    end: "// @exclude-end"
  - type: "regexp"
    file_pattern: "md" # Применить ко всем .md файлам
    pattern: "[\\s\\S]*?"

Не YAML единым: графический интерфейс

  • Config: Здесь вы можете указать основные параметры: root, formats, output, а также управлять списком include (добавлять, удалять, изменять пути и режимы) и exclude_patterns.

  • Exclusions: Эта вкладка посвящена правилам content_exclusions. Вы можете добавлять новые правила, выбирать их тип (delimiters или regexp), указывать file_pattern и соответствующие параметры (start/end или pattern). Каждое правило наглядно представлено, и его легко отредактировать или удалить.

  • Config Docs: Чтобы не держать всю структуру YAML в голове, на этой вкладке вы найдете подробную документацию по всем полям конфигурационного файла. Очень удобно, когда нужно быстро освежить назначение параметра.

  • Preview: Прежде чем запускать полную сборку, вы можете посмотреть, какие именно файлы будут обработаны согласно текущей конфигурации. На этой вкладке отображается список файлов. Выбрав файл, можно увидеть его оригинальное содержимое и то, как оно будет выглядеть после применения правил content_exclusions. Это помогает отладить правила исключений и убедиться, что в итоговый JSON попадет только нужное.

  • Run: Сборка JSON. Здесь вы видите краткую сводку текущей конфигурации и кнопку для запуска. После завершения можно сохранить результат.

  • Stats: После каждого запуска (или успешного Preview) здесь собирается статистика: количество обработанных файлов, общий размер выходных данных, время выполнения. Также отображается распределение файлов по форматам, режимам сбора и размерам (на основе данных из последнего Preview), и активные правила content_exclusions.

Что дальше?

Пилите YAML-конфиги и скармливайте результат нейросети. Сохраняйте и переиспользуйте свои конфиги.

Aistudio отлично читает такие JSON'ы. Копировать код пока приходится ручками - но есть задумки на получение от LLM ответа в виде JSON-патча.

Приглашаю вас на GitHub проекта. Предложения приветствуются.

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


  1. siberianlaika
    14.05.2025 10:33

    Repomap №... :) Для парсинга кода с отсечением лишнего в подобных проектах часто обращаются к treesitter (см. например https://aider.chat/2023/10/22/repomap.html). На Go к нему есть бинды. Однако, утилит делающих repomap на Go + treesitter до сих пор не встретил. Сам использую https://github.com/wyattcupp/codesnap, там тоже без treesitter. Жду, кто бы прикрутил это вместе :)


    1. zartdinov
      14.05.2025 10:33

      Уже не первый пост встречаю запаковки кода, а вы не пробовали https://repomix.com?


  1. zartdinov
    14.05.2025 10:33

    Зависит от модели, для лучшего результата советуют свои форматы:
    Claude - XML
    ChatGPT - Markdown
    Gemini - любой вроде


  1. worldown
    14.05.2025 10:33

    Подскажите. Правильно ли я понял суть. Структурно упаковать проект для того чтобы скормить его LLM. Что это дает? Чем такой подход лучше, нежели использования copilot llm в IDE?


    1. looqey Автор
      14.05.2025 10:33

      Не использовал копилот, не знаю, как он питается контекстом проекта. Но вот курсор мне совсем не понравился (не говоря о том, что я слишком прирос к IDEA), он не есть контекст нормально, сколько его ни тыкай, а на тыканье уходит время.

      К тому же, aistudio меня более чем устраивает по качеству ответов и удобству испольжования, а оплачивать API по ряду причин я не готов.

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


  1. Ankalim
    14.05.2025 10:33

    Как идея можно завернуть утилиту в mcp сервер и будет прям оч полезно.


    1. looqey Автор
      14.05.2025 10:33

      Отличная идея, попробую заняться