Туториал: от простого агента до мультиагентной системы

Практический туториал 2026 на Python: workflow с tools через Responses API, conversation state, evaluator step и multi-agent orchestration через OpenAI Agents SDK.

В этом туториале мы пройдём путь от простой tool-enabled системы до управляемой multi-agent orchestration. Главное отличие от старых туториалов: мы не будем начинать с текстового “ReAct-театра” и вручную парсить Thought/Action. В 2026 разумнее строить agent stack на актуальных primitives:

  • Responses API;
  • strict function tools;
  • conversation state;
  • OpenAI Agents SDK для handoffs и orchestration;
  • policy checks и tracing как часть архитектуры, а не как постфактум.
Идея туториала простая: не прыгать сразу к “умному автономному агенту”, а наращивать систему ступенями. Сначала делаем полезный workflow с tools. Потом добавляем state между шагами. Потом quality gate. И только после этого делаем multi-agent routing.
Не стройте “полноценного автономного агента” с первого шага. Для большинства задач рабочий baseline выглядит как workflow + tools + evaluator. Это легче тестировать, дешевле запускать и проще довести до production.

Что мы построим

Туториал состоит из четырёх шагов:

  1. Tool-enabled workflow на Responses API
  2. Conversation state и многошаговый run
  3. Evaluator gate для качества и safety
  4. Multi-agent orchestration через OpenAI Agents SDK
ПромптSupport workflow
Найди статус тикета INC-4421, проверь policy на раскрытие данных и дай короткий ответ клиенту.
Ответ модели

Система сначала вызывает tool для статуса тикета, затем policy-check шаг, и только потом формирует финальный ответ. Это уже полезный агентный workflow, даже без полной автономии.

Старый туториал
Пишем текстовый ReAct-loop, вручную парсим Thought/Action и притворяемся, что это production-ready agent.
Практичный туториал 2026
Используем structured tools, server-managed conversation state, evaluator step и только затем handoffs и specialist agents.

Шаг 1. Tool-enabled workflow на Responses API

Начинаем не с “полноценного автономного агента”, а с полезного workflow: модель получает задачу, при необходимости вызывает tool и возвращает ответ.

Почему это хороший старт:

  • меньше хрупкости;
  • нет ручного парсинга текстовых действий;
  • tools задаются schema-driven;
  • проще логировать и тестировать.

Что сделаем

  • один tool get_ticket_status;
  • один tool search_policy;
  • один вызов модели;
  • обработка tool calls в коде.
from openai import OpenAI
import json

client = OpenAI()

TOOLS = [
    {
        "type": "function",
        "name": "get_ticket_status",
        "description": "Вернуть текущий статус тикета поддержки.",
        "parameters": {
            "type": "object",
            "properties": {
                "ticket_id": {"type": "string"}
            },
            "required": ["ticket_id"],
            "additionalProperties": False,
        },
        "strict": True,
    },
    {
        "type": "function",
        "name": "search_policy",
        "description": "Найти внутреннюю policy по указанной теме.",
        "parameters": {
            "type": "object",
            "properties": {
                "topic": {"type": "string"}
            },
            "required": ["topic"],
            "additionalProperties": False,
        },
        "strict": True,
    },
]


def get_ticket_status(ticket_id: str) -> str:
    return f"{ticket_id}: waiting for security review"


def search_policy(topic: str) -> str:
    if "ticket" in topic.lower():
        return "Можно сообщать клиенту статус, но не внутренние комментарии и персональные данные сотрудников."
    return "Policy not found."


TOOL_IMPL = {
    "get_ticket_status": get_ticket_status,
    "search_policy": search_policy,
}


def run_once(user_input: str) -> str:
    response = client.responses.create(
        model="gpt-5.4",
        input=user_input,
        tools=TOOLS,
    )

    outputs = []
    for item in response.output:
        if item.type == "function_call":
            tool_fn = TOOL_IMPL[item.name]
            args = json.loads(item.arguments)
            result = tool_fn(**args)
            outputs.append({
                "type": "function_call_output",
                "call_id": item.call_id,
                "output": result,
            })

    if not outputs:
        return response.output_text

    followup = client.responses.create(
        model="gpt-5.4",
        previous_response_id=response.id,
        input=outputs,
    )
    return followup.output_text


print(run_once("Проверь статус тикета INC-4421 и кратко ответь клиенту."))

Что получили

Уже на этом шаге у нас не “чат-бот”, а управляемый tool-enabled workflow. Для многих внутренних support и ops задач этого уже достаточно.

Шаг 2. Conversation state и многошаговая continuity

Следующий шаг — не memory в широком смысле, а thread continuity. Система должна уметь продолжать работу между turns, не таща весь history вручную.

Самый простой вариант в OpenAI stack:

  • previous_response_id для продолжения цепочки;
  • или conversation_id / sessions, если нужен более явный managed state.
from openai import OpenAI

client = OpenAI()

first = client.responses.create(
    model="gpt-5.4",
    input="Пользователь работает над запуском продукта Альфа. Запомни это в рамках текущего потока.",
)

second = client.responses.create(
    model="gpt-5.4",
    previous_response_id=first.id,
    input="Подготовь короткий план задач на неделю.",
)

print(second.output_text)

Это ещё не long-term memory, но уже нормальная основа для multi-turn workflow и agent runs.

