Использование ACE для создания самосовершенствующихся рабочих процессов для студентов магистратуры и структурированных руководств.
Делиться

Контекст — это всё, что видит логическое мышление до того, как сгенерирует ответ. Сюда входят сам запрос, инструкции, примеры, полученные документы, результаты работы инструмента и даже история предыдущих бесед.
Контекст оказывает огромное влияние на качество ответа. Например, если вы попросите студента магистратуры написать SQL-запрос, не предоставив схему данных, результат почти наверняка будет неоптимальным. Хуже того, если у модели вообще нет доступа к базе данных, она может просто сгенерировать неработающий запрос. Даже при наличии инструментов модели все равно потребуется дополнительное время и усилия для определения схемы, прежде чем она сможет выдать правильный ответ.
Поскольку контекст играет центральную роль в приложениях на основе LLM, контекстная инженерия стала дисциплиной, сосредоточенной на систематической оптимизации информации, поступающей в подсказки модели. Цель состоит в создании «самосовершенствующихся» систем, которые учатся на опыте, не полагаясь на дорогостоящую тонкую настройку (переобучение моделей и обновление миллионов параметров).
Разработка контекста имеет ряд ключевых преимуществ:
- Это более экономично и не требует специальных навыков тонкой настройки;
- Контекст и инструкции остаются прозрачными, понятными и легко изменяемыми для человека;
- Циклы итераций значительно быстрее, поскольку обновления можно вносить мгновенно без переобучения или повторного развертывания моделей;
- Это более гибкий подход, особенно когда информацию необходимо удалить по соображениям конфиденциальности или в соответствии с законодательством.
Учитывая все эти преимущества, неудивительно, что контекстная инженерия привлекает к себе столько внимания. Однако интересно то, как быстро развиваются сами подходы. В этой статье я расскажу об этой эволюции, а затем поэкспериментирую с одной из новых фреймворков для оптимизации подсказок: агентной контекстной инженерией (ACE).
Эволюция подходов к проектированию контекста
Контекстная инженерия не появилась в одночасье. Она прошла несколько различных этапов развития.
На самом раннем этапе использовалась статическая подсказка. Здесь подсказки представляли собой инструкции, созданные вручную и никогда не менявшиеся. Большая часть усилий была направлена на классическое проектирование подсказок: тщательный подбор формулировок, структуры и форматирования для повышения эффективности модели.
Следующим важным шагом стало динамическое извлечение информации. Вместо того чтобы полагаться на фиксированную подсказку, системы начали извлекать релевантную информацию (документы, примеры или факты) во время процесса вывода. Генерация с использованием дополненной информации (Retrieval-Augmented Generation, RAG) стала одним из самых популярных подходов в этой категории. Благодаря использованию внешних данных в качестве основы для ответов, RAG значительно повысила точность и уменьшила количество ошибок, особенно при выполнении задач, требующих больших объемов знаний.
В последнее время акцент сместился на контексты самосовершенствования. Вместо того чтобы рассматривать контекст как нечто, что просто извлекается или внедряется, эти подходы позволяют системе обновлять и уточнять свой собственный контекст на основе прошлых результатов. Другими словами, само задание становится адаптивным, развиваясь посредством анализа и обратной связи.
Вокруг этой идеи возникло несколько концепций. Ниже представлены некоторые из наиболее влиятельных из них.
- Одно из самых ранних и значимых произведений — Исследование Шинна и др. « Рефлексия: языковые агенты с вербальным подкреплением в обучении» представило идею о том, что языковые агенты могут учиться на ошибках посредством рефлексии на естественном языке, а не путем градиентных обновлений. Агенты рефлексии анализируют обратную связь от предыдущих попыток, генерируют вербальные размышления о том, что пошло не так, и сохраняют эти размышления в буфере эпизодической памяти. Затем эти сохраненные размышления помогают принимать более взвешенные решения в последующих попытках.
- Еще одним важным вкладом является работа «TextGrad: автоматическое дифференцирование с помощью текста» Юксекгонула и др. TextGrad заимствует концепции из оптимизации глубокого обучения (такие как градиенты, обратное распространение ошибки и градиентный спуск), но заменяет числовые производные обратной связью на естественном языке. В этой структуре LLM-ы генерируют текстовые комментарии, описывающие, как следует изменить переменную для улучшения результата. Затем эти «текстовые градиенты» распространяются обратно через систему с помощью подсказок, фактически выполняя версию обратного распространения ошибки на естественном языке в сложной системе искусственного интеллекта.
- В статье Агравала и соавторов «GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning» используется другой подход, сочетающий эволюционные алгоритмы с рефлексией на основе языка. Подсказки рассматриваются как организмы: они мутируют, конкурируют и эволюционируют под давлением отбора. Со временем лучшие подсказки выживают и распространяются. Этот подход реализован в DSPy, а Hugging Face предоставляет практическое руководство по его применению в реальных условиях.
- Наконец, в работе Сузгуна и др. «Динамическая шпаргалка: обучение во время тестирования с адаптивной памятью» исследуется обучение во время тестирования с помощью персистентной памяти. В этой схеме модели LLM типа «черный ящик» предоставляется блокнот, куда она может записывать полезные стратегии, шаблоны и фрагменты кода во время вывода. Вместо того чтобы многократно заново открывать одни и те же идеи, модель накапливает и повторно использует знания в разных задачах. Эта адаптивная память значительно повышает производительность без необходимости явных меток или обратной связи от человека.
Агентное контекстное проектирование
Теперь, когда мы рассмотрели эволюцию контекстной инженерии, давайте подробнее рассмотрим агентную контекстную инженерию (ACE) , один из более новых подходов и основное направление этой статьи. ACE представлена в статье «Агентная контекстная инженерия: развивающиеся контексты для самосовершенствующихся языковых моделей» Чжана и др., опубликованной в 2025 году.
В начале статьи определяются две ключевые проблемы существующих методов контекстного самосовершенствования.
- Склонность к краткости Система склонна чрезмерно упрощать важные детали и постепенно скатываться к коротким, общим запросам. Хотя компактные запросы привлекательны, они часто теряют нюансы, которые на самом деле обеспечивают хорошую производительность.
- Сбой контекста. Когда системы многократно переписывают весь запрос целиком, они склонны забывать полезные знания, накопленные ранее. Со временем это приводит к нестабильности и регрессии, а не к устойчивому улучшению.
Для решения этих проблем авторы предлагают Agentic Context Engineering (ACE) — структуру, разработанную для масштабируемой и эффективной адаптации контекста как в офлайн-режиме (например, оптимизация системных подсказок), так и в онлайн-сценариях (например, адаптация памяти во время тестирования). Вместо сжатия знаний в единую статическую подсказку, ACE позволяет модели непрерывно развивать свой контекст, накапливая успешные стратегии, анализируя ошибки и структурированно организуя знания. Концептуально это напоминает ИИ-помощника, который совершенствуется со временем, ведя подробные записи и улучшая свой собственный сценарий действий.
В основе ACE лежит цикл агентного обучения, который отражает то, как люди учатся посредством экспериментирования: пробу, размышление и закрепление. Структура состоит из трех компонентов:
- Генератор , который генерирует траектории рассуждений в процессе решения задач;
- Reflector — инструмент, анализирующий успехи и неудачи и извлекающий из них практические выводы;
- Куратор , который интегрирует эти идеи в общий контекст в виде небольших, поэтапных обновлений.
Вместо использования единого монолитного инструмента, ACE организует контекст в виде руководства, состоящего из структурированных пунктов. Каждый пункт содержит метаданные (например, уникальный идентификатор и счетчики, отслеживающие, как часто он был полезен или вреден), а также контент, представляющий собой небольшую, многократно используемую единицу знаний. Это может быть общая стратегия, концепция, специфичная для конкретной области, или распространенный тип ошибки.

Процесс ACE состоит из нескольких этапов.
- Этап генерации. Генератор решает новые проблемы, используя текущий алгоритм действий, отмечая, какие пункты были полезными, а какие — вводящими в заблуждение.
- Этап рефлексии. Анализатор отражающей ситуации анализирует всю траекторию развития событий, извлекая уроки как из успехов, так и из неудач посредством итеративного совершенствования.
- Этап курирования. Куратор преобразует полученные данные в компактные «дельта-обновления» — новые или измененные пункты, которые объединяются с существующим руководством с использованием упрощенной логики, не относящейся к LLM.
- Этап расширения и уточнения. Добавляются новые пункты, существующие обновляются на месте, а периодическая дедупликация устраняет избыточность с помощью семантических вложений.
Данная конструкция обеспечивает параллельную обработку множества обновлений и поддерживает многоэпохальную адаптацию, при которой одни и те же запросы могут быть повторно обработаны для постепенного усиления контекста с течением времени.
Эмпирические данные показывают, что ACE демонстрирует впечатляющие результаты. В сравнительных тестах он превосходит другие подходы к контекстному анализу с самосовершенствованием, достигая улучшения на +10,6% в задачах, решаемых ИИ-агентами, и на +8,6% в специализированных областях, таких как финансы.

