ИИ в разработке уже не новость, а обыденность. На этом фоне набирает обороты другая тенденция — запускать модели локально. Причины понятны: приватность кода, работа без интернета, предсказуемая задержка и никакого вендор-лок. Вы контролируете, какая модель у вас крутится, какие данные она видит и что именно отправляется «наружу» (спойлер: ничего).
Обратная сторона тоже есть. Большие модели прожорливы: им нужны CPU/GPU, память.. Настройка окружения, версии — всё это ложится на вас. Но хорошая новость в том, что для задач автокомплишена не всегда нужен «монстр» на десятки миллиардов параметров. В связи с этим появились легковесные модели, которые шустро работают на обычном рабочем ноутбуке и при этом прекрасно справляются с рутиной в IDE.
В этом гайде от команды Spring АйО вы узнаете, как настроить локальную LLM для вашей IDE, будь то IntelliJ IDEA или OpenIDE. Я буду использовать последнюю.
LLM
В качестве модели будем использовать Mellum — это семейство «легких» моделей от JetBrains, заточенных под автодополнение кода и интерактивную работу прямо в IDE.
JetBrains пришли к Mellum из практической боли: нужен был быстрый и стабильный комплишн в IDE, который помнит длинный контекст проекта и не «расползается» в болтливый чат. Осенью 2024-го они запустили Mellum как облачную модель внутри JetBrains AI Assistant — по сути, новый движок автокомплита с упором на скорость и уместность подсказок.
Дальше — логичный шаг к open source. Весной 2025 JetBrains опубликовали Mellum: модель, узко заточенная под комплишн, с небольшим размером и понятной лицензией для локального использования и экспериментов. Причины довольно прозрачны: дать разработчикам приватность и офлайн, снизить порог входа для интеграций, а сообществу — возможность проверять и улучшать модель.
Как Mellum готовили?
Команда описала трёхэтапный пайплайн: предобучение на массиве автономных файлов, дообучение на контекстных примерах из реальной IDE и выравнивание через RL с AI-фидбеком, чтобы выжечь нежелательные паттерны и подогнать стиль под прод��ктовые сценарии. Контекстное окно — ~8K токенов, архитектура — LLaMA-style, акцент на комплишн и скорость инференса в редакторе.
В сухом остатке Mellum — открытая, компактная модель для IDE: не пытается быть всем сразу, зато уверенно дописывает код в стиле проекта и легко заводится локально через Ollama.
Запуск модели
Для установки и запуска модели нам понадобится рантайм. Будем использовать Ollama. Ollama запускает HTTP-сервер на localhost:11434. Мы общаемся не с моделью напрямую, а с Ollama по её REST-API; Ollama принимает запросы (например, /api/generate или /api/chat) и сама маршрутизирует их к выбранной модели.
Работает на macOS/Windows/Linux и не отправляет ваш код в облако — всё общение остаётся локально.
Установка выполняется прямо из терминала (версия для macOS):
# Устанавливаем Ollama через Homebrew brew install ollama # Запускаем Ollama ollama serve & # Выполняем pull нашей модели ollama pull JetBrains/Mellum-4b-sft-all # Проверяем, что Ollama запущена и видит установленные модели curl http://localhost:11434/api/tags
Linux:
# Устанавливаем Ollama curl -fsSL https://ollama.ai/install.sh | sh # Запускаем Ollama ollama serve # Выполняем pull нашей модели ollama pull JetBrains/Mellum-4b-sft-all # Проверяем, что Ollama запущена и видит установленные модели curl http://localhost:11434/api/tags
Для Windows:
Скачиваем ollama с официального сайта
После установки Ollama запустится автоматически
Выполняем pull нашей модели
ollama pull JetBrains/Mellum-4b-sft-all
Только Mellum?
На самом деле нет. Можно использовать и другие модели. Кстати, Continue в некоторых случаях сам рекомендует некоторые модели. Например, для chat-роли в топ открытых моделей входят Qwen3 Coder (480B), Qwen3 Coder (30B), gpt-oss (120B), gpt-oss (20B). Список топовых моделей доступен в соответствующих ролях.
Как научить общаться IDE и LLM
В среде разработки нет такой функциональности, который выступала бы посредником между средой разработки и самой моделькой. Для этой задачи есть несколько плагинов, которые доступны для скачивания из мар��етплейса OpenIDE. Мы будем использовать Continue — это открытый плагин-мост между вашей IDE и любыми LLM, в том числе локальными. Проще говоря, Continue — это «адаптер», который превращает локальную Mellum в нативный автокомплит OpenIDE: вы пишете код, Continue берёт контекст из IDE и дергает Mellum через Ollama, а подсказки появляются там, где вы печатаете.
Для его установки нам потребуется пройти в маркетплейс и нехитрым способом его установить.

