- BrainTools - https://www.braintools.ru -

[2-8] Autocompletion: как LLM создает подсказки кода

Дисклеймер. Я занимаюсь кодинговыми AI-ассистентами: начинал с автодополнения, сейчас работаю с агентами и внедрением AI в SDLC. Последние три года я регулярно выступаю с докладами и образовательными лекциями. За это время у меня накопилось много заметок о том, как устроены AI-ассистенты и агенты, написанных простым языком. Эта одна из них.

В прошлой заметке [1] мы разобрали Fill-in-the-Middle (FIM) – режим, в котором модель получает код до пропуска и после него, а затем восстанавливает середину. Если немного изменить угол зрения [2], то обычное написание кода в IDE тоже похоже на cloze-test: разработчик остановился в некоторой точке файла, а ассистент должен угадать, что он собирается написать дальше, и предложить подсказку. В редакторе это выглядит как полупрозрачный текст рядом с курсором. Пользователь нажимает Tab, и подсказка становится частью файла.

Позицию вставки текста в IDE корректнее называть кареткой, однако далее для лучшей читаемости мы будем использовать слово «курсор».

Само автодополнение появилось задолго до LLM. Уже в 1997 году IntelliSense в Visual Basic 5.0 подсказывал конструкции, допустимые в текущем контексте программы [1] [3]. Со временем автодополнение перестало ограничиваться статическим анализом. Подсказки начали ранжировать с помощью моделей, обученных на больших корпусах кода. В исследованиях появились подходы на основе attention и pointer networks [2] [4], а затем модели, способные предлагать уже не отдельный идентификатор, а целую строку кода [3] [5]. Похожие идеи постепенно дошли до пользователей: например, IntelliCode использовал машинное обучение [6] для ранжирования подсказок IntelliSense [4] [7]. Следующий важный этап произошел в 2021 году с запуском GitHub Copilot, который сделал LLM-based автодополнение кода массовым продуктом [5] [8].

Несмотря на смену технологий, основная задача автодополнения осталась прежней: по доступному контексту понять, какой код пользователь собирается написать дальше. Разберем, как количество и качество этого контекста влияют на подсказку.

Для примера предположим, что пользователь работает с некоторым интернет-магазином. Мы разберем системы автодополнения, которая будет помогать в этом проекте. Пусть он сейчас реализует создание возврата товара и находится в следующем файле:

// returns.ts
import { returnService } from "@/services/returnService";

interface ReturnRequest {
    orderId: string;
    reason: ReturnReason;
}

async function createReturn(request: ReturnRequest) {
    ret// <CURSOR>
}

Через // <CURSOR> обозначено то место, где он остановился. Именно тут мы и должны предложить ему подсказку с кодом. Если передать модели только текущий файл, система автодополнения фактически соберет FIM-запрос из PREFIX и SUFFIX:

Промпт для файла returns.ts

Промпт для файла returns.ts

В таком подходе пайплайн генерации подсказки выглядит совсем просто

Схема наивного подхода

Схема наивного подхода

а модель может предложить правдоподобный вариант дополнения кода:

Пример подсказки при наивном подходе

Пример подсказки при наивном подходе

Проблема этой подсказки в том, что она неправильная. Модель придумала метод create, так как из контекста текущего файла неясно, какой API на самом деле есть у returnService. Добавим в контекст кусок информации из других файлов, а именно – передадим в промпт содержание файлов, на которые ссылается наш returns.ts через import:

// returnService.ts
export interface ReturnService {
    submit(command: SubmitReturnCommand): Promise<Return>;
}

export interface SubmitReturnCommand {
    orderId: string;
    reason: ReturnReason;
    requestedBy: string;
}

В наш пайплайн добавляется еще один шаг – указание дополнительных релеватных файлов:

Схема подхода с добавлением связанных файлов

Схема подхода с добавлением связанных файлов

Теперь модель видит, как на самом деле устроен интерфейс сервиса returnService и предлагает более точную подсказку:

Подсказка с добавлением связанных файлов

Подсказка с добавлением связанных файлов

