Я поломался, поломался — и поломался на осколки. Признаю́: железные помощники Т9 действительно могут приносить пользу в разработке. Единственное, что мне не нраЯ поломался, поломался — и поломался на осколки. Признаю́: железные помощники Т9 действительно могут приносить пользу в разработке. Единственное, что мне не нра

Ragex: Гибридный RAG для анализа кода

Я поломался, поломался — и поломался на осколки. Признаю́: железные помощники Т9 действительно могут приносить пользу в разработке. Единственное, что мне не нравилось — то, что весь проект большой и хорошо натренированной модели не скормишь, а значит — неизбежны потери контекста, размывание смыслов и джойсовские галлюцинации.

Я уже давно понял: если мне нужно, чтобы что-то было сделано хорошо, — делегирование отпадает, придётся брать в руки молоток самому. Это касается любых жизненных аспектов: варки борща, замены сантехники, перевода Эдгара Аллана По или Антонио Мачадо на русский, или, там, программирования.

Когда БЯМ научились подключать сторонние MCP-сервера, произошел качественный скачок. Теперь не нужно файнтьюнить модель, можно файнтьюнить буковку «R» из акронима «RAG». Я-то лучше знаю, как правильно извлекать смыслы из моего личного контента. Если речь про код — лучше всего искать правду в AST.

Так и был зачат Ragex — MCP-сервер для семантического анализа кодовых баз с элементами чёрной магии. Проект, понятно, написан на Elixir, потому что ну а на чем еще?

Ragex — это (вроде, довольно успешная) попытка объединить статический анализ кода с векторными представлениями и графами знаний. В результате получается система, которая может ответить на вопросы типа «где у меня функция, которая парсит JSON?» не хуже, чем ваш коллега, который неделю назад это писал, но уже всё забыл.

Занахрена́?

Я задумывал всё это как retrieval engine для передачи в какой-нибудь клод, но, поскольку сам я ассистентами не пользуюсь, в результате получилось решение, подходящее и для подключения к БЯМ, и для помощи (если не замены) обычному LSP.

Три кита, на которых держится Ragex:

  1. Local-first: Никаких внешних API. Всё работает локально. Код не отправляется в облако на растерзание корпоративным серверам. Параноики оценят. Кроме того, использовать БЯМ для извлечения контекста из кода — глупо, когда у нас есть AST. Но простенький локальный семантический поиск я тоже, конечно, прикрутил.

  2. Гибридный поиск: Символьный анализ (AST) + семантический поиск (эмбеддинги) + графы знаний. Это как смотреть в тринокль: один окуляр видит близко, другой далеко, третий вообще смотрит в прошлое.

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

Архитектура

Ragex состоит из нескольких слоёв, каждый из которых старается не испортить работу остальных:

┌─────────────────────────────────────┐ │ MCP Server (JSON-RPC 2.0) │ │ stdio + Unix Socket │ └──────────────┬──────────────────────┘ │ ┌───────┴────────┐ ▼ ▼ ┌─────────────┐ ┌──────────────┐ │ Анализаторы │ │ Graph Store │ │ (AST) │ │ (ETS) │ └──────┬──────┘ └──────┬───────┘ │ │ │ ┌──────┴───────┐ │ ▼ ▼ │ ┌──────────┐ ┌─────────────┐ └──►│ Vector │ │ Bumblebee │ │ Store │ │ (ML Model) │ └──────────┘ └─────────────┘

Компоненты

1. MCP Server
Реализация Model Context Protocol — протокола, который позволяет AI-ассистентам общаться с внешними инструментами. JSON-RPC 2.0 через stdio (для интеграции) и Unix-сокет (для интерактивного использования). Люди, использующие ассистентов могут подключить этот MCP туда, я просто добавил его в свой LunarVim (см. ниже).

2. Анализаторы
Парсеры готовы для Elixir, Erlang, Python (частично), JavaScript/TypeScript (наброски), скоро добавлю раст и го, наверное. Каждый парсер извлекает AST, модули, функции, вызовы. Elixir и Erlang используют нативные парсеры, Python вызывает через порт в питоновский модуль ast, JavaScript использует регулярные выражения (потому что жизнь — боль).

3. Graph Store
Граф знаний на основе ETS (Erlang Term Storage). Узлы: модули, функции, вызовы. Рёбра: :calls, :imports, :defines. Поверх графа работают алгоритмы: PageRank, поиск путей, метрики центральности, детекция сообществ.

