ai-chat: защита от петель агента — убрать final-step lockdown, детектор токен-деградации, предупреждение о бюджете шагов, CORE-пометки в каталоге, MAX_AGENT_ST… #444

Open
opened 2026-07-10 06:40:41 +03:00 by agent_vscode · 0 comments
Collaborator

Контекст: третий класс петли (инцидент 2026-07-10, чат 019f49d8…)

После деплоя фиксов #407/#427/#431 агент (роль «Корректор», glm-5.2 через z.ai) снова «зациклился», но по-новому:

  • ран длился 02:25–02:49 UTC, ровно 20 шагов (кап), статус succeeded;
  • за 19 «рабочих» шагов: 48× searchInPage, 9× getNode, 1× getOutline/getPage/getCurrentPageни одного createComment и ни одного вызова loadTools;
  • на 20-м шаге сработал final-step lockdown (toolChoice: 'none' + FINAL_STEP_INSTRUCTION): у модели отняли инструменты посреди незаконченной работы. Модель вместо итога продолжила «работать» текстом и выродилась в токен-повтор: один текстовый блок 255 388 символов, в котором «loadTools.» написано 20 416 раз (~24 минуты генерации);
  • капа maxOutputTokens нет (сознательно: tool-аргументы — это выходные токены, глобальный кап режет их посреди JSON), поэтому повтор лился, пока провайдер сам не обрезал;
  • ирония: createComment — CORE-тул (tool-tiers.ts, CORE_TOOL_KEYS) и был доступен все 19 шагов. Идея «сначала надо loadTools» — галлюцинация модели, спровоцированная <tool_catalog>-механикой в системном промпте.

Драйверы петли: (а) модель сожгла весь бюджет шагов на чтение; (б) lockdown отнял инструменты, когда работа не сделана — прямой триггер деградации; (в) без ограничителя токен-деградация ничем не ограничена; (г) модель не знала, что комментарийные тулы всегда активны.

Решение по дизайну (выбор владельца): lockdown по умолчанию выключается, но остаётся под env-фичетоггл. Кап maxOutputTokens не вводим нигде — раз на любом шаге (включая последний) возможны tool-вызовы, кап резал бы tool-аргументы. Единственная защита от токен-болтовни — детектор деградации (п. 3), он универсальнее капа (ловит повторы на ЛЮБОМ шаге, не только финальном).

Изменения (пакет)

1. MAX_AGENT_STEPS: 20 → 50

apps/server/src/core/ai-chat/ai-chat.service.ts:59. Предупреждение о бюджете (п. 4) считается от константы. Проверить спеки на захардкоженные 19/20.

2. Final-step lockdown — под фичетоггл AI_CHAT_FINAL_STEP_LOCKDOWN, по умолчанию ВЫКЛЮЧЕН

Не удалять, а вынести под env-переключатель по образцу AI_CHAT_DEFERRED_TOOLS (environment.service.ts:288):

  • Геттер isAiChatFinalStepLockdownEnabled(): AI_CHAT_FINAL_STEP_LOCKDOWN, дефолт 'false' → выключен.
  • prepareAgentStep получает параметр finalStepLockdownEnabled: boolean (остаётся чистой функцией):
    • toggle ON (легаси-поведение): на последнем шаге { toolChoice: 'none', system: system + FINAL_STEP_INSTRUCTION } — как сейчас.
    • toggle OFF (новый дефолт): toolChoice не трогаем, инструменты доступны на всех 50 шагах (deferred-сужение activeTools продолжает действовать на каждом шаге, включая последний); на последнем шаге — мягкий финальный нудж в system (совместимо с deferred-веткой — вернуть и activeTools, и system):
This is the LAST step of this turn. Write your final answer to the user now.
You may still call tools, but the turn ends after this step either way —
prefer finishing with a clear text summary of what was done and what remains.
  • Спеки: существующие lockdown-тесты (ai-chat.service.spec.ts ~315–385) параметризовать по toggle (ON — старые ожидания, OFF — нудж без toolChoice).

Документация: описать AI_CHAT_FINAL_STEP_LOCKDOWN в .env.example (блок AI-переменных, рядом с AI_CHAT_DEFERRED_TOOLS): что делает, почему по умолчанию выключен (инцидент с токен-деградацией на отъёме тулов), когда включать (модель стабильно заканчивает ходы без текста).

2a. Митигировать возвращение «пустого хода» (при toggle OFF)

Lockdown вводился, чтобы ход, потративший все шаги на тулы, не заканчивался без текста. Компенсация на персисте: в onFinish, если ни один шаг не дал текста (steps без text, finishReason — исчерпание шагов), добавлять синтетическую текстовую часть-маркер, например:

