Головна / Статті

Нагий агент: що ламається, коли модель не захищена.

Створіть той самий незахищений туристичний агент у Claude Agent SDK та LangChain Deep Agents, а потім спостерігайте, як три збої покажуть, які засоби керування відсутні.

Видавець: REACTAPP.TOP9 хв читання2152 слів

Джерела: Harness Engineering: The Naked Agent: Why Your Framework Hands You a Loop, Not a Harness — I

Клієнт просить агента перебронювати рейс. Модель звучить впевнено, виклик інструменту успішний, і у відповіді йдеться, що все вирішено. Проте хтось помічає, що дата — 32 березня.

Саме в цю мить ілюзія руйнується. Моделі не бракувало інтелекту. Не спрацювало все навколо неї, адже жоден компонент не знав, коли потрібно втрутитися. Далі починається робота «голого» агента: без перевірників, без пам’яті, без можливості відновлення, без захисних механізмів. Побачивши його невдачу, стає зрозуміло, де помиляються продакшн-агенти, і це доводить, що середовище виконання мовної моделі має більше значення, ніж сама модель. Такий спрощений агент створюють двічі — за допомогою Claude Agent SDK та LangChain Deep Agents, — і він зламується трьома конкретними способами, кожен з яких вказує на механізм контролю, який мусить забезпечувати справжній агент. Результатом є метод, який дозволяє відрізнити проблему моделі від проблеми її інтерфейсу.

Інцидент 32 березня

Уявіть агента з бронювання подорожей, який отримує повідомлення «32 березня» від справжнього користувача. Модель передала цей рядок безпосередньо до API авіаквитків. Через приблизно сорок хвилин та п’ятдесят доларів на оплату послуг моделі, людині довелося пояснювати клієнту, якому було запевнено, що все вже вирішено, про відсутнє бронювання.

Модель нічого не створювала. Вона надсилала саме те, що їй було дано, оскільки керуючий механізм не мав можливості визначити, чи слід це робити. У цьому й полягає суть справи. Інтелект походить від моделі, а дисципліна — від керуючого механізму, тому коли агент у процесі виконання починає погано поводитися, причина майже завжди криється у рівні дисципліни. Заміна на потужнішу модель чи вдосконалення запиту не забезпечать контролю, якого немає, а створення механізмів контролю починається з усвідомлення витрат від їх відсутності. Саме тому вихідна точка є простою: один крихкий агент, дві фреймворки, без керуючого механізму.

Докази того, що час виконання має перевагу

Дані тестування підтверджують цей тезис. Дослідники з Прінстонського університету заморозили модель на рівні GPT-4 Turbo та змінили лише код інтерфейсу, який з’єднував її з базою коду. Частка проблем SWE-bench, які було вирішено, зросла з 3,8% до 12,47%, тобто майже втричі, при цьому ваги моделі залишилися незмінними. Робота під назвою „SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering“ була опублікована у 2024 році та прийнята на конференції NeurIPS того ж року. Саме проектування інтерфейсу спричинило цей позитивний ефект, а не інтелект моделі.

У цій галузі існує старіша назва – „механічна симпатія“: створення програмного забезпечення, яке працює разом із апаратним забезпеченням чи платформою, а не проти них. Підхід „Harness engineering“ розширює цю концепцію на нову основу – мовну модель із її вікном контексту та лімітом уваги. Модель встановлює певні обмеження, а механізм „Harness“ визначає, наскільки послідовно система може наближатися до цих обмежень.

Зміна способу мислення впливає на те, що ви робите після невдачі:

  • Раніше: після неправильної відповіді коригували запит, додавали умовні обмеження, пробували більш потужну модель та сподівалися
  • Тепер: після неправильної відповіді визначають, який елемент керування бракувало, видаляють його з запиту та переносять у час виконання

Запити переконують, а механізми контролю забезпечують дотримання правил.

Цикл у кожній платформі

