Проблема
Запит виглядає простим: оновити CRM-поле після перевірки профілю користувача.
У трейсах видно інше: за 11 хвилин один run зробив 18 кроків,
отримав 6 відповідей 200 OK, але 4 з них мали пошкоджений payload
(HTML замість JSON, обрізаний body або невалідні поля).
Сервіс формально "живий": timeout немає, статуси ніби успішні. Але агент приймає рішення на зламаних даних і робить хибні дії.
Система не падає.
Вона просто тихо псує результат під виглядом "успішної" відповіді.
Аналогія: уяви касира, який сканує пошкоджений штрихкод. Каса не вимикається, але в чек потрапляє не той товар. Response corruption в агентних системах виглядає так само: процес іде далі, але дані вже зламані.
Чому це стається
Response corruption зазвичай виникає не через один збій API, а через слабкий контроль якості tool output у runtime.
LLM має сильний bias до "завершеної" відповіді. Тому без schema-гейтів агент частіше продовжує run на "майже валідних" даних, ніж зупиняється з помилкою.
У production зазвичай так:
- інструмент повертає формально успішний статус, але пошкоджений body;
- runtime перевіряє лише status code і пропускає payload далі;
- schema/invariant checks відсутні або надто м'які;
- агент інтерпретує "майже валідні" дані як реальні факти;
- без fail-closed поламані дані доходять до write-дій.
У trace це видно як ріст tool_output_invalid_rate
при одночасно високому tool_2xx_rate.
Проблема не в одному битому JSON.
Runtime не відсікає пошкоджений tool output до того, як він стає рішенням або write-дією.
Які збої трапляються найчастіше
У production найчастіше видно чотири патерни response corruption.
Успішний 2xx, але зламаний payload
Інструмент повертає 200, але body не відповідає контракту.
Типова причина: контроль побудований лише на HTTP-статусі.
Частковий або обрізаний JSON (Partial payload)
Відповідь приходить не повністю: бракує полів або JSON обривається в середині.
Типова причина: немає strict parse і size/content-type gate.
Тихий schema drift
Постачальник інструмента міняє назви або типи полів, а агент продовжує працювати по старому контракту.
Типова причина: немає версіонування schema і контролю обов'язкових полів.
Логічна корупція після parse (Semantic corruption)
JSON формально валідний, але значення суперечать інваріантам
(currency="USD" + amount=-15, status="active" + deleted_at заповнений,
status="paid" + paid_at=null).
Типова причина: перевіряють синтаксис, але не бізнес-інваріанти.
Як виявляти ці проблеми
Response corruption добре видно по комбінації data-quality і runtime-метрик.
| Метрика | Сигнал response corruption | Що робити |
|---|---|---|
tool_output_invalid_rate | росте частка невалідних payload | ввести strict parse + schema/invariant gate |
tool_2xx_with_invalid_payload_rate | багато 2xx, але payload не проходить перевірку | не довіряти лише status code, перевіряти content-type і schema |
schema_mismatch_rate | часті невідповідності контракту | версіонувати schema і блокувати невідомі формати |
write_blocked_by_validation_rate | часто блокується write після валідації | перевірити залежність, ввімкнути degraded mode |
recovery_fallback_rate | часті fallback через неякісні дані | оновити контракт інструмента і runbook відновлення |
Як відрізнити response corruption від просто tool failure
Не кожен збій інструмента означає пошкодження даних. Ключове питання: чи проблема в доступності інструмента, чи в якості payload.
Нормально для tool failure, якщо:
- інструмент повертає
5xx/timeout, і виклик навіть не доходить до даних; - причина зупинки виглядає як
tool_timeoutабоtool_unavailable; - після retry той самий запит дає валідний payload.
Небезпечно для response corruption, якщо:
- багато
2xx, але payload не проходить parse/schema/invariant checks; - агент продовжує run на "майже валідних" даних;
- інцидент проявляється як хибні бізнес-дії, а не як явна помилка API.
Як зупиняти такі збої
Практично це виглядає так:
- ставиш size і content-type gate до будь-якого parse;
- робиш strict parse без "best effort" відновлення JSON;
- перевіряєш schema й бізнес-інваріанти перед використанням даних;
- при порушенні повертаєш stop reason і блокуєш write-дії.
Мінімальний guard для перевірки tool output:
import json
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class OutputLimits:
max_chars: int = 200_000
required_content_type: str = "application/json"
def parse_json_strict(raw: str, max_chars: int) -> Any:
if len(raw) > max_chars:
raise ValueError("output_too_large")
return json.loads(raw)
def validate_profile(obj: Any) -> None:
if not isinstance(obj, dict):
raise ValueError("schema:expected_object")
if not isinstance(obj.get("user_id"), str):
raise ValueError("schema:user_id_missing")
if obj.get("plan") not in {"free", "pro", "enterprise"}:
raise ValueError("schema:plan_invalid")
if obj.get("quota", 0) < 0:
raise ValueError("invariant:quota_negative")
def verify_tool_output(raw: str, content_type: str, limits: OutputLimits = OutputLimits()) -> str | None:
if content_type != limits.required_content_type:
return "response_corruption:content_type_mismatch"
try:
obj = parse_json_strict(raw, limits.max_chars)
validate_profile(obj)
except json.JSONDecodeError:
return "response_corruption:invalid_json"
except ValueError as e:
return f"response_corruption:{e}"
return None
Це базовий guard.
У production його зазвичай доповнюють schema versioning,
пер-інструментними validators і quarantine-потоком для підозрілих payload.
verify_tool_output(...) викликають до будь-якої write-дії,
щоб пошкоджені дані не потрапляли у зовнішні системи.
Де це реалізується в архітектурі
У production контроль response corruption майже завжди розкладений між трьома шарами системи.
Tool Execution Layer відповідає за першу лінію: content-type, size limits, strict parse, schema checks і версіонування контрактів. Саме тут формується якісна межа між "дані валідні" і "дані пошкоджені".
Agent Runtime приймає рішення, що робити далі: stop reasons, fail-closed, fallback і блокування write-дій. Якщо runtime не дисциплінований, пошкоджений payload швидко стає бізнес-інцидентом.
Policy Boundaries задає, коли система має завершувати run fail-closed і які дії заборонені при невалідних даних. Це критично для будь-яких write-інструментів.
Самоперевірка
Швидка перевірка перед релізом. Відмічайте пункти і дивіться статус нижче.
Це короткий sanity-check, а не формальний аудит.
Прогрес: 0/8
⚠ Є сигнали ризику
Бракує базових контролів. Закрийте ключові пункти цього чекліста перед релізом.
FAQ
Q: Якщо інструмент внутрішній, можна послабити перевірки?
A: Ні. Внутрішні інструменти теж дрейфують і повертають пошкоджений payload. Валідація потрібна так само, як і для зовнішніх API.
Q: Чому не дати моделі "виправити" невалідний JSON?
A: Бо модель не відновлює істину — вона лише генерує правдоподібну версію пошкоджених даних. Для write-сценаріїв безпечніше зупинити run і повернути явний stop reason.
Q: Чи обов'язкова повна JSON schema бібліотека з першого дня?
A: Ні. Почни з strict parse і критичних інваріантів, а schema-level покриття нарощуй у зонах з великим blast radius.
Q: Що показувати користувачу, коли payload невалідний?
A: Причину зупинки, що вже перевірили, і безпечний наступний крок: часткова відповідь або повторний запуск після відновлення залежності.
Response corruption майже ніколи не виглядає як гучна аварія. Це тиха деградація якості даних, яка непомітно ламає рішення агента. Тому production-агентам потрібні не лише доступні tools, а й жорстка валідація їхніх відповідей.
Пов'язані сторінки
Якщо ця проблема виникла у production, корисно також подивитися:
- Чому AI агенти ламаються — загальна карта збоїв у production.
- Tool failure — як відмови інструментів відрізняються від корупції payload.
- Hallucinated sources — як невалідні дані перетворюються на недовірені посилання.
- Prompt injection — чому untrusted tool output не можна трактувати як інструкції.
- Agent Runtime — де ставити stop reasons, fail-closed і fallback.
- Tool Execution Layer — де робити parse/schema/invariant validation.