Урок 0005 был про долгоживущих кодинг-агентов (git, features.json). Этот — про то, как устроен сервис НЕ-кодинг агента (каре-бот, обработка документов): контракт 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 для логов
}
Правила контракта:
session_id, сервис не «вспоминает» его по тексту.Главная ошибка новичка — держать состояние в контексте 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.
Агент — это не «фреймворк, который сам всё делает». Это цикл «позвать модель → если запросила тул → выполнить → вернуть результат → снова модель».
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.
Внутренний 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-тулов:
idempotency_key, сервис не выполняет дважды. Повтор сети ≠ двойное списание.awaiting_approval, ждём человека, потом выполняем.Источники: OpenAI Practical Guide, Anthropic Writing tools.
Не всё умещается в синхронный 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 — что уже есть в банке, то и берём.
Без логов 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.
Где живёт состояние диалога?
Зачем MAX_TURNS в цикле?
Идемпотентность action-тула нужна, чтобы?