Корупція відповіді tool (schema drift + truncation) + код

  • Побач ранні сигнали, поки рахунок не поліз вгору.
  • Зрозумій, що ламається в проді й чому.
  • Скопіюй guardrails: budgets, stop reasons, validation.
  • Знай, коли це не справжня root cause.
Сигнали виявлення
  • Tool calls на run зростають (або повторюються з args hash).
  • Витрати/токени ростуть без кращих результатів.
  • Retries стають постійними (429/5xx).
Корумпований або ‘попливший’ output tool’а перетворюється на неправильні дії. Валідуй outputs, обмежуй розмір і fail-closed, щоб агент не діяв по сміттю.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) Tool output сприймають як trusted і well-formed
  4. 2) Partial responses — реальність
  5. 3) Schema drift відбувається постійно
  6. 4) Модель намагається “згладити” корупцію
  7. Приклад реалізації (реальний код)
  8. Реальний інцидент (з цифрами)
  9. Компроміси
  10. Коли НЕ варто
  11. Чекліст (можна копіювати)
  12. Безпечний дефолтний конфіг (JSON/YAML)
  13. FAQ (3–5)
  14. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/2: Execution

Normal path: execute → tool → observe.

Проблема (з реального продакшену)

Твій tool повертає JSON.

Поки не перестає.

Може проксі підсунув HTML. Може відповідь обрізалась. Може версія tool’а змінилась і поле перейменували.

Модель бачить “щось” і намагається продовжити. Так ти отримуєш:

  • неправильні рішення
  • неправильні writes
  • і найгірший баг: “не впало, просто зробило не те”

Корупція відповіді tool’а — продакшен-фейл, бо tool output = реальність агента. Якщо реальність корумпована — рішення корумповані.

Чому це ламається в продакшені

1) Tool output сприймають як trusted і well-formed

Більшість команд валідовує inputs і ігнорує outputs. Для агентів це навпаки:

  • outputs — це те, на чому агент приймає рішення
  • outputs — найпростіший шлях для тихої корупції

2) Partial responses — реальність

Таймаути не завжди падають “чисто”. Інколи ти отримуєш:

  • половину JSON
  • 200 зі “стороннім” error payload
  • порожній body із success статусом

3) Schema drift відбувається постійно

Внутрішні API змінюються. Вендори змінюються. Твої tools змінюються.

Якщо агент не валідовує outputs — ти дізнаєшся з юзерських факапів.

4) Модель намагається “згладити” корупцію

Людина бачить невалідний JSON і зупиняється. Модель бачить “майже” і заповнює прогалини галюцинаціями.

Для тексту це інколи ок. Для tool-mediated дій — це баг.

Приклад реалізації (реальний код)

Безпечний патерн:

  1. ліміти розміру
  2. content-type
  3. schema + invariants
  4. fail closed (або degrade) при mismatch
PYTHON
import json
from typing import Any


class ToolOutputInvalid(RuntimeError):
  pass


def parse_json_strict(raw: str, *, max_chars: int = 200_000) -> Any:
  if len(raw) > max_chars:
      raise ToolOutputInvalid("tool output too large")
  try:
      return json.loads(raw)
  except Exception as e:
      raise ToolOutputInvalid(f"invalid JSON: {type(e).__name__}")


def validate_user_profile(obj: Any) -> dict[str, Any]:
  if not isinstance(obj, dict):
      raise ToolOutputInvalid("expected object")
  if "user_id" not in obj or not isinstance(obj["user_id"], str):
      raise ToolOutputInvalid("missing user_id")
  if "plan" in obj and obj["plan"] not in {"free", "pro", "enterprise"}:
      raise ToolOutputInvalid("invalid plan enum")
  return obj


def get_user_profile(user_id: str) -> dict[str, Any]:
  raw = http_get(f"https://api.internal/users/{user_id}")  # (pseudo)
  obj = parse_json_strict(raw)
  return validate_user_profile(obj)
JAVASCRIPT
export class ToolOutputInvalid extends Error {}

export function parseJsonStrict(raw, { maxChars = 200_000 } = {}) {
if (raw.length > maxChars) throw new ToolOutputInvalid("tool output too large");
try {
  return JSON.parse(raw);
} catch (e) {
  throw new ToolOutputInvalid("invalid JSON: " + (e && e.name ? e.name : "Error"));
}
}