Conversation state не равен настоящей памяти агента. Это continuity текущего потока, а не durable memory между независимыми сессиями.

Шаг 3. Evaluator gate

Теперь добавим production-friendly quality loop. Вместо расплывчатой “reflection” сделаем явный evaluator step:

  • generator пишет ответ;
  • evaluator проверяет ответ по rubric;
  • при необходимости generator исправляет результат.
def generate_answer(task: str) -> str:
    response = client.responses.create(
        model="gpt-5.4",
        input=f"Реши задачу:\n{task}",
    )
    return response.output_text


def evaluate_answer(task: str, answer: str) -> dict:
    response = client.responses.create(
        model="gpt-5.4-mini",
        input=(
            "Проверь ответ по трём критериям: factuality, policy compliance, completeness.\n"
            f"Задача:\n{task}\n\nОтвет:\n{answer}\n\n"
            "Верни JSON вида {pass: bool, feedback: string}."
        ),
        text={"format": {"type": "json_object"}},
    )
    return json.loads(response.output_text)


def generate_with_gate(task: str, max_rounds: int = 2) -> str:
    answer = generate_answer(task)
    for _ in range(max_rounds):
        verdict = evaluate_answer(task, answer)
        if verdict["pass"]:
            return answer
        answer = generate_answer(
            task + "\n\nИсправь предыдущий ответ с учётом feedback:\n" + verdict["feedback"]
        )
    return answer

Это уже похоже на production pipeline:

  • понятные критерии;
  • лимит по итерациям;
  • возможность логировать verdict и feedback.

Шаг 4. Multi-agent orchestration через OpenAI Agents SDK

Теперь, когда есть рабочий baseline, можно идти в multi-agent orchestration.

В current OpenAI Agents SDK это удобно делать через:

  • Agent;
  • Runner;
  • function_tool;
  • handoffs.

Пример triage + specialists:

from agents import Agent, Runner, function_tool


@function_tool
def get_ticket_status(ticket_id: str) -> str:
    return f"{ticket_id}: waiting for security review"


billing_agent = Agent(
    name="Billing specialist",
    handoff_description="Отвечает на вопросы об оплате, счетах и возвратах.",
    instructions="Ты специалист по billing support.",
)


technical_agent = Agent(
    name="Technical specialist",
    handoff_description="Отвечает на технические вопросы по продукту и тикетам.",
    instructions="Ты технический специалист поддержки.",
    tools=[get_ticket_status],
)


triage_agent = Agent(
    name="Triage agent",
    instructions="Маршрутизируй пользователя к правильному специалисту.",
    handoffs=[billing_agent, technical_agent],
)


result = Runner.run_sync(
    triage_agent,
    "Проверь статус тикета INC-4421 и объясни клиенту, что происходит."
)

print(result.final_output)
print(result.last_agent.name)

Когда handoff действительно нужен

Handoff полезен, когда specialist должен стать владельцем следующей части разговора. Если specialist решает только bounded subtask, часто лучше manager pattern или agent-as-tool.

Как это сопоставить с LangGraph

Если вам нужен не provider-native runtime, а более явный graph control, тот же tutorial path можно собрать и через LangGraph:

  • node для routing;
  • node для tool execution;
  • node для evaluator;
  • edge на retry или completion.

То есть выбор уже не между “агент” и “не агент”, а между:

  • provider-native agent runtime;
  • graph orchestration runtime;
  • plain code workflow.

Что считать результатом туториала

После этих четырёх шагов у вас есть:

  • полезный tool-enabled workflow;
  • continuity между turns;
  • evaluator gate;
  • multi-agent routing.

И только после этого имеет смысл думать о более тяжёлых вещах:

  • long-term memory;
  • computer use;
  • realtime voice agents;
  • background workers;
  • human approval layers.

Плюсы

  • Путь идёт от простого к сложному и не заставляет прыгать сразу в autonomous loop
  • Responses API и strict tools дают надёжнее baseline, чем текстовый ReAct-парсинг
  • Evaluator gate и handoffs ближе к production-практике 2026
  • Туториал легко расширяется до tracing, guardrails и memory

Минусы

  • Это уже не '50 строк магии', а более инженерный подход
  • Provider-native tutorial снижает portability
  • Для реального продакшена всё равно понадобятся observability и evals
  • Handoffs и multi-agent orchestration быстро усложняют debugging

Что добавить следующим

После этого tutorial path логично добавлять в такой последовательности:

  1. tracing;
  2. guardrails / policy checks;
  3. long-term memory;
  4. human approval;
  5. background jobs и queueing;
  6. multi-provider abstraction, если она реально нужна.

Minimal project structure

agent_app/
  app.py
  tools.py
  workflows.py
  evaluators.py
  agents_runtime.py
  traces/

Держите tools, evaluators и orchestration раздельно. Это сильно упрощает testing и замены framework/runtime later.

ПромптTutorial reviewer
Мы сделали workflow с tools. Когда переходить к handoffs?
Ответ модели

Только когда specialist действительно должен взять на себя следующую часть диалога или когда prompts становятся слишком широкими для одного агента. Если specialist лишь помогает с bounded subtask, handoff может быть лишним.

Проверьте себя

Проверьте себя

1. Почему в 2026 плохая идея начинать tutorial с текстового ReAct-парсинга?

2. Что добавляет evaluator gate?

3. Когда handoff обычно оправдан?