406 lines
34 KiB
Plaintext
406 lines
34 KiB
Plaintext
Контекстная инженерия определяет, **что видит модель**. Инженерия циклов определяет, **что система сделает дальше**.
|
||
|
||
Вместо того чтобы человек раз за разом читал ответ и писал следующий промпт, цикл превращает эту доводку в спроектированную систему. Она ставит перед AI цель, даёт ему действовать, наблюдает за реальной обратной связью, оценивает результат и либо адаптируется, либо останавливается.
|
||
|
||
<Callout type="info" title="Короткое определение">
|
||
Инженерия циклов — это практика проектирования цикла обратной связи вокруг AI-системы: цели, действий, наблюдений, оценки, состояния, защитных ограничений и правил остановки, которые ведут работу к проверяемому результату.
|
||
</Callout>
|
||
|
||
## От хорошего промпта к хорошему циклу
|
||
|
||
Промпт может дать сильную первую попытку. Цикл нужен, когда число шагов заранее неизвестно, среда может меняться или первую попытку нужно сверить с доказательствами.
|
||
|
||
<Compare
|
||
before={{
|
||
label: "Одиночный промпт",
|
||
content: "Спросить → Сгенерировать → Вернуть\n\nМодель выдаёт ответ. Человек решает, сработало ли, и пишет следующий промпт."
|
||
}}
|
||
after={{
|
||
label: "Спроектированный цикл",
|
||
content: "Постановка → Действие → Наблюдение → Оценка → Адаптация\n ↑______________________↓\n\nСистема собирает доказательства, фиксирует состояние и продолжает работу, только пока очередной проход приносит пользу."
|
||
}}
|
||
/>
|
||
|
||
Эта идея опирается на более ранние агентные паттерны. [Статья о ReAct](https://arxiv.org/abs/2210.03629) показала ценность чередования действий с наблюдениями из внешней среды. [Паттерн «оценщик-оптимизатор»](https://www.anthropic.com/engineering/building-effective-agents) от Anthropic добавляет отдельный шаг обратной связи, а [практическое руководство по созданию агентов](https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/) от OpenAI описывает работу агента как цикл, управляемый условиями выхода. Более новый термин — **инженерия циклов** — сосредотачивает внимание на осознанном проектировании всего этого цикла.
|
||
|
||
## Пять ходов
|
||
|
||
<InfoGrid items={[
|
||
{ label: "1. Постановка", description: "Превратите замысел в цель, ограничения, критерии успеха и бюджет.", color: "blue" },
|
||
{ label: "2. Действие", description: "Выберите одно ограниченное действие: поиск, правка, вычисление, вызов инструмента или вопрос человеку.", color: "amber" },
|
||
{ label: "3. Наблюдение", description: "Соберите то, что произошло на самом деле: вывод тестов, ответы API, ссылки на источники или отзывы пользователей.", color: "cyan" },
|
||
{ label: "4. Оценка", description: "Сравните доказательства с явными критериями — а не с уверенностью модели.", color: "purple" },
|
||
{ label: "5. Адаптация", description: "Обновите состояние, скорректируйте план, безопасно повторите попытку, передайте задачу человеку или остановитесь.", color: "green" },
|
||
]} />
|
||
|
||
Цикл — это не стрелки. Инженерия живёт в **контрактах между стрелками**: что считается действием, каким наблюдениям можно доверять, кто их оценивает, какое состояние сохраняется и в какой именно момент выполнение заканчивается.
|
||
|
||
<LoopEngineeringLab content={{
|
||
eyebrow: "Управление циклом / интерактивный симулятор",
|
||
title: "Пройдите рабочий цикл обратной связи по шагам",
|
||
description: "Выберите задачу и продвигайтесь по одному сигналу за раз. Обратите внимание: прогресс складывается из доказательств внешней среды и явной оценки — а не из вопросов к модели, считает ли она работу завершённой.",
|
||
chooseScenarioLabel: "Выберите цикл",
|
||
goalLabel: "Цель",
|
||
stopRuleLabel: "Правило остановки",
|
||
evidenceLabel: "Доверенные доказательства",
|
||
iterationLabel: "Итерация",
|
||
progressLabel: "Подтверждённый прогресс",
|
||
openIssuesLabel: "Открытые проблемы",
|
||
currentSignalLabel: "Текущий сигнал",
|
||
advanceLabel: "Продвинуться на шаг",
|
||
nextIterationLabel: "Начать следующую итерацию",
|
||
resetLabel: "Сбросить цикл",
|
||
completeLabel: "Цель подтверждена",
|
||
completeDescription: "Критерии успеха выполнены, доказательства зафиксированы, и цикл останавливается вместо того, чтобы тратить ещё одну итерацию.",
|
||
stages: [
|
||
{ id: "frame", label: "Постановка", verb: "Перечитать контракт" },
|
||
{ id: "act", label: "Действие", verb: "Сделать один ограниченный ход" },
|
||
{ id: "observe", label: "Наблюдение", verb: "Прочитать внешнюю обратную связь" },
|
||
{ id: "evaluate", label: "Оценка", verb: "Применить рубрику" },
|
||
{ id: "adapt", label: "Адаптация", verb: "Скорректировать следующий ход" },
|
||
],
|
||
scenarios: [
|
||
{
|
||
id: "code",
|
||
label: "Починить баг в оформлении заказа",
|
||
goal: "Исправить итоговые суммы заказа, не изменив корректное поведение скидок.",
|
||
stopRule: "Целевой регрессионный тест проходит, весь набор тестов оформления заказа проходит, линтер не выдаёт замечаний — либо три попытки исчерпаны, и цикл передаёт задачу человеку.",
|
||
evidence: "Шаги воспроизведения, вывод тестов, дифф кода и финальный прогон регрессионных тестов.",
|
||
cycles: [
|
||
{
|
||
action: "Воспроизвести сумму из баг-репорта и добавить падающий тест для налога в сочетании с процентной скидкой.",
|
||
observation: "Тест падает с расхождением в один цент только тогда, когда налог округляется до применения скидки.",
|
||
evaluation: "Баг воспроизведён с детерминированным сигналом, но его источник ещё не устранён.",
|
||
adaptation: "Изучить границу округления и менять только путь расчёта итоговой суммы заказа.",
|
||
progress: 34,
|
||
openIssues: 2,
|
||
},
|
||
{
|
||
action: "Перенести округление на финальную денежную границу и запустить целевые тесты оформления заказа.",
|
||
observation: "Регрессионный тест проходит, но один интеграционный тест купонов теперь падает на скидке с фиксированной суммой.",
|
||
evaluation: "Первый симптом устранён, но правка слишком широкая. Правило остановки не выполнено.",
|
||
adaptation: "Сузить патч и добавить кейс, разделяющий округление налога и округление купона.",
|
||
progress: 71,
|
||
openIssues: 1,
|
||
},
|
||
{
|
||
action: "Применить точечное исправление расчёта, затем запустить регрессионный тест, весь набор тестов оформления заказа и линтер.",
|
||
observation: "Все проверки проходят. Дифф затрагивает одну расчётную функцию и новый регрессионный тест.",
|
||
evaluation: "У каждого критерия успеха есть независимое подтверждение, и ни один лимит бюджета не превышен.",
|
||
adaptation: "Остановиться, сохранить вывод тестов и передать компактный дифф на ревью человеку.",
|
||
progress: 100,
|
||
openIssues: 0,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "research",
|
||
label: "Проверить утверждение из исследований",
|
||
goal: "Оценить утверждение, что удалённая работа всегда повышает продуктивность команды.",
|
||
stopRule: "Взвешенный вывод подкреплён минимум тремя заслуживающими доверия источниками, а разногласия и ограничения явно указаны.",
|
||
evidence: "Прямые ссылки на источники, даты публикаций, методы исследований, размеры выборок и цитируемые показатели.",
|
||
cycles: [
|
||
{
|
||
action: "Найти свежие данные и проследить самую растиражированную статистику продуктивности до её первоисточника.",
|
||
observation: "Большинство статей повторяют опрос одного вендора, не давая ссылок на его анкету и исходную выборку.",
|
||
evaluation: "Эти данные популярны, но недостаточно сильны, чтобы подтвердить абсолютное утверждение.",
|
||
adaptation: "Отдать приоритет рецензируемым исследованиям и открытым датасетам; искать в том числе противоположные результаты.",
|
||
progress: 28,
|
||
openIssues: 3,
|
||
},
|
||
{
|
||
action: "Сравнить два рецензируемых исследования с национальным датасетом по рынку труда.",
|
||
observation: "Результаты зависят от типа задач, метода измерения и того, идёт ли речь о полностью удалённой или гибридной работе.",
|
||
evaluation: "Данные противоречат слову «всегда». Условный вывод становится обоснованным.",
|
||
adaptation: "Проверить, различают ли исследования объём результатов, отработанные часы и субъективно воспринимаемую продуктивность.",
|
||
progress: 68,
|
||
openIssues: 1,
|
||
},
|
||
{
|
||
action: "Составить таблицу доказательств и сверить каждое утверждение с первоисточником.",
|
||
observation: "Источники говорят о смешанных эффектах и выделяют автономию, координацию и тип задач как ключевые переменные.",
|
||
evaluation: "Вывод подтверждён, неопределённость показана, порог по числу источников достигнут.",
|
||
adaptation: "Остановиться на выводе с оговорками и сохранить таблицу доказательств для проверки.",
|
||
progress: 100,
|
||
openIssues: 0,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "writing",
|
||
label: "Отшлифовать письмо о запуске",
|
||
goal: "Написать лаконичное письмо о запуске, которое объясняет пользу и приносит клики на демо от целевой аудитории.",
|
||
stopRule: "Письмо проходит рубрику тона, укладывается в 140 слов, содержит один чёткий призыв к действию и выдерживает проверку фактов.",
|
||
evidence: "Число слов, баллы по рубрике, проверка ссылок, факты о продукте из брифа и заметки рецензента.",
|
||
cycles: [
|
||
{
|
||
action: "Написать черновик по продуктовому брифу и оценить его по рубрике аудитории и тона.",
|
||
observation: "В черновике 204 слова, он начинается с перечисления функций и содержит два конкурирующих призыва к действию.",
|
||
evaluation: "Факты точны, но критерии иерархии и длины не выполнены.",
|
||
adaptation: "Начать с результата для клиента, убрать второй призыв к действию и сократить детали реализации.",
|
||
progress: 41,
|
||
openIssues: 3,
|
||
},
|
||
{
|
||
action: "Переписать начало и сжать основную часть в одну связку «проблема — польза — доказательство».",
|
||
observation: "В письме 126 слов и один призыв к действию, но фраза-доказательство преувеличивает результат бета-теста.",
|
||
evaluation: "Структура проходит. Точность фактов всё ещё не дотягивает до рубрики.",
|
||
adaptation: "Заменить общее утверждение измеренным результатом бета-теста и запросить финальную проверку фактов.",
|
||
progress: 79,
|
||
openIssues: 1,
|
||
},
|
||
{
|
||
action: "Вставить проверенную метрику, провалидировать ссылку и ещё раз прогнать полную рубрику.",
|
||
observation: "В письме 132 слова, ссылка открывается, факты совпадают с брифом, и каждый пункт рубрики выполнен.",
|
||
evaluation: "Артефакт соответствует заданной планке качества, и доказательства можно проверить.",
|
||
adaptation: "Остановиться и отправить утверждённую версию владельцу кампании.",
|
||
progress: 100,
|
||
openIssues: 0,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
}} />
|
||
|
||
## Сначала напишите контракт цикла
|
||
|
||
Прежде чем выбирать модель или фреймворк, напишите небольшой контракт. Если эти поля расплывчаты, цикл превратит неоднозначность в повторяющиеся расходы.
|
||
|
||
```yaml
|
||
goal: "Какое наблюдаемое состояние должно стать истинным?"
|
||
inputs: "Что запускает цикл?"
|
||
state: "Какие факты и попытки сохраняются между итерациями?"
|
||
actions: "Какими инструментами может пользоваться система и с какими правами?"
|
||
observations: "Какие внешние сигналы возвращаются после этих действий?"
|
||
evaluator: "Какая рубрика или детерминированные проверки оценивают результат?"
|
||
success: "Какие доказательства подтверждают, что цель достигнута?"
|
||
failure: "Какие условия требуют восстановления или помощи человека?"
|
||
budget: "Максимум ходов, времени, токенов, денег или побочных эффектов"
|
||
```
|
||
|
||
<Callout type="warning" title="Цикл без выхода — это готовый сценарий отказа">
|
||
Всегда определяйте три выхода: **успех**, когда доказательства подтверждают достижение цели; **неудача**, когда восстановление уже не имеет смысла; и **исчерпание бюджета**, когда цикл достигает допустимого предела затрат или риска.
|
||
</Callout>
|
||
|
||
## Наблюдения должны приходить из внешнего мира
|
||
|
||
Слова модели «это выглядит правильно» — слабое доказательство. Полезное наблюдение порождается чем-то за пределами того ответа, который оценивается.
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Задача</th>
|
||
<th>Слабое наблюдение</th>
|
||
<th>Сильное наблюдение</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Исправление кода</td>
|
||
<td>«Исправление должно сработать».</td>
|
||
<td>Исходный сбой воспроизведён, затем проходят регрессионный тест и весь набор тестов.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Исследование</td>
|
||
<td>«Несколько источников согласны».</td>
|
||
<td>Утверждения ссылаются на первоисточники; даты, методы и разногласия зафиксированы.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Извлечение данных</td>
|
||
<td>«JSON вроде бы валидный».</td>
|
||
<td>Валидация по схеме проходит, а выборочные записи совпадают с источником.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Контент</td>
|
||
<td>«Текст кажется понятным».</td>
|
||
<td>Он проходит именованную рубрику, проверку фактов, проверку ссылок и тестирование на аудитории.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Операции</td>
|
||
<td>«Запрос выполнен успешно».</td>
|
||
<td>Внешняя система возвращает ожидаемое состояние, и существует запись в журнале аудита.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
Именно поэтому важны инструменты: тесты, браузеры, базы данных, валидаторы и ревью человеком превращают внутреннюю догадку в наблюдаемый результат.
|
||
|
||
## Разделите исполнителя и проверяющего
|
||
|
||
Для задач с низким риском одна модель может и писать черновик, и проверять сама себя. Для важной работы используйте проверяющего с другой ролью и ограниченными полномочиями.
|
||
|
||
<InfoGrid columns={2} items={[
|
||
{ label: "Исполнитель", description: "Предлагает изменение, вызывает инструменты действий и объясняет, какие доказательства нужно собрать.", color: "amber" },
|
||
{ label: "Проверяющий", description: "Получает цель, артефакт и доказательства; применяет рубрику; не может втихую переписать работу, которую оценивает.", color: "green" },
|
||
]} />
|
||
|
||
Проверяющим не обязательно должен быть другой AI. Предпочитайте самый детерминированный из доступных оценщиков:
|
||
|
||
1. **Точные проверки** — типы, схемы, ограничения, права доступа, хеши
|
||
2. **Исполняемые проверки** — тесты, линтеры, симуляции, валидация ссылок
|
||
3. **Проверки по рубрике** — независимая модель или человек, работающие по именованным критериям
|
||
4. **Проверки по результату** — реальное поведение пользователей или продакшен-метрики во времени
|
||
|
||
<Callout type="tip" title="Уверенность — это не доказательство">
|
||
Оценка уверенности, сгенерированная той же моделью, которая создала артефакт, — это всё ещё вывод модели. Считайте её подсказкой для маршрутизации, а не доказательством.
|
||
</Callout>
|
||
|
||
## Состояние — позвоночник цикла
|
||
|
||
Контекст — это то, что модель видит **на текущем ходу**. Состояние — это компактная запись, которая позволяет следующему ходу продолжить работу, ничего не повторяя и не забывая.
|
||
|
||
<Checklist title="Полезное состояние цикла" items={[
|
||
{ text: "Текущая цель и критерии приёмки" },
|
||
{ text: "Уже предпринятые действия и их наблюдаемые результаты" },
|
||
{ text: "Созданные артефакты с версиями или стабильными ссылками" },
|
||
{ text: "Вердикты оценщика и нерешённые проблемы" },
|
||
{ text: "Израсходованный бюджет и всё ещё доступные права" },
|
||
{ text: "Причина продолжить, остановиться или передать задачу человеку" },
|
||
]} />
|
||
|
||
Храните сжатые решения и доказательства, а не внутреннюю цепочку рассуждений модели. Хорошее состояние — маленькое, прозрачное для проверки и безопасное для возобновления работы.
|
||
|
||
## Типичные формы циклов
|
||
|
||
### Цикл исправления
|
||
|
||
`воспроизвести → внести одно изменение → запустить проверки → диагностировать → повторить или остановиться`
|
||
|
||
Лучше всего подходит для кода, конфигурации, чистки данных и любых задач с исполняемой обратной связью.
|
||
|
||
### Цикл «оценщик-оптимизатор»
|
||
|
||
`сгенерировать → оценить по рубрике → вернуть адресную обратную связь → доработать`
|
||
|
||
Лучше всего подходит для текстов, перевода, критики дизайна и результатов, качество которых растёт от внятно сформулированной обратной связи.
|
||
|
||
### Исследовательский цикл
|
||
|
||
`искать → изучить источники → найти пробелы или противоречия → искать снова → синтезировать`
|
||
|
||
Лучше всего, когда полнота заранее неизвестна. Правило остановки должно измерять покрытие доказательствами, а не число результатов поиска.
|
||
|
||
### Цикл с одобрением человека
|
||
|
||
`подготовить → проверить автоматически → сделать паузу перед рискованным действием → человек одобряет или перенаправляет`
|
||
|
||
Лучше всего подходит для платежей, публикаций, удаления данных, изменения прав доступа, медицинских и юридических решений и других действий с серьёзными последствиями.
|
||
|
||
## Отказы, которые нужно устранять на этапе проектирования
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Отказ</th>
|
||
<th>Что произошло</th>
|
||
<th>Инженерный ответ</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Бесконечные повторы</td>
|
||
<td>У цикла нет измеримого выхода по успеху или бюджету.</td>
|
||
<td>Добавьте явные конечные состояния и жёсткий лимит итераций.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Самоодобрение</td>
|
||
<td>Исполнитель принимает собственный правдоподобный ответ без доказательств.</td>
|
||
<td>Используйте детерминированные проверки или независимого проверяющего.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Снежный ком контекста</td>
|
||
<td>Каждая итерация дописывает всё подряд, пока модель не теряет сигнал.</td>
|
||
<td>Храните структурированное состояние и пересобирайте только релевантный контекст.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Метания</td>
|
||
<td>Цикл мечется между двумя исправлениями.</td>
|
||
<td>Обнаруживайте повторяющиеся состояния и требуйте смены стратегии или передачи задачи человеку.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Дрейф цели</td>
|
||
<td>Локальные улучшения подменяют исходную задачу.</td>
|
||
<td>Перечитывайте неизменную цель и критерии приёмки на каждой итерации.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Небезопасные повторы</td>
|
||
<td>Обратимая ошибка становится вредной при многократном повторении.</td>
|
||
<td>Ограничьте права, побочные эффекты, частоту, расходы и масштаб возможного ущерба.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
## Минимальная реализация
|
||
|
||
```typescript
|
||
let state = initialize(goal, acceptanceCriteria, budget);
|
||
|
||
while (state.budget.remaining > 0) {
|
||
const context = assembleRelevantContext(state);
|
||
const proposedAction = await maker.chooseAction(context);
|
||
const observation = await tools.executeWithinPolicy(proposedAction);
|
||
const verdict = await evaluator.check({ goal, observation, state });
|
||
|
||
state = recordIteration(state, { proposedAction, observation, verdict });
|
||
|
||
if (verdict.status === "passed") return complete(state);
|
||
if (verdict.status === "unsafe" || verdict.status === "blocked") {
|
||
return escalateToHuman(state);
|
||
}
|
||
|
||
state = adaptPlan(state, verdict.feedback);
|
||
}
|
||
|
||
return stopWithBudgetReport(state);
|
||
```
|
||
|
||
Код — самая простая часть. Сложные вопросы — это вопросы предметной области: какой тест доказывает, что баг исчез? Какой источник авторитетен? Какое действие обратимо? Кто вправе одобрить финальный шаг? Эти решения и есть настоящая работа инженерии циклов.
|
||
|
||
## Практика: спроектируйте цикл
|
||
|
||
<TryIt
|
||
title="Набросайте контракт цикла"
|
||
description="Подставьте вместо переменных повторяющуюся задачу из вашей собственной работы. Попросите AI оспорить слабые доказательства и расплывчатые правила остановки."
|
||
prompt={`Спроектируйте ограниченный рабочий цикл AI для этой задачи:
|
||
|
||
ЗАДАЧА: \${task:каждое утро разбирать новые обращения в службу поддержки}
|
||
|
||
Определите:
|
||
1. Наблюдаемую цель
|
||
2. Триггер и необходимые входные данные
|
||
3. Разрешённые действия и права на инструменты
|
||
4. Доверенные наблюдения из внешней среды
|
||
5. Оценщика и его рубрику
|
||
6. Состояние, сохраняющееся между итерациями
|
||
7. Выходы: успех, неудача и исчерпание бюджета
|
||
8. Точки одобрения человеком для рискованных действий
|
||
9. Формат трассировки, делающий каждую итерацию проверяемой
|
||
|
||
Затем определите три самых вероятных сценария отказа в своём проекте и доработайте цикл, чтобы их предотвратить.`}
|
||
/>
|
||
|
||
<Quiz
|
||
question="Агент правит код, перечитывает дифф и говорит, что баг исправлен. Какой самой важной части цикла здесь не хватает?"
|
||
options={[
|
||
"Более длинного системного промпта",
|
||
"Внешнего наблюдения, оценённого по правилу остановки",
|
||
"Второго прохода редактирования",
|
||
"Более крупного контекстного окна"
|
||
]}
|
||
correctIndex={1}
|
||
explanation="Агент создал артефакт и осмотрел его, но не собрал независимых доказательств. Воспроизведение сбоя и запуск релевантных тестов дали бы наблюдаемый сигнал, который можно оценить по правилу остановки."
|
||
/>
|
||
|
||
## Дополнительные материалы
|
||
|
||
- [Loop Engineering — Addy Osmani](https://addyosmani.com/blog/loop-engineering/) — свежая постановка идеи о замене ручных повторных промптов спроектированной системой, а также практические предостережения о верификации и ответственности человека
|
||
- [Building Effective Agents — Anthropic](https://www.anthropic.com/engineering/building-effective-agents) — рабочие процессы «оценщик-оптимизатор», обратная связь из среды, условия остановки и рекомендации о том, когда агентная сложность оправдана
|
||
- [A Practical Guide to Building Agents — OpenAI](https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/) — запуски агентов, условия выхода, инструменты, защитные ограничения и вмешательство человека
|
||
- [ReAct: Synergizing Reasoning and Acting in Language Models](https://arxiv.org/abs/2210.03629) — основополагающее исследование о чередовании действий с наблюдениями из внешней среды
|
||
|
||
## Резюме
|
||
|
||
<Callout type="tip" title="Проектируйте обратную связь, а не только промпт">
|
||
У надёжного цикла есть измеримая цель, ограниченные действия, доверенные наблюдения, явный оценщик, компактное состояние, безопасные выходы и человеческое суждение там, где этого требуют последствия. Модель находится внутри цикла — но ответственность за цикл остаётся на инженере.
|
||
</Callout>
|