Суть
Прямой вызов фреймворка из бизнес-логики означает, что его имена, сигнатуры и способ возобновления сессии расползаются по всему коду. Адаптер сводит эту связь к одной точке: наружу торчит протокол в терминах предметной области, внутри — вызовы конкретной библиотеки.
class BankAgentOrchestrator(Protocol):
async def process_transaction(self, config, initial_state) -> dict: ...
async def resume_transaction(self, config, decision) -> dict: ...
class LangGraphBankAgentAdapter:
def __init__(self, compiled_graph):
self.app = compiled_graph
async def process_transaction(self, config, initial_state):
return await self.app.ainvoke(initial_state, config=config)
async def resume_transaction(self, config, decision):
# возобновление после HITL — единственное место, где приложение
# знало бы про Command(resume=...) из LangGraph
return await self.app.ainvoke(Command(resume=decision), config=config)
Методы названы действиями бизнеса — «провести транзакцию», «возобновить», — а не операциями фреймворка. Это и есть смысл: код приложения не знает слова ainvoke.
Зачем это нужно
Framework churn. Библиотеки оркестрации агентов меняются быстро: ломаются сигнатуры, переименовываются классы, меняется способ передать возобновление. Без адаптера каждое такое изменение расходится по десяткам файлов.
Тестируемость. Бизнес-логику можно проверять на подставном оркестраторе, реализующем тот же протокол, — без графа, чекпоинтера и живых вызовов модели.
Смена фреймворка становится обозримой. Переезд с LangGraph на что-то другое превращается из переписывания приложения в написание второго адаптера. Это не бесплатно — семантика у фреймворков разная, — но объём работы становится измеримым.
Границы применимости
Адаптер оправдан там, где приложение крупнее прототипа и живёт дольше одного релиза фреймворка. Для скрипта на сто строк это лишний слой: вы платите за развязку, которой не воспользуетесь.
Вторая граница — не всё абстрагируется. Специфические возможности фреймворка (отмотка истории, тонкая работа с чекпоинтами, потоковая выдача событий) либо протекают в интерфейс, либо остаются недоступными. Честный адаптер покрывает основной путь и не притворяется, что фреймворка не существует.
Вторая поверхность привязки: обвязка
Адаптер выше защищает от привязки к фреймворку оркестрации. Точка привязки не одна: взяв готовую исполнительную обвязку (Agent Harness), приложение начинает зависеть ещё и от неё — от формы её протокола, от её примитивов разговора и хода, от её способа спрашивать подтверждение.
Отличие от фреймворка в том, где проходит шов. Фреймворк вызывается из кода приложения, и адаптер прячет его вызовы. Обвязка, наоборот, вызывает приложение сама: она инициирует запрос подтверждения, она стримит события, она решает, когда ход закончен. Спрятать это тем же приёмом нельзя — прятать пришлось бы не вызов, а протокол.
Практический вывод: границу здесь проводят не адаптером, а выбором, что остаётся у приложения. Обвязка Codex оставляет три вещи: свои MCP-сервисы, свои границы песочницы, свой поток подтверждений. Это и есть места, за которые стоит держаться — они же оказываются наименее переносимыми при смене обвязки.
Признак протёкшей абстракции проверяется одним вопросом: можно ли заменить фреймворк за адаптером, не меняя вызывающий код? Если в интерфейсе адаптера фигурируют понятия конкретного фреймворка — узлы, рёбра, чекпоинтеры, — ответ отрицательный, и адаптер лишь добавил слой, ничего не изолировав. Честный интерфейс говорит на языке вашей задачи: «выполнить сценарий», «получить статус», «продолжить с шага», а как это раскладывается в граф — деталь реализации.
Второй признак того же: каждое новое требование заставляет добавлять метод в адаптер. Значит абстракция повторяет API снизу, а не описывает потребность сверху.
Заводить сразу или по факту поломки. Сразу — обычно преждевременно: пока фреймворк один и опыта работы с ним нет, интерфейс придумывается умозрительно и получается тем самым зеркалом чужого API. Ждать первой поломки совместимости — поздно: переписывать придётся под давлением. Практический момент между этими крайностями — когда появляется второй способ делать то же самое: второй фреймворк, второй способ запуска, необходимость мокать оркестрацию в тестах. Тогда интерфейс выводится из двух реализаций, а не из одной, и получается честным почти автоматически.
Та же граница на другой оси
Приём не привязан к оркестрации. Ровно так же выносится среда исполнения: инструменты вызывают контракт readFile / exec / stop, а за ним стоят локальная, виртуальная и облачная реализации (Sandbox Abstraction). Соображение то же самое — интерфейс дорожает не в момент написания, а в момент второй реализации, — и вывод тот же: делать его настолько малым, насколько получится.
Полезное различие: у среды исполнения нашлась дешёвая проверка структурности рефакторинга, которой у оркестрации нет. Поведение агента на тех же запросах после перевода на контракт обязано не измениться; изменилось — значит где-то инструмент по-прежнему тянется мимо контракта.
Связано с
- Sandbox Abstraction — тот же приём на оси среды исполнения
- Agent Harness — вторая поверхность привязки, которую адаптером не спрятать
- LangGraph — фреймворк, который этот паттерн изолирует
- Agent Architecture — где слой встраивается в общую схему приложения
- PydanticAI — альтернативный фреймворк, ради переезда на который адаптер и заводят
- LangGraph HITL — возобновление после паузы, единственная нетривиальная операция интерфейса
- Agent Governance — права и журнал, которые удобно вешать на тот же слой