ИИ в разработке уже не новость, а обыденность. На этом фоне набирает обороты другая тенденция — запускать модели локально. Причины понятны: приватность кода, работа без интернета, предсказуемая задержка и никакого вендор-лок. Вы контролируете, какая модель у вас крутится, какие данные она видит и что именно отправляется «наружу» (спойлер: ничего).
Обратная сторона тоже есть. Большие модели прожорливы: им нужны 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 ситуация еще более ощутима. Плюс, компактные модели всегда уступают «тяжелым» по глубине понимания — это нормальная цена за приватность и автономность.