Привет, Хабр!

Не так давно я рассказывал вам о рождении формата .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. snippetanchor и другие поля находятся на одном уровне с 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 и добавил новые тесты.

Буду рад вашим отзывам, идеям и, конечно же, пулл-реквестам. Давайте вместе сделаем взаимодействие с нашими кремниевыми помощниками ещё удобнее!

Предыдущая статья цикла

Комментарии (23)


  1. dyadyaSerezha
    18.10.2025 02:20

    Чистое любопытство. Автор, в каком IDE и каким ИИ-помощником пользуетесь обычно и, если платно, то сколько за подписку?


    1. unxed Автор
      18.10.2025 02:20

      Я сижу в обычном редакторе far2l и пользуюсь Gemini 2.5 Pro через браузер. Пробовал разное, не прижилось. Простые задачки IDE с ИИ плагином решают на отлично, а вот фикс багов в чужом запутанном коде им уже не по зубам, там прям промпт-инжиниринг нужен, итеративность и постоянные откаты неудачных веток диалога. Всё ещё не вижу способа делать это удобнее чем через чат, особенно, в режиме мышления модели.

      Был бы рад сравнительному обзору разных инструментов не на обычных для таких обзоров тасках типа напиши с нуля очередное никому не нужное мобильное приложение, а на производственных и сложных


  1. Awakum
    18.10.2025 02:20

    Нифига себе что я вспомнил: Opencart OCMOD. Спасибо за статью, автор. А как вообще это применять? Я до сих пор не использую модные Cursor и пр - только neovim и генерацию кода в окне браузера и переношу все вручную. Кто-нибудь знает с чего начать, не выбрасывая из этого уравнения любимый редактор?


    1. unxed Автор
      18.10.2025 02:20

      Рад слышать!

      Прикладываем ap.md из репозитория к промпту и просим модель дать ответ в формате ap. Потом применяем с помощью ap.py


      1. Awakum
        18.10.2025 02:20

        Хм, пока непонятен процесс. В каждом промпте нужно заново вводить актуальный код из файлов?


        1. unxed Автор
          18.10.2025 02:20

          ИИ может отслеживать состояние, но через некоторое время начинает путаться. Я в таких случаях обычно прошу «дай новый патч относительно исходной версии». Были мысли включить это требование в спеку, но в итоге решил оставить больше пространства для гибкости, пусть каждый работает так как ему удобно. Ещё были мысли сделать --revert для возможности легкого отката неудачных патчей, но это сильно усложняло формат и повышало число ошибок генерации, так что отказался. git справляется


          1. Awakum
            18.10.2025 02:20

            А почему изобрели свой синтаксис вместо упомянутого вами git, а конкретно git diff? ИИ его нормально воспринимает, и отдавать его ему быстро. Плюс, он совместим с правками руками.


            1. unxed Автор
              18.10.2025 02:20

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


  1. SabMakc
    18.10.2025 02:20

    Сейчас та же Ollama умеет отдавать форматированный ответ - т.е. формат ответа достаточно жестко задается. Не пробовали использовать для генерации ответов? По идее, будет жестко следовать нужному формату - удобнее использовать будет.


    1. unxed Автор
      18.10.2025 02:20

      Не, не пробовал. А в каком формате задаётся?


      1. SabMakc
        18.10.2025 02:20

        Описание и примеры есть тут: https://ollama.com/blog/structured-outputs
        Как понимаю, в поле format задается JSON-схема ответа - и этого достаточно.

        Судя по всему, с JSON в основном и работает (есть поддержка у многих провайдеров LLM).

        В llama.cpp можно и с другими форматами работать - но надо грамматику для них писать (см https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md). Но не факт, что ollama передает этот параметр в llama.cpp (я не пробовал с этим играться).


  1. Axelaredz
    18.10.2025 02:20

    Эм.. кажется делаешь велосипед)
    Ведь можно заюзать, ИИ CLI варианты у которых будет доступ к проекту и всем файлам, и они сами всё будут "патчить") сравнивать и создавать файлы, редачить, удалять в общем с полным доступом.
    Уже без этого гемора с копипастом туда сюда.
    Работая как с онлайн нейронками таки локальными моделями ( https://LMStudio.ai ).

    https://SourceCraft.dev
    https://qwenlm.github.io/qwen-code-docs/ru/
    https://www.geminicli.app
    и тд


    1. unxed Автор
      18.10.2025 02:20

      Это очередной виток споров из предыдущего поста, мол есть же редакторы и агенты. Я в курсе. Это всё эффективно только на тривиальных задачах. Когда нужно искать ошибки в чужом запутанном легаси коде, которого тонны, это всё бесполезно. Только обычный чат, режим thinking, промпт-инжиниринг, точный ручной выбор, что именно экспонировать модели, и постоянные откаты неправильных веток рассуждений.

      Чтоб постоянно об это не спотыкаться, я специально прописал в ридми, чем ap не является: «убийцей» вашего любимого редактора или ИИ агента. Если вам инструмент подходит и задачи ваши решает — это отлично же!


  1. s_yu_skorobogatov
    18.10.2025 02:20

    Мне кажется, что файл ap.md, который нужно прикладывать к каждому промпту, -- длинноват. Это особенно актуально для локально запускаемых моделей, длину контекста которых часто приходится ограничивать из-за недостаточного объёма памяти на GPU. Было бы полезно попытаться сократить этот файл, даже за счёт того, что формат станет не настолько красивым.


    1. unxed Автор
      18.10.2025 02:20

      Да, для моделей с маленьким окном это не годится, вы правы абсолютно. Я-то его на 1кк модели использую в основном, там это мизер на фоне мегабайт кода, которые модель анализирует. Некоторое время назад я думал, не сократить ли спеку, чтоб не усложнять модели генерацию, но практика (и сами модели) говорят, что это работает наоборот: чем подробнее спека, тем строже рамки и легче генерировать. А вот ужать её именно для локальных моделей — задачка интересная! Поделитесь, если получится


      1. s_yu_skorobogatov
        18.10.2025 02:20

        Кстати, а вам приходилось видеть, чтобы модель использовала start_snippet и end_snippet? А то пример из ap.md не использует эти возможности, и у меня есть подозрение, что в результате они должны быть значительно менее популярны, чем snippet.


    1. SabMakc
      18.10.2025 02:20

      Если в системный промт закинуть - то он в кеш попадет один раз и не будет несколько раз обрабатываться (в идеале).


      1. s_yu_skorobogatov
        18.10.2025 02:20

        Проблема не в том, что что-то будет несколько раз обрабатываться, а в том, что оно занимает место в контекстном окне.


        1. SabMakc
          18.10.2025 02:20

          Если задействовать structured output, то вероятно, размер промта можно будет значительно сократить.

          Но в чате уже не отправить вопрос (хотя может есть какой софт с поддержкой задания формата ответа). И, вероятно, на формат JSON придется перейти.


          1. s_yu_skorobogatov
            18.10.2025 02:20

            Формат JSON плох тем, что модели придётся засовывать строки в кавычки, добавляя escape-последовательности. Кроме того, диффы станут нечитаемыми человеком, потому что многострочные фрагменты будут записываться в одну строку с разделителями \n.

            В идеале, поддержку формата AP нужно было бы добавить в код инференса модели. Дело в том, что для качественного структурированного вывода нужно проверять формат на лету и не давать модели порождать токены, не соответствующие формату. К сожалению, это осуществимо разве что с локальными моделями (можно, скажем, разобраться и подредактировать исходники ollama).


            1. SabMakc
              18.10.2025 02:20

              Согласен, JSON далеко не идеален, хотя LLM достаточно неплохо с ним дружит.
              Да и от формата ответа многое зависит - тот же "массив строк кода" останется человеко-читаемым (и экранировать надо будет по минимуму).

              Вообще, в llama.cpp можно задать свой формат вывода - но надо определять грамматику в формате GBNF (GGML BNF). Но это не самый общепринятый формат, как я понимаю - у JSON-схемы поддержка гораздо шире.


              1. s_yu_skorobogatov
                18.10.2025 02:20

                GBNF -- это интересно, да. Правильная вещь. Но я использую ollama, а там, кажется, нет поддержки этого дела.


                1. SabMakc
                  18.10.2025 02:20

                  У ollama под капотом как раз llama.cpp - так что не все так однозначно )