export function validateUserProfile(obj) {
if (!obj || typeof obj !== "object") throw new ToolOutputInvalid("expected object");
if (typeof obj.user_id !== "string") throw new ToolOutputInvalid("missing user_id");
if ("plan" in obj && !["free", "pro", "enterprise"].includes(obj.plan)) {
  throw new ToolOutputInvalid("invalid plan enum");
}
return obj;
}

Це навмисно строго. Якщо tool output корумпований, правильна відповідь зазвичай:

  • stop
  • partial results
  • і логування класу фейлу

Не: “вгадай, що мав на увазі tool”.

Реальний інцидент (з цифрами)

У нас був агент, який оновлював CRM нотатки на основі user profile + останніх подій.

Tool user.profile почав інколи повертати HTML error pages зі статусом 200 (proxy misconfig). Модель “прочитала” HTML і витягла “plan = enterprise” з випадкового банера.

Impact:

  • 23 CRM записи отримали неправильний plan
  • sales команда витратила ~3 години на “enterprise leads”, яких не було
  • довелось запускати cleanup job і відновлювати з логів (які були неповні)

Fix:

  1. content-type checks + strict JSON parse
  2. schema validation + enum checks
  3. fail closed + safe-mode (skip write, якщо profile invalid)
  4. метрики: tool_output_invalid rate

Tools брешуть нудно. Агент має вміти сказати “ні”.

Компроміси

  • Строга валідація підвищує hard failures під час drift (добре — ти це бачиш).
  • Fail closed знижує success rate на короткий час, зате не дає тихої корупції.
  • Підтримка schemas — робота. Silent data corruption — гірша робота.

Коли НЕ варто

  • Якщо tool strongly typed end-to-end і ти повністю його контролюєш — можна валідовувати менше (але size limits лишай).
  • Якщо tool повертає free-form text за дизайном — обгорни extractor’ом у структурований output.
  • Якщо ти не можеш зупинятись — тобі потрібні fallback tools, а не “слабша” валідація.

Чекліст (можна копіювати)

  • [ ] Enforce max response size
  • [ ] Check content-type (JSON vs HTML)
  • [ ] Strict parse (без “best effort”)
  • [ ] Schema validation + enum constraints
  • [ ] Invariant checks (ids, counts, ranges)
  • [ ] Fail closed або degrade (ніяких writes при invalid output)
  • [ ] Метрики/алерти на invalid output rate
  • [ ] Лог args hash + tool version + error class

Безпечний дефолтний конфіг (JSON/YAML)

YAML
validation:
  tool_output:
    fail_closed: true
    max_chars: 200000
    require_content_type: "application/json"
    enforce_enums: true
safe_mode:
  on_invalid_output: "skip_writes"
metrics:
  track: ["tool_output_invalid_rate"]

FAQ (3–5)

Валідація output’ів зайва, якщо tool внутрішній?
Ні. Внутрішні tools теж дрейфують і ламаються. ‘Внутрішній’ означає лише, що це твоя проблема.
Найгірший кейс, якщо пропустити це?
Silent corruption: неправильні writes, які виглядають як успіх. Ти дізнаєшся про це через дні від людей.
Потрібна повна JSON schema бібліотека?
Не одразу. Почни з strict parse + ключових invariants. Додай schemas там, де великий blast radius.
Як це пов’язано з prompt injection?
Корумповані outputs часто містять untrusted текст. Валідуй і сприймай tool output як data, не інструкції.

Q: Валідація output’ів зайва, якщо tool внутрішній?
A: Ні. Внутрішні tools теж дрейфують і ламаються. “Внутрішній” означає лише, що це твоя проблема.

Q: Найгірший кейс, якщо пропустити це?
A: Silent corruption: неправильні writes, які виглядають як успіх. Ти дізнаєшся про це через дні від людей.

Q: Потрібна повна JSON schema бібліотека?
A: Не одразу. Почни з strict parse + ключових invariants. Додай schemas там, де великий blast radius.

Q: Як це пов’язано з prompt injection?
A: Корумповані outputs часто містять untrusted текст. Валідуй і сприймай tool output як data, не інструкції.

Пов’язані сторінки (3–6 лінків)

Не впевнені, що це ваш кейс?

Спроєктувати агента →
⏱️ 5 хв читанняОновлено Бер, 2026Складність: ★★☆
Реалізувати в OnceOnly
Guardrails for loops, retries, and spend escalation.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Kill switch та аварійна зупинка
  • Audit logs та трасування
  • Ідемпотентність і dedupe
  • Дозволами на інструменти (allowlist / blocklist)
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Приклад policy (концепт)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.