В репозитории было около 11 000 файлов. У агента был инструмент bash и расплывчатая инструкция: «найди, где мы валидируем подписи вебхуков». Он запустил grep -rn "signature" ., получил 600 совпадений в стороннем JS, тестовых фикстурах, CHANGELOG и трёх несвязанных криптографических хелперах, а затем с полной уверенностью указал не на тот файл. Мы наблюдали, как он проделывает это четыре раза подряд с разными формулировками, пока кто-то не произнёс очевидное: grep ищет строки, а агент задавал вопрос о смысле.
Вот вся проблема в одном предложении. grep и rg прекрасно отвечают на вопрос «где встречается именно этот токен». Они бесполезны для «где находится та штука, которая делает X», потому что код, который делает X, почти никогда не содержит слов, которыми вы бы описали X. Функция называется verify_hmac. Вы искали «webhook signature». Нулевое пересечение, и единственный инструмент, к которому агент тянется в первую очередь, не возвращает ничего полезного.
Решение — семантический поиск: получить эмбеддинг кода, эмбеддинг запроса, найти фрагменты, чей смысл ближе всего. Подвох в том, что большинство советов в духе «просто используй эмбеддинги» предполагают, что вы отправите исходники в облачный API и арендуете GPU. Для приватного монорепозитория ни то, ни другое неприемлемо: юристы не дадут добро на загрузку проприетарного кода третьей стороне, а вам не нужен счёт за токены, который растёт с каждой переиндексацией.
Так что вот конфигурация, которую мы реально используем: Octocode, индексирующий большой репозиторий для семантического поиска полностью локально — без GPU, без облачных эмбеддингов, ничего не покидает машину. Вот как именно это работает, какая конфигурация важна и четыре вещи, на которых мы обожглись.
Почему grep не хватает, конкретно
Прежде чем тянуться к эмбеддингам, стоит чётко понять, почему текстовый поиск проваливается для агентов, потому что это говорит, что должна делать замена.
- Несовпадение словаря. Запрос — это намерение («rate limiting»), код — это механизм (
Semaphore,token_bucket,RetryAfter). Общих токенов нет. - Нет ранжирования.
rgвыдаёт все совпадения с одинаковым весом. 600 совпадений — это не ответ; это вторая задача поиска. - Нет структуры. Текстовое совпадение в комментарии, в тесте и в основной функции для
grepвыглядят одинаково. Он не скажет вам, что третье — это определение.
Вместо этого вы хотите: ранжировать по семантической близости, но не теряя то, в чём grep действительно хорош, — точные совпадения идентификаторов. Ответ Octocode — делать и то, и другое и объединять, к чему я ещё вернусь. Сначала — как код вообще приводится к виду, пригодному для поиска.
Как Octocode разбивает и индексирует код
Стандартный RAG режет файлы на текстовые окна фиксированного размера: каждые N символов, с небольшим перекрытием, и эмбеддинг. Для прозы это нормально. Для кода это откровенно плохо: он разрезает функцию на два фрагмента, эмбеддит половину ветки match и теряет тот факт, что метод принадлежит типу.
Octocode сначала разбирает каждый файл с помощью tree-sitter, обходит AST и разбивает по реальным границам символов — функции, методы, классы, модули — а не по произвольным байтовым смещениям. Архитектура такая: парсер на каждый язык извлекает функции, классы, импорты и экспорты, затем идёт пофрагментная обработка всего, что слишком велико для эмбеддинга целиком. Метод, извлечённый изнутри блока impl или класса, несёт имя своего родительского типа в списке символов, так что поиск Suppression.mark_set или Foo.bar напрямую попадает во фрагмент метода.
Покрытие языков — это реальные грамматики tree-sitter, а не эвристики на регулярках. Согласно репозиторию, индексируются как код с полным разбором AST:
| Язык | Расширения |
|---|---|
| Rust | .rs |
| Python | .py |
| TypeScript / JavaScript | .ts, .tsx, .js, .jsx |
| Go | .go |
| PHP | .php |
| C / C++ | .c, .h, .cpp, .hpp, .cc, .cxx, плюс расширения модулей C++20 .cppm, .ixx, .mxx, .ccm, .cxxm |
| Ruby | .rb |
| Java | .java |
| Swift | .swift |
| JSON, Bash, CSS, Lua, Svelte, Markdown | выделенные пути tree-sitter |
Всё остальное, полезное как контекст (yaml, toml, dockerfile, makefile, ini, conf, env, xml, html, sql, rst и прочее), индексируется как текстовые блоки, так что по нему по-прежнему можно искать — просто без семантического извлечения символов.
Параметры разбиения живут в секции [index]:
[index]
chunk_size = 2000 # максимум символов на фрагмент кода
chunk_overlap = 100 # перекрытие между соседними фрагментами
quantization = true # квантизация векторов RaBitQ, ~32x, минимальная потеря качества
require_git = true # по умолчанию индексируется только внутри git-репозитория
chunk_size — это потолок, а не цель: функция в 40 строк, меньше лимита, остаётся целой. Лимит включается только для действительно больших тел, и Octocode уже индексирует крупные классы в Python, TypeScript, C++ и Ruby метод за методом, а не сваливает весь класс в один фрагмент. Смысл всего этого: когда вы позже ищете «функцию, которая настраивает удалённый pull», эмбеддингу подвергается функция — со своим именем и прикреплённым родительским типом, а не окно, которое случайно её пересекает.
Локальные эмбеддинги: без GPU, без облака, без API-ключа
Часть, которую люди считают требующей облака, — превращение кода в векторы. Это не так.
Octocode поставляет local-first стек, построенный на fastembed (под капотом ONNX Runtime). Вы указываете в конфигурации локальную модель с префиксом fastembed:, и эмбеддинги считаются на CPU, на вашей машине:
[embedding]
code_model = "fastembed:jinaai/jina-embeddings-v2-base-code" # 768-мерная, заточена под код
text_model = "fastembed:nomic-ai/nomic-embed-text-v1.5" # текст и документация
Строка модели всегда имеет вид provider:model. Префикс провайдера — это вся логика маршрутизации: fastembed: и huggingface: локальные (без ключа, без сети); voyage:, jina:, google:, openai:, together:, octohub: облачные и требуют соответствующий API-ключ в окружении. Смешивайте, если хотите (локальные эмбеддинги кода, облачные эмбеддинги текста), но для приватного репозитория полностью локальный путь — это и есть цель.
Здесь нет требования GPU и нет кодового пути для GPU: это инференс ONNX на CPU. На современной многоядерной машине с AVX2 это достаточно быстро, чтобы эмбеддер редко становился узким местом; обычно им оказываются дисковый ввод-вывод и разбор tree-sitter. Собственные заметки о производительности из репозитория оценивают локальную индексацию FastEmbed примерно в 45 секунд на 1 000 файлов как грубый порядок величины; ваш результат будет варьироваться в зависимости от размера файлов и числа ядер, так что считайте это иллюстрацией, а не обещанием.
Две честные оговорки:
- Первый запуск скачивает модель. Первый
octocode indexпосле указания моделиfastembed:подтягивает веса ONNX (несколько сотен МБ) в каталог кэша (~/.local/share/octocode/fastembed/в Linux/macOS). Единоразовая трата, после чего всё работает офлайн навсегда. - Локальные провайдеры зависят от платформы и спрятаны за фича-флагами. FastEmbed и HuggingFace требуют сборочных фич
fastembed/huggingface. Сборки по умолчанию включают их там, где они хорошо поддерживаются; на платформах, где это не так, вы откатываетесь на облачного провайдера. Проверьтеoctocode models list fastembed, чтобы увидеть, что реально доступно в вашем бинарнике. Недавняя работа над провайдерами локальных эмбеддингов в 0.17.0 как раз сделала этот список честным относительно того, что и где доступно.
Если вы никогда не настраивали семантический поиск по коду, введение в семантический поиск по коду Octocode покрывает базовые понятия; этот пост предполагает, что вы их уже знаете и хотите продакшен-настройку.
Индексируем репозиторий
С готовой конфигурацией индексация — одна команда из корня репозитория:
cd /path/to/big-monorepo
octocode index
# → Indexed 12,847 blocks across 342 files
Что на самом деле происходит, по порядку: найти файлы (соблюдая правила игнорирования), разобрать каждый через tree-sitter, извлечь символы, разбить по границам AST, локально посчитать эмбеддинг каждого фрагмента и записать всё в колоночное хранилище LanceDB. База данных не лежит в вашем репозитории — она привязана к проекту в системном каталоге данных (~/.local/share/octocode/<project-id>/storage в Linux/macOS). Это держит рабочее дерево чистым и означает, что git clean -fdx не снесёт ваш индекс.
Полезные флаги:
octocode index --force # игнорировать инкрементальный кэш, пересобрать с нуля
octocode index --verbose # видеть прогресс по файлам
octocode stats # сколько блоков кода/текста/документов, узлов графа, устарел ли индекс
octocode stats стоит запускать, когда что-то ощущается не так: он сообщает, совпадает ли ваш HEAD с последним проиндексированным коммитом, так что вы сразу видите, устарел ли индекс.
Инкрементальная vs. полная переиндексация
Последующие запуски octocode index инкрементальны: повторно разбираются и эмбеддятся только изменённые файлы. Каждый прогон индексации завершается проходом оптимизации таблиц, чтобы путь запроса оставался быстрым по мере роста базы (повторная инкрементальная индексация раньше оставляла непроиндексированный хвост, который поиску приходилось сканировать перебором; теперь это делается автоматически).
Когда вы меняете то, как индексируются вещи (новое правило разбиения, другая модель эмбеддингов), инкрементальный режим не станет задним числом переразбивать уже имеющиеся файлы. Смена модели эмбеддингов — главный случай: векторы одной модели несравнимы с векторами другой. После смены code_model или text_model сделайте чистую пересборку:
octocode clear # сбросить проиндексированные таблицы
octocode index # пересобрать с новой моделью
Режим watch: держим индекс живым
Одноразовый индекс устаревает в тот момент, когда кто-то делает коммит. Для агента, который должен отвечать на вопросы о текущем состоянии репозитория, запустите watcher:
octocode watch # авто-переиндексация при изменении файлов
octocode watch --debounce 5 # подождать 5с после последнего изменения перед переиндексацией
octocode watch --quiet
Watcher применяет debounce (настраивается, 1–30 секунд), так что git checkout, затрагивающий 400 файлов, запускает одну переиндексацию, а не 400. Он соблюдает те же правила игнорирования, что и полная индексация, так что правка файла в node_modules его не разбудит.
Если вы запускаете сервер MCP (следующий раздел), есть ещё более чистый вариант: установите mcp_index = true в секции [index], и сервер MCP будет индексировать и отслеживать изменения внутри своего процесса — одна работающая сущность вместо двух. По умолчанию false, что обслуживает существующий индекс только на чтение и предполагает, что вы индексируете отдельно; включайте его, когда хотите, чтобы сервер владел всем жизненным циклом.
Гибридный поиск: семантический и точный, объединённые
Вот часть, которая исправляет исходный провал grep, не выбрасывая единственную сильную сторону grep.
Чисто векторный поиск проигрывает на запросах, насыщенных идентификаторами. Поищите parse_remote, и плотные эмбеддинги с радостью вернут то, что семантически близко к parse_remote, пропустив функцию, буквально названную parse_remote. Ключевой поиск BM25 имеет противоположную беду: он точно ловит точные идентификаторы и промахивается по перефразированным намерениям вроде «функция, которая настраивает удалённый pull».
Octocode выполняет оба на каждом запросе и объединяет их с помощью Weighted Reciprocal Rank Fusion внутри LanceDB. Веса выставлены наружу:
[search.hybrid]
enabled = true
default_vector_weight = 0.7
default_keyword_weight = 0.3
Для насыщенного кодом репозитория, где точные идентификаторы несут много сигнала, склонитесь к ключевым словам. Собственный бенчмарк извлечения из репозитория (127 запросов поиска по коду с эталоном по диапазонам строк, выполненные полностью локально с jina-embeddings-v2-base-code и без реранкера) ясно показывает эффект: переход от разбиения по умолчанию 0.7/0.3 к настроенному на ключевые слова 0.3/0.7 поднял Hit@5 с 0,598 до 0,732 и Recall@10 с 0,671 до 0,807 без дополнительных затрат. Это +22% к Hit@5 от одной строки конфигурации. Для длинных документов склоняйтесь в другую сторону (0.8/0.2), поскольку там доминирует намерение.
Поверх объединения опциональный реранкер переупорядочивает топ-кандидатов:
[search.reranker]
enabled = true
model = "fastembed:jina-reranker-v2-base-multilingual" # локальный реранкер, без ключа
top_k_candidates = 50 # взять столько из векторного поиска
final_top_k = 10 # вернуть столько после реранкинга
Реранкер тоже работает локально, если указать ему модель fastembed:. Это проход cross-encoder по топ-50 кандидатам, который ловит случаи, когда ранжирование первой стадии перепутало порядок. Затраты реальны, но ограничены: он видит только top_k_candidates, а не весь индекс.
Из CLI поверхность поиска прямолинейна:
octocode search "webhook signature verification"
octocode search "auth" "middleware" "session" # мульти-запрос, до 10
octocode search "database connection pool" --mode code # ограничить блоками кода
octocode search "auth" --detail-level signatures # только сигнатуры, компактно
octocode search "authentication refactor" --mode commits # поиск по истории git
Путь --mode commits стоит знать: история коммитов индексируется лениво при первом поиске по коммитам (а не во время octocode index), так что можно спросить «когда мы меняли поток auth» и получить семантически ранжированные коммиты, а не просто git log --grep.
Структурный поиск: когда нужны паттерны AST, а не смысл
Иногда вам вообще не нужна семантика — вам нужны «все .unwrap() в Rust» или «все new Foo() в JS». Это структурный запрос, и текстовый поиск постоянно его проваливает (комментарий, упоминающий .unwrap(), — это не вызов). Octocode оборачивает ast-grep:
octocode grep '$FUNC.unwrap()' --lang rust
octocode grep 'new $CLASS($$$ARGS)' --lang javascript
octocode grep 'console.log($ARG)' --lang javascript --rewrite 'logger.info($ARG)' --update-all
$VAR сопоставляется с одним узлом AST, $$$ARGS — с последовательностью аргументов, а --rewrite делает AST-осознанный рефакторинг на месте. По-настоящему полезное для агентов: LLM стабильно ошибаются в kind узла — они напишут function_declaration для Python, хотя на самом деле это function_definition. Когда паттерн ничего не находит, Octocode пробует постепенно более свободные интерпретации и знает правильный kind для каждого языка, так что почти-попадание агента всё равно возвращает результаты, а не тихий ноль.
GraphRAG: связи, а не только сходство
Семантический поиск находит похожий код. Он не скажет вам, что auth_middleware.rs импортирует jwt.rs, вызывает user_store.rs и подключён к router.rs. Это вопрос графа, и Octocode строит граф во время индексации.
[graphrag]
enabled = true
use_llm = true
Он извлекает рёбра imports, calls, extends и implements для девяти языков из AST, а с use_llm = true добавляет архитектурные связи более высокого уровня (configures, uses, паттерны factory/observer/strategy), обнаруженные LLM. Из CLI:
octocode graphrag get-relationships --node-id "src/auth/middleware.rs"
octocode graphrag find-path --source-id "src/auth/mod.rs" --target-id "src/database/mod.rs"
octocode graphrag overview
В этом разница между «покажи код, похожий на auth» и «покажи всё, что зависит от модуля auth». Для агента, делающего рефакторинг, именно второй вопрос предотвращает поломки.
Есть и более новый, опциональный поворот: расширение извлечения с учётом графа. С [search] graph_expansion = true (и включённым GraphRAG) поиск подтягивает блоки кода из файлов, структурно связанных с вашими лучшими попаданиями, перед реранкингом — так что запрос, попавший в auth-middleware, может вытащить JWT-хелпер, который он вызывает, даже если этот хелпер не совпал с текстом запроса. По умолчанию выключено, и комментарии в коде прямолинейны на этот счёт: проведите A/B на своей собственной оценке, прежде чем доверять, потому что расширение может добавить шума так же легко, как и сигнала.
Подключаем к агенту через MCP
Всё вышеперечисленное выставляется ИИ-ассистентам через встроенный сервер MCP. Это основной способ использования Octocode — агент получает инструменты, а не шелл.
octocode mcp --path /path/to/your/project
Или в конфигурации клиента (Claude Desktop, Cursor, Windsurf, Claude Code):
{
"mcpServers": {
"octocode": {
"command": "octocode",
"args": ["mcp", "--path", "/path/to/your/project"]
}
}
}
Инструменты, которые видит агент, сверены с сервером:
| Инструмент MCP | Что делает |
|---|---|
semantic_search |
Гибридный семантический + ключевой поиск, мульти-запрос, все режимы, включая commits |
view_signatures |
Структура файлов (сигнатуры, классы, импорты) по glob, без чтения файлов целиком |
structural_search |
Сопоставление паттернов AST через ast-grep, с восстановлением по kind |
graphrag |
Запросы связей: search, get-node, get-relationships, find-path, overview |
view_signatures — незаслуженно недооценённый. Вместо того чтобы агент сжигал контекст, читая три файла по 800 строк ради сигнатуры функции, он запрашивает сигнатуры по glob и получает скелет. (В недавних версиях можно передать одну строку glob, а не только массив — мелочь, которая остановила множество некорректных вызовов инструментов.)
Для монорепозитория с множеством подпроектов есть мульти-репо режим: octocode mcp --multi --path /workspace сканирует непосредственные подкаталоги в поисках git-репозиториев и обслуживает их все из одной точки, причём каждый инструмент получает аргумент project для выбора цели. Один сервер MCP, все репозитории воркспейса.
Четыре вещи, на которых мы обожглись
Реальные, не гипотетические — это то, что стоило нам времени.
1. Индекс живёт вне репозитория, и это всех запутало. Когда кто-то впервые запустил агента на свежем клоне, индекса не было: база данных привязана к проекту в ~/.local/share/octocode/, а не в рабочем дереве. Клонировать ≠ проиндексировать. Решение — одна строка в онбординге: после клонирования один раз запустите octocode index. Очевидно задним числом; вовсе не очевидно в 2 часа ночи.
2. Смена модели эмбеддингов тихо ухудшила результаты. Кто-то сменил code_model, чтобы попробовать другую локальную модель, и не очистил индекс. Новые запросы эмбеддились новой моделью, старые фрагменты — старой, векторы несравнимы, ранжирование — мусор. Векторы разных моделей не живут в одном пространстве. Всегда octocode clear && octocode index после смены модели эмбеддингов. Здесь нет предупреждения, которое вас спасёт — оно просто тихо становится хуже.
3. Разбиение вектор/ключевые слова по умолчанию было неверным для нашего репозитория. Мы неделю работали с 0.7/0.3 (с упором на векторы) и постоянно промахивались по запросам точных идентификаторов. Код насыщен идентификаторами; бенчмарк выше показывает, что веса, настроенные на ключевые слова, заметно лучше для поиска по коду. Переход на 0.3/0.7 стал самой влиятельной правкой конфигурации, что мы сделали.
4. Бинарные и сгенерированные файлы не индексируются — и это правильно, но проверьте. Octocode пропускает бинарные файлы (он проверяет наличие нулевых байтов и долю печатаемых символов, прежде чем считать содержимое текстом) и соблюдает .gitignore, .git/info/exclude и файлы .noindex. Это ровно то, чего вы хотите — вам не нужны эмбеддинги минифицированных сторонних бандлов. Но если каталог, важный для вас, попал в gitignore (некоторые команды держат в gitignore сгенерированных API-клиентов), он не проиндексируется, и вы будете гадать, почему поиск его не находит. Положите .noindex, чтобы исключить дополнительные пути; проверьте .gitignore, если чего-то ожидаемого не хватает. octocode stats покажет вам количество блоков, чтобы проверить полноту покрытия.
Минимальная сквозная настройка
Собирая всё вместе, вот полный вариант для приватного монорепозитория, всё локально:
# ~/.local/share/octocode/config.toml (или локальное переопределение проекта)
[embedding]
code_model = "fastembed:jinaai/jina-embeddings-v2-base-code"
text_model = "fastembed:nomic-ai/nomic-embed-text-v1.5"
[search.hybrid]
enabled = true
default_vector_weight = 0.3 # код, насыщенный идентификаторами → в пользу ключевых слов
default_keyword_weight = 0.7
[search.reranker]
enabled = true
model = "fastembed:jina-reranker-v2-base-multilingual"
[graphrag]
enabled = true
use_llm = false # связи только из AST, без вызовов LLM, полностью офлайн
cd /path/to/monorepo
octocode index # первый запуск скачивает модели, затем индексирует
octocode search "webhook signature verification" # проверка вменяемости
octocode watch --quiet & # держим живым
claude mcp add octocode -- octocode mcp --path . # подключаем к агенту
Ничто в этом потоке не обращается к сети после единоразовой загрузки моделей. Без GPU. Без счёта за токены. Агент, который делал grep по «signature» и указывал не на тот файл, теперь вызывает semantic_search, получает verify_hmac на первом месте и view_signatures, чтобы подтвердить, прежде чем что-либо трогать.
Часто задаваемые вопросы
Нужен ли GPU? Нет. Локальные эмбеддинги считаются на CPU через ONNX Runtime. Современного многоядерного CPU достаточно; эмбеддер редко бывает узким местом.
Покидает ли какой-либо код мою машину? С моделями fastembed:/huggingface: и graphrag.use_llm = false — нет: индексация, поиск и реранкинг — всё локально. Облачные провайдеры эмбеддингов и функции GraphRAG/сообщений коммитов на LLM подключаются по желанию и, очевидно, требуют ключей.
Где живёт индекс, и находится ли он в моём репозитории? В системном каталоге данных, привязанный к проекту (~/.local/share/octocode/<project-id>/ в Linux/macOS), а не в рабочем дереве. git clean его не тронет; свежий клон его не будет иметь.
Насколько большой становится база данных? Примерно порядка ~10КБ на файл, при квантизации RaBitQ, дающей ~32x сжатие векторов. Репозиторий на 10к файлов — это десятки-сотни МБ, а не гигабайты.
Инкрементальная или полная переиндексация? По умолчанию инкрементальная: повторно эмбеддятся только изменённые файлы. Принудительная полная пересборка — octocode index --force, или octocode clear && octocode index после смены модели эмбеддингов.
Может ли он искать по истории git? Да, octocode search "..." --mode commits. Коммиты индексируются лениво при первом поиске по коммитам, так что начальный octocode index остаётся быстрым.
— Don
Octocode — открытый исходный код под Apache-2.0. Local-first стек появился в 0.15.0, а список провайдеров локальных эмбеддингов — в 0.17.x. Если ваша настройка натолкнулась на грабли, не описанные в этом посте, откройте issue — именно так и были записаны большинство этих советов.