Метод теперь правильный submit. Структура аргументов тоже правильная. Но появилась новая проблема – currentUser в текущем файле не определен. Модель поняла, что для оформления возврата нужен некоторый пользователь, но придумала способ как его получить. Одного определения сервиса оказалось недостаточно. Посмотрим, что изменится, если добавить еще один сигнал – недавние действия пользователя. Представим, что перед этим он редактировал файл cancelService.ts

// cancelService.ts
import { authService } from "@/services/authService";
import { returnService } from "@/services/returnService";

async function cancelReturn(id: string) {
    const userId = authService.requireUserId();

    return await returnService.cancel({
        id,
        requestedBy: userId,
    });
}

Этот файл не обязан быть прямой зависимостью файла returnService.ts, но он был недавно открыт или изменен, поэтому система автодополнения может использовать его как дополнительный источник контекста. Возможно, разработчик решает похожую задачу прямо сейчас. Добавление этой информации еще немного меняет схему нашего пайплайна:

Схема с добавлением последних редактированных файлов

Схема с добавлением последних редактированных файлов

В LLM уходит следующий промпт, который теперь состоит из контента файлов returnService.ts, cancelService.ts и returns.ts:

Полный промпт для примера

Полный промпт для примера

Один блок показывает место около курсора, другой – API сервиса, третий – недавно редактированный код. В реальном промпте система автодополнения может обрезать тела функций, оставлять только сигнатуры или помещать некоторую информацию рядом с префиксом в виде комментария. Теперь подсказка становится такой

Правильная подсказка

Правильная подсказка

Мы видим, что код после применения подсказки будет невалидным, т.к. переменной userId не существует в текущем окружении. Корректная подсказка не означает, что модель одним ответом обязана идеально дописать весь файл. В реальном редакторе автодополнение чаще работает серией коротких шагов. Сначала пользователь принимает подсказку с returnService.submit(...). Затем видит, что для поля requestedBy нужен userId, поднимается строкой выше, начинает писать

const us

и получает от системы автодополнения следующую подсказку:

Вторая подсказка

Вторая подсказка

После этого останется еще один шаг: добавить импорт authService. Его может предложить тот же ассистент (если пользователь “перепрыгнет” вверх файла), TypeScript language server или quick fix самой IDE. Важно, что модель больше не придумывает currentUser.id . Она подхватывает паттерн, который уже встречался в текущем проекте.

Решая задачу с подсказкой, мы на каждом шаге использовали вызывали одну и ту же модель, но меняли контекст:

  • [prefix + suffix] -> правдоподобная подсказка, но выдуманный метод

  • [prefix + suffix + определение сервиса] -> правильный метод и структура аргумента, но выдуманный способ получения currentUser

  • [prefix + suffix + определение сервиса + недавно редактированный файл] -> подсказка учитывает методы проекта и способ получения пользователя

FIM-модель по своему построению уже умеет вставлять код в нужное место. Но, как мы убедились на примере, чтобы подсказка была полезной, системе автодополнения нужно собрать контекст, который объяснит модели устройство проекта и текущую задачу разработчика.

Откуда брать этот контекст? Выше мы рассмотрели два источника: файл, связанный с текущим кодом через import, и файл, где были последние редактирования. Это понятные и полезные источники данных, но не единственные. Система автодополнения может учитывать

  • текущий PREFIX и SUFFIX;

  • импортированные файлы;

  • определения, полученные через Language Server Protocol [6] [9];

  • недавно открытые и недавно редактированные файлы;

  • похожие фрагменты кода из репозитория;

  • git diff и git history проекта;

  • инструкции проекта вроде CONTRIBUTING.md, docs/**/*.md, .cursorrules;

  • статистику принятых и отклоненных подсказок.

Часть этих данных может попасть в промпт как есть. Часть может быть использована только для выбора фрагментов. Например, история открытых файлов сама по себе не очень полезна для LLM, но она помогает понять, какие файлы сейчас важны пользователю. Если разработчик только что редактировал returnPolicy.ts, то фрагмент оттуда может оказаться полезнее случайного совпадения по слову return. На практике перед вызовом модели часто стоит retrieval-пайплайн:

Схема для retrieval-пайплайна

Схема для retrieval-пайплайна

