- BrainTools - https://www.braintools.ru -
ИИ в разработке уже не новость, а обыденность. На этом фоне набирает обороты другая тенденция — запускать модели локально. Причины понятны: приватность кода, работа без интернета, предсказуемая задержка и никакого вендор-лок. Вы контролируете, какая модель у вас крутится, какие данные она видит и что именно отправляется «наружу» (спойлер: ничего).
Обратная сторона тоже есть. Большие модели прожорливы: им нужны CPU/GPU, память [1].. Настройка окружения, версии — всё это ложится на вас. Но хорошая новость в том, что для задач автокомплишена не всегда нужен «монстр» на десятки миллиардов параметров. В связи с этим появились легковесные модели, которые шустро работают на обычном рабочем ноутбуке и при этом прекрасно справляются с рутиной в IDE.
В этом гайде от команды Spring АйО [2] вы узнаете, как настроить локальную LLM для вашей IDE, будь то IntelliJ IDEA или OpenIDE. Я буду использовать последнюю.
В качестве модели будем использовать Mellum [3] — это семейство «легких» моделей от JetBrains, заточенных под автодополнение кода и интерактивную работу прямо в IDE.
JetBrains пришли к Mellum [4] из практической боли [5]: нужен был быстрый и стабильный комплишн в IDE, который помнит длинный контекст проекта и не «расползается» в болтливый чат. Осенью 2024-го они запустили Mellum как облачную модель внутри JetBrains AI Assistant — по сути, новый движок автокомплита с упором на скорость и уместность подсказок.
Дальше — логичный шаг к open source. Весной 2025 JetBrains опубликовали [6] Mellum: модель, узко заточенная под комплишн, с небольшим размером и понятной лицензией для локального использования и экспериментов. Причины довольно прозрачны: дать разработчикам приватность и офлайн, снизить порог входа для интеграций, а сообществу — возможность проверять и улучшать модель.
Команда описала [7] трёхэтапный пайплайн: предобучение на массиве автономных файлов, дообучение на контекстных примерах из реальной IDE и выравнивание через RL с AI-фидбеком, чтобы выжечь нежелательные паттерны и подогнать стиль под продуктовые сценарии. Контекстное окно — ~8K токенов, архитектура — LLaMA-style, акцент на комплишн и скорость инференса в редакторе.
В сухом остатке Mellum — открытая, компактная модель для IDE: не пытается быть всем сразу, зато уверенно дописывает код в стиле проекта и легко заводится локально через Ollama.
Для установки и запуска модели нам понадобится рантайм. Будем использовать Ollama [8]. 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:
Скачиваем [9] ollama с официального сайта
После установки Ollama запустится автоматически
Выполняем pull нашей модели
ollama pull JetBrains/Mellum-4b-sft-all
На самом деле нет. Можно использовать и другие модели. Кстати, Continue в некоторых случаях сам рекомендует некоторые модели. Например, для chat-роли [10] в топ открытых моделей входят Qwen3 Coder (480B) [11], Qwen3 Coder (30B) [12], gpt-oss (120B) [13], gpt-oss (20B) [14]. Список топовых моделей доступен в соответствующих ролях [15].
В среде разработки нет такой функциональности, который выступала бы посредником между средой разработки и самой моделькой. Для этой задачи есть несколько плагинов, которые доступны для скачивания из маркетплейса OpenIDE. Мы будем использовать Continue [16] — это открытый плагин-мост между вашей IDE и любыми LLM, в том числе локальными. Проще говоря, Continue — это «адаптер», который превращает локальную Mellum в нативный автокомплит OpenIDE: вы пишете код, Continue берёт контекст из IDE и дергает Mellum через Ollama, а подсказки появляются там, где вы печатаете.
Для его установки нам потребуется пройти в маркетплейс и нехитрым способом его установить.

Почему и зачем он нужен:
Чтобы связать OpenIDE ↔ Ollama/Mellum. В config.yaml мы указываем provider: ollama и модель JetBrains/Mellum-4b-sft-all, а в roles — autocomplete. После этого Continue становится [17] «двигателем» комплишена внутри 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
Помимо указания названия модели и провайдера, важно правильно указать роли, которыми будет заниматься наша модель. Для нашей задачи нам нужна поддержка автокомплишена, поэтому указываем соответствующую роль. Почитать подробнее про роли можно тут [15].
Continue — не только мост к локальной модели для автокомплита. Плагин умеет работать в режиме агента: формулируете задачу в чате, и он пошагово предлагает правки, создает файлы, генерирует тесты и применяет диффы прямо в проекте. Если подключить сильную LLM (OpenAI, Anthropic и др.), можно получить более «умного» помощника, который способен не просто подсказывать строчки, а писать целые блоки кода.
На практике это выглядит так:
В чате ставим четкий TLDR-запрос: цель, ограничения, форматы вывода.
Примеры:
«Добавь REST-эндпоинт с валидацией и DTO, верни дифф патчем»;
«Перепиши поиск на Pageable, сохрани текущие статусы ответов»;
«Сгенерируй JUnit-тесты для OwnerController».
Работатать лучше короткими итерациями: одна задача → один дифф, затем обзор.
Обязательно проверяем изменения перед применением: агент ускоряет рутину, но финальная ответственность за код — на нас.
Подробнее про работу с другими режимами поговорим в следующих статьях.
В качестве “подопытного” проекта возьмём немалоизвестный spring-petclinic [18].
Откроем класс OwnerController.java [19]:
/*
* 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 ситуация еще более ощутима. Плюс, компактные модели всегда уступают «тяжелым» по глубине понимания — это нормальная цена за приватность и автономность.
Автор: befayer
Источник [20]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/20619
URLs in this post:
[1] память: http://www.braintools.ru/article/4140
[2] Spring АйО: https://t.me/+QmrKn0wA8CdkNjAy
[3] Mellum: https://www.jetbrains.com/help/ide-services/jetbrains-mellum.html
[4] пришли к Mellum: https://blog.jetbrains.com/blog/2024/10/22/introducing-mellum-jetbrains-new-llm-built-for-developers/
[5] боли: http://www.braintools.ru/article/9901
[6] Весной 2025 JetBrains опубликовали: https://blog.jetbrains.com/ai/2025/04/mellum-goes-open-source-a-purpose-built-llm-for-developers-now-on-hugging-face/
[7] описала: https://blog.jetbrains.com/ai/2025/04/mellum-how-we-trained-a-model-to-excel-in-code-completion
[8] Ollama: https://docs.ollama.com/
[9] Скачиваем: https://ollama.ai/download/windows
[10] chat-роли: https://docs.continue.dev/customize/model-roles/chat
[11] Qwen3 Coder (480B): https://hub.continue.dev/openrouter/qwen3-coder
[12] Qwen3 Coder (30B): https://hub.continue.dev/ollama/qwen3-coder-30b
[13] gpt-oss (120B): https://hub.continue.dev/openrouter/gpt-oss-120b
[14] gpt-oss (20B): https://hub.continue.dev/ollama/gpt-oss-20b
[15] соответствующих ролях: https://docs.continue.dev/customize/model-roles/00-intro
[16] Continue: https://www.continue.dev/
[17] Continue становится: http://docs.continue.dev
[18] spring-petclinic: https://github.com/spring-projects/spring-petclinic
[19] OwnerController.java: https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java
[20] Источник: https://habr.com/en/companies/spring_aio/articles/956028/?utm_source=habrahabr&utm_medium=rss&utm_campaign=956028
Нажмите здесь для печати.