Создание инструмента 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/"]. Этот путь считается зарегистрированным источником навыков: мидлвэр для обработки навыков сканирует его в поисках правильно структурированных подкаталогов, в каждом из которых находится файл SKILL.md. Указание на произвольную папку не превращает её в навык, поскольку мидлвэр ищет именно такую структуру внутри этого пути.
Стоит подтвердить два отдельных момента, поскольку они отвечают на разные вопросы. Первый — загружается ли вообще данная навык, или же он остаётся невостребованным. Логи реальной работы показали, что агент явно запрашивал файл навыка, что служит доказательством того, что механизм функционирует, а не просто существует. Второй момент — влияет ли содержимое навыка на поведение в значимой степени. Для тестирования потребовалась ситуация, требующая одновременного применения двух технических навыков: одного, связанного с проектированием систем поиска, и другого — с надёжностью потоков обработки данных, чтобы выяснить, используется ли в результатах оба навыка. Это действительно произошло: предложенная конструкция использовала метод разбиения на части, описанный в одном из навыков, а также обрабатывала перезагруженный документ как объект, который можно безопасно заменить, а не дублировать, следуя инструкции из другого навыка.
Подключение к внешней системе через 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 в объединённый код: приём событий, циклы работы агентов с контролем доступа, сохранение состояния, самовосстановление, возможности мониторинга и управление оператором.