Усі платформи для агентів ґрунтуються на одному п’ятикроковому циклі. Надається інструмент моделі, отримується відповідь про використання інструменту, він виконується, повертається результат та отримується наступний крок. Документація Anthropic щодо використання інструментів, API Responses від OpenAI та механізм виконання агентів у LangChain зводяться до одного й того ж принципу. SDK можуть змінюватися, але структура залишається незмінною.

Базова версія використовує примітивний SDK-провайдера, два інструменти для виконання дій та зовсім не має системи безпеки:

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY
TOOLS = [
    {"name": "search_flights",
     "description": "Search flights between two cities for a date.",
     "input_schema": {"type": "object", "properties": {
         "origin": {"type": "string"}, "destination": {"type": "string"},
         "date": {"type": "string", "description": "YYYY-MM-DD"}},
         "required": ["origin", "destination", "date"]}},
    {"name": "book_flight",
     "description": "Book a specific flight.",
     "input_schema": {"type": "object", "properties": {
         "flight_id": {"type": "string"}, "passenger_name": {"type": "string"}},
         "required": ["flight_id", "passenger_name"]}},
]
def run_naked(user_msg: str) -> str:
    messages = [{"role": "user", "content": user_msg}]
    while True:                                   # ① no iteration cap
        resp = client.messages.create(
            model="claude-sonnet-4-6", max_tokens=1024,
            tools=TOOLS, messages=messages,
        )
        if resp.stop_reason != "tool_use":
            return resp.content[0].text
        call = next(b for b in resp.content if b.type == "tool_use")
        result = dispatch(call.name, call.input)
                                             # ② direct side effect, no check
        messages.extend([
                                             # ③ whole history, every turn
            {"role": "assistant", "content": resp.content},
            {"role": "user", "content": [{"type": "tool_result",
                "tool_use_id": call.id, "content": result}]},
        ])

Читайте цей текст, маючи на увазі три проблемні аспекти:

  1. Цикл while True: не має обмежень. Неефективний план продовжує викликати модель до тих пір, поки не закінчиться бюджет, оскільки кількість ітерацій не враховується.
  2. Функція dispatch без попередньої перевірки передає аргументи, надані моделлю, безпосередньо до ефекту з боку, тож значення, яке коректно парсується, але не може існувати, потрапляє до робочого API.
  3. Повна історія повідомлень розширюється та пересилається щоразу, тихо збільшуючи обсяг контексту до тих пір, поки якість не погіршиться.

Жодна з властивостей безпеки, які ви б хотіли мати, не знаходиться всередині цього циклу. Вони розташовані у коді, який ще не був написаний. Використання фреймворку також не усуває цих проблем. Claude Agent SDK та Deep Agents пропонують краще структурований цикл із належним протоколом інструментів та чітким шляхом виконання, але за замовчуванням вони не містять жодних механізмів перевірки, жодної пам’яті для аналізу та жодних можливостей відновлення. Цикл є тим, що існує, а організація роботи з ним залишається завданням розробника.

Ось той самий агент у кожному з фреймворків.

Версія Claude Agent SDK

SDK автоматично обгортає цикл для вас. Інструменти оголошуються за допомогою декоратора @tool, виступають як сервер MCP у межах процесу та виконуються через функцію query(). Однак відсутній будь-який контроль: аргументи перевіряються лише за схемою, нічого не зберігається між викликами, і немає можливостей відновлення.

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions, tool,
    create_sdk_mcp_server, AssistantMessage, ResultMessage,
)

@tool("search_flights", "Search flights between two cities for a date.",
      {"origin": str, "destination": str, "date": str})
async def search_flights(args):
                                              # ① no check that date exists
    hits = flights_api.search(**args)
    return {"content": [{"type": "text", "text": str(hits)}]}
@tool("book_flight", "Book a specific flight.",
      {"flight_id": str, "passenger_name": str})
async def book_flight(args):
                                               # ② destructive, ungated
    confirmation = flights_api.book(**args)
    return {"content": [{"type": "text", "text": confirmation}]}
