Привет, Хабр!
Не так давно я рассказывал вам о рождении формата .ap (AI-friendly Patch) — моей попытке избавить мир от боли ручного копипаста при работе с AI-ассистентами. Идея была проста: вместо генерации блоков кода, который нужно переносить в исходники руками, ИИ генерирует семантический патч в специальном, удобном именно для ИИ формате, который применяется автоматически. Судя по числу добавлений статьи в закладки, идея многим пришлась по душе!
Но теория — это одно, а суровая практика — совсем другое. В процессе активного использования ap в реальных задачах (в том числе при работе над far2l) вскрылись узкие места и накопились идеи, как сделать формат ещё надёжнее, удобнее и, что самое главное, — ещё более «понятным» для нейросетей. Сегодня я хочу рассказать вам о результате этой работы — большом обновлении ap 2.0
Это не просто косметические правки, а серьезный шаг вперёд, основанный на главном инсайте: лучшие результаты ИИ показывает тогда, когда мы позволяем ему сначала спланировать свои действия на человеческом языке, и только потом — реализовать их в виде кода. Поехали!
Что нового в 2.0: от инструкций к осмысленным планам
Ключевых изменений несколько, и каждое из них решает конкретную проблему, выявленную в «боевых» условиях.
1. Принцип «Сначала План» (The "Plan-First" Principle)
Это, пожалуй, главное идеологическое изменение. В версии 1.0 ИИ должен был сразу формировать машиночитаемую структуру. Это похоже на то, как если бы вы попросили младшего разработчика внести правку, и он бы молча сразу начал писать код, не объяснив, что и почему он собирается делать.
В 2.0 мы вводим «Принцип „Сначала План“». Теперь ИИ обязан сначала описать свои намерения в виде комментария в начале .ap файла. Этот комментарий имеет чёткую структуру: Summary (что и зачем делаем) и Plan (пошаговый план, как именно).
Помните пример из прошлой статьи? Вот как он выглядел в v1.0:
# afix.ap (v1.0)
version: "1.0"
changes:
- file_path: "greeter.py"
modifications:
- action: REPLACE
target:
snippet: 'print("Hello, world!")'
content: 'print("Hello, AI-powered world!")'
А вот как выглядит тот же патч в v2.0, с учётом нового принципа:
# afix.ap (v2.0)
# Summary: Update the greeting message in greeter.py.
#
# Plan:
# 1. In `greeter.py`, replace the "Hello, world!" string with
# "Hello, AI-powered world!".
#
version: "2.0"
changes:
- file_path: "greeter.py"
modifications:
- action: REPLACE
snippet: |
print("Hello, world!")
content: |
print("Hello, AI-powered world!")
Чем это хорошо:
Для человека: Патч становится самодокументируемым. Вы сразу видите намерение ИИ, как в хорошем коммит-сообщении. Это в разы упрощает ревью.
Для ИИ: Мы разделяем две когнитивно сложные задачи — планирование и кодирование. Сначала ИИ строит логическую цепочку действий, а затем, опираясь на собственный план, генерирует код. Это снижает вероятность ошибок и повышает качество итогового патча.
2. Диапазоны: Прощайте, гигантские snippet'ы
Одна из главных болей v1.0 — замена больших, многострочных блоков кода. ИИ приходилось копировать весь исходный блок в snippet, что было неудобно и хрупко: ошибка всего в одном пробеле внутри блока ломала поиск.
Размышляя над тем, как справиться с этой ситуацией, я вспомнил, как модели генерируют инструкции, когда просишь их «объясни, как внести изменения, как джуну»: они пишут «замени блок с такого-то места и до такого-то». То есть задают не весь блок для замены, а уникальные фрагменты в его начале и конце. Но ведь то же самое можно делать и автоматически!
Встречайте, диапазонные модификации. Теперь для действий REPLACE и DELETE вместо одного snippet можно указать start_snippet и end_snippet. Патчер найдёт блок, начинающегося с одного фрагмента и заканчивающийся другим, и применит действие ко всему, что между ними (включая сами фрагменты).
Представьте, что нам нужно заменить сложный блок логики в функции:
def complex_calculation(data):
# ... какая-то подготовка ...
# Stage 2: Core logic (this whole block will be replaced)
# It has multiple lines and comments.
intermediate_result = 0
for val in processed_data:
intermediate_result += val * 1.1
result = intermediate_result / len(processed_data)
# Stage 3: Post-processing
return f"Final result: {result}"
С помощью диапазона патч становится лаконичным и супернадёжным:
version: "2.0"
changes:
- file_path: "22_range_replace.py"
modifications:
- action: REPLACE
start_snippet: |
# Stage 2: Core logic (this whole block will be replaced)
end_snippet: |
result = intermediate_result / len(processed_data)
content: |
# New, simplified implementation
result = sum(processed_data)
ИИ больше не нужно пытаться без ошибок воспроизвести 5-10 (а в реальных условиях это скорее 50-100) строк кода. Достаточно найти надёжное начало и надёжный конец — это на порядок проще и устойчивее к несущественным ошибкам генерации.
3. Упрощение и унификация формата
Опираясь на практику, я внес в формат ещё несколько улучшений, которые делают его чище и надёжнее:
Плоская структура: Теперь нет избыточной вложенности, добавляемой объектом
target.snippet,anchorи другие поля находятся на одном уровне сaction. Это упрощает и генерацию, и парсинг, и работу с ap патчами вручную.YAML-блоки (
|) обязательны: Чтобы раз и навсегда покончить с проблемами экранирования спецсимволов и забытых кавычек вокруг строки, теперь все поля с кодом (snippet,snippet_start,snippet_end,anchorиcontent) обязаны использовать YAML-синтаксис литеральных блоков. Даже для одной строки. Это обеспечивает единообразие и стопроцентную надёжность, и, опять-таки, сильно повышает читаемость патча человеком и удобство ручной работы с ним. Патчер не сработал? Не беда, перенос изменений вручную теперь ещё проще.Уточнение поиска: В спецификации явно прописано, что поиск
snippet'а послеanchor'а начинается со строки, следующей за концом якоря. Это более интуитивно и предотвращает случайные совпадения внутри самого якоря.
Полный пример в действии
Давайте посмотрим, как эти небольшие, но очень полезные нововведения работают вместе на примере из обновлённой спецификации. Допустим, у нас есть такой файл:
# src/calculator.py
import math
def add(a, b):
# Deprecated: use sum() for lists
return a + b
def get_pi():
return 3.14
А вот патч v2.0, который добавляет импорт, рефакторит функцию add и удаляет get_pi:
# Summary: Refactor the calculator module to enhance the `add` function
# and remove deprecated code.
#
# Plan:
# 1. Import the `List` type for type hinting.
# 2. Update the `add` function to also handle summing a list of numbers.
# 3. Remove the unused `get_pi` function.
#
version: "2.0"
changes:
- file_path: "src/calculator.py"
modifications:
- action: INSERT_AFTER
snippet: |
import math
content: |
from typing import List
- action: REPLACE
anchor: |
def add(a, b):
snippet: |
return a + b
content: |
# New implementation supports summing a list
if isinstance(a, List):
return sum(a)
return a + b
- action: DELETE
snippet: |
def get_pi():
return 3.14
include_leading_blank_lines: 1
Чисто, читаемо и надёжно. Именно таким и должен быть инструмент для автоматизации.
Отдельно про include_leading_blank_lines. По умолчанию ap при сопоставлении образцов игнорирует индентацию и пустые строки, это делает формат ещё более устойчивым как к ошибкам генерации, так и мелким ручным изменениям форматирования перед внесением патча. Поэтому, если мы хотим удалить вместе с неким блоком кода ещё и какое-то число пустых строк до или после него, для этого нужно указать специальный параметр. Я всё предусмотрел!
Самотестирование
Все изменения и в спецификацию, и в патчер, и в тесты вносились с помощью .ap файлов, обеспечивая дополнительный стресс-тест формата прямо в процессе работы над ним. Собственно, диапазонная модификация как раз и появилась как ответ на несколько сбоев, связанных с необходимостью заменять длинные блоки текста в спеке. Подобные проблемы случались и раньше, но я решал их нажатием кнопки «сгенерировать снова». С новым форматом нажимать эту кнопку приходится гораздо реже :)
Заключение
Переход на версию 2.0 — это не просто обновление синтаксиса. Это смена парадигмы. Мы смещаем фокус с «ИИ как генератора токенов» на «ИИ как партнёра по разработке», который сначала формулирует план, а потом его выполняет. Такой подход оказался гораздо эффективнее и для машин, и для людей.
Все изменения — от «плана» до диапазонов — нацелены на одно: повысить надёжность и предсказуемость автоматического применения патчей. Меньше хрупкости, меньше ошибок, больше сэкономленного времени и нервов.
Весь проект, включая обновлённую спецификацию, патчер и полный набор тестов, как всегда, доступен на GitHub. Я обновил там все примеры и документацию до версии 2.0 и добавил новые тесты.
Буду рад вашим отзывам, идеям и, конечно же, пулл-реквестам. Давайте вместе сделаем взаимодействие с нашими кремниевыми помощниками ещё удобнее!
Предыдущая статья цикла