ИИ в разработке уже не новость, а обыденность. На этом фоне набирает обороты другая тенденция — запускать модели локально. Причины понятны: приватность кода, работа без интернета, предсказуемая задержка и никакого вендор-лок. Вы контролируете, какая модель у вас крутится, какие данные она видит и что именно отправляется «наружу» (спойлер: ничего).

Обратная сторона тоже есть. Большие модели прожорливы: им нужны 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:

  1. Скачиваем ollama с официального сайта

  2. После установки Ollama запустится автоматически

  3. Выполняем 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, а в rolesautocomplete. После этого 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 предложит автокомплишн:

Пример автокомплишена от Mellum
Пример автокомплишена от Mellum

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

Аналогичным образом работает добавление метода, например, поиска владельца по id:

Пример автокомплишена от Mellum
Пример автокомплишена от Mellum

Вывод

То, что раньше требовало мощных серверов и сложной инфраструктуры, сегодня решается на обычном ноутбуке: легковесные модели вроде Mellum дописывают код в темпе IDE, не уводя исходники «куда-то».

Плюсы очевидны: приватность, офлайн-режим, предсказуемая задержка и полный контроль над окружением. Связка OpenIDE + Continue + Ollama показывает, что порог входа уже низкий, а выгода — ощутима.

Минусы тоже есть: локальные модели чувствительны к ресурсам. На слабом железе комплишены предлагаются с задержкой, первый «прогрев» занимаем время, а без GPU ситуация еще более ощутима. Плюс, компактные модели всегда уступают «тяжелым» по глубине понимания — это нормальная цена за приватность и автономность.