Почему и зачем он нужен:
Чтобы связать OpenIDE ↔ Ollama/Mellum. В config.yaml мы указываем
provider: ollamaи модельJetBrains/Mellum-4b-sft-all, а вroles—autocomplete. После этого Continue становится «двигателем» комплишена внутри IDE, используя локальную Mellum.
config.yaml – это главный конфигурационный файл плагина Continue. В нём вы описываете, какие модели использовать (например, в нашем случае, Mellum через provider: ollama), какие роли они выполняют (autocomplete,chat,edit,apply), а также дополнительные вещи: контекст-провайдеры, правила (rules), промпты и инструменты (MCP-серверы). Иначе говоря, config.yaml определяет «кто» и «как» будет работать в вашем ассистенте прямо из IDE.Единая точка настройки. Конфиг открывается прямо из боковой панели Continue в JetBrains, изменения подхватываются без перезапуска. Проще экспериментировать с моделями и параметрами.
Гибкость и контроль. Continue — open-source, поддерживает разные модели/режимы и не навязывает облако: хотите — всё крутится локально через Ollama; хотите — подключаете удалённый сервер.
Итак, после установки плагина нужно его настроить на общение с нашей локально поднятой моделью. Делается это в файле config.yaml. Он открывается при первом “касании” с плагином, однако зайти в него, скорее всего, придется не раз. Открыть его можно так:

