Перейти к содержимому
Ступень 88.5 Оркестрация агентов: фреймворки и продакшен

Agent SDK: OpenAI Agents SDK и Claude Agent SDK

Второй стиль оркестрации — агенты и передачи управления вместо явного графа. Ассистент онлайн-школы на OpenAI Agents SDK (запущен без сети на ScriptedModel) и на Claude Agent SDK, сравнение с LangGraph по коду, контролю, привязке к провайдеру, наблюдаемости и тестируемости, безопасность инструментов.

УРОКstage-8/8-5-orchestration/03-agent-sdks.mdx3 чУрок 3 из 5
После урока вы сможете
  • Собрать на OpenAI Agents SDK агентов с инструментами, передачами управления, guardrails, сессией и подтверждением действия человеком и протестировать их без сети
  • Настроить Claude Agent SDK — встроенные и свои инструменты, права, хуки, субагенты, бюджет — и объяснить, когда нужен агент с файлами и терминалом
  • Подключить к SDK локальную модель через OpenAI-совместимый endpoint и выключить или перенаправить трассировку
  • Сравнить LangGraph и два SDK по объёму кода, контролю потока, привязке к провайдеру, наблюдаемости и тестируемости и выбрать под задачу

В уроке 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 по-русски.

Весь сценарий ниже запускается без сети и ключа. В SDK есть модуль agents.testing с фейковой моделью со сценариемфейковая модель со сценариемEN scripted (fake) modelТестовая замена LLM, которая возвращает заранее записанные ответы и вызовы инструментов по порядку. Делает тесты агента детерминированными, быстрыми и бесплатными; проверяет код вокруг модели, но не качество самой модели. ScriptedModel. Между её «ответами» работает настоящий конвейер SDK: схемы инструментов, передачи управления, guardrails, подтверждения, сессия.

oa_school.py
import asyncio
import re
from 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_call
from school_domain import Cohort, enroll_student, search
@dataclass
class SchoolContext: # контекст запуска: виден инструментам, но не модели
student_id: str
cohorts: dict
@function_tool
def 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.

Трассировка в SDK включена по умолчанию и отправляет трассы на серверы OpenAI. Без ключа OpenAI это ошибки 401, с ключом — утечка ваших диалогов туда, куда вы, возможно, не собирались. Выключить можно тремя способами: OPENAI_AGENTS_DISABLE_TRACING=1, set_tracing_disabled(True) или RunConfig(tracing_disabled=True) для одного запуска. Чтобы трассы не терялись, а шли в свою систему, замените обработчики:

import asyncio
from agents import Runner, TracingProcessor, set_trace_processors
from agents.testing import ScriptedModel, assistant_message, function_call
from 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).

По умолчанию 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 anyio
from 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 asyncio
from claude_ops import block_dangerous
from 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.

Один и тот же ассистент в трёх вариантах. Строки считали без пустых, комментариев, докстрингов, домена и демо-кода.

LangGraphOpenAI Agents SDKClaude Agent SDK
Строк оркестрации≈ 60≈ 30 (с guardrail)≈ 35 (с проверкой прав)
Кто выбирает путьвы: рёбра графамодель: инструменты и передачимодель: цикл Claude Code
Человек в контуреinterrupt в любом узле, Command(resume)needs_approval, RunState.approve/rejectcan_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_coreagents.testing.ScriptedModelтолько свои функции; цикл — с ключом
Зрелость1.x, MIT0.x, MIT0.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» остаётся в силе: начинайте с простого и понимайте, что фреймворк делает под капотом.

Безопасность: права, песочница, подтверждения

Заголовок раздела «Безопасность: права, песочница, подтверждения»

Набор правил общий для всех трёх подходов. Различаются только имена настроек.

  1. Минимум инструментов. У каждого агента — только нужные: tools=[] и setting_sources=[] в Claude Agent SDK, отдельные списки инструментов у catalog и enrollment. Принцип принцип наименьших привилегийпринцип наименьших привилегийEN principle of least privilege из 8.3.2.
  2. Личность — из приложения. student_id приходит через контекст (RunContextWrapper, Runtime, замыкание), а не из аргументов, которые заполняет модель. Иначе инъекцияпромпт-инъекцияEN prompt injectionАтака, при которой инструкции злоумышленника попадают в контекст модели (напрямую или через веб-страницу, письмо, файл) и перехватывают управление. в документе запишет другого студента (урок 8.3.4).
  3. Необратимое — через человека, правила — в домене. Подтверждение не заменяет инварианты: агрегат откажет и после «да».
  4. Пауза — это данные. RunState, чекпоинт LangGraph, сессия Claude — всё это снимки с аргументами действий. Храните их на сервере. Перед resume проверяйте, что thread_id или сессия принадлежат этому пользователю.
  5. Код модели — только в песочнице. Bash Claude Code — в песочнице ОС, у OpenAI Agents SDK есть отдельные SandboxAgent с контейнерами, у своего кода — Docker без сети.
  6. Бюджеты — max_turns, max_budget_usd, recursion_limit. Подробнее о бюджетах на запрос и деградации — в уроке 8.5.4.
  7. Трассы без лишнего. Проверьте, куда уходят трассы и есть ли в них персональные данные (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_guardrail
def 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, timezone
from 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 в журнале — сигнал, что действие уже выполнено.

Проверь себя6 вопросов
1 Выходной guardrail «нет выдуманных скидок» повешен на агента triage. Студент спросил о скидках, triage передал разговор catalog, тот ответил «скидка 50%». Что произойдёт?
2 Что верно о трассировке OpenAI Agents SDK 0.23 (выберите все верные)?Можно выбрать несколько ответов
3 В Claude Agent SDK агенту нужно убрать Bash из доступных инструментов. Что из этого сработает (выберите все верные)?Можно выбрать несколько ответов
4 Под каким именем модель увидит инструмент enroll с MCP-сервера, подключённого как mcp_servers={"school": ...}?
5 Агента в OpenAI Agents SDK назвали «Запись». Что станет с инструментом передачи управления?
6 Нужен сервис, который по ночам разбирает репозиторий школы, запускает тесты и готовит отчёт об упавших. Модели — Claude. Что естественнее всего взять?
  • Второй стиль оркестрации — агенты и передачи управления: вы описываете агентов, путь выбирает модель, границы задают права, 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 и оценка траекторий агентов.

Закончили урок?Урок пройден

Отметьте его — и 5 карточек из урока попадут в колоду повторения. · 5 карточек — в колоде повторения

Первоисточники 6

Лицензия текста урока: CC-BY-SA-4.0 · обновлено 2026-10-06