Привет, Хабр.
Сейчас почти каждый 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:
-
Агент встречает в коде вызов
userRepo.findById(userId). -
Вызывает LSP-метод
textDocument/definition(запрос «перейти к определению») — получаетUserService.java:42. -
Вызывает
textDocument/hover(тип под курсором) — получает текстUserRepository. -
Вызывает
textDocument/references(найти использования) — получает список «файл:строка». -
Открывает файлы и читает их целиком, чтобы понять, что такое
UserRepository. -
Если
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-сервис
@Servicepublic 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. Агент написал:
@Transactionalpublic 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 строк лога, чтобы найти причину:
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:
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 |
|---|---|---|
|
Иерархия типов |
строка |
IDE-тулы выполняют формальные запросы к PSI, LLM получает проверенные факты |
|
Spring-семантика |
|
Spring-модуль возвращает точку внедрения, бин, контекст, профили |
|
Quality gates |
diagnostics — текст ошибок, LLM сама решает, что делать |
Сотни инспекций с Quick Fix-ами, применимыми без LLM |
|
Рефакторинг |
grep / |
Semantic rename — все usages, XML, Bean-ы, тесты за одно действие |
|
Запуск тестов |
Терминал с угадыванием JDK |
IDE Run Configuration с правильным SDK |
|
Отладка |
|
Брейкпоинты, 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 в разработке, оставляем ссылку на наш телеграм-канал.
ссылка на оригинал статьи https://habr.com/ru/articles/1055288/