Agent SDK: OpenAI Agents SDK и Claude Agent SDK
Второй стиль оркестрации — агенты и передачи управления вместо явного графа. Ассистент онлайн-школы на OpenAI Agents SDK (запущен без сети на ScriptedModel) и на Claude Agent SDK, сравнение с LangGraph по коду, контролю, привязке к провайдеру, наблюдаемости и тестируемости, безопасность инструментов.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В уроке 8.5.2 вы нарисовали ассистента онлайн-школы как граф: каждый переход написан в коде. Второй стиль оркестрации устроен иначе. Вы описываете агентов (инструкции, инструменты, кому можно передать управлениепередача управления (handoff)EN handoffОдин агент передаёт диалог другому, более подходящему агенту вместе с историей. Ключевая абстракция OpenAI Agents SDK; шаблон есть и в Microsoft Agent Framework.), а цикл «модель → инструменты → модель» крутит библиотека — это Agent SDKAgent SDK (библиотека агентного цикла)EN agent SDKБиблиотека, которая берёт на себя цикл «модель → инструменты → модель» и даёт абстракции поверх него: агенты, передачи управления, guardrails, сессии, трассы (OpenAI Agents SDK) или цикл Claude Code с файлами, терминалом, хуками и правами (Claude Agent SDK).. Кода меньше, но путь по шагам выбирает модель, а вы ограничиваете её правами, проверками и подтверждениями.
Два представителя этого стиля решают разные задачи.
- OpenAI Agents SDK — лёгкий каркас для агентов в вашем приложении: агенты, передачи управления, guardrails, сессии, трассы, подтверждение человеком. Работает и с не-OpenAI моделями.
- Claude Agent SDK — агентный цикл Claude Code в виде библиотеки. Агент сам читает файлы, ищет по коду, запускает команды. Права, хуки, субагенты и бюджет — в настройках. Он нужен, когда агенту нужны файловая система и терминал.
Проверено на openai-agents 0.23.1 и claude-agent-sdk 0.2.163 (в пакет вшит CLI Claude Code 2.1.286), Python 3.11,
октябрь 2026. Обе версии ниже 1.0: API меняется от релиза к релизу, закрепляйте версии
(pip install "openai-agents==0.23.1" "claude-agent-sdk==0.2.163"). Домен (каталог, поиск, поток с инвариантом вместимости)
берём без изменений из school_domain.py урока 8.5.2. Он не зависит от фреймворка, поэтому переезжает целиком.
Агенты и передачи управления
Заголовок раздела «Агенты и передачи управления»flowchart LR
U["Сообщение студента"] --> G{"input guardrail<br/>номер карты?"}
G -->|"да"| X["отказ до старта агента"]
G -->|"нет"| T["triage"]
T -->|"transfer_to_catalog"| C["catalog<br/>search_courses"]
T -->|"transfer_to_enrollment"| E["enrollment<br/>enroll"]
E --> H{"needs_approval:<br/>человек"}
H -->|"approve"| D["агрегат: инварианты"]
H -->|"reject"| EСравните с графом из 8.5.2. Там маршрутизатор — узел с условным ребром. Здесь маршрутизатор — агент triage,
у которого передачи управления выглядят как инструменты. Технически handoff — это инструмент
transfer_to_<имя_агента>. Модель вызывает его, Runner делает адресата активным агентом и передаёт ему историю.
Отсюда первая ловушка: имя агента идёт в имя инструмента. Агент с именем «Запись» даст предупреждение
Tool name 'transfer_to_Запись' contains invalid characters ... transformed to 'transfer_to_______', поэтому имена
агентов пишите латиницей, а для модели добавьте handoff_description по-русски.
OpenAI Agents SDK: ассистент школы целиком
Заголовок раздела «OpenAI Agents SDK: ассистент школы целиком»Весь сценарий ниже запускается без сети и ключа. В SDK есть модуль agents.testing с
фейковой моделью со сценариемфейковая модель со сценариемEN scripted (fake) modelТестовая замена LLM, которая возвращает заранее записанные ответы и вызовы инструментов по порядку. Делает тесты агента детерминированными, быстрыми и бесплатными; проверяет код вокруг модели, но не качество самой модели. ScriptedModel. Между её «ответами» работает
настоящий конвейер SDK: схемы инструментов, передачи управления, guardrails, подтверждения, сессия.
import asyncioimport refrom dataclasses import dataclass
from agents import (Agent, GuardrailFunctionOutput, InputGuardrailTripwireTriggered, RunConfig, RunContextWrapper, Runner, SQLiteSession, function_tool, input_guardrail)from agents.testing import ScriptedModel, assistant_message, function_callfrom school_domain import Cohort, enroll_student, search
@dataclassclass SchoolContext: # контекст запуска: виден инструментам, но не модели student_id: str cohorts: dict
@function_tooldef search_courses(query: str) -> str: """Найти в каталоге школы фрагменты по запросу.""" return "\n".join(search(query))
@function_tool(needs_approval=True) # запуск встанет на паузу и спросит человекаdef enroll(ctx: RunContextWrapper[SchoolContext], cohort_id: str) -> str: """Записать текущего студента на поток cohort_id, например python-2026-11.""" return enroll_student(ctx.context.cohorts, cohort_id, ctx.context.student_id)
@input_guardrail(run_in_parallel=False) # проверка до старта агента, а не параллельно с нимdef no_card_numbers(ctx, agent, user_input) -> GuardrailFunctionOutput: found = re.search(r"\b(?:\d[ -]?){16}\b", str(user_input)) is not None return GuardrailFunctionOutput(output_info={"card": found}, tripwire_triggered=found)
def build_agents(model): catalog = Agent(name="catalog", instructions="Отвечай по каталогу, вызывая search_courses.", tools=[search_courses], model=model, handoff_description="вопросы о курсах, ценах и правилах") enrollment = Agent(name="enrollment", instructions="Записывай на поток через enroll.", tools=[enroll], model=model, handoff_description="запись, отмена, лист ожидания") return Agent(name="triage", instructions="Передай разговор подходящему агенту.", handoffs=[catalog, enrollment], input_guardrails=[no_card_numbers], model=model)
async def main(): model = ScriptedModel([ # по шагу на каждый вызов модели [function_call("transfer_to_enrollment", {}, call_id="h1")], [function_call("enroll", {"cohort_id": "python-2026-11"}, call_id="c1")], [assistant_message("Готово: вы записаны на python-2026-11, осталось оплатить.")], ]) triage = build_agents(model) ctx = SchoolContext("s-002", {"python-2026-11": Cohort("python-2026-11", capacity=20)}) session = SQLiteSession("anna-1") # история разговора (в памяти) config = RunConfig(tracing_disabled=True) # не отправлять трассы в OpenAI
result = await Runner.run(triage, "Запишите меня на python-2026-11", context=ctx, session=session, run_config=config) print(result.last_agent.name, [(i.name, i.arguments) for i in result.interruptions]) print(ctx.cohorts["python-2026-11"].enrolled)
state = result.to_state() # пауза — сериализуемый RunState for item in result.interruptions: state.approve(item) # человек сказал «да» result = await Runner.run(triage, state, context=ctx, session=session, run_config=config) print(result.final_output, ctx.cohorts["python-2026-11"].enrolled) print(len(await session.get_items()), "элементов в сессии") model.assert_complete() # сценарий съеден целиком
try: await Runner.run(triage, "Моя карта 4276 1234 5678 9012, запишите", context=ctx, run_config=config) except InputGuardrailTripwireTriggered as e: print("guardrail:", e.guardrail_result.output.output_info)
if __name__ == "__main__": asyncio.run(main())enrollment [('enroll', '{"cohort_id":"python-2026-11"}')]set()Готово: вы записаны на python-2026-11, осталось оплатить. {'s-002'}6 элементов в сессииguardrail: {'card': True}Разберём по абстракциям.
Agent и Runner. Агент — это данные: имя, инструкции, инструменты, передачи, guardrails, модель. Цикл крутит
Runner.run (асинхронно), Runner.run_sync (обёртка для скриптов) или Runner.run_streamed (события по ходу,
result.stream_events()). Лимит — max_turns, по умолчанию 10, дальше исключение MaxTurnsExceeded.
Инструменты. @function_tool строит JSON-схему из сигнатуры и докстринга (в 0.23 есть и синоним
agents.decorators.tool). Параметр с типом RunContextWrapper[...] модель не видит. Через него приходит контекст
запуска, и student_id берётся из аутентификации, как Runtime в 8.5.2.
Человек в контуре. needs_approval=True (или асинхронная функция, решающая для конкретного вызова) ставит
вызов на паузу. result.interruptions перечисляет ожидающие вызовы. result.to_state() даёт RunState, где
вы вызываете approve или reject(..., rejection_message=...), и запуск продолжается через Runner.run(агент, state).
RunState сериализуется (to_json, to_string), так что «да» может прийти через час из другого процесса. Документация
SDK прямо предупреждает: RunState.from_json не проверяет, кто прислал снимок. Храните его на сервере, а
человеку показывайте только детали действия.
Guardrails. огражденияограждения (guardrails)EN guardrailsПроверки на входе и выходе LLM-сервиса — размер, темы, персональные данные, инъекции, формат, утечка системного промпта. Слой защиты, а не гарантия. — функции, которые возвращают GuardrailFunctionOutput с флагом
tripwire_triggered. Флаг поднят — запуск прерывается исключением. Внутри может быть регулярка, как у нас, или
вызов дешёвой модели-классификатора. По умолчанию входной guardrail работает параллельно с агентом: задержка
меньше, но агент мог уже потратить токены и вызвать инструменты. run_in_parallel=False сначала проверяет вход.
Главная ловушка — границы. Входные guardrails выполняются только у первого агента, выходные — только у агента,
давшего финальный ответ. Мы проверили: выходной guardrail на triage молча не сработал, когда ответ дал
catalog. Проверку каждого вызова инструмента делают tool guardrails на самом инструменте.
Сессии. Сессиясессия агентаEN agent sessionХранилище истории разговора, которое SDK сам подгружает перед запуском агента и пополняет после. В OpenAI Agents SDK — SQLiteSession, Redis, SQLAlchemy и др.; в Claude Agent SDK — session_id и resume. сама подгружает историю перед запуском и дописывает новые элементы
после: SQLiteSession(id) в памяти, SQLiteSession(id, "school.db") в файле, есть Redis, SQLAlchemy, MongoDB и
шифрованная обёртка. Шесть элементов в нашей сессии — сообщение студента, вызов и результат передачи, вызов и
результат enroll, ответ.
Тестирование. ScriptedModel — официальный способ детерминированных тестов. model.calls хранит, что SDK
отправил модели, model.assert_complete() ловит сценарий, который закончился раньше времени, а лишний вызов
модели даёт UnexpectedModelCall. Пример выше уже и есть тест, осталось завернуть его в pytest.
Трассировка: по умолчанию — в облако OpenAI
Заголовок раздела «Трассировка: по умолчанию — в облако OpenAI»Трассировка в SDK включена по умолчанию и отправляет трассы на серверы OpenAI. Без ключа OpenAI это ошибки
401, с ключом — утечка ваших диалогов туда, куда вы, возможно, не собирались. Выключить можно тремя способами:
OPENAI_AGENTS_DISABLE_TRACING=1, set_tracing_disabled(True) или RunConfig(tracing_disabled=True) для одного
запуска. Чтобы трассы не терялись, а шли в свою систему, замените обработчики:
import asynciofrom agents import Runner, TracingProcessor, set_trace_processorsfrom agents.testing import ScriptedModel, assistant_message, function_callfrom oa_school import SchoolContext, build_agents
class PrintSpans(TracingProcessor): """Свой процессор: печатает законченные спаны вместо отправки в облако OpenAI.""" def on_span_end(self, span): d = span.span_data.export() if d["type"] in ("agent", "function", "guardrail", "handoff"): # без служебных спанов print(" span:", d["type"], d.get("name") or f'{d["from_agent"]} -> {d["to_agent"]}') def on_trace_start(self, trace): print("trace:", trace.name) def on_trace_end(self, trace): pass def on_span_start(self, span): pass def shutdown(self): pass def force_flush(self): pass
set_trace_processors([PrintSpans()]) # заменить экспорт по умолчанию своимmodel = ScriptedModel([[function_call("transfer_to_catalog", {}, call_id="h1")], [function_call("search_courses", {"query": "возврат"}, call_id="s1")], [assistant_message("Полный возврат — при отмене за 7 суток и раньше [1].")]], emit_traces=True)result = asyncio.run(Runner.run(build_agents(model), "Вернут ли деньги?", context=SchoolContext("s-002", {})))print(result.final_output)trace: Agent workflow span: guardrail no_card_numbers span: handoff triage -> catalog span: agent triage span: function search_courses span: agent catalogПолный возврат — при отмене за 7 суток и раньше [1].Видно всю траекторию: проверка входа, передача, кто какой инструмент вызвал. set_trace_processors заменяет
экспорт в OpenAI, а add_trace_processor добавляет свой обработчик к нему. В документации SDK перечислены
готовые интеграции, среди них открытые Langfuse и Phoenix из урока 8.4.2.
Тексты запросов и ответов попадают в спаны. Если это нежелательно, поставьте
RunConfig(trace_include_sensitive_data=False).
Не-OpenAI модели
Заголовок раздела «Не-OpenAI модели»По умолчанию SDK ходит в Responses API OpenAI, которого у многих провайдеров нет. Для OpenAI-совместимого сервера
(vLLM, Ollama, DeepSeek) документация советует Chat Completions: модель
OpenAIChatCompletionsModel(model=..., openai_client=AsyncOpenAI(base_url=..., api_key=...)) на конкретном агенте
или глобально set_default_openai_client(...) и set_default_openai_api("chat_completions"). Для провайдеров без
такого API есть адаптеры LiteLLM и Any-LLM (openai-agents[litellm], openai-agents[any-llm]), сами разработчики
называют их бета-версией. Ограничения честные. Встроенные инструменты OpenAI (веб-поиск, поиск по файлам) работают
только через Responses API OpenAI. Structured outputs нужны от провайдера с поддержкой JSON Schema, иначе ответы будут
ломаться. Трассировку при чужой модели выключайте или перенаправляйте, как показано выше.
Claude Agent SDK: агентный цикл Claude Code как библиотека
Заголовок раздела «Claude Agent SDK: агентный цикл Claude Code как библиотека»Это другой класс инструмента. Вы получаете не каркас для своего цикла, а готового агента Claude Code. Он умеет
читать и править файлы, искать по коду, запускать команды, вызывать субагентов и MCP-серверы. Вы задаёте ему
права, хуки и бюджет. Пакет содержит CLI Claude Code, отдельно его ставить не нужно. Работает с моделями Claude
через API Anthropic или облачных провайдеров (в типах SDK перечислены Bedrock, Vertex AI и Foundry), нужен ключ.
Поэтому код ниже написан по README и examples/ репозитория, а вывода нет. Без ключа мы его не запускали и
выдумывать не будем. Зато свои инструменты и хуки — обычные асинхронные функции, и их мы проверили отдельно.
Два входа: query(prompt=..., options=...) для разовой задачи (асинхронный итератор сообщений) и
ClaudeSDKClient для диалога из нескольких запросов в одной сессии, с прерыванием (interrupt()) и сменой режима
прав на ходу (set_permission_mode()). Сообщения типизированы: AssistantMessage с блоками TextBlock и
ToolUseBlock, ResultMessage с num_turns, total_cost_usd, usage и session_id.
# claude_school.py — нужен ключ ANTHROPIC_API_KEY; CLI Claude Code уже вшит в пакетimport anyiofrom claude_agent_sdk import (AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient, PermissionResultAllow, PermissionResultDeny, ResultMessage, TextBlock, create_sdk_mcp_server, tool)from school_domain import Cohort, enroll_student, search
COHORTS = {"python-2026-11": Cohort("python-2026-11", capacity=20)}STUDENT_ID = "s-002" # из аутентификации, не из текста
def text(s: str, error: bool = False) -> dict: # ответ инструмента в формате MCP return {"content": [{"type": "text", "text": s}], "is_error": error}
@tool("search_courses", "Найти в каталоге школы фрагменты по запросу", {"query": str})async def search_courses(args): return text("\n".join(search(args["query"])))
@tool("enroll", "Записать текущего студента на поток cohort_id", {"cohort_id": str})async def enroll(args): result = enroll_student(COHORTS, args["cohort_id"], STUDENT_ID) return text(result, error=not result.startswith("Готово"))
school = create_sdk_mcp_server(name="school", version="1.0.0", tools=[search_courses, enroll])
async def ask_human(tool_name, tool_input, context): # вызывается, когда правила говорят «спросить» if tool_name == "mcp__school__enroll": answer = await anyio.to_thread.run_sync(input, f"Записать на {tool_input['cohort_id']}? ") if answer.strip().lower() == "да": return PermissionResultAllow() return PermissionResultDeny(message="Человек не подтвердил запись") return PermissionResultDeny(message=f"{tool_name} запрещён политикой")
options = ClaudeAgentOptions( system_prompt="Ты ассистент онлайн-школы. Факты о курсах бери только из search_courses.", tools=[], # встроенные Bash, Read, Write... отключены setting_sources=[], # не подмешивать ~/.claude и CLAUDE.md mcp_servers={"school": school}, # свои инструменты: mcp__school__<имя> allowed_tools=["mcp__school__search_courses"], # чтение — без вопросов can_use_tool=ask_human, # остальное — через человека max_turns=8, max_budget_usd=0.20, # потолок расходов на запрос)
async def main(): async with ClaudeSDKClient(options=options) as client: await client.query("Запишите меня на python-2026-11") async for msg in client.receive_response(): if isinstance(msg, AssistantMessage): print(*(b.text for b in msg.content if isinstance(b, TextBlock))) elif isinstance(msg, ResultMessage): print(msg.subtype, msg.num_turns, msg.total_cost_usd, msg.session_id)
if __name__ == "__main__": anyio.run(main)Что здесь важно.
- Свои инструменты — функции с декоратором
@tool(имя, описание, схема), собранные в MCP-сервер внутри процессаMCP-сервер внутри процессаEN in-process (SDK) MCP serverСвои инструменты-функции Python, оформленные как MCP-сервер, который работает в том же процессе, что и приложение, без отдельного процесса и транспорта. В Claude Agent SDK создаётся через @tool и create_sdk_mcp_server. черезcreate_sdk_mcp_server. Модель видит их под именамиmcp__<сервер>__<инструмент>. Это тот же MCP из урока 8.3.2, только без отдельного процесса. Внешние MCP-серверы ({"type": "stdio", "command": ...}) подключаются рядом. - Три разных списка.
toolsзадаёт набор встроенных инструментов,[]отключает все.allowed_tools— это не список доступных инструментов, а список «разрешено без вопросов», инструменты он не убирает.disallowed_toolsудаляет инструменты из контекста модели. - Права. режим прав агентарежим прав агентаEN permission modeНастройка Claude Agent SDK (permission_mode): что делать с вызовом инструмента, который не разрешён явно — спросить (default), принять правки файлов (acceptEdits), отказать (dontAsk), только планировать (plan) или пропускать всё (bypassPermissions).:
default(спрашивать),acceptEdits,plan,dontAsk(не разрешено явно — отказ),bypassPermissions,auto(решает модель-классификатор). Когда правила говорят «спросить», SDK зовётcan_use_tool. Это место для подтверждения человеком, какinterruptв LangGraph. Нашsearch_coursesвallowed_tools, поэтомуcan_use_toolего не увидит. SDK об этом честно предупредит (CanUseToolShadowedWarning), и здесь это и задумано. - Изоляция настроек. В 0.2.163
setting_sources=Noneзагружает все источники настроек (пользователя, проекта, локальные), как CLI. Правило «разрешить Bash» из~/.claude/settings.jsonразработчика или чужойCLAUDE.mdизменили бы поведение сервиса. Пустой список отключает их. Это ещё и пример дрейфа API: в 0.1.0 по умолчанию не загружалось ничего. - Бюджеты.
max_turnsиmax_budget_usd(при превышении — результатerror_max_budget_usd).
Проверка без ключа — вызываем обработчики инструментов и хук из следующего раздела напрямую:
import asynciofrom claude_ops import block_dangerousfrom claude_school import enroll, search_courses
async def main(): for _ in range(2): # второй раз — повторная запись print((await enroll.handler({"cohort_id": "python-2026-11"}))["content"][0]["text"]) print((await search_courses.handler({"query": "возврат при отмене"}))["content"][0]["text"]) for cmd in ["rm -rf /srv/school", "grep -c ERROR app.log"]: print(cmd, "->", await block_dangerous({"tool_input": {"command": cmd}}, "t1", None))
asyncio.run(main())Готово: s-002 записан на python-2026-11, статус PENDING_PAYMENTОтказ: студент s-002 уже записан на python-2026-11Отмена записи за 7 суток до старта и раньше: оплата возвращается полностью, позже — без возврата.rm -rf /srv/school -> {'hookSpecificOutput': {'hookEventName': 'PreToolUse', 'permissionDecision': 'deny', 'permissionDecisionReason': 'запрещённый фрагмент: rm -rf'}}grep -c ERROR app.log -> {}Когда нужны файлы и терминал: хуки, песочница, субагенты
Заголовок раздела «Когда нужны файлы и терминал: хуки, песочница, субагенты»Ассистенту школы терминал не нужен, и мы его отключили. Сильная сторона SDK — агенты, которым терминал нужен: разобрать логи ассистента, найти причину сбоя, подготовить отчёт. Здесь в ход идут хукихук агентаEN agent hookКоманда, которую среда агента запускает сама на определённом событии: после правки файла, перед действием, перед концом ответа. Работает независимо от памяти модели. В Claude Code хуки задают в .claude/settings.json; код выхода 2 блокирует действие и показывает агенту причину., песочницапесочницаEN sandboxИзолированная среда (контейнер, ВМ), где агент исполняет код и команды, не рискуя основной системой. и субагентысубагентEN subagent:
# claude_ops.py — агент, которому файлы и терминал действительно нужныfrom claude_agent_sdk import AgentDefinition, ClaudeAgentOptions, HookMatcher
BLOCKED = ["rm -rf", "sudo", "curl", "wget", "chmod", "> /"]
async def block_dangerous(input_data, tool_use_id, context): """PreToolUse: видит каждый вызов Bash, даже разрешённый правилами, и может его запретить.""" command = input_data["tool_input"].get("command", "") for pattern in BLOCKED: if pattern in command: return {"hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": f"запрещённый фрагмент: {pattern}"}} return {} # пустой ответ — решают обычные правила
ops_options = ClaudeAgentOptions( cwd="/srv/school/logs", # рабочая папка агента tools=["Read", "Grep", "Glob", "Bash", "Agent"], # Agent — запуск субагентов allowed_tools=["Read", "Grep", "Glob", "Agent", "Bash(grep:*)", "Bash(wc:*)", "Bash(tail:*)"], permission_mode="dontAsk", # что не разрешено явно — отказ без вопросов hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_dangerous])]}, sandbox={"enabled": True}, # Bash — в песочнице ОС (macOS и Linux) setting_sources=[], agents={"log-analyst": AgentDefinition( description="Разбирает логи ассистента и группирует повторяющиеся ошибки", prompt="Ты аналитик логов. Только читаешь, ничего не меняешь.", tools=["Read", "Grep"], model="haiku")}, max_budget_usd=1.0,)Защита здесь в три слоя. Правила Bash(grep:*) разрешают только команды с нужным началом, режим dontAsk
отказывает во всём остальном. Хук PreToolUse видит каждый вызов, даже разрешённый правилами. Хуки — ваш
детерминированный код в цикле агента. Кроме PreToolUse есть PostToolUse (аудит, проверка результата),
UserPromptSubmit, Stop, SubagentStart и другие. Чёрный список строк обходится легко (rm -r -f), поэтому
настоящая граница — песочница ОС и белый список, а хук — дополнительный слой и журнал. Субагент из agents
получает свой промпт, свои инструменты и свою модель. Главный агент вызывает его инструментом Agent, поэтому
тот должен быть в tools.
Сессии в Claude Agent SDK хранит CLI. Продолжить разговор — ClaudeAgentOptions(resume=session_id), пойти по
другой ветке — добавить fork_session=True.
Три подхода рядом
Заголовок раздела «Три подхода рядом»Один и тот же ассистент в трёх вариантах. Строки считали без пустых, комментариев, докстрингов, домена и демо-кода.
| LangGraph | OpenAI Agents SDK | Claude Agent SDK | |
|---|---|---|---|
| Строк оркестрации | ≈ 60 | ≈ 30 (с guardrail) | ≈ 35 (с проверкой прав) |
| Кто выбирает путь | вы: рёбра графа | модель: инструменты и передачи | модель: цикл Claude Code |
| Человек в контуре | interrupt в любом узле, Command(resume) | needs_approval, RunState.approve/reject | can_use_tool, хуки |
| Состояние и пауза | чекпоинт после каждого шага, SQLite и Postgres | сессии, сериализуемый RunState | сессии CLI, resume, fork_session |
| Модели | любые через LangChain, OpenAI-совместимые | OpenAI, OpenAI-совместимые, LiteLLM и Any-LLM (бета) | Claude: API Anthropic, Bedrock, Vertex AI, Foundry |
| Наблюдаемость | стриминг, история шагов, LangSmith по желанию | трассы включены и уходят в OpenAI, свои обработчики | поток сообщений, стоимость в ResultMessage, хуки, OpenTelemetry ([otel]) |
| Тесты без сети | фейковые модели langchain_core | agents.testing.ScriptedModel | только свои функции; цикл — с ключом |
| Зрелость | 1.x, MIT | 0.x, MIT | 0.x, MIT + условия Anthropic |
Как выбирать.
- Процесс известен заранее, нужны паузы на часы, аудит каждого шага, локальная модель — LangGraph. Платите объёмом кода и экосистемой LangChain.
- Несколько специалистов, путь между которыми решает модель, и хочется минимум кода — OpenAI Agents SDK. Следите за версиями 0.x, трассировкой по умолчанию и тем, что встроенные инструменты OpenAI работают только с OpenAI.
- Агенту нужны файлы, терминал, код — ревью, разбор логов, миграции, внутренние инструменты разработчиков — Claude Agent SDK. Платите привязкой к Claude и тем, что цикл — чёрный ящик CLI: тестируются ваши функции и права, а не сам цикл.
- Пять шагов и один инструмент — свой цикл на 100 строк из 8.3.1. Совет Anthropic из «Building effective agents» остаётся в силе: начинайте с простого и понимайте, что фреймворк делает под капотом.
Безопасность: права, песочница, подтверждения
Заголовок раздела «Безопасность: права, песочница, подтверждения»Набор правил общий для всех трёх подходов. Различаются только имена настроек.
- Минимум инструментов. У каждого агента — только нужные:
tools=[]иsetting_sources=[]в Claude Agent SDK, отдельные списки инструментов уcatalogиenrollment. Принцип принцип наименьших привилегийпринцип наименьших привилегийEN principle of least privilege из 8.3.2. - Личность — из приложения.
student_idприходит через контекст (RunContextWrapper,Runtime, замыкание), а не из аргументов, которые заполняет модель. Иначе инъекцияпромпт-инъекцияEN prompt injectionАтака, при которой инструкции злоумышленника попадают в контекст модели (напрямую или через веб-страницу, письмо, файл) и перехватывают управление. в документе запишет другого студента (урок 8.3.4). - Необратимое — через человека, правила — в домене. Подтверждение не заменяет инварианты: агрегат откажет и после «да».
- Пауза — это данные.
RunState, чекпоинт LangGraph, сессия Claude — всё это снимки с аргументами действий. Храните их на сервере. Передresumeпроверяйте, чтоthread_idили сессия принадлежат этому пользователю. - Код модели — только в песочнице. Bash Claude Code — в песочнице ОС, у OpenAI Agents SDK есть отдельные
SandboxAgentс контейнерами, у своего кода — Docker без сети. - Бюджеты —
max_turns,max_budget_usd,recursion_limit. Подробнее о бюджетах на запрос и деградации — в уроке 8.5.4. - Трассы без лишнего. Проверьте, куда уходят трассы и есть ли в них персональные данные
(
trace_include_sensitive_data=False).
Практика
Заголовок раздела «Практика»Соберите oa_school.py рядом с school_domain.py из 8.5.2 и запустите его. Затем выполните упражнения. Решения 1–2
запускались на openai-agents 0.23.1 без сети.
Упражнение 1. Выходной guardrail там, где он сработает. Модель-консультант придумала скидку: «Для вас скидка 50% на Python!» Скидок в каталоге нет. Напишите выходной guardrail, который блокирует ответы со словом «скидка», и повесьте его так, чтобы он сработал после передачи управления.
Решение
from agents import GuardrailFunctionOutput, OutputGuardrailTripwireTriggered, output_guardrail
@output_guardraildef no_discounts(ctx, agent, output) -> GuardrailFunctionOutput: found = "скидк" in str(output).lower() # скидок в каталоге нет — модель их выдумала return GuardrailFunctionOutput(output_info={"discount": found}, tripwire_triggered=found)
triage = build_agents(ScriptedModel([[function_call("transfer_to_catalog", {}, call_id="h1")], [assistant_message("Для вас скидка 50% на Python!")]]))catalog = next(a for a in triage.handoffs if a.name == "catalog")catalog.output_guardrails = [no_discounts] # на агенте, который даёт финальный ответRunner.run(...) бросает OutputGuardrailTripwireTriggered, а e.guardrail_result.output.output_info равно
{'discount': True}. Если повесить guardrail на triage, ответ пройдёт: финальный ответ дал catalog, а выходные
guardrails выполняются только у него. Мы проверили оба варианта.
Упражнение 2. Отказ с объяснением. Оператор отклоняет запись: «нет оплаты». Сделайте так, чтобы запись не создалась, а модель узнала причину и сказала её студенту.
Решение
state = result.to_state()state.reject(result.interruptions[0], rejection_message="Оператор отклонил: нет оплаты.")result = await Runner.run(triage, state, context=ctx, run_config=config)outputs = [i["output"] for i in model.last_call.input if i.get("type") == "function_call_output"]print(result.final_output, ctx.cohorts["python-2026-11"].enrolled)print(outputs[-1])Понял, запись не оформлена: сначала нужна оплата.set()Оператор отклонил: нет оплаты.model.last_call.input показывает, что SDK отправил модели вместо результата enroll — текст отказа. Так в тесте
проверяют не только итог, но и то, что видела модель.
Упражнение 3. Журнал действий в Claude Agent SDK. Добавьте хук, который записывает каждое выполненное
mcp__school__enroll в журнал аудита (время, сессия, аргументы, идентификатор вызова), как журнал доменных событий
в DDD.4.4. Проверьте хук без ключа, вызвав его напрямую.
Решение
from datetime import datetime, timezonefrom claude_agent_sdk import HookMatcher
AUDIT: list[dict] = [] # в продакшене — таблица в базе
async def audit(input_data, tool_use_id, context): """PostToolUse: журнал каждого выполненного действия.""" AUDIT.append({"at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "session": input_data["session_id"], "tool": input_data["tool_name"], "input": input_data["tool_input"], "call": input_data["tool_use_id"]}) return {}
options.hooks = {"PostToolUse": [HookMatcher(matcher="mcp__school__enroll", hooks=[audit])]}Поля session_id, tool_name, tool_input, tool_use_id и tool_response — из типа PostToolUseHookInput SDK.
Идентификатор вызова пригодится как ключ идемпотентности: повтор того же tool_use_id в журнале — сигнал, что
действие уже выполнено.
Проверь себя
Заголовок раздела «Проверь себя»- Второй стиль оркестрации — агенты и передачи управления: вы описываете агентов, путь выбирает модель, границы задают права, guardrails, подтверждения и инварианты домена.
- OpenAI Agents SDK:
Agent,Runner,function_tool, handoffs как инструментыtransfer_to_..., входные и выходные guardrails (у первого и последнего агента),needs_approvalиRunState, сессии, трассировка по умолчанию в OpenAI. Не-OpenAI модели — через Chat Completions. Тесты без сети —ScriptedModel. - Claude Agent SDK: цикл Claude Code с файлами и терминалом.
query/ClaudeSDKClient, свои инструменты через MCP-сервер внутри процесса,toolsиallowed_tools, режимы прав,can_use_tool, хуки, субагенты,max_budget_usd,setting_sources=[]для изоляции. Только модели Claude. - LangGraph даёт контроль и долгие паузы ценой кода, OpenAI Agents SDK — краткость ценой контроля и версий 0.x, Claude Agent SDK — готового агента для работы с кодом ценой привязки к провайдеру.
Дальше: урок 8.5.4 — мультиагентные системы в продакшене: шаблоны, долговечное выполнение, идемпотентность и ретраи, бюджеты, трассы по стандарту OpenTelemetry и оценка траекторий агентов.