Сначала система собирает кандидатов: соседние файлы, определения функций/классов, похожий код, недавние изменения. Затем ранжирует их. Потом выкидывает дубли, обрезаем слишком длинные куски и укладываем всё в доступное контекстное окно модели. Просто отправить модели весь репозиторий не получится, т.к. контекстное окно большое, но не бесконечное. К тому же лишний код может ухудшить подсказку: модель начнет ориентироваться на похожий, но неподходящий пример. Поэтому задача автодополнения перестает быть задачей “вызвать LLM”. Она становится задачей “собрать правильный промпт за десятки миллисекунд”.

Реальная система автодополнений состоит не только из “собрал контекст -> получил подсказку от LLM”. Пользователь печатает быстро. Курсор постоянно меняет положение. Файл может быть временно синтаксически сломан. Один запрос к модели может устареть еще до того, как вернулся ответ. А сама модель может вернуть пояснения, Markdown-разметку, повторить часть уже существующего кода, продолжить код из суффикса или сгенерировать фрагмент, который нельзя вставить в текущую позицию без дополнительной обработки. Поэтому вокруг LLM обычно появляется еще несколько слоев:

user input
    ↓
trigger / debounce
    ↓
context collection
    ↓
prompt construction
    ↓
LLM request
    ↓
validation / filtering
    ↓
post-processing
    ↓
rendering in IDE

На входе система решает, нужно ли вообще сейчас запускать генерацию подсказки. И делает это не после каждого нового добавленного символа: иначе редактор будет дергаться и мешать писать. Нужны debounce (небольшая задержка перед началом сбора контекста), отмена устаревших запросов и правила триггера генерации подсказки.

На выходе система автодопорлнения проверяет ответ модели:

  • не пустой ли он;

  • не повторяет ли уже написанный код;

  • не содержит ли Markdown или служебный текст;

  • не слишком ли он длинный для inline-completion;

  • можно ли его вставить в текущую позицию;

  • не ломает ли он очевидный синтаксис.

Только после всех проверок подсказка попадает в UI к пользователю, и он видит серую строку.

В следующих заметках мы отдельно разберем некоторые из частей этого пайплайна: поиск релевантных фрагментов кода, работу с LSP и ранжирование контекста.

Полезные ссылки

  1. Microsoft Announces Visual Basic 5.0 Professional Edition [3] – Microsoft, 1997. Один из ранних публичных примеров IntelliSense как IDE-функции подсказок.

  2. Code Completion with Neural Attention and Pointer Networks [4] – Jian Li et al., 2017. Работа о применении attention и pointer mechanisms к задаче автодополнения кода.

  3. Towards Full-line Code Completion with Neural Language Models [5] – Svyatkovskiy et al., 2020. Работа о full-line code completion и практических ограничениях нейросетевых подсказок в IDE.

  4. Introducing Visual Studio IntelliCode [7] – Microsoft, 2018. Описание ранжирования IntelliSense-подсказок с помощью моделей, обученных на публичных репозиториях.

  5. Introducing GitHub Copilot: your AI pair programmer [8] – GitHub, 2021. Анонс Copilot как массового продукта для генеративного автодополнения кода.

  6. Language Server Protocol [9] – Microsoft, 2016. Стандарт для общения между IDE и языковыми сервисами.

Автор: denisart

Источник [10]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/34550

URLs in this post:

[1] [1/8] Fill-in-the-Middle: как модель дописывает код в середине файла: https://habr.com/ru/articles/1071164/

[2] зрения: http://www.braintools.ru/article/6238

[3] [1]: https://news.microsoft.com/source/1997/02/03/microsoft-announces-visual-basic-5-0-professional-edition/

[4] [2]: https://arxiv.org/abs/1711.09573

[5] [3]: https://arxiv.org/abs/2009.08603

[6] обучение: http://www.braintools.ru/article/5125

[7] [4]: https://devblogs.microsoft.com/visualstudio/introducing-visual-studio-intellicode/

[8] [5]: https://github.blog/news-insights/product-news/introducing-github-copilot-ai-pair-programmer/

[9] [6]: https://microsoft.github.io/language-server-protocol/

[10] Источник: https://habr.com/ru/articles/1071284/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1071284

www.BrainTools.ru

Rambler's Top100