dartyushin.techОткрыть меню
← к книге
глава 0611 минут

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 CompletionInline Edit
НамерениеНеявно в prefix и suffixЯвно в инструкции
Локальный контекстКод около кареткиВыделение и окружающий код
Внешний контекстОпределения, недавние файлы, retrievalОпределения, usages, правила проекта
Ответ моделиПродолжение кодаЗамена или набор edit-операций
Post-processingTrim, deduplication, syntax filterParse, apply, format, validate
UIGhost textInline 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: система должна определить не только, что показать модели, но и что модели разрешено менять.

Как определить границы изменения

Выделение играет две разные роли:

  1. Указывает код, к которому относится инструкция.
  2. Задает область, внутри которой разрешены изменения.

Эти роли не всегда совпадают. Для инструкции «перепиши цикл через filter» достаточно заменить выделенный цикл. Для инструкции «сделай функцию асинхронной» могут понадобиться сигнатура, импорты и места вызова. Запрос «добавь логирование» иногда требует создать поле класса или подключить зависимость.

Поэтому редактор должен определить editable scope:

selection → file → workspace

Только выделенный фрагмент проще проверить и безопаснее применить, но он ограничивает возможности модели. Текущий файл позволяет менять импорты и соседние функции. Несколько файлов позволяют обновлять интерфейсы и usages, однако повышают риск несвязанных изменений и усложняют review.

Если явного выделения нет, редактор может найти функцию, класс или выражение около каретки с помощью синтаксического дерева, PSI или Language Server. Если выделена часть выражения, границы можно расширить до целого синтаксического узла.

cursor / selection

scope resolution

editable code range

Scope не следует путать с контекстом. Модели можно показать определение 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 files

Planner понимает намерение, ищет 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 помогает провести надежную границу между моделью и программой, но гарантирует только форму данных. Применимость изменения проверяет редактор, смысл — модель вместе с валидаторами, а окончательное решение остается за разработчиком.

Источники

  1. Inline chat - Visual Studio Code, 2026. Редактирование выделенного кода и просмотр inline diff.
  2. Change signature - JetBrains, 2026. Детерминированное изменение сигнатур и обновление связанных мест программы.
  3. Can It Edit? Evaluating the Ability of Large Language Models to Follow Code Editing Instructions - Federico Cassano et al., 2024. Формализация и оценка instruction-based code editing.
  4. Editing Files at 1000 Tokens per Second - Aman Sanger, 2024. Full-file rewrite, разделение planning и applying, обучение Fast Apply и speculative edits.
  5. Edit formats - Aider, 2026. Форматы whole, SEARCH/REPLACE и unified diff для получения изменений от разных моделей.
  6. VS Code API: WorkspaceEdit - Visual Studio Code, 2026. API для применения одного или нескольких текстовых изменений к файлам рабочего пространства.
  7. Prompting LLMs for Code Editing: Struggles and Remedies - Daye Nam et al., 2025. Неоднозначные пользовательские инструкции и автоматическое расширение промптов.
  8. InstructCoder: Instruction Tuning Large Language Models for Code Editing - Kaixin Li et al., 2023. Instruction-tuning-датасет для редактирования кода на основе GitHub-коммитов и синтетических примеров.
  9. Introducing Structured Outputs in the API - OpenAI, 2024. Отличие JSON mode от соответствия JSON Schema и constrained decoding для Structured Outputs.