Створення інструменту DeepAgents Harness: права доступу, підагенти та людське схвалення.
Створення інструментарію DeepAgents для планувальника системного проектування: бекенди файлів, правила контролю доступу, підагенти, механізми схвалення людиною, навички та інструменти MCP.
Джерела: The DeepAgents Architecture, Part 2: Building the Agent Harness
У документації кріплення легко оцінити, але в реальному використанні його складніше оцінити. У цьому огляді розглядається те, що сталося під час використання архітектури DeepAgents для створення повноцінного, функціонального додатку, з реальними багатоетапними діалогами щодо проектування та перевіркою результатів на основі доказів. Питання є практичними: як кріплення керує файлами та дозволами, як виглядає делегування обов’язків, як функціонує схвалення людиною, та де все ще виникають проблеми з оцінкою моделі, навіть коли інфраструктура виконує свою роботу.
Цей додаток є асистентом з планування системного проектування для організацій, які виконують свої обчислювальні та штучні інтелект-завдання на платформі Azure за допомогою Databricks. Цей вибір був усвідомленим. Те, що вважається „нативними інструментами“, наразі залишається неоднозначним через тісну інтеграцію Databricks та технологій Azure AI від Microsoft; саме така неоднозначність, яка передбачає кілька кроків, і є тим, з чим має допомогти ця архітектура. Повний вихідний текст опублікований у відкритому репозиторії.
Налаштування
git clone https://github.com/shubhodayahampiholi/system-design-planner.git
cd system-design-planner
uv sync
cp .env.example .env # fill in real API keys
Необхідний Python 3.11 або новіша версія разом із менеджером пакетів uv. У прикладах використовуються моделі як Anthropic, так і OpenAI, тому для повного виконання завдань потрібні ключі API обох сервісів. Ключ Tavily та доступ до робочого простору Databricks потрібні лише у тих розділах, де вони використовуються.
Файли та контроль доступу в DeepAgents
Де зберігаються файли агента
Щоб агент міг щось читати чи записувати, йому потрібне місце для зберігання файлів. DeepAgents пропонує кілька варіантів бекендів для цього, серед яких StateBackend, FilesystemBackend та варіанти, що базуються на сховищі чи сандбоксі. У цьому проекті використовуються два таких варіанти з різних причин.
Виклик функції create_deep_agent() без додаткових аргументів надає агенту StateBackend, який зберігає файли у власному стані LangGraph агента. Оскільки цей стан зберігається після кожного кроку, файли дійсно залишаються доступними доти, доки триває розмова. Проблема полягає у тому, що зберігання пов’язане з конкретною розмовною ниткою: те, що було написано в одній нитці, недоступне з іншої. Для багатьох застосувань це цілком прийнятно. Однак тут це не так, оскільки агент мусить читати справжні документи-джерела з диска незалежно від того, в якій розмові він знаходиться.
FilesystemBackend вирішує цю проблему. Він спрямовує агента до реальної папки, і кожне читання, запис чи перелік файлів стосується справжніх файлів, які залишаються доступними після завершення роботи. Усі приклади починаються з однієї функції-фабрики:
# src/system_design_planner/agent.py
from deepagents import create_deep_agent
MODEL = "claude-sonnet-5"
def build_planner_agent(*, system_prompt=PLANNER_SYSTEM_PROMPT, **kwargs):
return create_deep_agent(model=MODEL, system_prompt=system_prompt, **kwargs)
Кожен приклад створює свого агента за допомогою цієї функції. Коли потрібен реальний доступ до файлів, виклик отримує параметр backend=FilesystemBackend(root_dir=...). Параметр root_dir визначає межі, в межах яких працює агент – це обмежений кореневий директорій, з якого він за замовчуванням не може вийти, навіть якщо спробує використати шлях на кшталт ../../. Те, що саме може робити агент у межах цих обмежень, тобто які операції з якими шляхами, – це окреме питання, яке FilesystemMiddleware контролює за допомогою правил дозволів.
Визначення того, що може робити агент
Доступ до папки – це одне рішення, а точне обмеження того, що відбувається всередині неї, – інше, не менш важливе. DeepAgents вирішує це за допомогою послідовного списку правил, які вказують, які операції дозволені, заборонені чи потребують прямої згоди користувача, для кожного шляху окремо. Саме за цими правилами працюють агенти цього проекту:
# src/system_design_planner/permissions.py
from deepagents import FilesystemPermission
DESIGN_SESSION_PERMISSIONS = [
FilesystemPermission(operations=["read", "write"], paths=["/.env"], mode="deny"),
FilesystemPermission(operations=["write"], paths=["/knowledge_base/**"], mode="deny"),
FilesystemPermission(operations=["write"], paths=["/memory/AGENTS.md"], mode="interrupt"),
]
Правила перевіряються по черзі, кожне з власної причини.
Перше правило заважає агенту читати чи записувати файл .env, де зберігаються справжні API-ключі. Воно діє незалежно від того, що ще робить агент.
Друге правило робить матеріали для пошуку у каталозі knowledge_base/ лише для читання. DeepAgents вважає операцію delete записом, тому це ж правило блокує не лише зміни, а й видалення, що означає, що основні дані, на основі яких агент приймає рішення, не можуть бути пошкоджені його власними діями.
Третє правило має найбільший вплив. Записи у файл пам’яті агента дозволені, але лише після схвалення людиною. Цей механізм розглядається окремо нижче.
Перевірка того, чи агент читає те, що йому надано
Ніщо з цього не допоможе, якщо агент не буде їх правильно використовувати, і їх потрібно перевірити ретельніше, ніж припускається. У папці knowledge_base/ знаходяться чотири короткі довідкові документи про управління Unity Catalog, технологію Mosaic AI, інтеграцію Microsoft Foundry та питання ідентичності та мережевого з’єднання. Щоб переконатися, що агент справді їх читає, а не вгадує на основі назв файлів, йому показали цю папку та попросили узагальнити її вміст:
backend = FilesystemBackend(root_dir="knowledge_base")
agent = build_planner_agent(backend=backend)
result = agent.invoke({
"messages": [{
"role": "user",
"content": "List every file in your working directory, then give a one-line summary of what each one covers.",
}]
})
Результатом стали чотири точні, окремі короткі описи — по одному на файл. Попередження про нижчу достовірність у документі про ідентичність та мережу навіть залишилося недоторканим, на відміну від інших трьох, які мали більш впевнений тон. Трек виконання пояснює, чому це має значення: агент викликав ls, щоб переглянути наявні файли, а потім виконав по одному виклику read_file на кожен документ — всього чотири — перш ніж було написано хоча б одне слово у короткому описі. Відповідь ґрунтувалася на реальних операціях читання.
Делегування підагентам
Що означає делегування тут
Деякі завдання краще підходять спеціалісту, який працює самостійно, ніж якщо їх поєднувати з усіма іншими справами, якими вже займається головний агент. DeepAgents підтримує це за допомогою підагентів – автономних агентів, до яких головний агент може звернутися для виконання конкретного завдання. Підагент працює у власному ізольованому контексті, не бачачи повної історії, яка призвела до звернення, та повідомляє про результат тому, хто доручив завдання, після його виконання. Ця ізоляція стосується насамперед контексту розмови та не обов’язково поширюється на зберігання даних, як показано у розгляді пам’яті нижче.
Є практична перевага. Контекст головного агента залишається зосередженим на загальному завданні, а не на записуванні кожного проміжного кроку, який виконав спеціаліст для отримання однієї відповіді. Ще одну перевагу легко проігнорувати: оскільки кожен підагент налаштовується окремо, різні підагенти можуть працювати на абсолютно різних моделях, обраних відповідно до завдань, які від них вимагаються.
Різні моделі для різних підагентів
Не кожні завдання підагентів потребують найсильнішої та найдорожчої моделі. У цьому проекті один підагент існує лише для витягування фактів з довідкових документів — це механічне завдання, яке не вимагає глибоких міркувань. Замість того, щоб використовувати модель, яка керує рештою системи, він отримує меншу, дешевшу модель, придатну саме для цього вузькоспрямованого завдання:
from deepagents import SubAgent
REFERENCE_EXTRACTOR: SubAgent = {
"name": "reference-extractor",
"description": (
"Extracts structured, factual capabilities from the knowledge_base "
"reference files for a specific platform or topic. Delegate here "
"before proposing any subsystem design, so decisions are grounded "
"in verified platform facts rather than assumption. Do not use this "
"subagent for design reasoning or tradeoffs - extraction only."
),
"system_prompt": (
"You are a fact-extraction specialist. Given a topic, find the "
"relevant file(s) in your working directory, read them, and return "
"ONLY a bullet list of the concrete facts relevant to that topic - "
"no narrative, no design opinions, no recommendations. If a fact is "
"explicitly flagged as an open question or unverified in the source "
"file, preserve that flag in your output rather than smoothing it "
"over into a confident-sounding statement."
),
"model": "openai:gpt-5.6-luna",
}
Поле model дозволяє це зробити. Кожен підагент може назвати власну модель, незалежно від того, на чому працює основний агент. У цій версії основний планувальник працює на Claude, тоді як цей підагент працює на моделі OpenAI.
Проте встановлення поля є лише налаштуванням, і це не гарантує, що запити надходять туди, куди потрібно. Для цього потрібно переглянути те, що насправді відбулося під час виконання. Використовуючи інструменти відстеження, які дозволяють поступово переглядати виконання агента, виклики, здійснені основним агентом, були відображені під назвою Claude від початку до кінця, тоді як виклик, розміщений всередині підагента для вилучення даних, був відображений під назвою моделі OpenAI, вказаної в його налаштуваннях. Це рішення щодо маршрутизації на рівні постачальника, яке було реалізоване та може бути перевірене, а не просто оголошене в коді.
За замовчуванням підагент зберігає власну пам’ять
Ізоляція є свідомим дизайнерським рішенням у DeepAgents, і її вплив є більшим, ніж більшість людей спочатку очікують. Підагент має власний контекст, і він також не має автоматичного доступу до будь-чого, що знаходиться у пам’яті основного агента, навіть до того, що є прямо пов’язаним із дорученою задачею. Доступ має бути наданий явно.
Це зазвичай є правильним стандартним рішенням, адже підагенту не слід перебирати все, що записав основний агент, лише для виконання однієї конкретної роботи. Але коли задача залежить від рішення, яке вже було прийняте та зафіксовано, доступ має бути наданий навмисно:
def build_governance_specialist(backend) -> SubAgent:
from deepagents.middleware.memory import MemoryMiddleware
return {
"name": "governance-specialist",
"description": (
"Reasons about Unity Catalog governance, lineage, and the "
"Databricks-Microsoft Foundry governance boundary for this "
"architecture."
),
"system_prompt": (
"You are a governance specialist for an Azure + Databricks AI "
"architecture. Before answering, check /memory/AGENTS.md for any "
"recorded scoping decisions - they are binding constraints on "
"your recommendation, not suggestions."
),
"middleware": [MemoryMiddleware(backend=backend, sources=["/memory/AGENTS.md"])],
}
Цей елемент middleware виконує функцію надання дозволу. Спеціаліст отримує власне підключення до файлу в пам’яті, який використовує основний агент, і тому може читати попередні рішення перед тим, як почати міркування.
Людське схвалення та постійна пам’ять
Чому деякі дії потребують участі людини
Не кожну дію, яку виконує агент, слід залишати без нагляду. Запис у тимчасовий файл є менш значущим. Однак запис рішення, яке кожна наступна розмова буде вважати остаточним фактом, — це зовсім інша справа, адже помилка може призвести не лише до однієї неправильної відповіді, а й тихо спотворити кожну наступну відповідь. DeepAgents вирішує цю проблему за допомогою участі людини: обрані дії можна налаштувати так, щоб їх виконання було призупинено до тих пір, поки справжня людина їх не схвалить, не відхилить чи не змінить.
Тут механізм охоплює саме одну дію – запис у постійний файл пам’яті агента. Агент може запропонувати оновлення у будь-який час, і нічого не записується, поки хтось не погодиться.
Як працюють схвалення та відновлення
Щоб це функціонувало, необхідно дотримуватися двох умов. По-перше, вищезазначене правило дозволів встановлює mode="interrupt" для записів у файл пам’яті. По-друге, що легко залишається непоміченим, для призупинення потрібен контрольний показник – спосіб збереження точного стану агента, щоб його можна було призупинити та відновити, а не запускати заново з нуля.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command
checkpointer = MemorySaver()
agent = build_planner_agent(
backend=backend,
permissions=DESIGN_SESSION_PERMISSIONS,
checkpointer=checkpointer,
memory=["/memory/AGENTS.md"],
)
Як тільки агент намагається здійснити запис із блокуванням, виконання справді зупиняється, і файл залишається недоторканим. За поточною реалізацією заплановану дію можна прочитати зі стану графу:
state = agent.get_state(config)
if state.interrupts:
action = state.interrupts[0].value["action_requests"][0]
print(f"tool: {action['name']}")
print(f"args: {action['args']}")
Те, що виводиться, — це саме запит до інструменту, який агент хоче виконати, та саме текст, який він хоче зберегти, без узагальнень. Рецензент може схвалити його або відхилити, додавши конкретну причину, яка надсилається назад до моделі:
decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}
result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)
Якщо виконувати процес від початку до кінця, все працює. Пропоноване записування зупиняється, його поточний вміст можна повністю прочитати перед тим, як хтось прийме рішення, а схвалення дозволяє завершити записування; це підтверджується перевіркою файлу на диску пізніше, а не довірою до повідомлення агента про те, що він зробив.
Відхилення без пояснення
Під час виконання завдань відмова від запропонованої дії без пояснення причин не змусила модель запитати, що пішло не так. Вона вигадала правдоподібну причину та продовжила роботу. У одному випадку запис у пам’ять було відхилено без жодних пояснень, а стандартне повідомлення системи у такій ситуації лише зазначає, що дія не виконалась та не слід її повторювати, якщо тільки користувач не попросить про це. Проте модель одразу спробувала модифіковану версію тієї самої дії, обґрунтовуючи повторну спробу чимось, про що ніхто не казав. Це є реакцією моделі на те, що надає їй механізм відмови. Сам механізм не спричиняє цього безпосередньо.
Практичний урок простий: завжди слід додавати реальну, конкретну причину до відмови. Чи це запобігає такій поведінці у кожному випадку, не перевірялося окремо, але це усуває найбільш очевидну прогалину, яку модель може заповнити припущенням.
Чому схвалення кількох рішень одночасно вимагає обережності
Другий аспект стосується того, як об’єднуються запропоновані зміни. Коли під час обговорень щодо дизайну формулюються кілька пов’язаних висновків, агент схильний пропонувати записати їх усі разом: один блок нового контенту та одне схвалення, яке охоплює все, що в ньому є.
Це ефективно, але має свою ціну. У одному з задокументованих випадків один схвалений запис містив п’ять пунктів. Чотири з них були рішеннями, які людина прямо підтвердила. П’ятий — це те, що агент запропонував самостійно, з формулюванням про можливість відмови („вважати це вирішеним, якщо не буде сказано інакше“), без прямого запитання, на яке була б відповідь. Він потрапив у пам’ять із точно такими ж впевненими формулюваннями та, здавалося, такою ж авторитетністю, як і чотири підтверджені пункти.
Запис був схвалений повністю. На етапі схвалення ніщо не відокремлювало один припущуваний елемент від чотирьох підтверджених, і людина, яка швидко переглядає відмінності між кількома елементами, значно частіше помітить чотири чіткі рішення, які вона пам’ятає, ніж одне менш помітне припущення, розташоване серед них у ідентичній формулюванні.
Це справжня обмеженість, навколо якої потрібно розробляти рішення. Участь людини тут діє на рівні схвалення самого виклику інструменту, а не кожного окремого твердження всередині нього. Коли один виклик write_file об’єднує кілька логічних рішень, схвалення цього виклику означає схвалення всіх з них. Доки не з’явиться можливість більш детального схвалення, практичним захистом є ретельний перегляд кожного запропонованого запису в пам’яті та не сприймання схвалення як формальності, як тільки механізм здається надійним.
Координація кількох спеціалістів
Дозвіл агенту обирати, хто буде розбиратися з питанням
Кожен попередній субагент тестувався шляхом вказівки фахівця, якого потрібно використати. На практиці це рідко так працює: людина ставить запитання, а агент мусить сам вирішити, які фахівці є важливими. Це тестувалося за допомогою одного запитання, яке одночасно стосувалося управління, інфраструктури ШІ/Машинного навчання, розгортання та мереж, без вказівки конкретних осіб.
Перша відповідь не була делегуванням. Це був набір уточнювальних запитань щодо топології мережі, очікуваного масштабу та того, як слід поводитися з певною базою даних, поставлених перед тим, як прийняти рішення щодо дизайну. Це є розумним інстинктом, а не невдачею у діях, адже кілька з цих відповідей змінюють те, які фахівці є актуальними, тож краще спочатку запитати, ніж здогадуватися.
Паралельне делегування
Після того, як у наступному кроці були відповіді на запитання, усі чотири спеціалісти (управління, ШІ/Машинне навчання, оркестрація та мережі) отримали завдання від агента одночасно, а не по черзі. Історія операцій чітко це показує: чотири виклики щодо делегування були надіслані разом у єдиній відповіді.
Це має значення не лише з точки зору ефективності. Це також ситуація, описана у заяві щодо цієї платформи, де зазначається, що коли кілька підагентів працюють паралельно, а один створює виняток, інші можуть бути скасовані разом з ним, і проблема не обмежується лише тим, що зламався. У цьому випадку усі чотири спеціалісти успішно виконали свої завдання, тому такої поведінки не спостерігалося. Однак справжнє паралельне делегування створює саме ті умови, за яких може проявитися цей описаний баг.
Синтез, а не накопичення
Цікавішим результатом було не саме здійснення делегування. Цікавішим було те, що зробив головний агент із чотирма спеціалізованими відповідями пізніше. Більш слабка реалізація могла б розмістити кожну відповідь під окремим заголовком та на цьому зупинитися. Насправді ж сталося щось більш схоже на синтез: остаточна рекомендація поєднала всі чотири вхідні дані в єдиний послідовний дизайн, і, що ще важливіше, виявила щось, чого жоден із чотирьох не зміг би виявити самостійно.
Спеціаліст з мереж запропонував отримати доступ до зовнішньої бази даних через певний шаблон підключення. Спеціаліст з управління пояснив, як Unity Catalog автоматично відстежує ланцюжок змін для нативних сервісів Databricks. Для цього конкретного шляху інтеграції ці два підходи не поєднуються так гармонійно, як може здатися. Щоб це виявити, потрібно мати обидві відповіді у свідомості та бачити, де вони не збігаються, і саме це було зроблено під час синтезу. У цьому дизайні була чітко вказана ця прогалина, і було запропоновано використовувати ручний журнал аудиту для її усунення, не залишаючи її нерозв’язаною та не перетворюючи її на загальне правило щодо зовнішніх підключень.
Дві схвалені рішення, які суперечать одне одному
Окремий тест виявив щось ще більш тривожне. Під час двох послідовних записів у пам’ять, кожен з яких був перевірений та схвалений окремо, рекомендації щодо використання моделей суперечили одна одній. У першій рекомендувалося використовувати основну модель через API Foundation Model. У другій, схваленій через деякий час у тій самій сесії, пропонувалося замість цього використовувати налаштований пропускну здатність, причому прямо зазначалося не використовувати API Foundation Model, що суперечило тому, що щойно було збережено.
Обидві пропозиції пройшли перевірку. Жоден контрольний механізм не виявив суперечності, оскільки кожен запис розглядався як окрема пропозиція, і не порівнювався з тим, що було збережено кілька хвилин раніше. Це інша проблема, ніж проблема об’єднання пропозицій: об’єднання стосується того, наскільки ретельно схвалюється кожне рішення, тоді як тут йдеться про відсутність будь-якої перевірки послідовності між окремими рішеннями, ухваленими під час однієї розмови.
Разом ці два висновки вказують на одну проблему: процедура схвалення, яка тут реалізована, перевіряє, чи є запропонована операція запису прийнятною сама по собі, але не перевіряє, чи вона сумісна з усім, що вже було домовлено. Окрім короткої та простої сесії, що є обмеженням, яке потрібно прямо враховувати під час проектування, сам процес схвалення не повинен вважатися засобом захисту.
Надання агенту нових можливостей
Спеціальний інструмент
До цього моменту для всього використовувалися інструменти, вбудовані в DeepAgents: читання файлів, запис файлів, делегування завдань. У реальних застосунках зазвичай потрібно більше, і створення спеціального інструменту та передача його агенту є простим завданням, за допомогою того самого механізму, який фреймворк використовує внутрішньо.
Проєкт потребував функції, якої немає у вбудованих засобах: способу перевірки того, чи не стало застарілим щось у статичному довідковому матеріалі. Файли knowledge_base/ є копією на момент створення, коректною у момент запису, та ніколи не оновлюються самостійно. Спеціальний інструмент, підтримуваний сервісом онлайн-пошуку, усуває цю прогалину:
from langchain_core.tools import tool
from langchain_tavily import TavilySearch
_tavily_instance = None
def _get_tavily() -> TavilySearch:
global _tavily_instance
if _tavily_instance is None:
_tavily_instance = TavilySearch(max_results=3, topic="general")
return _tavily_instance
@tool
def check_current_standards(query: str) -> str:
"""Search the live web to verify whether a platform capability, naming,
or integration detail is still current.
Use this specifically to check something already pulled from
knowledge_base against what's true right now - not for open-ended
research.
"""
result = _get_tavily().invoke({"query": query})
return str(result)
Декоратор @tool та опис у документації роблять інструмент придатним для використання. Агент читає цей опис, щоб вирішити, чи підходить інструмент до виконуваних ним завдань, тому чіткий та конкретний опис має таке ж значення, як і код, який він супроводжує. Для приєднання інструменту до агента потрібен один аргумент: build_planner_agent(tools=[check_current_standards]).
Те, чи справді це стосується актуальної інформації, неможливо довести лише за змістом відповіді, адже модель може створити правдоподібну відповідь, прив’язану до конкретної дати, не викликаючи жодних функцій. Справжній доказ — у слідах виконання. Коли просили перевірити зміну назви певного продукту та його поточний статус, сліди показують, що фактично була викликана функція check_current_standards, яка повернула реальний результат, на основі якого потім була сформована кінцева відповідь, включаючи облік часу до конкретного терміну, розрахований правильно відносно фактичної поточної дати.
Навички: процедури, які агент може використовувати знову
Іншим видом можливостей є письмова процедура: поетапні інструкції щодо належного вирішення певного типу ситуації, на відміну від окремого факту чи дії. DeepAgents підтримує це за допомогою навичок — окремих файлів, які описують процедуру, яку агент може використовувати самостійно лише тоді, коли це необхідно в конкретній ситуації. Така поведінка „лише за потреби“ є свідомою та зазвичай називається поступовим розкриттям інформації. Агент завжди бачить коротку назву та опис кожної навички. Уся процедура потрапляє до його робочого контексту лише тоді, коли агент вважає цю навичку релевантною.
Навичка — це окремий файл у форматі Markdown із невеликим блоком структурованої інформації у верхній частині:
---
name: native-tool-scoping-check
description: Checks whether a request to use "native tooling" is genuinely unambiguous, given the deep current integration between Databricks-native and Azure-native (Microsoft Foundry) services.
license: MIT
---
# Native-Tool Scoping Check
## The procedure
1. Check /memory/AGENTS.md for an existing scoping decision covering this
question. If one exists, treat it as binding and stop here.
2. If no decision exists, do not assume either interpretation - surface
the ambiguity explicitly as a scoping question.
3. Once resolved, the decision should be persisted to memory, subject to
human approval.
Щоб надати підагенту доступ до певної навички, достатньо одного рядка: "skills": ["/skills/project/"]. Цей шлях вважається зареєстрованим джерелом навичок: середовище middleware сканує його на наявність належним чином структурованих підкаталогів, кожен з яких містить файл SKILL.md. Вказівка на якийсь випадковий каталог не перетворює його на навичку, оскільки middleware шукає саме таку структуру під цим шляхом.
Варто підтвердити два окремі моменти, адже вони стосуються різних питань. По-перше, чи завантажується ця навичка взагалі, чи залишається невикористаною. Журнал справжнього запуску показав, що агент прямо запитував файл навички, що є доказом того, що механізм функціонує, а не просто існує. По-друге, чи впливає зміст навички на поведінку у значущий спосіб. Для тестування було потрібно сценарій, який, ймовірно, вимагатиме одночасного використання двох технічних навичок — однієї з дизайну системи пошуку та іншої з надійності каналу передачі даних, — щоб перевірити, чи буде результат ґрунтуватися на обох. Це справді сталось: запропонований дизайн використовував метод часткового оброблення даних, описаний у одній з навичок, а також розглядав перезавантажений документ як щось, що можна безпечно замінити, а не дублювати, що відповідає інструкції з іншої навички.
Підключення до зовнішньої системи через MCP
Інструменти та навички досі знаходяться всередині коду проекту. MCP, протокол Model Context Protocol, працює інакше. Це стандарт, який дозволяє агенту знаходити та викликати інструменти на віддаленому сервері, без необхідності ручної інтеграції для кожного з них.
Databricks запускає керований сервер MCP, який публікує функції Unity Catalog як інструменти MCP у межах відповідних дозволів. Агент може знаходити та викликати таку функцію, і нікому не потрібно писати для неї обгортку на Python. З’єднання виглядає так:
import asyncio
from databricks.sdk import WorkspaceClient
from databricks_langchain import DatabricksMCPServer, DatabricksMultiServerMCPClient
async def _fetch_uc_function_tools():
workspace_client = WorkspaceClient()
host = workspace_client.config.host
mcp_client = DatabricksMultiServerMCPClient([
DatabricksMCPServer(
name="uc-functions",
url=f"{host}/api/2.0/mcp/functions/{CATALOG}/{SCHEMA}",
workspace_client=workspace_client,
),
])
return await mcp_client.get_tools()
def get_databricks_uc_function_tools():
return asyncio.run(_fetch_uc_function_tools())
Одну з відмінностей легко сплутати, тому варто бути точними. Функція Unity Catalog та інструмент MCP — це не одне й те саме. Функція є керованою можливістю, яка має власні дозволи та власне місце в Databricks, і існує незалежно від MCP та існувала задовго до нього. Сервер MCP — це окремий шар перед нею. Це не універсальний перекладач, оскільки він також бере участь у процесі автентифікації та керованому доступі, що означає, що виклик через нього все одно дотримується правил щодо того, хто може викликати яку функцію. Це можна побачити безпосередньо: функція під назвою knowledge_platform.default.mcp_verification_check у Databricks надходить до агента у форматі knowledge_platform__default__mcp_verification_check, тобто у форматі найменувань, який вимагає протокол.
Щоб довести, що з’єднання було справжнім, а не симульованим, була застосована та сама ідея, що й під час тестування спеціального інструменту: запитати про щось, що неможливо вгадати. Функція Unity Catalog була написана так, щоб повертати одне довільне значення, обране заздалегідь. Від агента вимагали викликати її та точно повідомити, що було повернуто, і він дійсно повернув саме це значення. Це підтверджує наявність справжнього мережевого обміну даними, виконання операцій усередині Databricks та передачу результату через протокол, виключаючи можливість переконливої фальсифікації.
Інтерактивний інтерфейс
Виходження за межі скриптів
Усе вищезазначене працювало як скрипт на Python, який запускався один раз з терміналу, генерував результати та завершував роботу. Справжнє додаток зазвичай функціонує більше як розмова: хтось вводить запитання, спостерігає за роботою агента та відповідає на його запити протягом процесу. Останньою частиною проекту є інтерфейс браузера, створений за допомогою Streamlit на основі того самого агента, який використовувався раніше.
Для цього знадоблося прийняти одне архітектурне рішення, яке не є очевидним ззовні. Інструменти, які роблять проект корисним, зокрема з’єднання Databricks MCP, вимагають асинхронної обробки, тобто коду, який може чекати на відповідь мережі, поки інші процеси продовжують працювати. Streamlit знову запускає весь скрипт з початку щоразу, коли хтось взаємодіє з сторінкою, що створює суттєві труднощі у поєднанні з тривалими асинхронними операціями, незважаючи на те, що Streamlit постійно додає підтримку асинхронності. Ці труднощі добре описані в інших джерелах, тому краще було спланувати рішення з урахуванням їх, ніж виявляти їх під час розробки.
Для додатку, який обслуговує багатьох користувачів одночасно, більш надійним підходом є розділення функцій: відповідний бекенд-сервіс керує агентом, а інтерфейс є суто відображувальним шаром. Для локального додатку для одного користувача таке розділення додає складності без відповідної користі. Тому асинхронна робота відбувається всередині інтерфейсу, з використанням вбудованої функції Python asyncio.run() для виконання одного повного етапу розмови за раз. Це свідомий компроміс, придатний для масштабу цього додатку, і це не ствердження про те, що це правильний вибір у всіх випадках.
Показування того, що робить агент
Інтерфейс має перевагу над скриптом: він може відображати процес виконання завдання в реальному часі, а не лише кінцевий результат. Тут показується живий запис — список кожного виклику інструменту у потоці дій основного агента, позначений типом дії. Видимість закінчується на межі, описаній далі, і не поширюється на ізольовану роботу підагента.
Це означає необхідність перевірки кожного виклику інструменту та визначення категорії, до якої він належить:
def classify_tool_call(tool_call, mcp_tool_names):
name = tool_call["name"]
args = tool_call.get("args", {})
if name == "task":
return "subagent", args.get("subagent_type", "?")
if name in mcp_tool_names:
return "mcp", name
if name == "read_file":
path = str(args.get("file_path", ""))
if "/skills/project/" in path and path.endswith("SKILL.md"):
skill_name = path.split("/")[-2]
return "skill", skill_name
if name in ("read_file", "write_file", "edit_file", "ls", "glob", "grep", "delete"):
return "filesystem", name
return "tool", name
Під час роботи агента кожен новий виклик інструменту класифікується та додається до списку, тож делегування завдання спеціалісту відрізняється від звичайного читання файлу, а обидва варіанти відрізняються від зовнішнього виклику MCP. У реальному використанні це дозволяє розрізняти делегування завдань, запити до власного інструменту веб-пошуку та запити до інструменту MCP Databricks; у міру розвитку розмови кожен з них отримує власну мітку.
Схвалення чи відхилення через інтерфейс
Механізм участі людини тут працює так само, але спосіб взаємодії з ним змінюється. Замість того, щоб вводити відповідь у командному рядку, запланована дія відображається на сторінці з реальними елементами керування:
if st.session_state.pending_interrupt:
action = st.session_state.pending_interrupt
st.warning(f"**Approval needed**\n\n**Tool:** `{action['name']}`\n\n**Args:** `{action['args']}`")
col1, col2 = st.columns(2)
with col1:
if st.button("Approve"):
run_turn(Command(resume={"decisions": [{"type": "approve"}]}))
with col2:
reason = st.text_input("Reason for rejecting")
if st.button("Reject"):
decision = {"type": "reject", "message": reason or "Rejected by the human reviewer."}
run_turn(Command(resume={"decisions": [decision]}))
Справжня сесія перевірила це за допомогою реального відхилення. Від агента вимагали розробити іншу частину системи, і він запропонував зберегти свої висновки в пам’яті. Запис було відхилено через інтерфейс із конкретною причиною. Проте у заключному повідомленні агента чітко зазначалося, що висновки були збережені в пам’яті.
Це твердження було хибним. Перевірка файлу пам’яті безпосередньо, а також окремий пошук інших файлів у проекті, які могли змінитися, підтвердили, що нічого не було записано ніде. Основний механізм повністю дотримався рішення про відхилення, а інтерфейс та система переривань виконали саме те, що мали робити. Проблемою було опис подій агентом: він повідомив про невдачу, проте все одно описав успіх. Це та сама поведінка, яку спостерігали раніше з версією терміналу; тепер вона підтверджена через абсолютно інший шлях виконання, що робить її повторюваною рисою того, як модель поводиться з відхиленою дією, а не одноразовим явищем.
Що інтерфейс не може показати
Одна з попередніх функцій не була повністю перевірена в інтерфейсі: використання навичок. Живий запис походить з власного потоку діяльності основного агента, тоді як внутрішні кроки спеціаліста, включаючи завантаження файлу навички, відбуваються в ізольованому процесі цього спеціаліста, на рівень нижче, ніж те, що відображається у основному потоці. За замовчуванням ця внутрішня діяльність об’єднується з основним потоком та не відображається окремо.
LangGraph може її відобразити: передача параметра subgraphs=True у функцію потокового передавання даних дозволяє отримати інформацію про діяльність, що відбувається під час роботи підагента. Цей параметр було виявлено, але не додано до цієї версії, оскільки він змінює форму кожного потокового оновлення, а відображення записів вимагало б додаткової роботи. Використання навичок спеціалістом залишається повністю підтвердженим шляхом перевірки запису виконання, описаного раніше. Цей інтерфейс просто не відображає це в реальному часі, і чесне пояснення краще, ніж припущення протилежного.
Що підтверджує процес будування
Каркас витримав
Основною ідеєю на початку було те, що складну роботу агента найкраще розуміти як проблему керування станом. Агент не зазнає невдачі через те, що модель не може міркувати, а через те, що він втрачає контроль над тим, що знає, що було вирішено чи що йому наразі дозволено робити. Усе, що було перевірено вище, підтверджувало це твердження тим чи іншим способом.
Щодо цього питання результати були постійно гарними. Підагент, який мав доступ до спільного файлу пам’яті, діяв правильно завдяки цьому доступу. Спеціаліст з управління дотримувався рішення, про яке ніколи не чув під час розмови, просто тому, що міг його знайти. Правила дозволів залишалися незмінними навіть під тиском: багаторазові відмови в реальному інтерфейсі, коли реальна особа кілька разів натискала кнопку відмови, — жодна з відмовлених дій так і не була здійснена. Делегування між постачальниками, коли одна частина системи працює за однією моделлю, а інша — за іншою, є деталлю конфігурації, а не інженерним проектом.
Механізми відрізняються за своєю природою. Дозволи файлової системи надають авторизацію, налаштування маршрутизації між постачальниками формують моделі, а доступ підагента до пам’яті керує станом та контекстом. Кожен з них є елементом системи, призначеним для вирішення проблеми керування станом, і кожен виконував те, для чого був налаштований. Це твердження є вужчим та точнішим, ніж ствердження про те, що вся система працювала так, як планувалося, а наступна частина розглядає випадки, коли це не відбулося.
Що не виправляє каркасна структура
Цікаві результати були отримані у випадках, коли каркасна структура працювала ідеально, проте щось все одно пішло не так.
Отримавши відмову без пояснень, модель не запитувала, у чому проблема. Вона вигадала правдоподібне пояснення та діяла відповідно до нього, причому два зовсім різні шляхи виконання підтвердили цю закономірність. Їй дали п’ять пов’язаних фактів для зберігання: чотири підтвердили люди, а один був лише виведений, проте вона записала всі п’ять з однаковою впевненістю та формулюванням, тож факт, виведений штучно, не мав слабшого сигналу, ніж підтверджені факти поруч. Два записи в пам’ять, зроблені з інтервалом у кілька хвилин під час однієї сесії, кожен з яких був схвалений окремо, дали абсолютно протилежні рекомендації, і ніщо у процесі не викликало підозр.
Строго кажучи, жоден із цих випадків не є помилкою у керуванні станом у форматі втраченого чи пошкодженого файлу. Завдяки об’єднаній процедурі схвалення та суперечливим записам необхідна інформація була збережена правильно та була доступна для перевірки. Ніхто та ніщо її не перевіряло. Ситуація з відхиленням є більш тонкою: модель ніколи не бачить самого відхилення, лише генероване повідомлення про нього, тож те, як вона реагує на це повідомлення, ставить під сумнів її власні міркування та нічого не говорить про відсутність стану. У всіх трьох випадках провалюється саме оцінювання: сприйняття нез’ясованого відмовлення за чисту монету, розділення припущень від підтверджених фактів, помічення того, що нова відповідь суперечить старій. Це проблема міркувань, а не проблема структури, і додавання структури не вирішує її безпосередньо.
Чому це важливо
Для типу проблем, які вирішує інструментарій на кшталт DeepAgents, управління станом є найважливішим елементом, а якість міркувань є тим, що залишається, є складнішим та не піддається контролю саме по собі. Бекенди, дозволи, ізольоване делегування та контрольована пам’ять не замінюють гарного судження. Вони роблять дії, стан та результати системи спостережуваними та керованими: видимими, пріоритезованими та здатними до скасування до того, як вони стануть постійними. Це полегшує оцінку суджень пізніше, хоча не може гарантувати їхню правильність. Відхилений запис, який все одно описується як успіх, є справжньою невдачею, але її виявляють одразу, оскільки система спроєктована так, що саме такі твердження можна перевірити, а не просто приймати їх на віру.
У кінцевому підсумку саме для цього існує такий інструментарій.
Пов’язана література
- Робочі процеси LangGraph з багатьма агентами: порівняння зграї та керівника — Розділіть робочий процес LangGraph на підагентів із функцією зберігання стану, потім створіть фінансового асистента як у форматі зграї, так і керівника, щоб побачити, де кожен з цих підходів припиняє працювати ефективно.
- Система для роботи з Claude Code: шість рівнів від Slack до об’єднаного коду — Як шестирівнева система перетворює запити з Slack на об’єднаний код: прийом подій, цикли агентів із контролем доступу, стійкий стан, самовідновлення, можливості моніторингу та керування з боку оператора.