Я поломался, поломался — и поломался на осколки. Признаю́: железные помощники Т9 действительно могут приносить пользу в разработке. Единственное, что мне не нравилось — то, что весь проект большой и хорошо натренированной модели не скормишь, а значит — неизбежны потери контекста, размывание смыслов и джойсовские галлюцинации.
Я уже давно понял: если мне нужно, чтобы что-то было сделано хорошо, — делегирование отпадает, придётся брать в руки молоток самому. Это касается любых жизненных аспектов: варки борща, замены сантехники, перевода Эдгара Аллана По или Антонио Мачадо на русский, или, там, программирования.
Когда БЯМ научились подключать сторонние MCP-сервера, произошел качественный скачок. Теперь не нужно файнтьюнить модель, можно файнтьюнить буковку «R» из акронима «RAG». Я-то лучше знаю, как правильно извлекать смыслы из моего личного контента. Если речь про код — лучше всего искать правду в AST.
Так и был зачат Ragex — MCP-сервер для семантического анализа кодовых баз с элементами чёрной магии. Проект, понятно, написан на Elixir, потому что ну а на чем еще?
Ragex — это (вроде, довольно успешная) попытка объединить статический анализ кода с векторными представлениями и графами знаний. В результате получается система, которая может ответить на вопросы типа «где у меня функция, которая парсит JSON?» не хуже, чем ваш коллега, который неделю назад это писал, но уже всё забыл.
Занахрена́?
Я задумывал всё это как retrieval engine для передачи в какой-нибудь клод, но, поскольку сам я ассистентами не пользуюсь, в результате получилось решение, подходящее и для подключения к БЯМ, и для помощи (если не замены) обычному LSP.
Три кита, на которых держится Ragex:
Local-first: Никаких внешних API. Всё работает локально. Код не отправляется в облако на растерзание корпоративным серверам. Параноики оценят. Кроме того, использовать БЯМ для извлечения контекста из кода — глупо, когда у нас есть AST. Но простенький локальный семантический поиск я тоже, конечно, прикрутил.
Гибридный поиск: Символьный анализ (AST) + семантический поиск (эмбеддинги) + графы знаний. Это как смотреть в тринокль: один окуляр видит близко, другой далеко, третий вообще смотрит в прошлое.
Производительность: Запросы выполняются за разумное время, иногда — в ущерб качеству. Потому что жизнь коротка, а ждать результатов анализа кода — это издевательство и вообще прерогатива создателей 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" },
}
Использование

Пример 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_modelFile 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-диаграммки, которые можно пошевелить мышкой.
Ограничения и подводные камни
Потому что честность — моё третье «я»:
JavaScript/TypeScript анализатор: Использует регулярные выражения. Работает для «простых» случаев. Когда-нибудь, может быть, руки дойдут.
Семантический рефакторинг: Пока только для Elixir. Erlang/Python/JS в планах, но не сегодня.
Плотные графы: Если у функции >100 вызовов, поиск путей может занять вечность. Используйте
max_pathsиmax_depth.Память: 400 MB для модели — это цена локального ML. Если RAM критична, можно отключить эмбеддинги (но зачем тогда вообще Ragex?).
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
