Урок 0007 · Структура сервиса агента: API, состояние, тулы

Урок 0005 был про долгоживущих кодинг-агентов (git, features.json). Этот — про то, как устроен сервис НЕ-кодинг агента (каре-бот, обработка документов): контракт API, состояние, цикл, тулы, очереди.

Цель урока Нарисовать на доске контур сервиса агента: один вход, состояние в сторe (не в модели), явный цикл, тулы как слой, очереди для долгих задач.

1. Один вход: API-контракт

У сервиса агента — один публичный вход, а не «эндпоинт на каждый скилл».

POST /v1/agent/run
Authorization: Bearer <service-token>   ← auth в заголовке, НЕ в теле и НЕ в промпте

{
  "session_id": "sess_9f2c…",          ← ключ состояния (см. раздел 2)
  "message": "какая комиссия за перевод?",
  "context": {                           ← не PII! только что нужно для маршрутизации
    "user_segment": "premium",
    "locale": "es-MX"
  }
}

→ 200 {
  "reply": "SPEI — 2%, источник [1]",
  "cited_chunks": ["chunk_0041"],
  "tool_events": ["get_balance:ok"],
  "trace_id": "trc_7a11…"               ← сквозной id для логов
}

Правила контракта:

2. Состояние: сессия в сторе, а не в контексте модели

Главная ошибка новичка — держать состояние в контексте LLM: «модель и так помнит диалог».

Модель помнит ровно до конца контекстного окна, и только пока жив процесс. Упал воркер — умер диалог.

Правильно: stateless-воркеры + состояние в Redis/БД по ключу session_id.

session_id → Redis hash:
  messages:      [последние N сообщений]
  status:        running | awaiting_input | awaiting_approval | done
  tool_state:    {"approval_requested": "block_card"}   ← для HITL
  created_at, updated_at

Что это даёт:

В SDK это уже есть: OpenAI Agents SDK называет это sessions — персистентный слой контекста между запусками агента.

Источник: OpenAI Agents SDK.

3. Цикл агента — явный, в коде, с лимитами

Агент — это не «фреймворк, который сам всё делает». Это цикл «позвать модель → если запросила тул → выполнить → вернуть результат → снова модель».

messages = [system, user_message]

for turn in range(MAX_TURNS):            # лимит! 8–15 ходов
    if guardrail_input(messages) is BLOCK:
        return reply_guard_blocked()

    response = llm(messages, tools=tool_schemas)

    if response.tool_calls:
        for call in response.tool_calls:
            result = execute_tool(call)      # каждый тул — свой timeout
            messages.append(tool_result(call, result))
        continue

    if guardrail_output(response) is BLOCK:
        return reply_guard_blocked()

    return response.text                   # нет тул-коллов → это финальный ответ

Ключевые решения:

Источники: Anthropic Building Effective Agents, OpenAI Agents SDK.

4. Тулы как слой сервиса

Внутренний API банка ≠ схема тула для модели. Между ними — тонкий адаптер.

Внутренний API: POST /internal/cards/block с внутренними форматами.

Тул для модели: имя + описание + JSON-schema + бизнес-правила.

{
  "name": "cards_block",
  "description": "Блокирует карту клиента. Звать ТОЛЬКО если клиент авторизован
                  и подтвердил потерю. НЕ звать для проверки статуса.",
  "input_schema": {
    "type": "object",
    "required": ["card_id", "reason"],
    "properties": {
      "card_id":  {"type": "string", "pattern": "^card_[0-9]+$"},
      "reason":   {"enum": ["lost", "stolen", "fraud"]}
    }
  }
}

Три типа тулов (OpenAI):

Правила для action-тулов:

Источники: OpenAI Practical Guide, Anthropic Writing tools.

5. Очереди для долгих задач

Не всё умещается в синхронный HTTP-запрос. OCR пачки документов, research, обработка треда из 40 писем — секунды и минуты.

Паттерн: принял → 202 + job_id → очередь → воркеры → статус по GET.

POST /v1/jobs           → 202 {"job_id": "job_12ab"}
GET  /v1/jobs/job_12ab  → 200 {"status": "running", "progress": 0.4}
                        → 200 {"status": "done", "result": {...}}

Почему очередь, а не «долгий запрос»:

Инфраструктура: Redis Streams, RabbitMQ, SQS — что уже есть в банке, то и берём.

6. Наблюдаемость: trace_id и версии

Без логов eval мёртв: невозможно сказать «retrieval виноват или генерация».

Минимальный набор полей на каждый запрос:

trace_id, session_id,
prompt_version, config_version,      ← чтобы откатывать
model, temperature,
tokens_in, tokens_out, cost, latency,
tool_calls: [{name, args_hash, status, latency}],
chunk_ids, guard_decision, handoff

Почему config_version — это важно: промпт, схема тулов, индекс и модель меняются вместе. Одна версия = связный лог и возможность откатить «вчерашний ответ».

Источники: Jason Liu, Anthropic Demystifying evals.

7. Деплой и безопасность

Проверь себя

Где живёт состояние диалога?

Зачем MAX_TURNS в цикле?

Идемпотентность action-тула нужна, чтобы?

Видео и статьи по теме

Видео (в порядке смотрения)

Статьи и доки

Первоисточник урока OpenAI — A Practical Guide to Building Agents — модель, тулы, инструкции, guardrails, HITL. Протоколы и контракты: MCP.