Помимо точности, ACE также более экономически эффективен благодаря механизму поэтапного обновления, демонстрируя снижение стоимости токенов на 83,6% по сравнению с базовыми методами.
В совокупности эти результаты позиционируют ACE как практичный и масштабируемый шаг вперед в создании самосовершенствующихся систем LLM.
Использование ACE для сбора данных о намерениях в отношении банковских услуг.
На бумаге фреймворк ACE выглядит многообещающе, поэтому следующий шаг — посмотреть, как он покажет себя на практике. К счастью, авторы опубликовали реализацию с открытым исходным кодом на GitHub, что дает нам надежную отправную точку.
Загрузка данных
Чтобы эксперимент оставался целенаправленным, я решил применить ACE к задаче классификации. Я использую общедоступный набор данных о намерениях в банковской сфере, предоставленный PolyAI (CC BY 4.0). Этот набор данных отражает очень распространенную реальную проблему: определение намерений клиента при обращении в службу поддержки. Точная классификация намерений имеет решающее значение для перенаправления запросов в нужную команду, запуска полуавтоматических ответов или просто мониторинга повторяющихся проблем.
В этом наборе данных каждое сообщение клиента (например, «Я не понимаю, почему моя карта не сработала») необходимо сопоставить с конкретным банковским намерением, таким как «отклоненная оплата картой». В общей сложности существует 77 различных категорий намерений.
Чтобы эксперимент был управляемым, я отобрал 500 примеров из набора данных и разделил их на обучающую, тестовую и валидационную выборки. Ниже приведен код, используемый для загрузки данных и создания этих разделений.
full_df = pd.read_csv('./poly_ai_banking_data/train.csv') # параметры total_number_of_samples = 500 train_share = 0.5 test_share = 0.4 val_share = 0.1 sample_df = full_df.sample(n=total_number_of_samples, random_state=42) .reset_index(drop=True) random.seed(42) sample_df['group'] = random.choices(['train', 'test', 'val'], weights=(train_share, test_share, val_share), k=total_number_of_samples) train_df = sample_df[sample_df['group'] == 'train'].reset_index(drop=True) test_df = sample_df[sample_df['group'] == 'test'].reset_index(drop=True) val_df = sample_df[sample_df['group'] == 'val'].reset_index(drop=True)
Расширение возможностей ACE для обработки данных о банковских намерениях.
Следующий шаг — расширение структуры ACE, чтобы она могла работать с нашим набором данных о банковских намерениях. К счастью, авторы предоставляют подробное руководство, которое делает этот процесс относительно простым.
Помимо добавления нового набора данных, я внес несколько небольших изменений в основную структуру для поддержки антропных моделей и настраиваемых параметров температуры. Полную, измененную версию кода можно найти на GitHub.
Подготовка данных
Первое, что нам нужно сделать, это подготовить набор данных в формате, ожидаемом ACE. Я сохранил обучающую, валидационную и тестовую выборки в виде CSV-файлов в папке banking/data. Каждый пример содержит:
- текст: сообщение службы поддержки клиентов,
- категория: целевая метка намерения, которую мы хотим предсказать.
- group: вспомогательное поле, указывающее, относится ли пример к обучающему, тестовому или валидационному набору данных.
Поле «Группа» не будет использоваться самой системой в дальнейшем, но оно удобно для управления набором данных и обеспечения воспроизводимости результатов.
Вот как выглядит формат данных.
текст,категория,группа Могу ли я изменить свой PIN-код?,изменить_пин,тест Что такое транзакция в 1 доллар на моем счете?,дополнительная_платеж_в_выписке,тест Сколько стоит комиссия за пополнение счета?,пополнение_с_карты,тест Я живу в ЕС — могу ли я получить карту?,поддержка_страны,тест
Далее нам нужно указать ACE, где найти каждое разделение. Это делается путем указания путей к наборам данных в файле banking/data/task_config.json.
{ «banking»: { «train_data»: «./banking/data/train.csv», «val_data»: «./banking/data/val.csv», «test_data»: «./banking/data/test.csv» } }
Реализация процессора данных
Для интеграции новой задачи фреймворку требуется пользовательский модуль DataProcessor. Согласно руководству, это включает в себя реализацию трех основных методов: process_task_data, answer_is_correct и evaluate_accuracy.
Кроме того, нам нужна вспомогательная функция для загрузки исходных данных с диска. Давайте начнём с этого.
Ниже представлена реализация функции загрузки данных. Она считывает CSV-файл, проверяет его наличие и преобразует каждую строку в словарь, с которым может работать остальная часть конвейера.
def load_data(data_path: str) -> List[Dict[str, Any]]: «»» Загрузка и обработка данных из CSV-файла. Ожидаемый формат CSV: текст, категория, группа (с заголовком). Аргументы: data_path: Путь к CSV-файлу. Возвращает: Список словарей, содержащих данные. «»» if not os.path.exists(data_path): raise FileNotFoundError(f»Файл данных не найден: {data_path}») data = [] with open(data_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: data.append({ 'text': row['text'], 'category': row['category'], 'group': row.get('group', '') }) print(f»Загружено {len(data)} образцов из {data_path}») return data
После реализации функции загрузки данных мы можем перейти к реализации оставшихся методов DataProcessor.
Основная цель функции process_task_data — преобразование исходного набора данных в стандартизированный формат ввода ACE.
ACE ожидает, что каждый пример будет содержать три поля: контекст, вопрос и цель. В нашем случае сопоставление довольно простое. Мы напрямую сопоставляем категорию намерения с целью, а поле контекста оставляем пустым, поскольку для классификации не требуется дополнительная информация.
Самое важное здесь — это сам вопрос. Мы добавили дополнительный контекст, чтобы дать понять магистранту, что он должен классифицировать запрос, а не отвечать на вопросы напрямую, а также предоставили список доступных тем, которые помогут магистранту в подготовке ответа.
def process_task_data(self, raw_data: List[Dict]) -> List[Dict]: «»» Преобразует необработанные данные CSV в стандартизированный формат для ACE. Аргументы: raw_data: Необработанные данные, загруженные из CSV (список словарей с ключами 'text', 'category'). Возвращает: Список словарей с ключами: 'context', 'question', 'target' «»» processed_data = [] # Собираем список тем для включения в вопрос topics_list = «, «.join(self.allowed_topics) for item in raw_data: customer_query = item.get('text', '') ground_truth_topic = item.get('category', '') # Вопрос содержит инструкцию по классификации question = ( f»Классифицируйте следующий запрос в службу поддержки клиентов банка по одной из предопределенных тем.nn» f»Запрос клиента: {customer_query}nn» f»Доступные темы: {topics_list}nn» f»Отвечайте ТОЛЬКО названием темы, ничего больше.» ) processed_item = { «context»: «», # Дополнительный контекст не требуется «question»: question, «target»: ground_truth_topic, «others»: { «original_text»: customer_query, «task»: self.task_name, } } processed_data.append(processed_item) return processed_data
Следующий метод, answer_is_correct, проверяет, соответствует ли предсказание модели истинной метке. Поскольку мы явно указываем LLM отвечать только названием категории, здесь достаточно простого сравнения строк без учета регистра.
def answer_is_correct(self, predicted: str, ground_truth: str) -> bool: «»» Проверяет, совпадает ли предсказанная тема с истинной. Использует простое сравнение без учета регистра. Аргументы: predicted: Предсказанная модель тема ground_truth: Истинная тема Возвращает: bool: True, если предсказание верное, False в противном случае «»» return predicted.lower().strip() == ground_truth.lower().strip()
Последний метод, который нам нужно реализовать, — это evaluate_accuracy, который вычисляет общую точность классификации по нескольким прогнозам. Здесь нет ничего сложного. Мы просто вычисляем долю случаев, когда answer_is_correct(prediction, ground_truth) возвращает True.
def evaluate_accuracy(self, predictions: List[str], ground_truths: List[str]) -> float: «»» Вычисляет точность классификации по нескольким прогнозам. Аргументы: predictions: Список прогнозов модели ground_truths: Список тем истинных значений Возвращает: Точность в виде числа с плавающей запятой от 0 до 1 «»» if len(predictions) != len(ground_truths): raise ValueError(«Прогнозы и истинные значения должны иметь одинаковую длину») if not predictions: return 0.0 correct = sum( 1 for pred, truth in zip(predictions, ground_truths) if self.answer_is_correct(pred, truth) ) return correct / len(predictions)
Составление сценария рабочего процесса
После установки DataProcessor следующим шагом является создание комплексного скрипта запуска для ACE. Я создал скрипт run_ace_workflow, который принимает несколько ключевых аргументов:
- Параметр api_provider выбирает API языковой модели для использования ('anthropic', 'openai', 'together' или 'sambanova'), по умолчанию используется 'anthropic'.
- Параметр generator_model задает модель для агента Generator (по умолчанию: 'claude-haiku-4-5').
- Параметр reflector_model задает модель для агента Reflector (по умолчанию: 'claude-sonnet-4-5').
- Параметр curator_model задает модель для агента Curator (по умолчанию: 'claude-sonnet-4-5').
- max_train и Параметр max_test — это необязательные ограничения на размеры обучающего и тестового наборов данных, полезные для быстрых экспериментов или отладки.
Давайте обсудим, как на самом деле работает этот скрипт. Скрипт начинается с загрузки данных о банковских намерениях и инициализации DataProcessor. Вот вспомогательная функция, которую я для этого написал.
def load_banking_data(max_train=None, max_test=None): «»»Загрузка и обработка банковского набора данных.»»» from banking.data_processor import DataProcessor, load_data base_path = os.path.dirname(__file__) data_path = os.path.join(base_path, «data») # Загрузка необработанных данных train_raw = load_data(os.path.join(data_path, «train.csv»)) val_raw = load_data(os.path.join(data_path, «val.csv»)) test_raw = load_data(os.path.join(data_path, «test.csv»)) # Ограничение количества выборок, если указано if max_train: train_raw = train_raw[:max_train] val_raw = val_raw[:max(max_train // 4, 10)] if max_test: test_raw = test_raw[:max_test] # Обработка данных processor = DataProcessor(task_name=»banking») train_samples = processor.process_task_data(train_raw) val_samples = processor.process_task_data(val_raw) test_samples = processor.process_task_data(test_raw) return train_samples, val_samples, test_samples, processor train_samples, val_samples, test_samples, processor = load_banking_data( max_train=args.max_train, max_test=args.max_test )
Следующий шаг — определение шаблона сценария действий. Это важно, потому что текущая реализация ACE не может динамически создавать новые разделы, поэтому мы предварительно определяем структуру, чтобы она служила ориентиром для модели. Вот шаблон, который я использовал для банковской сферы.
BANKING_PLAYBOOK_TEMPLATE = «»» ## ОБЩИЕ ## ПРИНЦИПЫ КЛАССИФИКАЦИИ ## РАЗЛИЧИЕ КАТЕГОРИЙ ## ЗНАНИЯ В БАНКОВСКОЙ СФЕРЕ ## ОБЩИЕ ШАБЛОНЫ ## ОБРАБОТКА НЕОДНОЗНАЧНЫХ ЗАПРОСОВ ## РАСПРОСТРАНЕННЫЕ ОШИБКИ, КОТОРЫХ СЛЕДУЕТ ИЗБЕГАТЬ ## ДРУГИЕ «»»
Подготовив данные и шаблон, мы можем инициализировать объект ACE основными параметрами.
ace_system = ACE( api_provider=args.api_provider, generator_model=args.generator_model, reflector_model=args.reflector_model, curator_model=args.curator_model, max_tokens=4096, initial_playbook=BANKING_PLAYBOOK_TEMPLATE, use_bulletpoint_analyzer=True, # включение дедупликации пунктов списка в плейбуке generator_temperature=0.1, # приоритет согласованности для генератора reflector_temperature=0.7, # приоритет креативности для отражателя и куратора curator_temperature=0.7, )
Наконец, мы определяем функцию для запуска процесса обучения ACE, который включает в себя первоначальную оценку, итеративное осмысление, курирование и заключительную оценку.
def run_ace_training(ace_system, train_samples, val_samples, test_samples, processor, results_dir): «»»Обучение ACE для улучшения сценария (включает начальную и конечную оценки).»»» config = { 'num_epochs': 1, 'max_num_rounds': 3, # максимальное количество раундов отражения на выборку 'curator_frequency': 5, # запускать куратор каждые 5 шагов 'eval_steps': max(len(train_samples) // 10, 10), # оценивать 10 раз во время обучения 'save_steps': max(len(train_samples) // 10, 10), 'playbook_token_budget': 80000, 'task_name': 'banking_ace', 'json_mode': False, 'no_ground_truth': False, 'save_dir': os.path.join(results_dir, «training»), 'test_workers': 10, } results = ace_system.run( mode='offline', train_samples=train_samples, val_samples=val_samples, test_samples=test_samples, data_processor=processor, config=config ) # Извлечение результатов initial_acc = results.get('initial_test_results', {}).get('accuracy', 0) final_acc = results.get('final_test_results', {}).get('accuracy', 0) training_results = results.get('training_results', {}) return ace_system.best_playbook, results best_playbook, training_results = run_ace_training( ace_system, train_samples, val_samples, test_samples, processor, results_dir )
Вот и всё! Это вся основная логика, необходимая для работы ACE. Для удобства я добавил немного логирования в начало рабочего процесса, но это не является обязательным для основной функциональности.
Результаты
Давайте посмотрим на результаты и разберемся, как все это работает. Для начала, ознакомьтесь с лучшим руководством, которое вы найдете по адресу results/banking_{dt}/best_playbook.txt. Руководство организовано в виде пунктов, сгруппированных в соответствии с категориями, которые мы определили в нашем первоначальном шаблоне. Каждый пункт содержит подробные инструкции и стратегии, а также метаданные, показывающие, как часто он был отмечен как полезный или вредный. Такая структура позволяет легко увидеть, какие темы и стратегии система сочла наиболее полезными во время обучения.
## ОБЩИЕ ## ПРИНЦИПЫ КЛАССИФИКАЦИИ [cls-00001] helpful=1 harmful=0 :: Временные индикаторы, такие как «было возможно раньше», «работало ранее» или «работало раньше», являются сильными сигналами того, что проблема специфична для текущей транзакции, а не является общей проблемой возможностей системы. Эти фразы указывают на изменение статуса для конкретного объекта (получателя, карты, счета), а не на общую функциональность. [cls-00002] helpful=18 harmful=4 :: Примените иерархию специфичности: если может применяться несколько категорий, выберите наиболее специфичную, соответствующую контекстным подсказкам. Например, beneficiary_not_allowed (специфично для получателя) более специфично, чем declined_transfer (общая ошибка). [cls-00009] полезно=0 вредно=3 :: Иерархия специфичности работает в обоих направлениях: выбирайте конкретные категории, когда контекстные подсказки указывают на определенный тип транзакции, но используйте общие категории (например, «дополнительная_плата_по_выписке»), когда запросу не хватает достаточного контекста для определения специфического характера транзакции. Не навязывайте специфичность, если запрос клиента по своей сути общий. [cls-00017] полезно=5 вредно=1 :: Различие между процесс-ориентированным и отслеживающим статус: различайте вопросы о том, КАК получить/приобрести что-либо (процесс-ориентированный подход), и вопросы о том, КОГДА что-либо прибудет или ПРИБЫЛО ЛИ ОНО (отслеживающий статус). Вопросы о процессе сосредоточены на необходимых шагах и компонентах, в то время как вопросы о статусе сосредоточены на сроках и подтверждении доставки. Используйте это различие для выбора между категориями приобретения и категориями отслеживания/прибытия. ## РАЗЛИЧИЕ КАТЕГОРИЙ [dis-00003] helpful=1 harmful=0 :: declined_transfer vs beneficiary_not_allowed: Если клиент упоминает, что мог переводить раньше, но внезапно не может, это убедительно указывает на beneficiary_not_allowed (получатель заблокирован/ограничен), а не на declined_transfer (общая ошибка перевода из-за средств, лимитов или системных ошибок). [dis-00011] helpful=11 harmful=0 :: pending_* vs failed_* vs declined_*: Состояние транзакции имеет решающее значение для классификации. «Еще не выполнено» или «слишком долго» = состояние ожидания. «Не сработало», «было отклонено» или «было отклонено» = состояние сбоя/отклонения. «Деньги вернулись» или «было возвращено» = состояние возврата. Сопоставьте категорию с фактическим описанным состоянием транзакции. [dis-00012] helpful=0 harmful=1 :: country_support vs supported_cards_and_currencies: Запросы о географической доступности («какие страны», «где я могу», «какие регионы») следует классифицировать как «country_support». В отличие от этого, «supported_cards_and_currencies» предназначен для вопросов о типах карт (Visa, Mastercard) и вариантах валют, а не о географической доступности. [dis-00014] helpful=2 harmful=0 :: Проблемы со снятием наличных: Различайте по состоянию и результату транзакции: «pending_cash_withdrawal» (еще не завершено, все еще обрабатывается), «declined_cash_withdrawal» (отклонено, наличные не получены), «cash_withdrawal_not_recognised» (клиент не помнит транзакцию) и «wrong_amount_of_cash_received» (транзакция завершена, но выдана неправильная сумма). Если наличные были получены, но сумма указана неверно, используйте наиболее конкретную категорию: wrong_amount_of_cash_received. [dis-00015] helpful=3 harmful=3 :: card_arrival vs get_physical_card: Различайте вопросы отслеживания статуса (card_arrival) и вопросы процесса получения карты (get_physical_card). 'card_arrival' используется для отслеживания существующих заказов («Пришла ли моя карта?», «Где моя карта?»). 'get_physical_card' охватывает весь процесс получения физической карты, включая все компоненты, такие как PIN-код («Где я могу найти свой PIN-код?», «Как мне получить карту и PIN-код?»). Вопросы об отсутствующих PIN-кодах с фразой «еще не получил» указывают на то, что клиент находится в процессе получения карты, а не просто отслеживает доставку. [dis-00021] helpful=1 harmful=0 :: card_payment_not_recognised vs extra_charge_on_statement: Когда клиент упоминает «платеж», который он не узнает или не совершал («платеж, который я никогда не отправлял», «платеж, который я не авторизовал»), классифицируйте его как «card_payment_not_recognised», поскольку «платеж» — это конкретный тип транзакции. Используйте «extra_charge_on_statement» только тогда, когда клиент описывает неожиданные суммы, сборы или комиссии БЕЗ указания типа транзакции (например, «я вижу дополнительные 5 долларов в своей выписке», «там странный сбор», не упоминая платеж/перевод/снятие средств). [dis-00024] helpful=0 harmful=1 :: Специфика категорий комиссий/сборов: Когда клиенты спрашивают о комиссиях или сборах, отдавайте приоритет категориям комиссий, специфичным для типа транзакции, а не «extra_charge_on_statement». Если в запросе указан конкретный тип транзакции (перевод, платеж, снятие средств, пополнение счета), используйте соответствующую категорию комиссий: 'transfer_fee_charged' для комиссий за перевод, 'card_payment_fee_charged' для комиссий за платеж, 'atm_fee_charged' для комиссий за снятие средств, 'top_up_fee' для комиссий за пополнение счета. 'extra_charge_on_statement' следует использовать только для запросов о комиссиях, где не указан конкретный тип транзакции (например, «Почему взимается дополнительная плата в размере 5 долларов?» без контекста). [dis-00026] helpful=0 harmful=0 :: receiving_money vs transfer_into_account: Различайте пассивное получение и активный перевод. 'receiving_money' используется для запросов о получении средств ОТ другой стороны (пассивный, инициированный отправителем). 'transfer_into_account' используется для запросов о том, что клиент инициирует перевод для пополнения своего счета (активный, инициированный им самим). Контекстные подсказки: пустой/низкий баланс + вопросы о переводах = вероятно, перевод на счет. Вопросы о том, «могу ли я перевести средства» в контексте необходимости пополнения счета = перевод на счет, а не получение денег. [dis-00029] полезно=0 вредно=0 :: beneficiary_not_allowed vs declined_transfer: Когда запрос явно упоминает «бенефициара» или «получателя» в сочетании с ограничительными формулировками («не разрешено», «заблокировано», «ограничено», «нельзя добавить», «невозможно добавить»), классифицируйте как «beneficiary_not_allowed» даже без временных индикаторов. Сочетание конкретного термина банковской организации (бенефициар/получатель) с ограничительными формулировками является сильным прямым сигналом об ограничениях на уровне получателя, а не об общих сбоях переводов. ## ЗНАНИЯ В БАНКОВСКОЙ СФЕРЕ [bank-00006] helpful=0 harmful=0 :: В банковской сфере, когда ранее успешный перевод внезапно прерывается, распространенными причинами являются: блокировка/пометка получателя системами обнаружения мошенничества, ограничения на счет получателя или удаление получателя из списка разрешенных. Это отличается от обычных отказов в переводе из-за недостатка средств или системных ошибок. [bank-00008] helpful=0 harmful=6 :: Небольшие неожиданные суммы (например, 1 фунт стерлингов, 0,01 фунта стерлингов), появляющиеся в выписках, часто указывают на блокировку авторизации, плату за проверку или различные сборы. Когда клиенты задают вопросы по этому поводу без дополнительного контекста, их следует классифицировать как «дополнительные_сборы_в_выписке», а не как более конкретные типы транзакций. [bank-00018] helpful=0 harmful=0 :: «card_swallowed» — это термин банковской индустрии для сценариев удержания карты банкомата, когда устройство удерживает карту клиента. Это относится к случаям, когда карты застряли, не вынимаются или удерживаются банкоматом, независимо от конкретной формулировки, используемой клиентом. [bank-00020] helpful=10 harmful=4 :: В банковской терминологии существует иерархия специфичности для ссылок на транзакции. Ключевые слова для конкретных типов транзакций включают: «платеж» (платежи картой), «перевод» (денежные переводы), «снятие наличных» (снятие наличных), «пополнение счета» (пополнение счета), «прямой дебет», «постоянное поручение». Общие термины включают: «платеж», «сумма», «транзакция», «комиссия». Когда клиент использует ключевое слово для конкретного типа транзакции, это обеспечивает достаточный контекст для классификации по категориям, специфичным для данного типа транзакции, а не по общим категориям. ## ОБЩИЕ ШАБЛОНЫ [pat-00004] helpful=0 harmful=0 :: Шаблон: «Раньше работало, теперь нет» + контекст перевода = вероятно, ограничение на уровне получателя, а не отказ на системном уровне. Предыдущий успех указывает на работоспособность счета и механизма перевода, что свидетельствует о наличии конкретного ограничения для текущего получателя. [pat-00007] helpful=3 harmful=6 :: Шаблон: Клиент описывает транзакцию как «странную», «неожиданную», «необъяснимую» или спрашивает «что это за списание» в выписке, не предоставляя конкретного контекста типа транзакции (перевод, платеж, снятие средств и т. д.) = классифицируется как «дополнительное_списание_в_выписке». Это подходящая общая категория, когда характер списания неясен. [pat-00010] helpful=8 harmful=1 :: Шаблон: Фразы типа «еще не обработано», «все еще в ожидании», «не завершено» или «все еще в ожидании» указывают на транзакцию в состоянии ОЖИДАНИЯ, а не в состоянии НЕУДАЧИ. Выбирайте категории «ожидание_*» вместо категорий «неудачно_*» или «отклонено_*», если присутствуют эти языковые подсказки. [pat-00013] helpful=0 harmful=2 :: Шаблон: Вопросы с географическими индикаторами, такими как «какие страны», «где я могу», «какие регионы» или «в каких местах», касаются доступности услуг по географическому признаку и классифицируются как «поддержка страны». Основная цель — понять географический охват услуг. [pat-00016] helpful=2 harmful=9 :: Шаблон: Формулировки «Где я могу найти» или «Как мне получить» указывают на вопросы, ориентированные на процесс и направленные на получение информации о приобретении чего-либо, а не на отслеживание статуса. Обычно они должны соответствовать категориям приобретения/настройки (например, «получить_физическую_карту»), а не категориям доставки/отслеживания (например, «получение_карты» или «оценка_доставки_карты»). [pat-00019] helpful=0 harmful=0 :: Шаблон: Фразы, указывающие на физическое удержание карты банкоматом («карта застряла в банкомате», «карта не выходит», «банкомат забрал мою карту», «извлечь карту из банкомата», «извлечь карту из аппарата»), следует классифицировать как «card_swallowed». Ключевым индикатором является физическое удержание/задержание карты банкоматом, а не другие проблемы с картой, такие как повреждение, потеря или проблемы с функциональностью. [pat-00022] helpful=1 harmful=0 :: Шаблон: Ключевое слово конкретного типа транзакции + «не распознано»/«не совершено»/«никогда не отправлено» = использовать категорию «не распознано» для конкретного типа транзакции. Примеры: «платеж, который я не совершил» → card_payment_not_recognised; «перевод, который я не распознаю» → transfer_not_received_by_recipient или связанная проблема с переводом; 'снятие средств, которое я никогда не совершал' → cash_withdrawal_not_recognised. Наличие ключевого слова, указывающего на конкретный тип транзакции (платеж, перевод, снятие средств), является достаточным контекстом, чтобы избежать общих категорий. [pat-00025] helpful=1 harmful=0 :: Шаблон: Ключевое слово типа транзакции + вопрос о времени ('как долго', 'когда', 'сколько времени') + географическое упоминание = приоритет категории времени, специфичной для транзакции (например, 'transfer_timing', 'card_delivery_estimate'). Географические упоминания следует рассматривать как контекстную информацию о происхождении/назначении транзакции, если запрос явно не спрашивает о доступности услуги ('в каких странах', 'где я могу ее использовать', 'доступна ли она'). Пример: 'перевод из Китая, как долго?' → 'transfer_timing' (а не 'country_support'). [pat-00027] helpful=0 harmful=0 :: Шаблон: Контекст баланса счета + запрос на перевод = намерение пополнить счет. Когда клиент упоминает, что его счет пуст/нет средств/нуждается в деньгах, И спрашивает о переводе, он спрашивает о перемещении средств НА свой счет (transfer_into_account), а не о получении денег от других (receiving_money). Состояние счета предоставляет критически важный контекст для различения намерений, связанных с переводом. ## ОБРАБОТКА НЕОДНОЗНАЧНЫХ ЗАПРОСОВ ## РАСПРОСТРАНЕННЫЕ ОШИБКИ, КОТОРЫХ СЛЕДУЕТ ИЗБЕГАТЬ [err-00005] helpful=2 harmful=0 :: Не следует по умолчанию использовать общие категории (например, declined_transfer), когда временной контекст («раньше было возможно») указывает на более конкретную проблему. Изменение во времени является ключевым фактором, который часто указывает на ограничения, специфичные для конкретного субъекта (получатель, карта, счет), а не на общие сбои. [err-00023] helpful=2 harmful=0 :: Не используйте по умолчанию 'extra_charge_on_statement', когда клиент упоминает конкретный тип транзакции (платеж, перевод, снятие средств, пополнение), который он не узнает. 'extra_charge_on_statement' следует использовать только в действительно неоднозначных случаях, когда тип транзакции не указан. Когда клиент говорит: «Платеж, которого я никогда не совершал», слово «платеж» предоставляет достаточный контекст для использования 'card_payment_not_recognised' вместо общего 'extra_charge_on_statement'. [err-00028] helpful=0 harmful=0 :: Не применяйте правила шаблонов или знания предметной области, не имеющие отношения к запросу. Если в запросе нет географических индикаторов, не применяйте географические шаблоны. Если нет упоминания о комиссиях, не применяйте правила, связанные с комиссиями. Сосредоточьтесь на правилах, которые напрямую соответствуют семантическому содержанию и контексту запроса клиента, а не пытайтесь найти любое подходящее правило. Применение нерелевантных правил приводит к неправильной классификации. ## ДРУГИЕ
Для более подробного изучения работы каждого агента вы можете ознакомиться с подробными журналами выполнения по адресу results/banking_{dt}/training/ace_run_{dt}/detailed_llm_logs. Я настоятельно рекомендую просмотреть эти журналы. По крайней мере, бегло просмотрите подсказки и посмотрите, как взаимодействуют Генератор, Рефлектор и Куратор. Это отличный способ понять, как ACE шаг за шагом развивает контекст.
Конечно, наиболее интересным показателем является точность. Результаты начального и конечного тестирования можно найти в файлах results/banking_{datetime}/training/initial_test_results.json и results/banking_{datetime}/training/final_test_results.json.
# Начальные результаты { «test_results»: { «accuracy»: 0.7512437810945274, «correct»: 151, «total»: 201, «no_answer»: 0 }, «error_log»: { «accuracy»: 0.7512437810945274, «errors»: [ { «index»: 2, «prediction»: «declined_card_payment», «ground_truth»: «declined_transfer» }, { «index»: 9, «prediction»: «top_up_limits», «ground_truth»: «automatic_top_up» }, { «index»: 7, «prediction»: «transfer_not_received_by_recipient», «ground_truth»: «balance_not_updated_after_cheque_or_cash_deposit» }, … ] } } # окончательные результаты { «test_results»: { «accuracy»: 0.736318407960199, «correct»: 148, «total»: 201, «no_answer»: 0 }, «error_log»: { «accuracy»: 0.736318407960199, «errors»: [ { «index»: 9, «prediction»: «top_up_limits», «ground_truth»: «automatic_top_up» }, { «index»: 2, «prediction»: «declined_card_payment», «ground_truth»: «declined_transfer» }, { «index»: 7, «prediction»: «pending_transfer», «ground_truth»: «balance_not_updated_after_cheque_or_cash_deposit» }, … ] } }
Результаты, надо признать, не очень впечатляют. Фактически, точность немного снизилась после оптимизации, с 75,1% до 73,6%. Но даже отрицательные результаты могут научить нас чему-то ценному.
Существует несколько вероятных причин, по которым программа ACE в данном случае не принесла существенной пользы:
- Ограниченное количество данных по каждой категории. У нас было всего 248 обучающих примеров, 201 тестовый пример и 51 пример для валидации. Однако наша задача включала 77 различных категорий. При таком малом количестве примеров на класс модель, возможно, просто не располагала достаточным количеством данных для изучения значимых различий.
- Небольшой и нерепрезентативный набор данных для проверки. Всего 51 пример, поэтому набор данных, возможно, не охватывает всё разнообразие запросов клиентов, что затрудняет для ACE получение полезных выводов и предложений по улучшению.
- Сложность задачи. Наш сценарий использования относительно прост. Как отмечают авторы, ACE, как правило, наиболее эффективен в сценариях с большим объемом узкоспециализированных знаний в предметной области или более сложными рабочими процессами агентов, где рефлексия и итеративное уточнение контекста могут значительно повысить производительность.
Использование ACE для генерации кода
Вдохновленный предыдущим экспериментом, я решил попробовать ACE еще раз. На этот раз на наборе данных Mostly Basic Python Problems (доступен под лицензией cc-by-4.0). Надеюсь, результаты будут более многообещающими при решении задачи генерации кода.
Обзор данных
Каждый пример в наборе данных содержит три ключевых компонента:
- Например, вопрос : «Напишите функцию для переворачивания слов в заданной строке».
- Эталонная реализация — эталонный код на Python. Например, для ответа на вопрос выше.
def reverse_words(s): return ' '.join(reversed(s.split()))
- Тестовые примеры — это утверждения, используемые для проверки сгенерированного кода, например:
[ assert reverse_words(«python program»)==(«program python»), assert reverse_words(«java language»)==(«language java»), assert reverse_words(«indian man»)==(«man indian») ]
Добавление новой задачи в структуру ACE.
Аналогичным образом мы можем расширить фреймворк ACE для обработки задач программирования. Я не буду вдаваться в подробности реализации, поскольку полный код можно найти на GitHub. Однако стоит отметить ключевые отличия по сравнению с примером банковских намерений.
Задачи программирования по своей природе сложнее. В случае банковских намерений модель выдает один класс из 77, который легко напрямую сравнить с эталонными данными. Однако при генерации кода LLM может создавать произвольный код, поэтому мы не можем просто проверить точное совпадение. Вместо этого нам необходимо провести тесты, чтобы определить, является ли сгенерированное решение правильным.
# банковское дело def answer_is_correct(self, predicted: str, ground_truth: str) -> bool: return predicted.lower() == ground_truth.lower() # программирование def answer_is_correct(self, predicted: str, ground_truth: str, test_list: List[str], idx: int, save_dir: str) -> bool: code = extract_code_from_response(predicted) result = execute_code_with_tests(code, test_list, timeout=5) return result['success']
Из-за этой дополнительной сложности мне пришлось внедрить несколько улучшений в DataProcessor для генерации кода:
- Извлечение кода. В LLM-файлах часто содержится дополнительный контекст, например, форматирование Markdown («python …»). Нам необходимо очистить и извлечь код, чтобы обеспечить его корректную компиляцию.
- Безопасное выполнение. Поскольку мы запускаем сгенерированный код для проверки его корректности, важно внедрить базовые меры безопасности, такие как тайм-ауты и изолированные среды выполнения.
- Необходимо предоставить полный контекст. Крайне важно включить в вопрос всю необходимую информацию. Если мы просто попросим LLM сгенерировать код, он вряд ли пройдет тесты, поскольку будет непонятно, какое имя функции или сигнатура ожидается. Именно поэтому крайне важно предоставить все необходимые детали в вопросе при стандартизации данных в функции process_task_data.
вопрос = ( f»Напишите функцию на Python для решения следующей задачи:nn» f»Задача: {текст_задачи}nn» f»Ваш код должен пройти следующие тестовые случаи:n» f»{форматированные_тестовые_случаи}nn» f»Важно: тестовые случаи будут выполняться для вашего кода. » f»Убедитесь, что имя и сигнатура вашей функции соответствуют ожиданиям тестов.nn» f»Ответьте ТОЛЬКО кодом на Python, без пояснений.» )
В оригинальной реализации ACE Reflector сравнивал сгенерированный код напрямую с эталонным, что подходит для задач классификации. Однако для программирования такой подход не имеет смысла: может существовать несколько правильных решений, и оптимизация кода, «похожего» на эталонный, не гарантирует его прохождение тестов.
Для решения этой проблемы я реализовал новый метод, get_test_feedback, который предоставляет Reflector фактические результаты выполнения тестов и сообщения об ошибках. Результаты выполнения тестов становятся основным сигналом корректности, предоставляя гораздо более информативную обратную связь, чем простое сравнение кода.
def get_test_feedback(self, predicted: str, ground_truth: str, test_list: List[str] = None) -> str: «»» Получает подробную обратную связь о выполнении тестов для рефлектора. Этот метод предоставляет рефлектору фактические результаты тестов и сообщения об ошибках, что более информативно, чем просто сравнение сгенерированного кода с эталонным. Выходные данные тестов являются основным сигналом корректности в задачах генерации кода. Аргументы: predicted: прогнозируемый код модели ground_truth: эталонный код (только для справки, не используется для оценки) test_list: список утверждений тестов для выполнения Возвращает: str: подробная строка обратной связи с результатами выполнения тестов «»» if test_list is None: return «Тестовые случаи не предоставлены — невозможно оценить код.» # Извлекаем код из ответа, если необходимо code = extract_code_from_response(predicted) # Выполняем код с тестами result = execute_code_with_tests(code, test_list, timeout=self.timeout) # Формируем подробную обратную связь feedback_parts = [] if result['success']: feedback_parts.append(f»✓ Все {result['total']} тестов ПРОЙДЕНЫ») feedback_parts.append(«nТестовые случаи успешно выполнены:») for i, test in enumerate(test_list, 1): feedback_parts.append(f» {i}. {test} ✓») else: feedback_parts.append(f»✗ Тесты НЕ ПРОЙДЕНЫ: {result['passed']}/{result['total']} тестов пройдены») if result['timeout']: feedback_parts.append(«n⏱ ТАЙМ-АУТ: Выполнение кода превысило лимит времени») if result['errors']: feedback_parts.append(«n— ПОДРОБНОСТИ ОШИБКИ —«) for error in result['errors']: feedback_parts.append(f» • {error}») # Показать, какие тесты пройдены, а какие нет feedback_parts.append(«n— РЕЗУЛЬТАТЫ ТЕСТОВ —«) for i, test in enumerate(test_list, 1): # Проверить, встречается ли этот конкретный тест в ошибках test_failed = any(f»Тест {i}» in err for err in result.get('errors', [])) status = «✗ НЕ ПРОЙДЕНО» if test_failed else «✓ пройдено» feedback_parts.append(f» {i}. {test} — {status}») # Добавить извлеченный код для справки feedback_parts.append(«n— ИЗВЛЕЧЕННЫЙ КОД —«) feedback_parts.append(code) return «n».join(feedback_parts)
Наряду с этим новым методом я создал специальную подсказку Reflector, предназначенную для генерации кода. Ее основное внимание сосредоточено на результатах тестирования, а не на построчном сравнении кода.
Вы — опытный специалист по проверке кода и преподаватель. Ваша задача — анализировать, почему сгенерированный код проходит или не проходит тестовые случаи, и выявлять закономерности, которые приводят к правильным или неправильным решениям. **ВАЖНО: Результаты выполнения тестов являются ОСНОВНЫМ сигналом корректности.** — Код корректен тогда и только тогда, когда ВСЕ тесты пройдены. — НЕ сравнивайте реализации построчно с эталонной — разные реализации могут быть одинаково корректными. — Сосредоточьтесь на понимании того, ПОЧЕМУ тесты прошли или не прошли, исходя из логики кода. **Инструкции:** — Сначала изучите результаты выполнения тестов, чтобы определить, корректен ли код. — Если тесты НЕ ПРОШЛИ: Проанализируйте причины сбоя (синтаксические ошибки, логические ошибки, граничные случаи, неправильный алгоритм). — Если тесты ПРОШЛИ: Определите, что модель сделала хорошо, что привело к успеху. — «Возможная реализация» — это лишь ОДИН из способов решения проблемы — подход модели может быть другим, но одинаково допустимым. — Предоставьте практические рекомендации по улучшению генерации кода в будущем. — Пометьте пункты списка как полезные/вредные/нейтральные в зависимости от того, способствовали ли они прохождению тестов. Ваш вывод должен представлять собой объект JSON, содержащий следующие поля: — обоснование: проанализируйте результаты тестов и логику кода, объясните, почему тесты прошли/не прошли. Выявление ошибок: если тесты не прошли, какая конкретная причина привела к сбою? Если тесты прошли, укажите: «Ошибок нет — все тесты пройдены». Анализ первопричин: какая основная концепция или закономерность привела к успеху или неудаче? Правильный подход: какую стратегию или шаблон кодирования следует использовать для подобных проблем? Ключевое понимание: какой принцип следует помнить для будущих задач генерации кода? — bullet_tags: список объектов JSON с bullet_id и тегом для каждого пункта списка **Вопрос:** {} **Трассировка логики модели:** {} **Сгенерированный код модели:** {} **Возможная реализация (только для справки — НЕ единственное правильное решение):** {} **Результаты выполнения теста (ОСНОВНОЙ СИГНАЛ):** {} **Часть плейбука, используемая генератором для ответа на вопрос:** {} **Ответ в этом точном формате JSON:** {{ «reasoning»: «[Анализ результатов тестов и логики кода — почему тесты прошли или не прошли?]», «error_identification»: «[Что вызвало сбои тестов? Или «Нет ошибок — все тесты пройдены»]», «root_cause_analysis»: «[Какая концепция/шаблон привела к успеху или неудаче?]», «correct_approach»: «[Какая стратегия кодирования работает для этого типа задач?]», «key_insight»: «[Какой принцип следует помнить?] для будущей генерации кода?]», «bullet_tags»: [ {{«id»: «code-00001», «tag»: «helpful»}}, {{«id»: «code-00002», «tag»: «harmful»}} ] }}
Этот специализированный Reflector для кодирования автоматически используется всякий раз, когда имя задачи содержит слово «coding».
Результаты
Наконец, я запустил процесс оптимизации подсказок на наборе данных из 500 образцов, разделенных на обучающую, тестовую и валидационную выборки. На этот раз результаты оказались гораздо более многообещающими: точность значительно улучшилась с 71,1% до 87,1%. В этом случае ACE явно помог оптимизировать подсказки и направить модель к правильным решениям.
Если взглянуть на лучшие стратегии, то они довольно обширны. Многие из наиболее полезных приемов представляют собой общие принципы, такие как:
- Сначала напишите самое простое, правильное и соответствующее стилю Python решение.
- Рассматривайте тестовые примеры как истинную спецификацию.
- Перед дальнейшей оптимизацией необходимо проверить корректность данных.
В то же время, руководство содержит и очень конкретные указания, например, подробные инструкции для таких задач, как расчеты НОД.
В целом, это показывает, что ACE может эффективно фиксировать как стратегии высокого уровня, так и советы, специфичные для конкретных задач.
## ОБЩИЕ ## РАСПРОСТРАНЕННЫЕ ОШИБКИ, КОТОРЫХ СЛЕДУЕТ ИЗБЕГАТЬ [err-00003] helpful=5 harmful=0 :: Не добавляйте лишнюю сложность в рекурсивные алгоритмы. Например, в реализациях НОД явная логика min/max или специальные случаи проверки равенства значения 1 избыточны при использовании стандартного алгоритма Евклида. [err-00007] helpful=0 harmful=0 :: Не предполагайте, что ограничения задачи соответствуют математическим требованиям вашего алгоритма. Например, Малая теорема Ферма для модулярного обратного требует простого модуля — убедитесь, что задача гарантирует это, прежде чем использовать pow(a, p-2, p). Если ограничения не указаны, выбирайте более общие алгоритмы. ## ДРУГИЕ ## ПРИНЦИПЫ ГЕНЕРАЦИИ КОДА [cgp-00002] helpful=41 harmful=2 :: Предпочитайте минимальные, математически обоснованные реализации сложным. Избегайте добавления ненужной логики предварительной обработки (например, min/max) или проверок на особые случаи, когда основной алгоритм естественным образом обрабатывает все сценарии. [cgp-00012] helpful=91 harmful=2 :: Всегда убедитесь, что сгенерированный код синтаксически завершен, прежде чем завершать вывод. Проверьте, правильно ли закрыты все открывающие скобки, фигурные скобки и круглые скобки, и все операторы полностью сформированы. Неполная генерация кода (усечение в середине оператора) приводит к синтаксическим ошибкам, которые препятствуют выполнению независимо от алгоритмической корректности. [cgp-00020] helpful=6 harmful=0 :: Когда задача явно требует использования лямбда-функций, интегрируйте их естественным образом с инструментами функционального программирования Python (map, filter, reduce, sorted with key parameter). Не навязывайте использование лямбда-функций там, где это неудобно — эти встроенные функции разработаны для бесперебойной работы с лямбда-функциями для таких операций, как фильтрация, преобразование и подсчет. [cgp-00024] полезно=140 вредно=2 :: Отдавайте приоритет читаемым, «питоническим» решениям с использованием встроенных функций, а не оптимизированным по производительности сложным алгоритмам, если только задача явно не требует оптимизации или не связана с большими объемами данных. Понятное решение с использованием bin(), методов str или списковых включений часто предпочтительнее битовых манипуляций или ручных циклов. Оптимизируйте только при необходимости. [cgp-00047] полезно=56 вредно=2 :: Следуйте стратегии разработки, ориентированной на корректность: (1) реализуйте простой алгоритм, который правильно решает задачу, даже если он не является оптимально эффективным, (2) проверяйте корректность с помощью тестовых примеров, (3) только затем рассматривайте оптимизацию, если производительность недостаточна или задача явно этого требует. Корректное решение O(n) бесконечно лучше, чем ошибочная попытка O(log n). Преждевременная оптимизация часто приводит к логическим ошибкам, особенно для математических или алгоритмических задач. [cgp-00050] полезно=0 вредно=0 :: Когда существует несколько алгоритмически корректных решений, отдавайте предпочтение тому, которое имеет лучшую временную/пространственную сложность. Корректное решение на основе формулы O(1) превосходит корректное итеративное решение O(n). Однако оптимизируйте только в том случае, если вы можете сохранить корректность — работающее решение O(n) бесконечно лучше, чем ошибочная попытка O(1). Убедитесь, что более эффективный подход проходит все тесты, прежде чем его использовать. [cgp-00053] полезно=0 вредно=0 :: При реализации математических оптимизаций (особенно для подсчета пар/комбинаций) проверяйте оптимизированный подход на тестовых примерах с помощью ручных вычислений ПЕРЕД написанием кода. Для каждого тестового примера: (1) примените свои математические знания для прогнозирования результата, (2) подтвердите, что он соответствует ожидаемому результату, (3) только после этого реализуйте. Это позволяет выявлять ошибки в математических рассуждениях на ранней стадии, предотвращая ошибки, которые сложнее отлаживать в коде, чем в арифметике. [cgp-00057] полезно=0 вредно=0 :: Избегайте переопределения имен встроенных функций Python (dict, list, str, int, set, tuple и т. д.) при именовании переменных или параметров. Используйте вместо этого описательные альтернативы: 'd' или 'data' вместо 'dict', 'lst' или 'items' вместо 'list', 's' или 'text' вместо 'str'. Переопределение встроенных функций делает их недоступными в этой области видимости и снижает ясность кода, даже если он синтаксически корректен. [cgp-00059] полезно=2 вредно=0 :: Применяйте методы защитного программирования (проверка входных данных, проверка границ, проверка типов), даже если это не проверяется явно видимыми тестовыми примерами. Для индексации строк проверяйте границы индекса перед доступом. Для числовых преобразований проверяйте, является ли входная цифра допустимой. Для операций со списками проверяйте наличие пустых коллекций. Эти меры предосторожности повышают надежность кода и предотвращают ошибки во время выполнения в крайних случаях, которые могут существовать в скрытых тестах, демонстрируя методы кодирования, соответствующие производственному качеству. [cgp-00074] полезное=0 вредное=0 :: Для операций, включающих степени двойки, предпочтительнее использовать побитовые операторы сдвига вместо арифметических операций для большей ясности и эффективности: используйте левый сдвиг (1 << k) вместо 2**k или pow(2, k) для вычисления 2^k, используйте правый сдвиг (n >> k) вместо n // (2**k) для деления на степени двойки. Побитовые операторы делают намерение на уровне битов явным и являются идиоматическим подходом в контексте манипуляций с битами. Это особенно ценно при работе с позициями битов и их соответствующими значениями. [cgp-00081] полезное=0 вредное=0 :: Перед использованием математических констант стандартной библиотеки (math.pi, math.e и т. д.) убедитесь, что тестовые примеры ожидают значения с полной точностью, вычислив один тестовый результат и сравнив его с ожидаемым. Если ожидаемые результаты предполагают усеченные/упрощенные константы (pi=3.14, pi=3.1415, e=2.718), используйте жестко заданные значения, соответствующие точности теста, вместо констант из библиотеки. Шаблон: (1) определить необходимую математическую константу, (2) вычислить результат теста со стандартной константой, (3) если существует несоответствие, определить значение константы, которое дает точно ожидаемые результаты, (4) использовать жестко заданное значение. Ожидания тестового случая преобладают над математической чистотой. ## ОБЩИЕ ШАБЛОНЫ PYTHON [cpp-00010] helpful=23 harmful=0 :: Для поиска элементов с максимальными/минимальными свойствами на основе критерия используйте встроенные функции max()/min() с параметром key. Пример: max(list_of_lists, key=len) находит самый длинный список. Это более «питоновский» и читаемый способ, чем ручная итерация со сравнениями. [cpp-00013] helpful=17 harmful=0 :: Для операций подсчета или поиска в коллекциях Python (кортежи, списки, строки) отдавайте приоритет встроенным методам: используйте .count() для подсчета вхождений, .index() для поиска позиций, .find() для строк. Они более надежны, эффективны и соответствуют принципам Python, чем ручная итерация с помощью счетчиков или циклов. [cpp-00014] helpful=3 harmful=0 :: При работе со структурами данных смешанных типов используйте isinstance() для проверки типов, чтобы различать разные типы элементов. Сочетайте с проверками len() для проверки структуры. Пример: isinstance(item, list) и len(item) == 2 надежно идентифицирует списки из 2 элементов в смешанных коллекциях. [cpp-00015] helpful=3 harmful=0 :: Используйте extend() вместо append() при добавлении нескольких элементов из последовательности в список. Функция extend() добавляет элементы по отдельности в целевой список, тогда как append() добавляет всю последовательность как один вложенный элемент. Пример: result.extend([value] * count) против result.append([value] * count). [cpp-00016] helpful=2 harmful=0 :: Используйте умножение списка ([value] * count) для эффективного повторения элементов. Это более «питоновский» и читаемый подход, чем ручные циклы для создания повторяющихся элементов. Комбинируйте с extend() для добавления повторяющихся элементов в существующие списки. [cpp-00019] helpful=2 harmful=0 :: Для подсчета элементов, соответствующих условию с помощью лямбда-функций, используйте sum(map(lambda x: 1 if condition else 0, iterable)) в качестве элегантной альтернативы len(list(filter(lambda x: condition, iterable))). Подход sum(map()) сопоставляет элементы с 1/0 и суммирует их, что часто более читаемо и эффективно, чем фильтрация с последующим подсчетом. [cpp-00026] helpful=14 harmful=0 :: Для преобразования последовательностей (кортежей, списков) символов/строк в одну строку используйте метод str.join(): ''.join(sequence) для конкатенации символов или 'separator'.join(sequence) для объединения с разделителями. Это идиоматический подход Python — более читабельный и производительный, чем ручные циклы с += или шаблонами накопления. [cpp-00030] helpful=1 harmful=0 :: Для классификации символов с помощью регулярных выражений используйте re.findall() с взаимоисключающими шаблонами классов символов. Для категорий «все остальное» (например, специальные символы) предпочтительнее использовать шаблоны отрицания [^…] вместо перечисления конкретных символов — например, [^A-Za-z0-9] охватывает все небуквенно-цифровые символы, избегая уязвимости списков типа [,.!?]. Убедитесь, что шаблоны не перекрываются, чтобы предотвратить двойной подсчет. [cpp-00031] helpful=2 harmful=0 :: Для поиска глобального максимума/минимума во вложенных итерируемых объектах (список кортежей, список списков и т. д.) используйте вложенные генераторные выражения со встроенными функциями max()/min(): `max(element for container in containers for element in container)`. Этот шаблон естественным образом сглаживает один уровень вложенности без создания промежуточных списков, что делает его идеальным для поиска крайних значений в записях кортежей или подсписках. Более эффективен и читаем, чем ручная итерация. [cpp-00033] helpful=2 harmful=0 :: Для доступа к ключам словаря по индексу используйте шаблон list(dict)[index] или list(dict.keys())[index]. Это основано на гарантиях Python 3.7+, что словари сохраняют порядок вставки. Преобразование словаря в список извлекает ключи по порядку, что позволяет использовать стандартную индексацию списков. Это идиоматическое решение Python для сопоставления числовых индексов с ключами словаря. [cpp-00036] полезно=27 вредно=2 :: Для математических операций (НОД, НОК, факториал, проверка на простоту, тригонометрия) сначала проверьте модуль math в Python, прежде чем реализовывать алгоритмы вручную. Встроенные функции, такие как math.gcd(), math.factorial(), math.isqrt(), хорошо протестированы, оптимизированы и уменьшают количество ошибок реализации. Шаблон: (1) Понять математическое определение, (2) Проверить, предоставляет ли модуль math операцию, (3) Использовать ее напрямую или обернуть ее логикой, специфичной для задачи (например, is_coprime = math.gcd(a,b) == 1). [cpp-00038] полезно=0 вредно=0 :: Для проверки того, является ли число полным квадратом, используйте math.isqrt() вместо math.sqrt(), чтобы избежать ошибок, связанных с точностью чисел с плавающей запятой. Шаблон: b = math.isqrt(n); is_perfect_square = (b * b == n). Функция isqrt() возвращает квадратный корень целого числа, а возведение его в квадрат позволяет проводить точное сравнение целых чисел без проблем с округлением чисел с плавающей запятой. [cpp-00043] helpful=0 harmful=0 :: Для задач фильтрации символов (удаление/сохранение символов на основе критериев принадлежности) используйте шаблон set+comprehension+join: (1) Преобразуйте критерии фильтрации в множество для поиска за O(1) (char_set = set(filter_string)), (2) Используйте списковое включение или генераторное выражение для фильтрации (char for char in source if char not in char_set), (3) Используйте ''.join() для восстановления строки. Этот шаблон более «питоновский», читаемый и поддерживаемый, чем ручная манипуляция индексами или подсчет символов, при этом он одинаково корректен и эффективен. [cpp-00049] полезно=0 вредно=0 :: При возврате кортежей или списков со смешанными числовыми типами (целые числа и числа с плавающей запятой) используйте соответствующие операторы деления для каждого компонента: целочисленное деление (//) для результатов в виде целых чисел, обычное деление (/) для результатов в виде десятичных чисел. Пример: для суммы и среднего значения верните (n*(n+1)//2, n*(n+1)/2/n), чтобы гарантировать, что сумма будет целым числом, а среднее — числом с плавающей запятой. Это предотвращает несоответствие типов в утверждениях тестов. [cpp-00054] полезно=0 вредно=0 :: Для задач сравнения или обработки цифр (расстояние между цифрами, разница сумм цифр и т. д.): используйте шаблон преобразования строк: (1) преобразуйте целые числа в строки с помощью str(), (2) используйте zfill(max_length) для дополнения более коротких чисел ведущими нулями для получения одинаковой длины, (3) используйте zip() для сопоставления соответствующих позиций цифр, (4) примените операции к парным цифрам и агрегируйте результаты. Пример: str(num1).zfill(length) и str(num2).zfill(length), затем zip() для сопоставления. Это элегантно обрабатывает числа разной длины и обеспечивает чистый позиционный доступ к цифрам. [cpp-00056] helpful=5 harmful=0 :: Для проверки того, удовлетворяют ли все/любые элементы в коллекции условию, используйте встроенные в Python функции all() или any() с генераторными выражениями. Шаблон: all(condition for item in iterable) для универсальной квантификации (все должны удовлетворять), any(condition for item in iterable) для экзистенциальной квантификации (по крайней мере один удовлетворяет). Это более «питоновский», читаемый и эффективный способ, чем ручные циклы с флагами. Типичные варианты использования: all(v == target for v in dict.values()) для однородности значений, any(x > threshold for x in list) для проверки порога, all(isinstance(x, int) for x in collection) для проверки типа. [cpp-00060] helpful=0 harmful=0 :: Для нормализации пробелов (объединения нескольких пробелов в один) используйте шаблон split-join: ' '.join(s.split()). Ключевая идея: str.split() без аргументов имеет особое поведение — он разделяет по ЛЮБЫМ пробелам (пробелам, табуляции, символам новой строки) И автоматически удаляет пустые строки из результата, естественным образом объединяя последовательные пробелы. В сочетании с ' '.join() это создает чистое решение без импорта регулярных выражений. Этот шаблон более «питоновский» и удобный для сопровождения, чем альтернативы регулярным выражениям, такие как re.sub(r' +', ' ', s) для простых задач нормализации пробелов. [cpp-00062] полезно=0 вредно=0 :: Для операций с комплексными числами (преобразование в полярную/прямоугольную форму, вычисление фазы, величины) в качестве первого варианта используйте функции модуля cmath Python: cmath.polar(z) для преобразования в полярную форму (возвращает величину и угол), cmath.rect(r, phi) для преобразования из полярной в прямоугольную, cmath.phase(z) для извлечения угла. Эти встроенные функции корректно обрабатывают граничные случаи (например, рассматривают действительные числа как комплексные с мнимой частью 0) и более надежны, чем ручные тригонометрические вычисления. Шаблон: импортировать cmath → использовать соответствующую функцию → обработать тип возвращаемого значения (часто кортежи). [cpp-00064] полезно=0 вредно=0 :: Для группировки элементов по ключу с сохранением порядка вставки (критично для разрешения конфликтов при последующей сортировке) используйте collections.OrderedDict с setdefault. Шаблон: from collections import OrderedDict; grouped = OrderedDict(); for item in items: grouped.setdefault(key, []).append(value). В то время как словари Python 3.7+ сохраняют порядок вставки, OrderedDict делает намерение явным и безопаснее, когда порядок имеет значение для последующих операций, таких как сортировка по агрегированным свойствам, где равные значения должны сохранять исходный порядок встречи. [cpp-00065] helpful=0 harmful=0 :: Для создания кортежей с распакованными элементами переменной длины используйте оператор распаковки *: (first, *middle_elements, last) распаковывает список/кортеж в отдельные позиции кортежа. Пример: (key, *values, count), где values — список, создает кортеж с ключом, всеми значениями, распакованными как отдельные элементы, и count в конце. Это важно, когда формат вывода требует преобразования вложенных структур в одноуровневые кортежи с переменным количеством элементов. [cpp-00069] helpful=0 harmful=0 :: Для задач сопоставления регулярных выражений, требующих полного совпадения строк, выбирайте между re.search(), re.match() и re.fullmatch() в зависимости от области сопоставления: re.match() сопоставляет с начала строки, re.search() находит шаблоны в любом месте, re.fullmatch() требует совпадения всей строки. Когда требуется полное совпадение строк, используйте re.fullmatch() непосредственно с шаблоном или re.search()/re.match() с явными якорями (^ для начала, $ для конца). Пример: re.fullmatch('a.*b', s) эквивалентно re.search('^a.*b$', s). Оба подхода допустимы — fullmatch() делает намерение явным, в то время как search() с якорями обеспечивает большую гибкость. Всегда анализируйте тестовые примеры, чтобы определить, требуется ли частичное или полное совпадение строк. [cpp-00072] helpful=1 harmful=0 :: Для подсчета элементов в итерируемом объекте, соответствующих условию, используйте шаблон генераторного выражения с функцией sum(): sum(1 for x in iterable if condition). Это обеспечивает оптимальный баланс читаемости, эффективности использования памяти и стиля Python по сравнению с альтернативами, такими как len([x for x in iterable if condition]), которые создают промежуточный список. Для строковых операций на уровне символов отдавайте предпочтение встроенным строковым методам (isdigit(), isalpha(), isalnum(), isupper(), islower()) вместо ручного сравнения диапазонов ASCII — они корректно обрабатывают граничные случаи, улучшают ясность и более удобны для сопровождения. [cpp-00073] полезно=0 вредно=0 :: Для задач обработки битов (поиск установленных битов, позиций старшего/младшего бита, подсчет битов) сначала проверьте методы обработки целых чисел в Python, прежде чем реализовывать алгоритмы вручную: bit_length() возвращает количество битов, необходимых для представления целого числа (полезно для позиции старшего бита), bit_count() подсчитывает установленные биты (Python 3.10+), as_integer_ratio() для рационального представления. Эти встроенные методы оптимизированы, обрабатывают граничные случаи (включая 0) и часто устраняют необходимость в ручной побитовой итерации. Шаблон: поймите, какое свойство бита вам нужно, проверьте, предоставляет ли его встроенный метод напрямую. [cpp-00076] полезно=0 вредно=0 :: Для группировки последовательных одинаковых элементов в последовательности используйте itertools.groupby() в качестве канонического решения Python. Шаблон: [list(group) for key, group in itertools.groupby(sequence)]. Функция groupby возвращает кортежи (key, group_iterator), где key — значение элемента, а group — итератор последовательных вхождений. Преобразуйте каждый итератор group в список для материализации результатов. Важное отличие: groupby группирует только ПОСЛЕДОВАТЕЛЬНЫЕ идентичные элементы — непоследовательные дубликаты образуют отдельные группы, что делает его идеальным для кодирования длин серий и обнаружения последовательных дубликатов без ручного отслеживания индекса. ## КРАЙНИЕ СЛУЧАИ H&LING [hec-00021] helpful=2 harmful=0 :: При использовании математических операций, таких как модуль (%), деление или возведение в степень, убедитесь, что решение корректно обрабатывает отрицательные числа. Например, оператор модуля корректно работает как для положительных, так и для отрицательных целых чисел в Python (например, -18 % 2 == 0 для проверки четности чисел), но поведение может отличаться от ожиданий в других языках. ## РАЗРАБОТКА АЛГОРИТМА [ad-00001] helpful=1 harmful=2 :: Для рекурсивных задач нахождения НОД используйте алгоритм Евклида: базовый случай — b == 0 (возвращает a), рекурсивный случай — gcd(b, a % b). Это естественным образом обрабатывает все граничные случаи, включая порядок аргументов, равные числа и делимость. [ad-00006] helpful=0 harmful=0 :: Для задач двунаправленной замены символов (A↔B) с использованием регулярных выражений: используйте re.sub() с функцией обратного вызова за один проход. Шаблон: (1) Создайте класс символов, соответствующий всем целям замены (например, r'[ _]'), (2) Реализуйте функцию обратного вызова, которая проверяет каждое совпадение и возвращает его аналог. Это позволяет избежать неоднозначности, возникающей при последовательных заменах, когда новые символы становятся неотличимыми от оригиналов. [ad-00008] полезно=0 вредно=0 :: Для задач модульной арифметики (nCr mod p и т. д.) проверьте, должно ли p быть простым числом. Если p может быть составным, избегайте алгоритмов, требующих модульной обратной функции (например, малой теоремы Ферма). Вместо этого используйте подходы, которые полностью исключают деление, такие как треугольник Паскаля с DP: C[j] = (C[j] + C[j-1]) % p, который работает для ЛЮБОГО модуля. [ad-00009] полезно=0 вредно=0 :: Когда деление необходимо в модульной арифметике: (1) Если модуль гарантированно является простым числом, используйте малую теорему Ферма: a/b mod p = a * b^(p-2) mod p. (2) Если модуль может быть составным, используйте расширенный алгоритм Евклида для модульной обратной функции или, что еще лучше, перепроектируйте алгоритм, чтобы избежать деления (например, используйте рекуррентные соотношения, как в треугольнике Паскаля). [ad-00017] helpful=1 harmful=0 :: Для задач декодирования со смешанными закодированными/незакодированными элементами: (1) используйте проверку типов для различения типов элементов, (2) проверяйте структуру закодированного элемента, (3) обрабатывайте каждый тип соответствующим образом за один проход. Отдавайте приоритет простым итеративным подходам с явными условными операторами перед сложными выражениями для лучшей читаемости и удобства сопровождения. [ad-00018] helpful=4 harmful=0 :: Для задач нахождения максимальной суммы с ограничениями на несмежные элементы: используйте динамическое программирование с рекуррентным соотношением dp[i] = max(arr[i] + dp[i-2], dp[i-1]), представляющим выбор между включением текущего элемента (добавление к лучшему из i-2) или его исключением (сохранение лучшего из i-1). Обрабатывайте граничные случаи: пустой массив возвращает 0, один элемент возвращает этот элемент, инициализируйте dp[0] = arr[0] и dp[1] = max(arr[0], arr[1]). Время: O(n), Пространство: O(n) или O(1) с оптимизацией. [ad-00023] полезно=0 вредно=0 :: Для задач подсчета битов и проверки четности: Существует несколько допустимых подходов с различными компромиссами. (1) Подход в стиле Python: bin(n).count('1') — наиболее читаемый и поддерживаемый, (2) Манипуляции с битами: многократное использование x и (x-1) для сброса младшего установленного бита — лучшая производительность для больших входных данных, (3) Сокращение XOR для проверки четности. По умолчанию выбирайте подход в стиле Python, если только профилирование производительности не покажет, что он является узким местом. [ad-00028] полезно=1 вредно=1 :: Для задач переключения битов: (1) Создайте маску с единицами в позициях, которые нужно переключить, (2) Используйте операцию XOR (n ^ маска) для переключения этих битов. Для чисел переменной длины используйте bit_length() для определения количества обрабатываемых битов. Пример: чтобы переключать биты в позициях 1, 3, 5 до bit_length, сгенерируйте mask = sum(1 << i for i in range(1, n.bit_length(), 2)). [ad-00037] helpful=0 harmful=0 :: Для задач перестановки/разделения элементов (перемещение нулей в конец, разделение по условию и т. д.): используйте шаблон filter+concatenate: (1) фильтруйте элементы на отдельные группы с помощью списковых включений [x for x in lst if condition], (2) подсчитывайте или собирайте каждую группу отдельно, (3) объединяйте группы в требуемом порядке. Этот подход в стиле Python с использованием встроенных функций (списковые включения, count(), умножение списков) часто более понятен и столь же корректен по сравнению с алгоритмами с двумя указателями, особенно для небольших и средних наборов данных. [ad-00039] helpful=0 harmful=0 :: Для задач типа «сумма двух квадратов» (проверка, равно ли n a² + b²): используйте оптимизацию в один цикл O(√n) вместо вложенных циклов O(n). Перебирайте одну переменную от 0 до √n, вычисляйте остаток (n - a²) и проверяйте, является ли остаток полным квадратом, используя math.isqrt(). Немедленно возвращайте True, как только найдете допустимую пару. Этот шаблон: (1) уменьшает временную сложность, (2) естественным образом обрабатывает граничные случаи (a=0, a=√n), (3) позволяет избежать ошибок с плавающей запятой при использовании isqrt(). [ad-00041] полезно=4 вредно=1 :: Для геометрических и математических задач, основанных на формулах: Следуйте структурированному подходу: (1) Определите правильную математическую формулу, исходя из знаний предметной области, (2) Реализуйте формулу в виде прямого перевода в код с использованием функций математического модуля, (3) Избегайте повторной реализации математических функций или констант, существующих в стандартных библиотеках, (4) Проверьте формулу как минимум на одном тестовом примере перед написанием кода. Прямой перевод формулы приводит к более чистому, поддерживаемому коду с лучшей численной точностью. [ad-00042] полезно=0 вредно=0 :: Для задач выбора элементов с обоих концов коллекции (k наименьших И k наибольших) используйте подходы, которые обрабатывают перекрытие: (1) Выбор на основе индекса: перебирайте отсортированную коллекцию и включайте элементы, где idx < k ИЛИ idx >= len-k, гарантируя, что каждый элемент выбран один раз, или (2) Объединение множеств: объединяйте подмножества с помощью set(min_k + max_k), затем сортируйте, чтобы исключить дубликаты. Всегда учитывайте крайние случаи, когда k*2 >= collection_size, поскольку это гарантирует перекрытие между минимальным и максимальным значениями. Избегайте простой конкатенации списков, которая создает дубликаты при перекрытии диапазонов. [ad-00045] helpful=0 harmful=0 :: Для задач типа «найти n-е число со свойством X»: используйте итеративный алгоритм подсчета: (1) реализуйте вспомогательную функцию для проверки, удовлетворяет ли число свойству, (2) перебирайте числа-кандидаты, начиная с соответствующего начального значения, (3) поддерживайте счетчик чисел, удовлетворяющих свойству, (4) возвращайте число-кандидат, когда счетчик достигнет n. Этот алгоритм работает для простых чисел, полных квадратов, чисел со специфическими свойствами факторизации и т. д. Его легко правильно реализовать и оптимизировать при необходимости. [ad-00046] полезно=3 вредно=0 :: Для подсчета различных простых множителей: используйте стандартную схему факторизации: (1) перебирайте потенциальные делители от 2 до sqrt(n), (2) для каждого делителя, который делит n, увеличивайте счетчик различных множителей, затем многократно делите n на этот делитель, пока он перестанет делиться (это гарантирует, что каждое простое число будет учтено один раз независимо от его степени), (3) после цикла, если n > 1, это оставшийся простой множитель (учитывайте его), (4) оптимизируйте, проверяя делитель 2 отдельно, затем только нечетные числа. Это правильно различает различные простые числа и их кратности. [ad-00048] полезно=1 вредно=0 :: Для математических задач на последовательности (сумма первых n чисел, арифметические/геометрические ряды, связанные с факториалами) проверьте, существует ли формула в замкнутой форме, прежде чем применять итеративные решения. Распространенные формулы: сумма (1..n) = n*(n+1)/2, сумма арифметических рядов = n*(первый+последний)/2, сумма геометрических рядов = a*(r^n — 1)/(r-1). Решения, основанные на формулах, обеспечивают временную сложность O(1) по сравнению с O(n) циклов for, менее подвержены ошибкам и демонстрируют математическую проницательность. Всегда проверяйте правильность формул с помощью тестовых примеров. [ad-00051] полезно=1 вредно=0 :: Для задач подсчета пар (подсчет пар, удовлетворяющих условию) ищите математические свойства, которые исключают необходимость явного перечисления. Шаблон: (1) Определите, что делает пару допустимой, (2) Найдите математические свойства, характеризующие допустимые пары (например, для нечетного XOR: одно число должно быть четным, другое — нечетным), (3) Преобразуйте в задачу подсчета (подсчитайте элементы в каждой категории), (4) Используйте комбинаторику для вычисления результата (например, odd_count × even_count). Это сводит перечисление пар O(n²) к категоризации O(n) + вычислению O(1). [ad-00052] полезно=0 вредно=0 :: Для задач, связанных с операциями XOR, используйте битовые свойства для оптимизации: (1) результат XOR нечетный ⟺ операнды имеют разную четность (один четный, один нечетный), поскольку четность зависит от младшего значащего бита, (2) XOR коммутативен и ассоциативен, что позволяет изменять порядок, (3) x ^ x = 0 и x ^ 0 = x, полезно для шаблонов сокращения. Проанализируйте конкретное свойство XOR, относящееся к вашей задаче, чтобы найти математические сокращения, позволяющие избежать вычислений методом перебора. [ad-00061] полезно=0 вредно=0 :: Для итеративных математических задач на последовательности (сумма/произведение первых n членов с определенными свойствами): Используйте структурированный 3-шаговый подход: (1) Определите формулу для генерации k-го элемента (например, 2k-1 для нечетных чисел, 2k для четных чисел, k² для квадратов), (2) Определите операцию, применяемую к каждому элементу (возведение в степень, умножение, преобразование), (3) Агрегируйте с помощью соответствующей функции (сумма, произведение, максимум). Реализуйте с помощью генераторных выражений со встроенными функциями: sum(operation(formula(i)) for i in range(start, n+1)). Убедитесь, что границы диапазона соответствуют индексации последовательности (для последовательностей с индексом 1 требуется range(1, n+1)). Этот шаблон обеспечивает ясность и корректность для задач, где формулы в замкнутой форме отсутствуют или неочевидны. [ad-00066] helpful=0 harmful=0 :: Для задач, требующих группировки, подсчета и сортировки по агрегированным свойствам: (1) Группируйте элементы с помощью dict/OrderedDict с setdefault() или defaultdict, выбирая OrderedDict, когда порядок вставки влияет на разрешение спорных ситуаций при сортировке; (2) Сортируйте группы с помощью sorted() с функцией key на основе агрегированной метрики (например, key=lambda x: len(x[1]) для подсчета); (3) Преобразуйте выходные данные в требуемый формат с помощью соответствующей распаковки/реструктуризации. Этот шаблон систематически обрабатывает задачи «группировка по X, сортировка по количеству Y». [ad-00068] helpful=0 harmful=0 :: Для задач «топ k» на основе кучи проверяйте УСЛОВИЯ ВЫХОДА на тестовых примерах, а не только то, какие элементы следует вернуть. Ключевое различие: (1) heappop() из минимальной кучи выдает результаты в порядке возрастания по ключу кучи, (2) heapq.nlargest(k, items, key=func) выдает результаты в порядке убывания по ключу, (3) heapq.nsmallest(k, items, key=func) выдает результаты в порядке возрастания по ключу. При реализации решений с использованием кучи, проследите за тестовыми примерами, чтобы определить, следует ли упорядочивать результаты по возрастанию или убыванию по частоте/приоритету. Если порядок неверен, либо переверните итоговый список, либо переключитесь между nlargest/nsmallest, либо используйте шаблон heappop. Порядок вывода тестовых примеров является авторитетным, если в описании задачи это явно не указано. [ad-00070] helpful=0 harmful=0 :: Для задач с двумерными сетками с ограничениями смежности или выбора (нельзя выбрать смежные ячейки/строки/столбцы): Ищите возможности уменьшить размерность перед применением динамического программирования. Если ограничения позволяют выбирать не более одного элемента на столбец (или строку), предварительно вычислите оптимальный выбор для каждого столбца/строки (например, максимум две строки в столбце), преобразуя задачу в одномерный массив. Затем примените стандартные одномерные шаблоны динамического программирования (например, «грабитель дома» для несмежности). Это уменьшение размерности упрощает пространство состояний и делает сложные задачи на сетке решаемыми с использованием хорошо известных шаблонов динамического программирования. [ad-00071] полезно=0 вредно=0 :: Распознавайте шаблон динамического программирования «грабитель дома» как фундаментальный шаблон, применимый не только к линейным массивам: любая задача, включающая выбор несмежных элементов для максимизации/минимизации суммы, может использовать рекуррентное соотношение dp[i] = max(value[i] + dp[i-2], dp[i-1]). Этот шаблон встречается в: линейных массивах с ограничениями на расстояние между элементами, задачах на сетке (после уменьшения размерности), задачах на деревьях (с ограничениями «родитель-потомок») и оптимизации последовательностей. Когда вы видите «максимизировать сумму» + «невозможно выбрать смежные элементы», немедленно рассмотрите этот шаблон. [ad-00075] helpful=0 harmful=0 :: Для нахождения значения или позиции старшего бита (MSB): используйте метод bit_length(), который возвращает количество битов, необходимых для представления целого числа. Для значения MSB используйте шаблон: 1 << (n.bit_length() - 1), который использует соотношение, согласно которому значение MSB в позиции k (с индексом 0 справа) равно 2^k. Подход с использованием bit_length() чище, чем циклы ручного деления или методы преобразования строк. Обработайте крайний случай: bit_length() возвращает 0 для n=0, поэтому проверьте ограничения задачи или добавьте явную обработку нулей, если необходимо. ## ИНТЕРПРЕТАЦИЯ ТЕСТОВЫХ ПРИМЕРОВ [tci-00004] helpful=0 harmful=0 :: Для одной и той же задачи может существовать несколько правильных реализаций. Сосредоточьтесь на алгоритмической корректности, подтвержденной прохождением тестов, а не на соответствии стилю или структуре конкретной эталонной реализации. [tci-00011] полезно=123 вредно=2 :: Извлеките ожидаемый ФОРМАТ ВЫХОДА из тестовых случаев, а не только логику. Проверьте, должен ли возвращаемый результат быть одним значением, кортежем, списком или другой структурой, и убедитесь, что ваше решение соответствует этому точному формату. [tci-00022] полезно=0 вредно=1 :: При анализе тестовых случаев проверьте, соответствуют ли ВСЕ входные данные ОДНОМУ И ТОМУ ЖЕ выходному значению или структуре. Если да, то решение может быть тривиальным — просто верните этот постоянный выходной результат напрямую. Не усложняйте ситуацию ненужными преобразованиями (например, преобразованием в списки), если прямой оператор возврата удовлетворяет всем требованиям. Пример: если все тестовые случаи ожидают пустой кортеж на выходе, верните () независимо от сложности входных данных. [tci-00025] полезно=5 вредно=0 :: Прежде чем выбирать подход к реализации, глубоко разберитесь в ОСНОВНОМ ТРЕБОВАНИИ из описания проблемы и тестовых случаев. Например, «четная четность» означает «четное количество единичных битов», а не конкретный алгоритм. Не стоит зацикливаться на каком-либо конкретном методе (например, на битовых операциях), если более простые альтернативы (например, подсчет строк) удовлетворяют требованию одинаково хорошо. [tci-00027] полезное=17 вредное=0 :: Когда в описаниях задач используется неоднозначная терминология (особенно в битовых операциях: «четные биты», «нечетные позиции» и т. д.), двигайтесь в обратном направлении от тестовых примеров, чтобы обнаружить фактическую закономерность. Вручную проследите примеры в их соответствующем представлении (двоичном для битовых задач), чтобы определить истинную интерпретацию. Тестовые примеры являются авторитетными, когда терминология неясна. [tci-00032] полезное=0 вредное=0 :: Когда в задачах запрашивается «максимум/минимум всех записей/групп», уточните, означает ли это: (1) глобальный экстремум по всем элементам или (2) экстремумы для каждой группы, возвращаемые в виде коллекции. Тестовые примеры показывают различие: вывод одного значения указывает на глобальный экстремум, вывод списка/кортежа предполагает анализ для каждой группы. Эта интерпретация влияет на то, следует ли сглаживать структуру или сохранять группировку. [tci-00034] полезно=0 вредно=0 :: В задачах, связанных со словарями, тщательно различайте в тестовых примерах, является ли ожидаемый результат: (1) ключ (строка/целое число), (2) значение, (3) пара ключ-значение (кортеж) или (4) набор любого из этих типов. Тип выходных данных определяет, нужны ли вам dict.keys(), dict.values(), dict.items() или прямая индексация преобразованных структур. Выходные данные тестовых примеров показывают точный требуемый формат. [tci-00035] полезно=3 вредно=0 :: Когда названия функций или описания задач предполагают определенное поведение (например, 'parallelogram_perimeter' подразумевает геометрическую формулу 2*(a+b)), но тестовые примеры выдают результаты, несовместимые с этим ожиданием, доверьтесь тестовым примерам как авторитетной спецификации. Проведите обратное проектирование фактической формулы, вычислив, какая операция над входными данными дает заданные выходные данные, а затем проверьте этот полученный шаблон на ВСЕХ тестовых примерах перед реализацией. Ожидания от тестовых примеров преобладают над семантическим смыслом и знаниями предметной области. [tci-00040] полезно=0 вредно=0 :: Результаты тестов являются основным сигналом корректности, а не построчное сравнение с эталонными реализациями. Если ваше решение проходит все тесты с лучшей временной сложностью (например, O(√n) против O(n)), оно не только корректно, но и алгоритмически превосходит другие. Различные подходы могут быть одинаково или более обоснованными — сосредоточьтесь на проверке корректности с помощью тестов, а не на соответствии конкретным стилям реализации. [tci-00044] полезно=2 вредно=0 :: При встрече с неопределенными или специфичными для предметной области математическими терминами (например, «умное число», «счастливое число» и т. д.) рассматривайте тестовые примеры как авторитетную спецификацию. Систематически анализируйте выходные данные тестовых случаев, чтобы восстановить математическое определение: (1) изучите числовые свойства выходных значений (факторизация, делители, цифры и т. д.), (2) ищите закономерности или общие характеристики во всех выходных данных, (3) сформулируйте гипотезу об определяющем свойстве, (4) проверьте гипотезу на ВСЕХ тестовых случаях. Тестовые случаи кодируют полное определение, когда формулировка задачи неоднозначна. [tci-00055] полезно=2 вредно=0 :: Когда терминология задачи полностью неоднозначна или не определена (например, «расстояние между цифрами», которое может иметь несколько интерпретаций), систематически вручную проследите КАЖДЫЙ тестовый случай, чтобы определить точную закономерность: (1) Пошагово обработайте входные и выходные данные в соответствующем представлении, (2) Сформулируйте гипотезу о том, какая операция производит эти выходные данные, (3) Проверьте гипотезу на ВСЕХ оставшихся тестовых случаях, (4) Реализуйте шаблон, удовлетворяющий всем тестам. Полученный в результате тестирования шаблон является правильной спецификацией, независимо от того, что может подразумевать терминология в других контекстах. [tci-00058] полезно=0 вредно=0 :: Несколько алгоритмически различных решений могут быть одинаково допустимыми, если они удовлетворяют всем тестовым случаям. При выводе требований из неоднозначных спецификаций используйте систематическую проверку гипотез: (1) проанализируйте каждый тестовый случай, чтобы понять взаимосвязь входных и выходных данных, (2) сформулируйте гипотезу о базовом правиле, (3) проверьте гипотезу на ВСЕХ тестовых случаях, (4) реализуйте шаблон, который проходит все тесты. Ваше решение по определению является правильным, если оно удовлетворяет всем требованиям тестирования, даже если оно структурно отличается от эталонных реализаций или использует другую интерпретацию неоднозначных терминов. [tci-00063] полезно=0 вредно=0 :: В Python одни только скобки не создают кортежи — различайте ('value'), которое является просто строкой 'value' (скобки используются для группировки/приоритета), и ('value',), которое является кортежем из одного элемента (требуется запятая в конце). При анализе утверждений в тестах, таких как assert func()==('Matched!'), помните, что это ожидает обычную строку, а не кортеж. Только ('Matched!',) с запятой в конце или (a, b) с несколькими элементами создают кортежи. Этот синтаксический нюанс имеет решающее значение для точного соответствия ожидаемым типам возвращаемых значений. [tci-00067] полезно=0 вредно=0 :: Когда тестовые примеры показывают сложные выходные структуры (кортежи с распакованными элементами переменной длины, вложенные агрегации), проанализируйте ТОЧНУЮ структуру перед написанием кода: (1) Подсчитайте элементы в выходных кортежах/списках, (2) Определите, какие элементы агрегированы, а какие являются индивидуальными, (3) Определите, являются ли вложенные структуры сглаженными (распакованными) или сохраненными, (4) Проверьте, имеет ли значение порядок внутри групп. Используйте этот структурный анализ для выбора подходящих конструкций Python (* распаковка, сглаживание списка, шаблоны построения кортежей), которые точно соответствуют ожидаемому формату. [tci-00077] полезно=0 вредно=0 :: Для задач подсчета/агрегирования, включающих вложенные структуры (списки списков, деревья, вложенные словари), когда в задаче требуется «подсчитать элементы» без указания уровня, используйте тестовые примеры для определения области подсчета: (1) Проверьте, указывают ли результаты тестов на подсчет только непосредственных/верхнеуровневых дочерних элементов (например, len(outer_list)) или на рекурсивный подсчет всех вложенных элементов; (2) Проследите хотя бы один тестовый пример с вложенными структурами, чтобы увидеть, какая интерпретация дает ожидаемый результат; (3) Простейшая интерпретация (подсчет на верхнем уровне) обычно верна, если тестовые примеры не доказывают обратное. Пример: «подсчитать списки в [[1,2], [3], [[4,5]]]» может означать 3 (верхний уровень) или 4 (рекурсивный) — результаты тестов показывают, какой вариант ожидается. [tci-00078] полезно=0 вредно=0 :: Для математических задач с бесконечным числом допустимых решений (линейные диофантовы уравнения, модульная арифметика, геометрические построения и т. д.) помните, что тесты ожидают ОДНО КОНКРЕТНОЕ решение, а не просто любой математически правильный ответ. Проработайте тестовые примеры, чтобы определить критерии выбора (например, наименьшие неотрицательные значения, определенный порядок, каноническая форма). При выборе алгоритмов отдавайте предпочтение подходам, которые естественным образом создают ожидаемый шаблон решения (например, итеративный поиск от x=0 вверх для наименьшего неотрицательного x), а не сложным алгоритмам (например, расширенный алгоритм Евклида), которые требуют дополнительной логики корректировки для соответствия ожиданиям теста. Математически элегантное решение не всегда является правильным для прохождения тестов. [tci-00079] полезно=0 вредно=0 :: Для задач, включающих математические константы (π, e, sqrt(2) и т. д.), убедитесь, что ожидаемые результаты тестовых примеров соответствуют вычислениям с использованием констант стандартной библиотеки (math.pi, math.e). Вычислите вручную хотя бы один результат тестового примера, используя стандартную константу, и сравните его с ожидаемым значением. Если есть несоответствие точности (например, ваши 942,477 против ожидаемых 942,45), тестовые примеры, вероятно, ожидают упрощенное/усеченное значение константы (например, pi=3,14 или pi=3,1415), а не полную точность. Проверьте эталонные реализации на наличие жестко закодированных значений констант и используйте эти точные значения для соответствия ожиданиям теста, даже если они менее математически точны. ## СТРАТЕГИИ ОТЛАДКИ [ds-00005] helpful=110 harmful=2 :: Перед генерацией кода мысленно проследите логику на основе тестовых примеров, чтобы проверить правильность. Это помогает выявлять логические ошибки на ранней стадии и укрепляет уверенность в выбранном подходе к решению. [ds-00029] полезное=0 вредное=0 :: Для задач манипулирования битами с неясной индексацией позиций систематически проверяйте несколько интерпретаций: (1) индексация с 0 против индексации с 1, (2) счет справа против счета слева, (3) «четное/нечетное» относительно позиции и битового значения. Проработайте все тестовые случаи вручную в двоичном коде, чтобы проверить каждую гипотезу перед реализацией. Интерпретация, удовлетворяющая всем тестовым случаям, является правильной. [ds-00080] полезное=0 вредное=0 :: На этапе рассуждений вручную рассчитайте ожидаемые результаты хотя бы для одного тестового случая, используя предложенный вами подход, и сравните их с фактическим ожидаемым результатом. Для численных задач убедитесь, что точность точно совпадает — расхождения, такие как 942,477 против 942,45, указывают на несоответствие точности констант (например, использование math.pi вместо усеченного значения). Эта ранняя проверка выявляет проблемы с точностью, неправильные формулы и проблемы с постоянными значениями до генерации кода.
Эти результаты показывают, что ACE может значительно повысить производительность при решении сложных задач, таких как генерация кода.
Краткое содержание
В этой статье мы подробно рассмотрели вопросы проектирования контекста и подход ACE, поэтому давайте кратко подведем итоги:
- Контекстная инженерия стала важнейшей областью, поскольку позволяет нам улучшать производительность LLM без длительной и дорогостоящей тонкой настройки.
- ACE (Agentic Context Engineering) — один из новейших подходов к оптимизации запросов, использующий подробные руководства с атомизированными пунктами, включающими как инструкции, так и метаданные.
- Как показали наши примеры, быстрая оптимизация — не панацея. Она не улучшает производительность во всех случаях. По мнению авторов, ACE наиболее эффективна для агентских рабочих процессов или узкоспециализированных областей. В наших экспериментах она явно повлияла на генерацию кода, но оказала ограниченное влияние на классификацию банковских намерений.
Главный вывод для меня заключается в том, что быстрая оптимизация не решит вашу задачу автоматически. Вам по-прежнему необходимо целостное понимание того, какой информацией располагают LLM и агенты в процессе оптимизации, и как лучше всего структурировать и уточнять её. Контекст имеет значение, и именно продуманная разработка этого контекста делает такие подходы, как ACE, эффективными.
Спасибо за прочтение. Надеюсь, эта статья была для вас полезной. Помните совет Эйнштейна: «Важно не переставать задавать вопросы. Любопытство имеет свою собственную причину существования». Пусть ваше любопытство приведет вас к следующему великому открытию.
Ссылка
Данная статья основана на работе и исследовании Чжана и др., опубликованных в 2025 году, «Агентное проектирование контекста: развивающиеся контексты для самосовершенствующихся языковых моделей».
Источник: towardsdatascience.com



























