Привет, Хабр.

Сейчас почти каждый AI-кодинг-агент подключает LSP — Language Server Protocol. Это тот самый протокол, по которому редактор общается с языковым сервером: go-to-definition, find usages, hover с типом, диагностика. На этом обычно и пишут: «агент понимает код семантически».

Но мы в Veai делаем агента для JetBrains IDE, и нас периодически спрашивают: а зачем вообще нужен IDE, если LSP уже всё умеет? Хороший вопрос. LSP и правда решает много задач, но он проектировался для редактора, а не для агента. Подсветить ошибку, показать тип под курсором, найти ссылки — для этого LSP достаточно. А вот поменять Spring-бин в enterprise-проекте и не сломать сборку — тут нужно чуть больше.

Под катом разберём, что именно LSP даёт агенту, где этого перестаёт хватать и что поверх той же модели проекта предлагает JetBrains Platform. Спойлер: сравнивать LSP и PSI один в один бессмысленно — LSP это протокол, PSI это модель. Речь пойдёт о LSP vs весь стек IDE.

Часть 1. LSP как протокол: что видит агент

1. Координаты вместо модели

Посмотрим, как выглядит типовой цикл агента, который использует только LSP:

  1. Агент встречает в коде вызов userRepo.findById(userId).

  2. Вызывает LSP-метод textDocument/definition (запрос «перейти к определению») — получает UserService.java:42.

  3. Вызывает textDocument/hover (тип под курсором) — получает текст UserRepository.

  4. Вызывает textDocument/references (найти использования) — получает список «файл:строка».

  5. Открывает файлы и читает их целиком, чтобы понять, что такое UserRepository.

  6. Если UserRepository лежит в .jar — LSP-сервер либо вернёт декомпилированный текст, либо ничего не вернёт (подробнее — в разделе 8).

Чем больше шагов, тем больше шума. В итоге агент не понимает код, а гадает на тексте.

Почему так? Потому что LSP изначально проектировался не для агентов. Он проектировался для отрисовки: подсветить ошибку, показать тип под курсором, найти ссылки. Каждый LSP-ответ — плоская, language-agnostic структура, которую редактор превращает в виджет на экране.

Автор rust-analyzer matklad написал об этом в 2023 году:

«LSP just doesn’t provide a semantic model of the code base. Instead, it is focused squarely on the presentation.» (источник)

Даже там, где LSP даёт иерархию (LSP 3.17+ добавил textDocument/prepareTypeHierarchy), он возвращает плоские локации — «файл:строка». Не объектную модель, по которой можно пройти программно. Framework-специфичные иерархии (Spring, JPA) LSP не знает в принципе.

Агент на базе JetBrains IDE работает иначе. IDE-тулы выполняют формальные запросы к PSI (Program Structure Interface — in-memory дерево всего проекта: классы, методы, поля, типы, иерархии) — например, resolve типа или проверку совместимости.

И возвращают LLM не сырой текст, а проверенный факт.

Плата за эту глубину — время индексации: PSI нужно построить полную модель проекта, что замедляет первый запуск. LSP стартует быстрее — и для Python/Go с их сильными LSP-серверами (pyright, gopls) может быть достаточен. Подробнее об ограничениях — в конце статьи.

Возьмём типичный Spring-сервис

@Service
public class PaymentService {
    private final UserRepository userRepo;

    public PaymentService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public PaymentResponse processPayment(String userId, BigDecimal amount) {
        User user = userRepo.findById(userId);
    }
}

Если агент использует только LSP, вот что у него есть: textDocument/definition для userRepo возвращает PaymentService.java:4, textDocument/hover возвращает UserRepository, textDocument/references — список пар «файл:строка».

Всё, что агент знает о коде — то, что LSP посчитал нужным показать. Через LSP агент не может узнать, является ли UserRepository JPA-репозиторием и какие методы он наследует. Не может узнать, какой именно Spring-бин попадёт в PaymentService — если есть несколько реализаций или профили. Не может узнать, маппится ли User на таблицу базы. Не может узнать, какие типы аргументов у findById в конкретной версии библиотеки.

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