(Достигнут лимит шагов — итоговый ответ не сформулирован; работа могла остаться незавершённой. Напишите «продолжай», чтобы агент продолжил.)

UI и реплей уже умеют fallback-текст (assistantParts/flushAssistant) — маркер делает состояние явным для пользователя и для модели на следующем ходе.

3. Детектор токен-деградации в onChunk — теперь единственная защита от болтовни

inProgressText уже накапливается в onChunk. Добавить дешёвую проверку каждые ~2 КБ прироста текста, два эвристических правила по хвосту (~3000 символов):

  • ≥25 подряд одинаковых непустых строк;
  • хвост состоит из ≥20 подряд повторов одного блока длиной ≤150 символов (проверка периодичности суффикса — копеечная).

При срабатывании — абортить ран через внутренний контроллер:

const degenerationController = new AbortController();
// ...
abortSignal: AbortSignal.any([effectiveSignal, degenerationController.signal]), // Node 22 — ok
// in onChunk, on detection:
degenerationController.abort(new Error('Output degeneration detected (repeated token loop)'));

Финализация: в ai_chat_runs.error — внятная причина, отличимая от пользовательского Stop и рестарта сервера. Опционально: перед персистом обрезать деградировавший хвост content до первых N повторов + маркер — чтобы сотни КБ мусора не тащились в БД и в реплей истории.

Правила — чистыми функциями + юнит-тесты (позитивные: «loadTools.\n»×N, повтор без переносов; негативные: длинные списки правок, таблицы, код).

4. Предупреждение о бюджете шагов

В prepareAgentStep: при stepNumber >= MAX_AGENT_STEPS - 6 (и не последний шаг — у него свой нудж/lockdown из п. 2) конкатенировать в system:

Only N tool-use steps remain in this turn. Stop exploring and start acting now
(make the edits / create the comments / produce results). Leave room to finish
with a final text answer.

Юнит-тесты границ (при 50: шаги 0–43 — чисто, 44–48 — предупреждение с убывающим N, 49 — финальный нудж или lockdown по toggle).

5. Явная пометка «CORE-тулы всегда активны» в описании loadTools и преамбуле каталога

  • LOAD_TOOLS_DESCRIPTION (tool-tiers.ts:82): добавить Tools NOT listed in the catalog are CORE and ALWAYS active — call them directly, NEVER via loadTools (e.g. createComment, listComments, resolveComment, editPageText, searchInPage).
  • Преамбула <tool_catalog> (ai-chat.prompt.ts:228, buildToolCatalogBlock): в note добавить core tools are always active and are not listed here. Список core-тулов рендерить динамически из CORE_TOOL_KEYS, не хардкодить.

Критерии приёмки

  • MAX_AGENT_STEPS = 50; спеки зелёные без захардкоженных 19/20.
  • AI_CHAT_FINAL_STEP_LOCKDOWN: дефолт выключен — на последнем шаге тулы доступны, в system финальный нудж; при true — легаси-lockdown (toolChoice: 'none' + FINAL_STEP_INSTRUCTION). Спеки параметризованы по toggle.
  • AI_CHAT_FINAL_STEP_LOCKDOWN задокументирован в .env.example (что/почему/когда включать).
  • Ход, закончившийся без текста, получает синтетический маркер-текст (2a) — виден в UI и в реплее.
  • Детектор деградации: юнит-тесты правил; при срабатывании ран завершается с Output degeneration detected… в ai_chat_runs.error, стрим корректно закрывается, лизы внешних MCP-клиентов освобождаются (существующий lifecycle onError/onAbort).
  • Предупреждение о бюджете — на шагах MAX-6…MAX-2, N убывает; тесты границ.
  • Описание loadTools и преамбула каталога явно говорят, что core-тулы всегда активны; список — из CORE_TOOL_KEYS.
  • maxOutputTokens не выставляется ни на одном шаге (tool-аргументы никогда не режутся).

Не входит (отдельные темы)

  • Идемпотентность/дедуп createComment (защита от флуда дублей) — инциденты 03–04.07 и ночи 10.07.
  • resolveComment с обрезанным UUID → внятная ошибка.
  • 403 у EmbeddingProcessor (z.ai policy) — конфигурация провайдера.
  • Пересмотр glm-5.2 для роли «Корректор».