4. Embeddings & Vector Store
Локальная ML-модель (по умолчанию — sentence-transformers/all-MiniLM-L6-v2, настраивается в конфигах) через Bumblebee. Генерирует 384-мерные векторные представления для каждой функции и модуля. Косинусная близость для семантического поиска. Всё работает без интернета.

5. Hybrid Retrieval
Reciprocal Rank Fusion (RRF) — алгоритм объединения результатов символьного и семантического поиска. Три стратегии: fusion (RRF), semantic-first, graph-first.

6. Editor System
Безопасное редактирование кода с атомарными операциями, бэкапами, валидацией синтаксиса, форматированием. Поддержка multi-file транзакций и семантического рефакторинга через AST.

Зачем это вообще нужно?

1. Семантический поиск по кодовой базе

Проблема: Вы помните, что где-то была функция для работы с HTTP-запросами, но где именно — забыли. Название тоже не помните. Grep не поможет.

Решение: Семантический поиск, да.

# Подключаемся к Ragex alias Ragex.VectorStore # Ищем функцию {:ok, results} = VectorStore.search("HTTP request handler", limit: 5, threshold: 0.7, node_type: :function ) # Результаты отсортированы по релевантности Enum.each(results, fn {node, similarity} -> IO.puts("#{node.name} (#{similarity})") IO.puts(" File: #{node.metadata.file}") IO.puts(" Line: #{node.metadata.line}") end)

2. Анализ зависимостей и вызовов

Проблема: Нужно понять, откуда вызывается функция process_data/2, и что она сама вызывает. Рефакторинг без такой информации — русская рулетка (которая в запутанных случаях с макросами — до сих пор лучше удается^W удавалась эвристикам из навороченных IDE, чем БЯМ).

Решение: Граф вызовов.

alias Ragex.Graph.Store alias Ragex.Graph.Algorithms # Найти все функции, которые вызывают process_data/2 callers = Store.get_callers({:function, "MyModule.process_data/2"}) IO.puts("Callers:") Enum.each(callers, fn caller -> IO.puts(" - #{caller.id}") end) # Найти все функции, которые вызывает process_data/2 callees = Store.get_callees({:function, "MyModule.process_data/2"}) IO.puts("\nCallees:") Enum.each(callees, fn callee -> IO.puts(" - #{callee.id}") end) # Найти все пути между двумя функциями (с лимитом) paths = Algorithms.find_all_paths( {:function, "MyModule.start/0"}, {:function, "MyModule.process_data/2"}, max_depth: 10, max_paths: 100 ) IO.puts("\nFound #{length(paths)} paths")

Защита от плотных графов: Если у узла >10 рёбер, Ragex предупредит о потенциальных проблемах с производительностью. Параметр max_paths предотвращает зависание на экспоненциальных взрывах. Не вижу проблемы дать этот параметр на откуп разработчику.

3. Поиск бутылочных горлышек в архитектуре

Проблема: Какие функции являются критичными для всей системы? Если они упадут — всё рухнет.

Решение: Betweenness Centrality (метрика центральности по посредничеству — если кто знает, как перевести менее коряво, свистните, пожалуйста).

alias Ragex.Graph.Algorithms # Вычислить betweenness centrality для всех функций scores = Algorithms.betweenness_centrality( max_nodes: 1000, normalize: true ) # Отсортировать по убыванию top_bottlenecks = scores |> Enum.sort_by(fn {_node, score} -> score end, :desc) |> Enum.take(10) IO.puts("Top 10 bottleneck functions:") Enum.each(top_bottlenecks, fn {node_id, score} -> IO.puts(" #{node_id}: #{Float.round(score, 4)}") end)

4. Определение (извлечение) архитектурных модулей

Проблема: Код разросся, структура неочевидна. Хотелось бы понять, какие модули логически связаны и образуют кластеры.

Решение: Community Detection (алгоритм Louvain).

alias Ragex.Graph.Algorithms # Найти сообщества (кластеры модулей) communities = Algorithms.detect_communities( algorithm: :louvain, hierarchical: true, resolution: 1.0 ) IO.puts("Found #{map_size(communities)} communities") # Группировка по сообществам grouped = Enum.group_by(communities, fn {_node, community} -> community end) Enum.each(grouped, fn {community_id, members} -> IO.puts("\nCommunity #{community_id} (#{length(members)} nodes):") Enum.each(members, fn {node_id, _} -> IO.puts(" - #{node_id}") end) end)

5. Безопасный рефакторинг

Проблема: Нужно переименовать функцию old_function/2 в new_function/2 во всём проекте. Ручной рефакторинг — это ошибки и страдания.