server = create_sdk_mcp_server("travel", tools=[search_flights, book_flight])
async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"travel": server},
        allowed_tools=["mcp__travel__search_flights",
                       "mcp__travel__book_flight"],
    )
    async for msg in query(prompt="Rebook this customer for March 32nd.",
                           options=options):
        if isinstance(msg, AssistantMessage):
            for b in msg.content:
                if hasattr(b, "text"):
                    print(b.text)
        elif isinstance(msg, ResultMessage):
            print("done:", msg.subtype)
                                              # ③ no state survives this run

Три речі вирізняються:

  1. search_flights приймає все, що пропонує модель та відповідає схемі, тож дата, яка не може існувати, потрапляє до основної частини інструменту.
  2. book_flight є руйнівним, і ніщо його не захищає. Декоратор під’єднує його до циклу без будь-яких механізмів перевірки чи схвалення.
  3. Після того, як надходить ResultMessage та query() повертає результат, сесія завершується, і ніщо з цього запуску не зберігається для наступного виклику.

SDK надав чистий цикл. Він не контролює некоректні дати, подвійні оплати чи те, що потребує запам’ятовування.

Версія Deep Agents

Deep Agents обробляє цей цикл інакше. Звичайні інструменти LangChain використовують функцію create_deep_agent(), яка повертає скомпільовану структуру графу для подальшого виклику через invoke. Принцип роботи залишається таким самим: немає механізму перевірки, і оскільки не надається checkpointer, граф забуває все, що було між викликами.

from langchain.tools import tool
from deepagents import create_deep_agent

@tool
def search_flights(origin: str, destination: str, date: str) -> str:
    """Search flights between two cities for a date (YYYY-MM-DD)."""
    return str(flights_api.search(origin, destination, date))
                                                # ① no date check
@tool
def book_flight(flight_id: str, passenger_name: str) -> str:
    """Book a specific flight."""
    return flights_api.book(flight_id, passenger_name)
                                                 # ② ungated side effect
agent = create_deep_agent(
                                                 # ③ the loop, no controls
    model="anthropic:claude-sonnet-4-6",
    tools=[search_flights, book_flight],
)
result = agent.invoke({"messages": [{"role": "user",
    "content": "Rebook this customer for March 32nd."}]})
print(result["messages"][-1].content)
# Ask a follow-up in a second invoke, and it starts from zero: no thread,
# no memory.

Знову три моменти:

  1. У функції search_flights викликається API з необробленим рядком date, і ніщо не підтверджує, що дата є справжньою, до того, як надійде запит.
  2. book_flight виконує бронювання в момент, коли модель це обирає, без жодного посередника між пропозицією та наслідками.
  3. create_deep_agent повертає граф, який виконує цикл та ще дещо. Без контрольного показника немає ні потоку, ні пам’яті між викликами.

Три способи, якими «голий» агент зламується

Два фреймворки забезпечили два чисті цикли та повну відсутність дисципліни, що і є суттю «голого» агента у 2026 році. Це не умовний приклад — це форма десятків демонстрацій агентів, які щотижня опублікуються на GitHub. Під реальним навантаженням він зламується трима способами, і кожен з цих зламів відповідає контролю, який додає належний механізм керування.

Неможливий вхідний даний. При введенні „32 березня“ модель робить те, що роблять моделі, — заповнює поле date рядком, який має правильну структуру, але не має сенсу. Схема лише підтверджує, що date є рядком, і не може визначити, чи існує такий день. Тоді book_flight виконується щодо фантомного маршруту, і модель повідомляє про успіх.

Виклик — це book_flight із вигаданим ідентифікатором рейсу та ім’ям пасажира, і він повертає „Заброньовано“. Дія була виконана, і ніщо у циклі не перевіряло, чи слід це робити.

book_flight(flight_id=”AC-PHANTOM”, passenger_name=”J. Moffatt”)
# -> “Booked.” The action fired. Nothing in the loop asked whether it should.