IDE-тулы работают иначе. Они выполняют формальные запросы к проектной модели:

  • Запросить иерархию наследования для UserRepository — получить JpaRepository, PagingAndSortingRepository, Repository. Это проверенный факт, а не вероятностная догадка.

  • Выполнить resolve конструктора PaymentService(UserRepository) — выяснить, какой бин инжектится, из какого модуля.

  • Запросить JPA-маппинг для User — таблица, колонки, тип загрузки.

Везде LLM получает не текст для гадания, а проверенный результат.

2. Типы: строка из hover vs формальная проверка

textDocument/hover возвращает строку java.util.List<java.lang.String>. LLM читает её и понимает как человек: «список строк». Всё, что она знает про этот тип — додумала из текста.

IDE-тул возвращает структурированное описание: тип java.util.List с параметром java.lang.String. LLM может опереться на формальные операции тула: узнать категорию типа, получить подстановки обобщений, пройти по иерархии наследования, проверить совместимость формально.

Например:

Map<String, List<Integer>> cache = new HashMap<>();

Агент на одном LSP получает текст java.util.Map<java.lang.String, java.util.List<java.lang.Integer>>. LLM проверяет совместимость вероятностно, по памяти обучающих данных — и в сложных иерархиях ошибается.

IDE-тулы запрашивают проверку у платформы и возвращают формальный ответ: «Да, ArrayList<Integer> совместим с Collection<? extends Number>, потому что ArrayList — наследник AbstractList, реализующий List, а для ? extends Number ковариантность разрешает.» Проще говоря, IDE проверяет типы так же, как компилятор — детерминированно, а не «наверное, совместимо». А не «я где-то это видел в обучающих данных».

3. Почему LSP не знает про Spring (и почему IDE знает)

Для VSCode существуют расширения вроде Spring Boot Tools, которые добавляют Spring-семантику поверх LSP: endpoint mapping, autowiring, properties validation. Но это отдельные серверы, каждый со своим парсером — они не складываются в единую модель проекта.

LSP как протокол не даёт framework-семантики. @Autowired для LSP — просто аннотация, ещё один Java-символ. Что она означает, как Spring обрабатывает DI, какие бины будут инжектиться — LSP не знает, потому что это знание не нужно для отрисовки редактора.

IDE-платформа JetBrains включает десятки модулей поддержки фреймворков: Spring, Spring Boot, Spring Data, JPA, Hibernate, Micronaut, Quarkus, Maven, Gradle. Они работают как единая проектная модель — не через отдельные серверы, а как часть PSI.

Пример — типовой REST-контроллер