Решение: Семантический рефакторинг через AST. Да, даже для питона.

alias Ragex.Editor.Refactor # Переименовать функцию во всём проекте result = Refactor.rename_function( :MyModule, :old_function, :new_function, 2, # arity scope: :project, validate: true, format: true ) case result do {:ok, details} -> IO.puts("Success! Updated files:") Enum.each(details.edited_files, &IO.puts(" - #{&1}")) {:error, reason} -> IO.puts("Rollback performed. Error: #{inspect(reason)}") end

Через граф знаний нашли все места, откуда вызывается функция, заменили AST по месту (то есть, работает даже для сгенерированного мюнхгаузен-кода), форматирует результат, валидирует синтаксис, и (!) атомарно применяет изменения (или откатывает всё при ошибке).

6. Multi-File транзакции

Проблема: Нужно одновременно изменить несколько файлов. Если хоть одно изменение невалидно — откатить всё.

Решение: Атомарные транзакции.

alias Ragex.Editor.{Transaction, Types} # Создать транзакцию txn = Transaction.new(validate: true, format: true) |> Transaction.add("lib/module_a.ex", [ Types.replace(10, 15, "def new_version do\n :ok\nend") ]) |> Transaction.add("lib/module_b.ex", [ Types.insert(20, "@doc \"Updated documentation\"") ]) |> Transaction.add("test/module_test.exs", [ Types.replace(5, 5, "# Updated test") ]) # Применить все изменения атомарно case Transaction.commit(txn) do {:ok, result} -> IO.puts("Edited #{result.files_edited} files successfully") {:error, result} -> IO.puts("Transaction rolled back!") IO.puts("Errors: #{inspect(result.errors)}") end

Тут важна атомарность: при любой ошибке — автоматический откат всех изменений, все бэкапы хранятся (в ~/.ragex/backups/<project_hash>/) и т. д.

7. Бонус-трек — экспорт в DOT и D3.js

Интеграция с LunarVim

LunarVim — это мой редактор кода, Neovim на стероидах. Ragex интегрируется через MCP и предоставляет команды для семантического поиска прямо из редактора.

Установка

Скопируйте конфигурационные файлы:

cp ragex/lvim.cfg/lua/user/*.lua ~/.config/lvim/lua/user/

Добавьте в ~/.config/lvim/config.lua:

-- Ragex integration local ragex = require("user.ragex") local ragex_telescope = require("user.ragex_telescope") -- Setup ragex.setup({ ragex_path = vim.fn.expand("~/Proyectos/Ammotion/ragex"), enabled = true, debug = false, }) -- Keybindings (using "r" prefix) lvim.builtin.which_key.mappings["r"] = { name = "Ragex", s = { function() ragex_telescope.ragex_search() end, "Semantic Search" }, w = { function() ragex_telescope.ragex_search_word() end, "Search Word" }, f = { function() ragex_telescope.ragex_functions() end, "Find Functions" }, m = { function() ragex_telescope.ragex_modules() end, "Find Modules" }, a = { function() ragex.analyze_current_file() end, "Analyze File" }, d = { function() ragex.analyze_directory(vim.fn.getcwd()) end, "Analyze Directory" }, c = { function() ragex.show_callers() end, "Find Callers" }, r = { function() vim.ui.input({ prompt = "New name: " }, function(name) if name then ragex.rename_function(name) end end) end, "Rename Function" }, g = { function() ragex.graph_stats() end, "Graph Stats" }, b = { function() ragex.show_betweenness_centrality() end, "Betweenness" }, n = { function() ragex.show_communities("louvain") end, "Communities" }, e = { function() ragex.export_graph("graphviz") end, "Export Graph" }, }

Использование

<leader>r
<leader>r

Пример Workflow

Типичный сценарий работы с Ragex в LunarVim:

1. Открыли проект: <leader>rd (анализировать директорию, по умолчанию — автоматом) 2. Нужно найти функцию: <leader>rs → "database connection" 3. Нашли функцию, открыли файл <Enter> в телескопе 4. Хотим узнать, кто вызывает: <leader>rc 5. Решили переименовать: <leader>rr → "connect_to_db" 6. Проверили статистику: <leader>rg 7. Экспортировали граф для визуализации: <leader>re

Настройка Auto-Analyze

Ragex может автоматически анализировать код при сохранении файлов:

ragex.setup({ auto_analyze = true, auto_analyze_on_start = true, auto_analyze_dirs = { "/path/to/project" }, })

Инкрементальные обновления: Ragex отслеживает изменения через SHA256-хеширование и перегенерирует эмбеддинги только для изменённых файлов.

Память

На совсем жиденьких лэптопах я бы не стал пользоваться Ragex.

  • ML-модель: ~400 MB RAM

  • ETS-таблицы: линейный рост, ~400 bytes на узел

  • Эмбеддинги: ~400 bytes на вектор (384 float32)

  • Кеш-файлы: ~15 MB на 1000 сущностей

Настройка

Поддержка пользовательских Embedding-Моделей

Ragex поддерживает 4 предконфигурированных модели, но не для смены на лету:

# config/config.exs config :ragex, :embedding_model, "sentence-transformers/all-MiniLM-L6-v2" # Альтернативы: # - "sentence-transformers/all-MiniLM-L12-v2" (больше точность) # - "sentence-transformers/paraphrase-MiniLM-L3-v2" (быстрее) # - "sentence-transformers/multi-qa-MiniLM-L6-cos-v1" (для Q&A)

Миграция моделей:

mix ragex.embeddings.migrate --from old_model --to new_model

File Watching

Автоматическое переиндексирование при изменении файлов:

# Через MCP {"method": "tools/call", "params": { "name": "watch_directory", "arguments": {"path": "/project/lib"} }} # В коде Ragex.FileWatcher.watch("/project/lib")

Экспорт Графа для Визуализации

alias Ragex.Graph.Algorithms # Graphviz DOT format {:ok, dot} = Algorithms.export_graphviz( color_by: :betweenness, include_communities: true ) File.write!("graph.dot", dot) # Рендерим через Graphviz System.cmd("dot", ["-Tpng", "graph.dot", "-o", "graph.png"]) # D3.js JSON format (для веб-визуализации) {:ok, json} = Algorithms.export_d3_json(include_communities: true) File.write!("graph.json", json)

Ух, обожаю картинки и D3.js-диаграммки, которые можно пошевелить мышкой.

Ограничения и подводные камни

Потому что честность — моё третье «я»:

  1. JavaScript/TypeScript анализатор: Использует регулярные выражения. Работает для «простых» случаев. Когда-нибудь, может быть, руки дойдут.

  2. Семантический рефакторинг: Пока только для Elixir. Erlang/Python/JS в планах, но не сегодня.

  3. Плотные графы: Если у функции >100 вызовов, поиск путей может занять вечность. Используйте max_paths и max_depth.

  4. Память: 400 MB для модели — это цена локального ML. Если RAM критична, можно отключить эмбеддинги (но зачем тогда вообще Ragex?).

  5. Cold start: Первая генерация эмбеддингов занимает время. После этого — кеширование спасает.

В общем

Ragex — это попытка сделать анализ кода менее болезненным и более семантическим. Граф знаний + векторные эмбеддинги + алгоритмы на графах = инструмент, который может ответить на вопросы типа «где это используется?», «что это делает?», «почему всё сломалось?». А еще это MCP-сервер, который можно подключить к вашему ассистенту кода, чтобы тому тоже было проще ответить на эти вопросы.

MCP-протокол, потому что AI-ассистенты — будущее (или настоящее, в зависимости от того, насколько вы параноик).

Интеграция с LunarVim превращает рефакторинг в нечто почти приятное. Семантический поиск работает, граф не врёт, рефакторинг никогда не ломает код. В теории.


P. S. Если возникло желание попробовать, скачайте проект, скачайте зависимости, запустите ./start_mcp.sh и наслаждайтесь. Если что-то сломается — issue в GitHub. Если всё заработало — это, конечно, тоже можно написать в issue, но кто так делает?

P. P. S. Проект open-source, лицензия MIT. Делайте что хотите, на свой страх и риск. Автор не несёт ответственности за потерянное время, сломанный код и экзистенциальные кризисы, вызванные чтением чужих графов вызовов.

Репозиторий: https://github.com/am-kantox/ragex

Источник

Отказ от ответственности: Статьи, размещенные на этом веб-сайте, взяты из общедоступных источников и предоставляются исключительно в информационных целях. Они не обязательно отражают точку зрения MEXC. Все права принадлежат первоисточникам. Если вы считаете, что какой-либо контент нарушает права третьих лиц, пожалуйста, обратитесь по адресу service@support.mexc.com для его удаления. MEXC не дает никаких гарантий в отношении точности, полноты или своевременности контента и не несет ответственности за любые действия, предпринятые на основе предоставленной информации. Контент не является финансовой, юридической или иной профессиональной консультацией и не должен рассматриваться как рекомендация или одобрение со стороны MEXC.