Inline Edit: как научить модель редактировать код по инструкции
В автодополнении разработчик останавливается у каретки, а система пытается угадать, какой код появится дальше. В Inline Edit намерение задается явно: пользователь выбирает код и описывает желаемое изменение на естественном языке.
code completion
prefix + suffix + context → generated code
inline edit
selected code + instruction + context → edited codeНапример, пользователь может выделить функцию и написать:
Добавь проверку прав на выполнение операции.Или:
Перепиши этот код без рекурсии.Или даже просто:
Упрости.После генерации IDE показывает изменение прямо внутри файла в виде diff и предлагает принять или отклонить его. Похожий интерфейс используется, например, в Inline Chat в Visual Studio Code: пользователь может ограничить запрос выделенным блоком кода, а затем применить или отменить предложенное изменение [1].
На уровне интерфейса это новая функция. На уровне архитектуры — вариация уже знакомой системы автодополнения.
| Компонент | Code Completion | Inline Edit |
|---|---|---|
| Намерение | Неявно в prefix и suffix | Явно в инструкции |
| Локальный контекст | Код около каретки | Выделение и окружающий код |
| Внешний контекст | Определения, недавние файлы, retrieval | Определения, usages, правила проекта |
| Ответ модели | Продолжение кода | Замена или набор edit-операций |
| Post-processing | Trim, deduplication, syntax filter | Parse, apply, format, validate |
| UI | Ghost text | Inline diff с accept/reject |
Главное изменение находится в двух строках таблицы. Во-первых, вместо неявного намерения появляется пользовательская инструкция, поэтому обычно используется instruct-модель. Во-вторых, ответ уже недостаточно показать рядом с кареткой: его нужно превратить в операции редактирования и безопасно применить к документу. Именно формат применимого ответа станет новой инженерной задачей этой главы.
Та же система, новый вход
Классические IDE тоже умеют редактировать код: переименовывать символы, извлекать методы, менять сигнатуры функций и обновлять связанные места проекта. Например, Change Signature в IntelliJ IDEA может добавить параметр в метод и автоматически изменить места его вызова [2].
Такие рефакторинги надежны, потому что для каждого действия заранее написан алгоритм. Inline Edit снимает ограничение каталога команд: пользователь может описать почти произвольное преобразование обычным текстом. Но вместе с гибкостью исчезают прежние гарантии. IDE знает, как выполнить Rename; LLM генерирует текст, который выглядит подходящим ответом на инструкцию.
В исследованиях instruction-based code editing часто записывают так:
M(code, instruction) → edited_codeМодель получает исходный код code, инструкцию instruction и возвращает измененный код edited_code [3]. В редакторе входов и выходов больше:
edit(
current_file,
selected_range,
instruction,
repository_context
) → code_editsПредставим, что разработчик реализует создание возврата товара:
import { authService } from "@/services/authService";
import { returnService } from "@/services/returnService";
async function createReturn(request: ReturnRequest) {
const userId = await authService.requireUserId();
return await returnService.submit({
orderId: request.orderId,
reason: request.reason,
requestedBy: userId,
});
}Пользователь выделяет функцию createReturn и пишет:
Проверь, что возврат разрешен, перед отправкой команды.Простейшая реализация Inline Edit отправит instruct-модели выделение, окружающий код и инструкцию:
You are a code editor.
Modify the selected code according to the user instruction.
Preserve all unrelated behavior.
Return only the replacement for the selected code.
<FILE path="src/returns/createReturn.ts">
<BEFORE_SELECTION>
import { authService } from "@/services/authService";
import { returnService } from "@/services/returnService";
</BEFORE_SELECTION>
<SELECTION>
async function createReturn(request: ReturnRequest) {
...
}
</SELECTION>
</FILE>
<INSTRUCTION>
Проверь, что возврат разрешен, перед отправкой команды.
</INSTRUCTION>Модель может вернуть правдоподобную проверку через returnPolicy.canReturn(...). Но она будет вынуждена придумать объект, название метода, аргументы и способ обработки ошибки: из выделенного кода понятно, где сделать изменение, но не как это принято делать в проекте.
Предположим, что в репозитории уже есть интерфейс:
// returnPolicy.ts
export interface ReturnPolicy {
assertCanReturn(command: {
orderId: string;
requestedBy: string;
}): Promise<void>;
}
export const returnPolicy: ReturnPolicy;Добавим его в контекст запроса. Теперь модель может использовать реальный API:
async function createReturn(request: ReturnRequest) {
const userId = await authService.requireUserId();
await returnPolicy.assertCanReturn({
orderId: request.orderId,
requestedBy: userId,
});
return await returnService.submit({
orderId: request.orderId,
reason: request.reason,
requestedBy: userId,
});
}Это тот же эффект, который мы наблюдали в главе об автодополнении:
локальный код + намерение
↓
правдоподобное изменение, но выдуманный API
локальный код + намерение + определение ReturnPolicy
↓
изменение использует API проектаНовая модель не устранила потребность в retrieval, ranking и context builder. Она лишь получила дополнительный сигнал — явную инструкцию. Система по-прежнему должна найти определения, usages, недавние изменения и правила проекта, а затем уложить полезные фрагменты в контекстный бюджет.
Но после добавления проверки файл все равно не соберется: в нем отсутствует импорт returnPolicy.
import { returnPolicy } from "@/returns/returnPolicy";Мы выделили функцию, а необходимое изменение находится за ее границами. Так из знакомой проблемы качества контекста возникает новая проблема Inline Edit: система должна определить не только, что показать модели, но и что модели разрешено менять.
Как определить границы изменения
Выделение играет две разные роли:
- Указывает код, к которому относится инструкция.
- Задает область, внутри которой разрешены изменения.
Эти роли не всегда совпадают. Для инструкции «перепиши цикл через filter» достаточно заменить выделенный цикл. Для инструкции «сделай функцию асинхронной» могут понадобиться сигнатура, импорты и места вызова. Запрос «добавь логирование» иногда требует создать поле класса или подключить зависимость.
Поэтому редактор должен определить editable scope:
selection → file → workspaceТолько выделенный фрагмент проще проверить и безопаснее применить, но он ограничивает возможности модели. Текущий файл позволяет менять импорты и соседние функции. Несколько файлов позволяют обновлять интерфейсы и usages, однако повышают риск несвязанных изменений и усложняют review.
Если явного выделения нет, редактор может найти функцию, класс или выражение около каретки с помощью синтаксического дерева, PSI или Language Server. Если выделена часть выражения, границы можно расширить до целого синтаксического узла.
cursor / selection
↓
scope resolution
↓
editable code rangeScope не следует путать с контекстом. Модели можно показать определение ReturnPolicy, не разрешая ей менять этот файл. И наоборот, файл может входить в editable scope, хотя в prompt отправлены только относящиеся к задаче символы.
В каком виде модель должна вернуть изменение
Для локальной правки достаточно попросить замену выделенного фрагмента:
<REPLACEMENT>
async function createReturn(...) {
...
}
</REPLACEMENT>Клиент удаляет исходное выделение и вставляет содержимое REPLACEMENT. Модели не нужно считать строки или строить патч, но изменить импорт таким ответом невозможно.
Для нескольких участков можно попросить SEARCH/REPLACE:
src/returns/createReturn.ts
<<<<<<< SEARCH
import { authService } from "@/services/authService";
import { returnService } from "@/services/returnService";
=======
import { authService } from "@/services/authService";
import { returnService } from "@/services/returnService";
import { returnPolicy } from "@/returns/returnPolicy";
>>>>>>> REPLACEКлиент ищет исходный текст и заменяет его новым. Формат компактен и не зависит от номеров строк, но секция SEARCH должна совпасть с документом. Пробел, пропущенный комментарий или параллельное редактирование могут сделать операцию неприменимой. Aider, например, использует SEARCH/REPLACE наряду с полным rewrite и разновидностями unified diff [5].
Еще один вариант — вернуть файл целиком. Это удобно для модели: она просто генерирует новую версию документа. Но ради одной строки приходится повторять сотни или тысячи неизмененных токенов. Генерация становится медленнее, а модель может случайно удалить комментарий, поменять форматирование или исправить код, которого не касалась инструкция. При этом full-file rewrite не обязательно менее точен: в экспериментах Cursor некоторые модели лучше создавали новый файл, чем патч, а отдельная Fast Apply model переносила описанное изменение в исходный документ [4].
Наконец, можно запросить unified diff. Он знаком разработчикам и инструментам, но содержит служебную структуру и позиции, в которых модель тоже может ошибиться.
Все перечисленные варианты пока являются форматами, описанными в prompt. Инструкция «верни JSON» или «используй только SEARCH/REPLACE» влияет на поведение модели, но не запрещает ей добавить Markdown, пропустить поле или сломать синтаксис формата. Для программного применения ответа нужна более сильная граница.
Structured Output: синтаксический контракт с моделью
Вместо свободного текста модель может вернуть набор операций, соответствующий заранее заданной схеме:
{
"files": [
{
"path": "src/returns/createReturn.ts",
"edits": [
{
"oldText": "import { returnService } from \"@/services/returnService\";",
"newText": "import { returnService } from \"@/services/returnService\";\nimport { returnPolicy } from \"@/returns/returnPolicy\";"
},
{
"oldText": " return await returnService.submit({",
"newText": " await returnPolicy.assertCanReturn({\n orderId: request.orderId,\n requestedBy: userId,\n });\n\n return await returnService.submit({"
}
]
}
]
}Схема для такого ответа может требовать массив файлов, путь и непустые пары oldText/newText, запрещая любые неизвестные поля:
{
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": { "type": "string" },
"edits": {
"type": "array",
"items": {
"type": "object",
"properties": {
"oldText": { "type": "string", "minLength": 1 },
"newText": { "type": "string" }
},
"required": ["oldText", "newText"],
"additionalProperties": false
}
}
},
"required": ["path", "edits"],
"additionalProperties": false
}
}
},
"required": ["files"],
"additionalProperties": false
}Здесь важно различать три механизма:
- prompt formatting: модель попросили написать JSON;
- JSON mode: ответ должен быть синтаксически валидным JSON;
- Structured Output: генерация ограничена предоставленной JSON Schema.
JSON mode не гарантирует наличие нужных полей и типов. Schema-constrained output гарантирует структуру ответа: например, files будет массивом, а каждый edit будет содержать oldText и newText. В API, поддерживающих Structured Outputs, это достигается ограничением допустимых продолжений во время декодирования, а не только текстовой просьбой в prompt [9].
Но схема проверяет форму, а не смысл значений:
валидный JSON
≠
ответ по заданной JSON Schema
≠
применимый edit
≠
правильное изменениеМодель может вернуть идеально валидный объект, в котором path указывает на несуществующий файл, oldText не встречается в документе, а newText не компилируется. Structured Output устраняет отдельный класс ошибок интеграции — пояснения вместо данных, отсутствующие поля, неожиданные типы, — но не заменяет проверку координат, исходного текста и кода.
Выбор полей тоже определяет надежность. Диапазоны line/character компактны и напрямую преобразуются в TextEdit, но устаревают при сдвиге строк. Пары oldText/newText легче привязать к содержимому, однако oldText может встретиться несколько раз. Можно вернуть оба представления и использовать текст как проверку координат. Несколько операций затем удобно объединить в один WorkspaceEdit редактора [6].
Structured edits не являются универсально лучшим edit format. Для маленькой локальной замены свободный replacement проще. Для моделей, хорошо обученных полному rewrite, промежуточный JSON может снизить качество. Практическое сравнение выглядит так:
| Формат | Сильная сторона | Главный риск |
|---|---|---|
| Replacement | Минимум служебных токенов | Только один участок |
| Whole file | Простая задача для модели | Лишняя генерация и случайные изменения |
| SEARCH/REPLACE | Компактность, нет номеров строк | SEARCH может не совпасть |
| Unified diff | Стандартный формат патча | Хрупкая служебная структура |
| Structured edits | Схему можно проверить до применения | Валидная схема не гарантирует валидный edit |
Поэтому edit format выбирают под размер задачи, интерфейс модели и способ применения, а не по принципу «JSON всегда надежнее текста».
Как безопасно применить ответ
Даже идеальный формат устаревает, если пользователь продолжает печатать. Inline Edit мог быть запущен для версии 41 и строк 10–20, а к моменту ответа документ уже имеет версию 42 и нужный фрагмент находится на строках 12–22.
В запросе нужно сохранить снимок документа или его версию:
request = {
file,
documentVersion,
selectedRange,
selectedText,
instruction
}Если версия изменилась, клиент может отменить ответ, найти исходный текст в новой версии, построить трехстороннее слияние или повторить генерацию. Для небольшой интерактивной функции безопаснее отменить устаревший результат, чем незаметно применить его не туда.
Ответ проходит отдельный pipeline:
LLM response
↓
parse / schema validation
↓
check document snapshot
↓
locate editable ranges
↓
apply edits in memory
↓
format code
↓
validate syntax / diagnostics
↓
render inline diffКлиент проверяет, существуют ли файлы и диапазоны, не вышло ли изменение за editable scope и можно ли найти ожидаемый исходный текст. Изменения сначала применяются к сохраненному снимку в памяти. Полученный код можно передать formatter проекта, а затем проверить, разбирается ли файл, появились ли новые diagnostics, существуют ли добавленные символы и не удален ли большой несвязанный участок.
Даже успешная проверка не доказывает, что изменение правильно по смыслу. Поэтому результат показывается как diff:
import { authService } from "@/services/authService";
import { returnService } from "@/services/returnService";
+import { returnPolicy } from "@/returns/returnPolicy";
async function createReturn(request: ReturnRequest) {
const userId = await authService.requireUserId();
+ await returnPolicy.assertCanReturn({
+ orderId: request.orderId,
+ requestedBy: userId,
+ });
+
return await returnService.submit({Пользователь принимает или отклоняет конкретные строки, а не доверяет абстрактному сообщению «готово».
Как дополнить короткую инструкцию контекстом
Явная инструкция уменьшает неопределенность по сравнению с completion, но не устраняет ее. Фраза «Упрости этот код» может означать: сократить код, убрать дублирование, использовать стандартную библиотеку, улучшить читаемость или повысить производительность.
Исследование внутренней функции Transform Code в Google выделило пять видов часто отсутствующей информации: specifics, operationalization plan, localization/scope, codebase context и user intent. Автоматическое дополнение prompt данными из окружающего кода исправило 9 из 33 ранее неудовлетворительных изменений в тестовом наборе [7].
Это не значит, что пользователь обязан писать техническое задание. Context builder может внутренне расширить короткий запрос:
Добавь проверку политики возврата.до более однозначной инструкции:
Add a return-policy check before returnService.submit.
Use ReturnPolicy.assertCanReturn from src/returns/returnPolicy.ts.
Pass orderId from request.orderId and requestedBy from userId.
Update imports if necessary.
Preserve all unrelated behavior.Как и в completion, хороший prompt собирается из нескольких сигналов:
user instruction
+
selected code and editable scope
+
surrounding file
+
related definitions and usages
+
project instructions
+
diagnosticsПередавать весь репозиторий не нужно. Лишний похожий код может подсказать неправильный API или расширить изменение за пределы запроса.
Нужно ли дообучать edit-модель
В небольшой реализации достаточно обычной instruct-модели. Специализированную модель можно обучать на тройках BEFORE + INSTRUCTION + AFTER, в том числе извлеченных из Git-коммитов. Но реальные коммиты шумны: сообщение бывает слишком общим, один commit объединяет независимые изменения, а часть diff состоит из форматирования.
В InstructCoder отфильтрованные GitHub-коммиты использовались как начальные примеры, а затем набор расширялся синтетическими инструкциями и парами исходного и измененного кода. Датасет включал более 114 тысяч примеров редактирования [8].
Сложное изменение также можно разделить на planning и applying:
instruction + codebase context
↓
planning model
↓
structured change description
↓
apply model
↓
updated filesPlanner понимает намерение, ищет API и определяет затронутые места. Apply model переносит уже сформулированное изменение в код, сохраняя остальной документ. Такое разделение использовалось в Cursor Fast Apply [4].
Это необязательный следующий уровень. Центральная архитектура от числа моделей не меняется.
Полный pipeline
Inline Edit начинается там же, где completion: с пользовательского сигнала, сбора контекста и вызова модели. Различие появляется на входе — намерение сформулировано явно — и особенно на выходе, где сгенерированный текст должен стать проверяемым изменением документа.
instruction + cursor / selection
↓
scope resolution
↓
context collection and ranking
↓
prompt construction
↓
editing model / planner
↓
replacement / patch / structured output
↓
schema and snapshot checks
↓
apply, format and validate
↓
inline diff
↓
accept / rejectВ completion ответ можно отфильтровать и показать как ghost text. В Inline Edit его нужно разобрать, привязать к версии документа, применить к допустимой области и представить как diff. Structured Output помогает провести надежную границу между моделью и программой, но гарантирует только форму данных. Применимость изменения проверяет редактор, смысл — модель вместе с валидаторами, а окончательное решение остается за разработчиком.
Источники
- Inline chat - Visual Studio Code, 2026. Редактирование выделенного кода и просмотр inline diff.
- Change signature - JetBrains, 2026. Детерминированное изменение сигнатур и обновление связанных мест программы.
- Can It Edit? Evaluating the Ability of Large Language Models to Follow Code Editing Instructions - Federico Cassano et al., 2024. Формализация и оценка instruction-based code editing.
- Editing Files at 1000 Tokens per Second - Aman Sanger, 2024. Full-file rewrite, разделение planning и applying, обучение Fast Apply и speculative edits.
- Edit formats - Aider, 2026. Форматы whole, SEARCH/REPLACE и unified diff для получения изменений от разных моделей.
- VS Code API: WorkspaceEdit - Visual Studio Code, 2026. API для применения одного или нескольких текстовых изменений к файлам рабочего пространства.
- Prompting LLMs for Code Editing: Struggles and Remedies - Daye Nam et al., 2025. Неоднозначные пользовательские инструкции и автоматическое расширение промптов.
- InstructCoder: Instruction Tuning Large Language Models for Code Editing - Kaixin Li et al., 2023. Instruction-tuning-датасет для редактирования кода на основе GitHub-коммитов и синтетических примеров.
- Introducing Structured Outputs in the API - OpenAI, 2024. Отличие JSON mode от соответствия JSON Schema и constrained decoding для Structured Outputs.