Проблема виникла через відсутність меж дії — не було жодного верифікатора, який би контролював відмінності між тим, що запропонував модель, та тим, що насправді сталося. Заповнення цієї прогалини — завдання контрактів інструментів та верифікаторів.

Надмірний контекст. Простий цикл пересилає всю історію на кожному кроці. Довгий процес перебронювання, наповнений результатами пошуку, текстом правил та спробами повтору, може скласти п’ятдесят тисяч токенів, і ніщо не скорочує його обсяг. Виникає те, що в літературі називають «запліснявінням контексту», і агент відкидає кроки, які раніше виконував без проблем у межах одного запуску. Це не викликає жодних помилок. Агент просто поступово погіршує свою роботу.

У цьому випадку відсутніми елементами були робоча пам’ять та обробка контексту, які належать до категорій пам’яті та стійкого стану, а також до процесу обробки контексту.

Виправлені помилки. Інструмент повертається з інформацією про помилку, і модель, частіше, ніж хтось очікує, пише ввічливий огляд, який ігнорує цю несправність. Один з аналізів інцидентів у продакшені описав ці інструменти як впевнених брехунів: база даних, яка повертає нуль рядків, сприймається як фактична відповідь, а не як ознака помилки. Вибір правильного інструменту ніколи не був складною частиною; складніше — відрізнити інструмент, який справді працює, від того, що переконливо імітує роботу. Тут не спостерігалося жодних зворотних зв’язків, а їх наявність належить до категорії можливостей спостереження та виявлення відхилень.

Розглядайте це як діагностичну схему, а не як список проблем. У всіх трьох випадках модель робила те, що роблять моделі, а оточуюча система не змогла це виявити. Жоден з випадків не стосується моделі, яка була недостатньо розумною.

Функціональний цикл для інженерії управління

Чотирикроковий процес повторюється під час створення будь-якого механізму керування:

  1. Введіть функцію кріплення та ситуацію збою, яку вона контролює.
  2. Реалізуйте її в обох фреймворках у вигляді окремого фрагмента коду.
  3. Викличте збій, щоб довести, що механізм керування справді працює.
  4. Перевірте, як реагує система кріплення.

У вищенаведених розділах цей процес було виконано тричі без позначення його назви: виявлення відкритого з’єднання, виклик збою, який воно спричиняє, та позначення механізму керування, на який він вказує. Потім з’єднання закривають по одному.

Спробуйте самі

Цю проблему можна відчути, не читаючи далі. У редакторі кілька швидких експериментів це покажуть:

  • Направте „оголений“ агент у будь-якому з фреймворків на інструкцію щодо перебронювання клієнта на 32 березня та спостерігайте, як неможлива дата потрапляє до функції book_flight
  • Обмежте кількість ітерацій цього циклу п’ятьма. Це перший фактично написаний механізм керування, і він належить до частини програми під час виконання, а не до інтерфейсу введення даних
  • Нехай функція book_flight повертає {"is_error": true, ...}, і перевірте, чи в остаточному підсумку враховується ця помилка чи її ігнорують
  • Перед початком виправте одну версію claude-agent-sdk та одну версію deepagents або langchain, оскільки обидві API змінюються між версіями, і розглядайте назви моделей (claude-sonnet-4-6, anthropic:claude-sonnet-4-6) як позначення поточної версії

Кожна платформа забезпечує охайний, добре створений цикл, але жодна з них не надає інструментів для керування ним. Це є особливістю ситуації, а не недоліком інструментів; саме тут полягає справжня робота. Claude Code, OpenCode, Codex та Deep Agents усі вважаються інструментами для керування агентами, і розробник, який має труднощі з ними, відрізняється від того, хто досягає надійних результатів, створюючи механізми контролю, яких бракує в циклі. Модель вже деякий час є простою частиною процесу. Надійність формується завдяки дисципліні, побудованій навколо неї, через поступове усунення помилок, а першим природним елементом є межа дії — перше правило, яке дозволяє вийти за межі запиту та функціонувати під час виконання.