TL;DR
Chain-of-Function — промпт-стратегия, которая просит LLM организовывать код в независимые функции вместо монолитного скрипта. Сначала высокоуровневая функция (например, main()) с выбором подхода, затем вспомогательные функции для конкретных подзадач. Каждая функция получает docstring с объяснением логики — это Chain-of-Thought для кода.
LLM часто генерирует код "потоком сознания": всё в одном месте, без четкой структуры, сложно отлаживать и понимать логику. Если попросить разбить на функции постфактум — модель не всегда видит естественные границы, функции получаются искусственными. В математике есть Chain-of-Thought (шаг за шагом), в коде такой явной структуры нет — LLM не знает где "шаг".
Chain-of-Function задаёт структуру до генерации: модель сначала решает какой подход использовать (main с выбором алгоритма), потом реализует части (dijkstra(), build_graph()). Получается читаемый модульный код, где каждая функция — изолированный блок логики с объяснением в docstring.
Схема метода
ПРОМПТ задаёт правила организации кода:
1. Высокоуровневые функции первыми (main, основной алгоритм)
2. Docstring в каждой функции — объяснение подхода и логики
3. Вспомогательные функции следом
4. Избегать вложенных функций — все на верхнем уровне
5. Выносить повторяющуюся логику в отдельные методы
→ LLM генерирует код следуя этой структуре
Выполняется в одном запросе — это инструкции для генерации кода.
Пример применения
Задача: Ты делаешь Telegram-бота для трекинга привычек. Нужна функция, которая проверяет серии выполнения: если пользователь отметил привычку N дней подряд — бот отправляет поздравление. Входные данные — список дат выполнения привычки, нужно найти текущую серию и максимальную в истории.
Промпт:
Напиши Python-функцию для трекинга серий выполнения привычки.
Следуй правилам организации кода:
1. Высокоуровневую функцию размести первой (основная логика).
Вспомогательные функции — после неё.
2. Добавь docstring в каждую функцию (используй тройные кавычки ''').
В основной функции объясни подход, выбор алгоритма, ключевые шаги.
Во вспомогательных — конкретную логику и назначение.
3. Выноси логику в отдельные методы, если операции повторяются
или вычисления можно изолировать.
4. Избегай вложенных функций — держи все функции на верхнем уровне.
Входные данные: список datetime объектов (даты выполнения).
Вывод: текущая серия и максимальная серия за всё время.
Результат:
Модель выдаст код с чёткой структурой:
1. Функция calculate_streaks(dates) — основная логика с docstring, объясняющим подход (сортировка дат, поиск последовательных серий)
2. Вспомогательные функции типа find_consecutive_days(sorted_dates) и get_current_streak(sorted_dates) с docstrings о конкретной логике
3. Каждая функция изолирована — можно тестировать и отлаживать отдельно
4. Код читается как план решения: сначала видишь "что делаем" (main функция), потом "как делаем" (helpers)
Почему это работает
LLM обучена на огромном количестве кода из GitHub — там есть и хорошо структурированный код, и спагетти. Без явных инструкций модель может выбрать любой стиль. "Напиши функцию" часто даёт монолитный блок: всё в одной функции, переменные на одном уровне, логика перемешана. Потом сложно понять где ошибка, какая часть за что отвечает.
Chain-of-Function использует сильную сторону LLM — следование структурированным инструкциям. Явно описываешь архитектуру: сначала high-level, потом детали. Модель видит иерархию задачи и переносит её в иерархию функций. Docstrings усиливают эффект: модель объясняет логику перед написанием кода → генерирует более осознанно.
Рычаги управления промптом:
Порядок функций — "high-level first" даёт нисходящее проектирование. Если убрать → модель может начать с деталей, потеряв общую картину.
Docstrings — если убрать требование docstrings, модель генерирует быстрее, но теряется объяснение логики. Оставь для сложных алгоритмов, убери для простых скриптов.
"Избегать вложенных функций" — модель иногда создаёт closures (функция внутри функции). Для коротких скриптов это ОК, для сложной логики — хуже отладка и повторное использование. Замени на "допустимы вложенные функции для локального контекста" если нужны замыкания.
Правило "выносить повторяющуюся логику" — увеличивает модульность, но больше функций. Для простых задач можно упростить: "выноси только значительные повторения (3+ раза)".
Меняя эти элементы, контролируешь баланс между структурированностью (больше функций, больше docstrings) и простотой (компактный код, меньше накладных расходов).
Шаблон промпта
Напиши {описание_задачи} на Python.
Следуй правилам организации кода:
1. Функции-решатели размести первыми (основной алгоритм, main логика).
Вспомогательные функции идут следом.
2. Каждая функция должна иметь docstring в тройных кавычках (''').
- В основных функциях объясни подход, выбор алгоритма, ключевые шаги.
- Во вспомогательных функциях опиши конкретную логику и назначение.
3. Выноси логику в отдельные методы, если:
- Операции повторяются
- Сложные вычисления можно изолировать
- Блок логики имеет четкую обязанность
4. Избегай вложенных функций — держи все функции на верхнем уровне.
{дополнительные_требования}
Плейсхолдеры:
- {описание_задачи} — что должен делать код (например, "функцию для поиска кратчайшего пути в графе")
- {дополнительные_требования} — опционально: формат входных данных, ограничения, требования к производительности
🚀 Быстрый старт — вставь в чат:
Вот шаблон Chain-of-Function для структурированной генерации кода.
Адаптируй под мою задачу: [опиши свою задачу].
Задай вопросы, чтобы заполнить требования к коду.
[вставить шаблон выше]
LLM спросит про входные данные, ожидаемый формат вывода, есть ли специфичные алгоритмы или ограничения — всё что нужно чтобы написать правильно структурированный код. Она возьмёт паттерн модульной организации из шаблона и применит к твоей задаче.
Ограничения
⚠️ Простые скрипты: Для задач на 5-10 строк кода избыточная структура. LLM создаст функции там, где достаточно линейного скрипта — больше букв, не больше ясности.
⚠️ Исследовательский код: Jupyter notebooks, быстрые эксперименты — там модульность часто мешает. Нужна скорость итераций, а не архитектура.
⚠️ Специфичные паттерны: Если задача требует конкретный стиль (например, functional programming с композицией, или ООП с классами) — Chain-of-Function даст процедурный стиль с функциями. Нужно явно указывать парадигму.
Как исследовали
Команда из UC San Diego решала проблему: Process Reward Models (PRM) — модели, которые оценивают промежуточные шаги решения, а не только финальный ответ — отлично работают в математике (Chain-of-Thought даёт явные шаги), но буксуют в коде. Почему? Нет чёткого определения "шага" в коде. Некоторые используют строку кода как шаг — слишком мелко, дорого считать. Другие оценивают natural language план до кода — игнорируют логику самого кода.
Исследователи предложили функцию = шаг рассуждения. Написали Chain-of-Function промпт, натренировали Qwen-2.5-Coder-3B как PRM модель на 601 задаче с LiveCodeBench (проблемы до августа 2024). Для обучения PRM использовали Qwen3-Coder-30B для генерации кода по Chain-of-Function и Monte-Carlo сэмплирование для меток промежуточных шагов. Добавили мета-обучение для коррекции шумных меток: нижний уровень оптимизации — обучение PRM на промежуточных шагах, верхний уровень — проверка на чистых метках финальных решений (unit tests). Если PRM после обучения на "зашумлённых" промежуточных метках плохо работает на финале — метки корректируются.
Тестировали на 131 задаче после февраля 2025 (строгое временное разделение, никакого пересечения). Результат: DreamPRM-Code достиг 80.9% pass@1 на LiveCodeBench — обогнал OpenAI o4-mini (77.1%) и другие топовые модели. Применяли test-time scaling: o4-mini генерирует 4 кандидата, PRM выбирает лучший по средней оценке шагов.
Почему сработало: Chain-of-Function даёт PRM понятные единицы для оценки (функции), а мета-обучение очищает шумные метки через обратную связь от реальных тестов. Абляция показала: без коррекции меток (PRM-CoF) — 80.2%, с коррекцией (DreamPRM-Code) — 80.9%. Небольшой прирост, но стабильный, особенно на сложных задачах (Hard: 62.3% → 63.9%).
Адаптации и экстраполяции
🔧 Техника: Явный вывод шагов → прозрачность рассуждений
Оригинальный Chain-of-Function фокусируется на структуре кода. Добавь явное требование "объясни промежуточные состояния":
Напиши {описание_задачи} на Python.
Следуй правилам организации кода:
1. Основная функция первой, вспомогательные — после.
2. В docstring основной функции распиши пошаговый план:
"""
Шаг 1: [что делаем]
Шаг 2: [что делаем]
...
"""
3. В конце каждой вспомогательной функции добавь print-statement
с описанием результата:
print(f"build_graph completed: {num_nodes} nodes, {num_edges} edges")
4. Выноси логику в отдельные методы для сложных вычислений.
Эффект: Видишь не только финальный код, но и промежуточные результаты при выполнении. Удобно для отладки и понимания потока данных.
🔧 Техника: Персонажи-агенты → критика кода
Chain-of-Function создаёт структуру. Добавь мультиагентную критику для проверки:
Напиши код для {задача} следуя Chain-of-Function структуре
(высокоуровневые функции первыми, docstrings, модульность).
После генерации кода:
Агент "Оптимизатор":
- Проверь сложность алгоритма каждой функции
- Найди узкие места (вложенные циклы, повторные вычисления)
- Предложи оптимизации с оценкой прироста скорости
Агент "Тестировщик":
- Найди edge cases для каждой функции
- Предложи unit-тесты с pytest
- Проверь обработку ошибок (пустые входные данные, None, некорректные типы)
Выведи финальную версию кода с учётом замечаний.
Эффект: Модель симулирует peer review — генерирует код, потом критикует с двух ракурсов, потом улучшает. Получаешь не просто структурированный код, но и протестированный с покрытием edge cases.
Ресурсы
DreamPRM-Code: Function-as-Step Process Reward Model with Label Correction for LLM Coding | Project Page | Ruiyi Zhang, Peijia Qin, Qi Cao, Pengtao Xie | University of California, San Diego
Related: LiveCodeBench (бенчмарк для code generation), Chain-of-Thought prompting (Wei et al., 2022), Process Reward Models (PRMs)