# Контекст: третий класс петли (инцидент 2026-07-10, чат `019f49d8…`) После деплоя фиксов #407/#427/#431 агент (роль «Корректор», glm-5.2 через z.ai) снова «зациклился», но по-новому: - ран длился 02:25–02:49 UTC, ровно 20 шагов (кап), статус `succeeded`; - за 19 «рабочих» шагов: 48× `searchInPage`, 9× `getNode`, 1× `getOutline`/`getPage`/`getCurrentPage` — **ни одного `createComment` и ни одного вызова `loadTools`**; - на 20-м шаге сработал final-step lockdown (`toolChoice: 'none'` + FINAL_STEP_INSTRUCTION): у модели отняли инструменты посреди незаконченной работы. Модель вместо итога продолжила «работать» текстом и выродилась в токен-повтор: **один текстовый блок 255 388 символов, в котором «loadTools.» написано 20 416 раз** (~24 минуты генерации); - капа `maxOutputTokens` нет (сознательно: tool-аргументы — это выходные токены, глобальный кап режет их посреди JSON), поэтому повтор лился, пока провайдер сам не обрезал; - ирония: `createComment` — CORE-тул (`tool-tiers.ts`, `CORE_TOOL_KEYS`) и был доступен все 19 шагов. Идея «сначала надо loadTools» — галлюцинация модели, спровоцированная `<tool_catalog>`-механикой в системном промпте. Драйверы петли: (а) модель сожгла весь бюджет шагов на чтение; (б) **lockdown отнял инструменты, когда работа не сделана — прямой триггер деградации**; (в) без ограничителя токен-деградация ничем не ограничена; (г) модель не знала, что комментарийные тулы всегда активны. **Решение по дизайну (выбор владельца): lockdown по умолчанию выключается, но остаётся под env-фичетоггл.** Кап `maxOutputTokens` не вводим нигде — раз на любом шаге (включая последний) возможны tool-вызовы, кап резал бы tool-аргументы. Единственная защита от токен-болтовни — детектор деградации (п. 3), он универсальнее капа (ловит повторы на ЛЮБОМ шаге, не только финальном). # Изменения (пакет) ## 1. `MAX_AGENT_STEPS`: 20 → 50 `apps/server/src/core/ai-chat/ai-chat.service.ts:59`. Предупреждение о бюджете (п. 4) считается от константы. Проверить спеки на захардкоженные 19/20. ## 2. Final-step lockdown — под фичетоггл `AI_CHAT_FINAL_STEP_LOCKDOWN`, по умолчанию ВЫКЛЮЧЕН Не удалять, а вынести под env-переключатель по образцу `AI_CHAT_DEFERRED_TOOLS` (`environment.service.ts:288`): - Геттер `isAiChatFinalStepLockdownEnabled()`: `AI_CHAT_FINAL_STEP_LOCKDOWN`, дефолт `'false'` → выключен. - `prepareAgentStep` получает параметр `finalStepLockdownEnabled: boolean` (остаётся чистой функцией): - **toggle ON** (легаси-поведение): на последнем шаге `{ toolChoice: 'none', system: system + FINAL_STEP_INSTRUCTION }` — как сейчас. - **toggle OFF** (новый дефолт): `toolChoice` не трогаем, инструменты доступны на всех 50 шагах (deferred-сужение `activeTools` продолжает действовать на каждом шаге, включая последний); на последнем шаге — **мягкий финальный нудж** в `system` (совместимо с deferred-веткой — вернуть и `activeTools`, и `system`): ``` This is the LAST step of this turn. Write your final answer to the user now. You may still call tools, but the turn ends after this step either way — prefer finishing with a clear text summary of what was done and what remains. ``` - Спеки: существующие lockdown-тесты (`ai-chat.service.spec.ts` ~315–385) параметризовать по toggle (ON — старые ожидания, OFF — нудж без `toolChoice`). **Документация:** описать `AI_CHAT_FINAL_STEP_LOCKDOWN` в `.env.example` (блок AI-переменных, рядом с `AI_CHAT_DEFERRED_TOOLS`): что делает, почему по умолчанию выключен (инцидент с токен-деградацией на отъёме тулов), когда включать (модель стабильно заканчивает ходы без текста). ### 2a. Митигировать возвращение «пустого хода» (при toggle OFF) Lockdown вводился, чтобы ход, потративший все шаги на тулы, не заканчивался без текста. Компенсация на персисте: в `onFinish`, если ни один шаг не дал текста (`steps` без `text`, `finishReason` — исчерпание шагов), добавлять синтетическую текстовую часть-маркер, например: ``` (Достигнут лимит шагов — итоговый ответ не сформулирован; работа могла остаться незавершённой. Напишите «продолжай», чтобы агент продолжил.) ``` UI и реплей уже умеют fallback-текст (`assistantParts`/`flushAssistant`) — маркер делает состояние явным для пользователя и для модели на следующем ходе. ## 3. Детектор токен-деградации в `onChunk` — теперь единственная защита от болтовни `inProgressText` уже накапливается в `onChunk`. Добавить дешёвую проверку каждые ~2 КБ прироста текста, два эвристических правила по хвосту (~3000 символов): - ≥25 подряд одинаковых непустых строк; - хвост состоит из ≥20 подряд повторов одного блока длиной ≤150 символов (проверка периодичности суффикса — копеечная). При срабатывании — абортить ран через внутренний контроллер: ```ts const degenerationController = new AbortController(); // ... abortSignal: AbortSignal.any([effectiveSignal, degenerationController.signal]), // Node 22 — ok // in onChunk, on detection: degenerationController.abort(new Error('Output degeneration detected (repeated token loop)')); ``` Финализация: в `ai_chat_runs.error` — внятная причина, отличимая от пользовательского Stop и рестарта сервера. Опционально: перед персистом обрезать деградировавший хвост `content` до первых N повторов + маркер — чтобы сотни КБ мусора не тащились в БД и в реплей истории. Правила — чистыми функциями + юнит-тесты (позитивные: «loadTools.\n»×N, повтор без переносов; негативные: длинные списки правок, таблицы, код). ## 4. Предупреждение о бюджете шагов В `prepareAgentStep`: при `stepNumber >= MAX_AGENT_STEPS - 6` (и не последний шаг — у него свой нудж/lockdown из п. 2) конкатенировать в `system`: ``` Only N tool-use steps remain in this turn. Stop exploring and start acting now (make the edits / create the comments / produce results). Leave room to finish with a final text answer. ``` Юнит-тесты границ (при 50: шаги 0–43 — чисто, 44–48 — предупреждение с убывающим N, 49 — финальный нудж или lockdown по toggle). ## 5. Явная пометка «CORE-тулы всегда активны» в описании loadTools и преамбуле каталога - `LOAD_TOOLS_DESCRIPTION` (`tool-tiers.ts:82`): добавить `Tools NOT listed in the catalog are CORE and ALWAYS active — call them directly, NEVER via loadTools (e.g. createComment, listComments, resolveComment, editPageText, searchInPage).` - Преамбула `<tool_catalog>` (`ai-chat.prompt.ts:228`, `buildToolCatalogBlock`): в `note` добавить `core tools are always active and are not listed here`. Список core-тулов рендерить динамически из `CORE_TOOL_KEYS`, не хардкодить. # Критерии приёмки - [ ] `MAX_AGENT_STEPS = 50`; спеки зелёные без захардкоженных 19/20. - [ ] `AI_CHAT_FINAL_STEP_LOCKDOWN`: дефолт выключен — на последнем шаге тулы доступны, в system финальный нудж; при `true` — легаси-lockdown (`toolChoice: 'none'` + FINAL_STEP_INSTRUCTION). Спеки параметризованы по toggle. - [ ] `AI_CHAT_FINAL_STEP_LOCKDOWN` задокументирован в `.env.example` (что/почему/когда включать). - [ ] Ход, закончившийся без текста, получает синтетический маркер-текст (2a) — виден в UI и в реплее. - [ ] Детектор деградации: юнит-тесты правил; при срабатывании ран завершается с `Output degeneration detected…` в `ai_chat_runs.error`, стрим корректно закрывается, лизы внешних MCP-клиентов освобождаются (существующий lifecycle onError/onAbort). - [ ] Предупреждение о бюджете — на шагах MAX-6…MAX-2, N убывает; тесты границ. - [ ] Описание loadTools и преамбула каталога явно говорят, что core-тулы всегда активны; список — из `CORE_TOOL_KEYS`. - [ ] `maxOutputTokens` не выставляется ни на одном шаге (tool-аргументы никогда не режутся). # Не входит (отдельные темы) - Идемпотентность/дедуп `createComment` (защита от флуда дублей) — инциденты 03–04.07 и ночи 10.07. - `resolveComment` с обрезанным UUID → внятная ошибка. - 403 у EmbeddingProcessor (z.ai policy) — конфигурация провайдера. - Пересмотр glm-5.2 для роли «Корректор».
agent_vscode changed title from ai-chat: защита от петель агента — кап вывода на финальном шаге, детектор токен-деградации, предупреждение о бюджете шагов, CORE-пометки в катал… to ai-chat: защита от петель агента — убрать final-step lockdown, детектор токен-деградации, предупреждение о бюджете шагов, CORE-пометки в каталоге, MAX_AGENT_ST… 2026-07-10 06:45:43 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#444