@GetMapping("/users/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
    return service.findById(id)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

Что дают IDE-тулы?

@GetMapping("/users/{id}") — Spring-модуль возвращает распарсенные атрибуты: value = “/users/{id}”, method = GET. Структурированное описание, а не строка. Spring-модуль знает, что это endpoint, и отдаёт HTTP-метод, path, параметры, возвращаемый тип — формально.

@PathVariable Long id — Spring-модуль знает, что это path variable с именем id.

service.findById(id) — IDE находит (resolve) поле service, определяет его тип, находит метод, отдаёт тип возврата и параметров.

Без доступа к PSI @GetMapping("/users/{id}") видится как строка.

Но ведь сильная модель прочитает код и сама поймёт Spring-семантику, верно? Отчасти. Но модель догадывается вероятностно, и в enterprise-коде с профилями, условными бинами и автоконфигурациями ошибается системно. Исследование «Towards Mitigating API Hallucination» (arxiv:2505.05057, май 2025) показывает: LLM генерируют код с методами, которых нет в реально подключённой версии библиотеки, в значительной доле случаев. Это касается и Spring-зависимостей: модель может «вспомнить» сигнатуру из другой версии или выдумать метод, которого не существует. PSI решает эту проблему структурно: агент не гадает о сигнатуре, а получает её из подключённой зависимости — механизм детерминированный, в отличие от вероятностной догадки LLM.

4. Diagnostics vs IDE-инспекции

LSP publishDiagnostics присылает диагностику четырёх уровней: Error, Warning, Information, Hint. Каждый diagnostic — это текст: номер строки, сообщение, severity. Никакой структуры «какая инспекция сработала, какой quick fix».

LSP имеет textDocument/codeAction, который может предложить базовые исправления — «добавить import», «organize imports». Но это плоские текстовые замены без metadata инспекции. Framework-специфичных исправлений (Spring, JPA) codeAction не даёт.

Стоит признать: продвинутые LSP-серверы вроде jdtls умеют находить потенциальные NPE через null analysis. Но они не предложат Spring-специфичный Quick Fix — только базовые исправления.

IDE-инспекции — это сотни формальных правил с типом проблемы, severity и Quick Fix-ом, который можно применить без LLM. Разработчик видит их сразу при редактировании — агент получает то же самое:

Потенциальный NPE. Агент написал:

String name = user.getName();
System.out.println(name.toUpperCase());

IDE-тул «получить инспекции» возвращает: тип = “Constant conditions & exceptions”, quickFix = “Add ‘@Nullable’ check”. Применение Quick Fix-а — без LLM, без токенов.

И ведь компилятор это пропустит — код валидный. Бегите в прод.

SQL-инъекция. Агент написал:

String sql = "SELECT * FROM users WHERE id = " + userId;

IDE-тул возвращает: тип = “SQL injection”, quickFix = “Use PreparedStatement”. Quick Fix переписывает код на PreparedStatement за одну операцию.

Неправильный @Transactional. Агент написал:

@Transactional
public List<User> getUsers() {
    return repo.findAll();
}

Spring-специфичная инспекция IntelliJ подсветит, что @Transactional на read-only операции можно заменить на @Transactional(readOnly = true) — это отключает dirty check в Hibernate, что даёт оптимизацию. Quick Fix применяет readOnly флаг.

LSP-агент с базовым сервером не увидит эти проблемы — ни NPE, ни SQL-инъекция, ни @Transactional без readOnly не являются ошибками компиляции.

Примеры проблем, которые видит IDE-инспектор:

Разрыв между «текстом для отображения» и «моделью для программного обхода» не закрывается версиями протокола — это просто разные вещи.

Часть 2. За пределами LSP: где нужен весь стек IDE

Разделы ниже — уже не про LSP как протокол. Они про то, что агент без доступа к платформе IDE не получает, независимо от того, использует он LSP или нет.

5. Рефакторинг

Типичный агент делает рефакторинг через текстовый поиск и замену: grep → прочитать файлы → решить, что менять → применить замену. Что может пойти не так? На практике мы регулярно видим, как это ломает проект.

LSP-тул textDocument/rename частично лучше grep. Некоторые серверы (jdtls) умеют переименовывать геттеры и сеттеры вместе с полем. Но XML-конфиги Spring, JPA-маппинги, имена бинов — не обновятся, можно случайно задеть символ из другой библиотеки.

Без grep’а и без риска пропустить неочевидное вхождение.

6. Сборка и тесты: терминал vs Run Configurations

Обычно AI-агенты запускают сборку через терминал:

./gradlew test

На практике это даёт сбои, которые хорошо видны по тикетам Claude Code. Issue #39694: remote-окружение поставляет JDK 21, проект под Java 22+. Агент запускает тесты с неверным JDK, не может верифицировать изменения и не знает, что проблема в JDK. Он пытается исправлять код, который и так правильный.

Дело не в LSP. Агент без IDE-интеграции просто не знает, как настроено окружение: какой JDK, какой venv, какие переменные, какие Spring-профили.

IDE Run Configurations — это конкретные настройки, которые разработчик уже сделал:

Агент с IDE-тулами находит нужную конфигурацию и запускает её — с тем же SDK, env-переменными и профилями, что и разработчик. Результат возвращается структурированными данными, а не сырым логом: сколько тестов прошло, какой упал, какой стектрейс. LLM не парсит 500 строк лога, чтобы найти причину:

Покрытие тестов с IDE-фидбэком
Покрытие тестов с IDE-фидбэком

7. Отладка: принты vs дебаггер

Когда AI-агент без интеграции с отладчиком видит баг, его основной метод — вставить в код System.out.println. Метод научного тыка. Мы встречали это на практике: цикл «добавить печать → запустить → не хватило → добавить ещё печать → запустить» повторяется 5–15 раз, с сожжёнными токенами и замусоренным кодом, который потом нужно чистить.

Блог Syncause описывает реальный кейс (Java + H2 Database): Cursor Debug Mode нашёл и исправил баг — невидимый whitespace в данных — но процесс шёл медленно, через серию перезапусков и логирования. С полноценным дебаггером тот же баг локализуется за один проход: брейкпоинт, проверка переменной, готово. Академическая работа Debug2Fix (arxiv, февраль 2026) подтверждает: даже более слабые модели с доступом к дебаггеру match or exceed более сильные модели без него.

IDE-тулы дают полноценный дебаггер через JDI (Java Debug Interface) в связке с PSI:

Агент формулирует гипотезу → ставит брейкпоинт → запускает под дебаггером → проверяет состояние → подтверждает или опровергает. Без единого изменения в коде и без токенов на лишние итерации.

8. Зависимости: .jar, который LSP видит как текст

Enterprise-проект состоит не только из исходников репозитория. Значительная часть логики лежит во внутренних библиотеках — JAR, DLL, .so.

Если метод в .jar — агент на LSP вызывает textDocument/definition. В лучшем случае LSP-сервер отдаст декомпилированный .class как плоский текст, без типов и иерархии. В худшем — ничего не вернёт.

IDE-тулы декомпилируют класс из любой подключённой зависимости — не как текст, а как PSI-дерево. Агент видит те же сигнатуры, те же типы, те же иерархии, что и разработчик, нажавший Ctrl+Click:

Декомпилированная зависимость в IDE
Декомпилированная зависимость в IDE

PSI даёт структурированное дерево с типами и брейкпоинтами. LSP — плоский текст.

Часть 3. Выводы

9. Serena: рыночное подтверждение

У Serena два бэкенда. Бесплатный (tree-sitter + LSP) — определения, ссылки, типы. JetBrains-бэкенд — платный.

Что есть только в JetBrains-бэкенде: полная индексация библиотек и зависимостей, инструменты рефакторинга и семантического поиска, недоступные в LSP-бэкенде, интерактивная отладка, поддержка полиглотов и фреймворков.

Serena продаёт JetBrains-бэкенд отдельно и дороже — создатели видят в нём самостоятельную ценность. LSP покрывает базовый сценарий, глубокая работа с проектом — за неё рынок готов платить отдельно.

10. Сводка: что даёт каждый инструмент

Ниже — сводка по всем сценариям. Первые три строки — ограничения LSP как протокола; остальные — про отсутствие доступа к IDE-платформе. Гипотетический комбинированный стек (LSP + DAP + BSP + framework extensions) в таблице не отражён — он рассмотрен в разделе 11.

Сценарий

Без IDE-платформы

Агент на базе JetBrains IDE

Иерархия типов

строка Map<String, List<Integer>>, LLM извлекает смысл из текста

IDE-тулы выполняют формальные запросы к PSI, LLM получает проверенные факты

Spring-семантика

@Autowired — строка аннотации, LLM догадывается

Spring-модуль возвращает точку внедрения, бин, контекст, профили

Quality gates

diagnostics — текст ошибок, LLM сама решает, что делать

Сотни инспекций с Quick Fix-ами, применимыми без LLM

Рефакторинг

grep / textDocument/rename — плоский список замен

Semantic rename — все usages, XML, Bean-ы, тесты за одно действие

Запуск тестов

Терминал с угадыванием JDK

IDE Run Configuration с правильным SDK

Отладка

System.out.println в цикле

Брейкпоинты, evaluate, step без правки кода

Зависимости в .jar

Плоский текст или ничего, без типов и брейкпоинтов

Декомпиляция в PSI-дерево, breakpoints в коде библиотеки

Некоторые инструменты (Cursor) имеют частичную интеграцию с JetBrains, но она не даёт полного доступа к проектной модели, инспекциям и дебаггеру.

А что насчёт границ…?

Честно признаем: у подхода на базе JetBrains IDE есть ограничения, а у LSP — свои сильные стороны.

Для Python и Go глубина PSI меньше, чем для Java и Kotlin. Там сильные LSP-серверы (pyright, gopls) дают большую часть нужной информации, и LSP + хорошая модель вполне справляются — особенно для прототипирования, скриптов, проектов без сложных фреймворков.

PSI дольше стартует: LSP можно запустить почти мгновенно, а PSI требует индексации проекта — на больших кодовых базах это минуты. И есть lock-in: LSP работает с любым редактором, а агент на базе JetBrains IDE — только в JetBrains. Компромисс: глубина анализа за портабельность.

А нельзя ли скомбинировать протоколы? Теоретически да: DAP (Debug Adapter Protocol) даёт отладку, BSP (Build Server Protocol) — сборку. Можно представить стек LSP + DAP + BSP + framework extensions.

Но на практике среди конкурирующих AI-агентов их не используют. Для отладки Cursor, Claude Code, Cline, Kilo Code, GitHub Copilot — все работают через терминал и print-логи. Единственные агенты с доступом к дебаггеру — Junie (дебаггер доступен в Ultimate) и Claude Code (через JetBrains MCP Server) — получают его через JetBrains Platform, а не через DAP.

Вывод

Мы начали с вопроса: достаточно ли LSP для агента, который работает со сложным кодом? Короткий ответ — зависит от сложности.

Через все сценарии проходит один механизм: каждый инструмент JetBrains Platform превращает вероятностную догадку LLM в детерминированный проверенный факт. Типы приходят из компилятора, а не из памяти обучающих данных. Endpoint mapping — из парсера фреймворка, а не из текста аннотации. Окружение — из настроенной Run Configuration, а не из угадывания. А состояние переменных в рантайме — из дебаггера, а не из принтов.

На простом скрипте эта разница незаметна. На enterprise-коде с Spring, JPA и закрытыми библиотеками — критична: меньше багов, меньше итераций отладки и расход токенов.

При этом мы не утверждаем, что JetBrains-стек нужен всем — для простых стеков без сложных фреймворков LSP + сильная модель достаточны (подробнее — в предыдущем разделе). Но если команда пишет на Java, Kotlin или работает с Spring, JPA, enterprise legacy — глубина всей платформы окупает себя.

А комбинированный стек LSP + DAP + BSP? Теоретически возможен, но сегодня его никто не использует. Если появится — это будет главный конкурент для позиции статьи. Будем следить.

А что вы думаете? Хватит ли LSP для ваших задач, или упираетесь в его пределы? Делитесь в комментариях — обсудим.

Попробовать Veai  бесплатно в JetBrains IDE. А если в работе вам не хватает каких-то возможностей или сценариев, пишите нам в чат.

Для всех, кому интересно следить за продуктом, новостями из мира AI и техниками использования AI в разработке, оставляем ссылку на наш телеграм-канал.

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


  1. ritorichesky_echpochmak
    03.07.2026 15:49

    Единственные агенты с доступом к дебаггеру — Junie (дебаггер доступен в Ultimate) и Claude Code (через JetBrains MCP Server) — получают его через JetBrains Platform, а не через DAP.

    Спасибо. Я как-то пытался найти беглым поиском чем все эти "варианты подключений" отличаются и где есть возможость реально воспользоваться фичами IDEшки, но в итоге только ещё больше каши в понимании. У JB вообще зоопарк с этим, потому что есть JetBrains AI Assistant, Junie, какие-то "нативно поддерживаемые AI чаты", ACP, BYOK (что бы это ни было), собсвенные плагины... а нормального сравнения по возможностям нет. Т.е. выбираешь методом тыка и "хоть бы как-то заработало". Пытаешься найти хоть что-то чтобы прикрутить отладку - поиск шлёт в копилот, который наглухо не доступен с учёткой которая хоть как-то когда-то засветилась в картофельном союзе.

    Нужно ли для этих агентов, плагинов и прочего нейрослопа отдельно держать MCP-сервер уже и так запущенного райдера - очень непонятно. И вроде ещё и порт у него меняется на каждом проекте, что автоматом делит на ноль попытки работать с чуть более чем одним проектом. Отдельно непонятно, почему MCP-сервер тупит настолько, что его регулярно все теряют (причём курсор если не достучался, то отключает с концами, включить потом только ручками можно, когда заметишь что всё плохо). И сам райдер не умеет дожидаться, когда же он стартанёт, прежде чем опрашивать MCP-сервера. Так же в самом райдере из трёх конфигов (SSE, stdio, HTTP Stream) которые он сам предлагает скопировать - заработал только /stream.

    Дальше веселее, ведь всякие MCP один в один не переносятся из курсора, всё что прикручивается, например, через npx - нужно прописывать в command как npx.cmd, иначе райдер не сможет запустить. MCP которые подключались прямо по сети тоже как-то не через все варианты работали.

    Выглядит будто JB продаёт на волне хайпа то, чем само не пользуется вообще.

    И такие ляпы в этих нейрослопных поделках кругом. Cursor сожрал лимит, предложил переключиться на Auto, переключает тебя из собственной диаложки на "auto" (мелким шрифтом) и начинает истерить что такой модели нет. Переключаешь на Auto - истерит что ни одна модель недоступна и нужно срочно ещё мешок денег. Нужно прям руками переключиться сначала на composer и отключить fast, потом как-то работает...

    Gemini - были какие-то лютые тормоза с регистрацией. Ты типа зарегался, но сразу авторизоваться не получается, надо подождать. Только ты об этом не знаешь, тебя отпинывает что ты не зареган, ты пытаешься ещё раз зарегаться, тебя отпинывает что ты уже зареган... и так пока регистрация не синхронизируется.

    Zed - я прописал в конфиге Gemini (через LLM Providers - Google AI) и Cursor (через ACP). Gemini как-то работает, Cursor - нет в UI вообще.

    Cursor и DeepSeek... санкций вроде бы нет... но без VPN регистрация не проходила... но потом как-то работают, да... но сменить учётку в курсоре - тот ещё цирк. Плюс он теперь почему-то стал считать что нет браузеров в мире кроме хрома (непредвиденные сложности у вайбкодеров).

    Отдельно у меня качественно полыхает с того, что вайбкодеры так и не осилили более-менее единых стандартов, где держать все эти конфиги, скилы, стопицот копий .ignore и правил аля AI.md, AI-RULES.md, AGENTS.md, GEMINI.md, CLAUDE.md, "каждаявтораяиде.md", ".директориядлякаждойвторойиде/" и ещё вагончик... которые тупо невозможно в адеквате как-то засинхронизировать между проектами или в команде где есть не один единственный дефолтный агент и одна единственная IDE.


  1. al-chemist
    03.07.2026 15:49

    Если агент использует только LSP, вот что у него есть: textDocument/definition  для userRepo возвращает PaymentService.java:4textDocument/hover возвращает UserRepositorytextDocument/references — список пар «файл:строка».

    Всё, что агент знает о коде — то, что LSP посчитал нужным показать. Через LSP агент не может узнать, является ли UserRepository JPA-репозиторием и какие методы он наследует.

    Есть гипотеза, что вы не понимаете, что буковка «P» означает в аббревиатуре «LSP».

    То, что какая-то вами протестированная реализация (какая, кстати?) не поддерживает спецификацию — не означает, что протокол плох. Вот вам ссылка на то, как узнать всё про UserRepository.

    Правильно имплементированный LSP, за которым стоит семантический анализ и граф зависимостей, — лучше, быстрее и аккуратнее любого IDE.


  1. vitiok78
    03.07.2026 15:49

    Я не вдавался в тонкости, но чисто из практики наблюдаю, что инструменты JetBrains (я использовал их MCP) не дают какого-то ощутимого преимущества, на которое я бы мог указать. Зато вот токенов забирают больше. Это субъективно, но зато на практике. Поэтому я просто отказался от подписки JetBrains, IDE мне больше не нужна.