Continue в свою очередь умеет напрямую говорить с Ollama, что нам и нужно.
Наш config.yaml будет выглядеть так:
name: Local Agent version: 1.0.0 schema: v1 models: - name: Mellum provider: ollama model: JetBrains/Mellum-4b-sft-all:latest roles: - autocomplete
Помимо указания названия модели и провайдера, важно правильно указать роли, которыми будет заниматься наша модель. Для нашей задачи нам нужна поддержка автокомплишена, поэтому указываем соответствующую роль. Почитать подробнее про роли можно тут.
А еще...
Continue — не только мост к локальной модели для автокомплита. Плагин умеет работать в режиме агента: формулируете задачу в чате, и он пошагово предлагает правки, создает файлы, генерирует тесты и применяет диффы прямо в проекте. Если подключить сильную LLM (OpenAI, Anthropic и др.), можно получить более «умного» помощника, который способен не просто подсказывать строчки, а писать целые блоки кода.
На практике это выглядит так:
В чате ставим четкий TLDR-запрос: цель, ограничения, форматы вывода.
Примеры:
«Добавь REST-эндпоинт с валидацией и DTO, верни дифф патчем»;
«Перепиши поиск на Pageable, сохрани текущие статусы ответов»;
«Сгенерируй JUnit-тесты для OwnerController».Работатать лучше короткими итерациями: одна задача → один дифф, затем обзор.
Обязательно проверяем изменения перед применением: агент ускоряет рутину, но финальная ответственность за код — на нас.
Подробнее про работу с другими режимами поговорим в следующих статьях.
Тестирование
В качестве “подопытного” проекта возьмём немалоизвестный spring-petclinic.
Откроем класс OwnerController.java:
/* * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.samples.petclinic.owner; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import jakarta.validation.Valid; import org.springframework.web.servlet.mvc.support.RedirectAttributes; /** * @author Juergen Hoeller * @author Ken Krebs * @author Arjen Poutsma * @author Michael Isvy * @author Wick Dynex */ @Controller class OwnerController { private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm"; private final OwnerRepository owners; public OwnerController(OwnerRepository owners) { this.owners = owners; } @InitBinder public void setAllowedFields(WebDataBinder dataBinder) { dataBinder.setDisallowedFields("id"); } @ModelAttribute("owner") public Owner findOwner(@PathVariable(name = "ownerId", required = false) Integer ownerId) { return ownerId == null ? new Owner() : this.owners.findById(ownerId) .orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId + ". Please ensure the ID is correct " + "and the owner exists in the database.")); } @GetMapping("/owners/new") public String initCreationForm() { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } @PostMapping("/owners/new") public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { redirectAttributes.addFlashAttribute("error", "There was an error in creating the owner."); return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } this.owners.save(owner); redirectAttributes.addFlashAttribute("message", "New Owner Created"); return "redirect:/owners/" + owner.getId(); } @GetMapping("/owners/find") public String initFindForm() { return "owners/findOwners"; } @GetMapping("/owners") public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result, Model model) { // allow parameterless GET request for /owners to return all records if (owner.getLastName() == null) { owner.setLastName(""); // empty string signifies broadest possible search } // find owners by last name Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, owner.getLastName()); if (ownersResults.isEmpty()) { // no owners found result.rejectValue("lastName", "notFound", "not found"); return "owners/findOwners"; } if (ownersResults.getTotalElements() == 1) { // 1 owner found owner = ownersResults.iterator().next(); return "redirect:/owners/" + owner.getId(); } // multiple owners found return addPaginationModel(page, model, ownersResults); } private String addPaginationModel(int page, Model model, Page<Owner> paginated) { List<Owner> listOwners = paginated.getContent(); model.addAttribute("currentPage", page); model.addAttribute("totalPages", paginated.getTotalPages()); model.addAttribute("totalItems", paginated.getTotalElements()); model.addAttribute("listOwners", listOwners); return "owners/ownersList"; } private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) { int pageSize = 5; Pageable pageable = PageRequest.of(page - 1, pageSize); return owners.findByLastNameStartingWith(lastname, pageable); } @GetMapping("/owners/{ownerId}/edit") public String initUpdateOwnerForm() { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } @PostMapping("/owners/{ownerId}/edit") public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { redirectAttributes.addFlashAttribute("error", "There was an error in updating the owner."); return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } if (owner.getId() != ownerId) { result.rejectValue("id", "mismatch", "The owner ID in the form does not match the URL."); redirectAttributes.addFlashAttribute("error", "Owner ID mismatch. Please try again."); return "redirect:/owners/{ownerId}/edit"; } owner.setId(ownerId); this.owners.save(owner); redirectAttributes.addFlashAttribute("message", "Owner Values Updated"); return "redirect:/owners/{ownerId}"; } /** * Custom handler for displaying an owner. * @param ownerId the ID of the owner to display * @return a ModelMap with the model attributes for the view */ @GetMapping("/owners/{ownerId}") public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { ModelAndView mav = new ModelAndView("owners/ownerDetails"); Optional<Owner> optionalOwner = this.owners.findById(ownerId); Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException( "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); mav.addObject(owner); return mav; } }
Добавим метод удаления владельца. С новой строки начнем писать сигнатуру нашего метода. Достаточно написать public String deleteOwner, после чего Mellum предложит автокомплишн:

По нажатию на Tab комплишн будет принят, код будет дополнен. Mellum смотрит на контекст проекта, на код до и после каретки, поэтому он знает, как устроена архитектура вашего приложения и предлагает только то, что будет наиболее уместным.
Аналогичным образом работает добавление метода, например, поиска владельца по id:

Вывод
То, что раньше требовало мощных серверов и сложной инфраструктуры, сегодня решается на обычном ноутбуке: легковесные модели вроде Mellum дописывают код в темпе IDE, не уводя исходники «куда-то».
Плюсы очевидны: приватность, офлайн-режим, предсказуемая задержка и полный контроль над окружением. Связка OpenIDE + Continue + Ollama показывает, что порог входа уже низкий, а выгода — ощутима.
Минусы тоже есть: локальные модели чувствительны к ресурсам. На слабом железе комплишены предлагаются с задержкой, первый «прогрев» занимаем время, а без GPU ситуация еще более ощутима. Плюс, компактные модели всегда уступают «тяжелым» по глубине понимания — это нормальная цена за приватность и автономность.
