STACHKA
Ограничения AI-агентов и мультиагентных систем
Ожидания и инженерная реальность
Уроки пилота: пять агентов в Сбере
Суриков Антон · Сбер
ИТОГ · О ЧЁМ ДОКЛАД
LLM-агент — ненадёжный компонент
02
STACHKA · Суриков Антон · Сбер
Доклад про уроки проектирования, а не про готовый продукт
Надёжность дают не промпты, а обычная инженерия на Python.
Метки на слайдах: ПРОТОТИП — работает · ТЗ — целевое требование · ИНЦИДЕНТ — реально ломалось. • контракт задачи · состояние в БД · идемпотентность • права вне текста · сквозные тесты
КОНТЕКСТ · СБЕР
Почему здесь вообще нужны несколько агентов
03
STACHKA · Суриков Антон · Сбер
Повод для сети агентов — не число систем, а разные владельцы и права
Владельцы
за данные и решения отвечают разные команды
Системы
HR, Finance, Docs, почта, календарь
Идентичность
одной техучётки уже мало
Время
задача живёт дольше сессии
ПРОТОТИП · УСТРОЙСТВО
Пять агентов: один руководитель и четыре блока
04
STACHKA · Суриков Антон · Сбер
Руководитель ставит поручение, блоки 1–4 держат свои данные. Шлюз A2A и хранилище взаимодействий отвечают за доставку, состояния, защиту от дублей и аудит. • ✓ Каждое сообщение идёт через шлюз • ✓ Права у каждого агента свои • ✓ Шлюз не читает текст поручения
ТЗ · ЦЕЛЕВАЯ АРХИТЕКТУРА
Три слоя и кто за что отвечает
05
STACHKA · Суриков Антон · Сбер
Свой формат обмена в пилоте. Совместимость со стандартом A2A — следующий шаг. • A2A — идентичность агента · поручения · статусы · результаты • CONTROL PLANE — права · состояния · сроки · повторы · согласования · аудит • MCP — готовые бизнес-запросы · API · действия в системах
A2A
CONTROL PLANE
MCP
ТЗ · СКВОЗНОЙ СЦЕНАРИЙ
«Соберите статусы четырёх блоков к 18:00»
06
STACHKA · Суриков Антон · Сбер
Руководитель — инициатор. Отвечают четыре блока, каждый за свои данные
• Блоки отвечают параллельно: status · blockers · date · help • Правило завершения ALL_OR_DEADLINE — ждём все четыре или до дедлайна • Сводка со ссылками на источники, пропуски и противоречия отмечены
РУКОВОДИТЕЛЬ
БЛОКИ 1-4
ДЕДЛАЙН 18:00
СВОДКА
ТЗ · КОНТРАКТ
До отправки задаём цель, формат, сроки и лимиты
07
STACHKA · Суриков Антон · Сбер
kind: TASK goal: статусы 4 блоков к 18:00 expected_result: StatusReport@1 deadline: "18:00" completion: ALL_OR_DEADLINE autonomy_mode: PREPARE_DRAFT budget: {llm_calls: 20, tokens: 200_000} max_depth: 1 idempotency_key: status-report:v1
INFORMATION
только уведомление
REQUEST
нужен ответ
TASK
статус и критерий готовности
ТЗ · PYTHON · КОД 1
Pydantic ловит форму и ссылки, но не смысл
08
STACHKA · Суриков Антон · Сбер
Валидный объект ≠ выполненное поручение. Смысл проверяем отдельно
class StatusReport(BaseModel): model_config = ConfigDict(strict=True, extra="forbid") status: Literal["on_track", "at_risk", "blocked"] blockers: list[str] sources: list[str] = Field(min_length=1) @field_validator("sources") @classmethod def known(cls, v, info: ValidationInfo): missing = set(v) - info.context["known_ids"] if missing: raise ValueError(f"нет источников: {missing}") return v
Типы
Literal, strict, extra=forbid
Источники
каждый id есть в known_ids
Смысл
цель достигнута? схема не знает
ТЗ · ПОНИМАНИЕ
Получатель сначала подтверждает, что понял задачу
09
STACHKA · Суриков Антон · Сбер
Непонятную задачу уточняем до выполнения, а не после
• understood_goal — «статус блока 3 на 18:00» • expected_output — StatusReport@1 • missing_information — «дедлайн по задаче 3.2» • clarification_required — true
Контракт
ОТВЕТ ПОЛУЧАТЕЛЯ
РАБОТА
До работы
совпали цель и ограничения?
После работы
заполнены поля результата?
Источники
есть ссылки на данные?
ТЗ · КООРДИНАЦИЯ
Сроки и повторы контролирует код, а не модель
10
STACHKA · Суриков Антон · Сбер
Модель пишет текст. Когда задача закончена, решает код
LLM
• понимает запрос • пишет сводку по готовым ответам
КОД
• считает ответы и сроки • делает повторы • правило завершения ALL_OR_DEADLINE • пишет состояние в БД QUORUM(N) — другое правило: «хватит N ответов из M». В сценарии со статусами не используется.
ТЗ · PYTHON · КОД 2
asyncio ждёт ответы, код решает, что дальше
11
STACHKA · Суриков Антон · Сбер
Задача агента должна пережить рестарт — состояние живёт в БД
async def collect(task: Task) -> Summary: pending = {asyncio.create_task(ask(b, task)) for b in task.blocks} replies = [] while pending and (left := task.deadline - now()) > 0: done, pending = await asyncio.wait( pending, timeout=left, return_when=asyncio.FIRST_COMPLETED) for t in done: # ответ или ошибка — сразу в БД replies.append(await store.save_reply(task.id, t)) for t in pending: t.cancel() # сами не отменятся await store.mark(task.id, "COLLECTED", missing=len(pending)) return await summarize(replies) # LLM — только здесь
Не TaskGroup
отменяет всех при первой ошибке, а нужны частичные ответы
После рестарта
asyncio.Task умер вместе с процессом, Recovery scan поднимает COLLECTING из БД
ТЗ · ДОСТАВКА
Когда можно подтверждать обработку
12
STACHKA · Суриков Антон · Сбер
At-least-once: дубли будут, порядок не гарантирован. ACK — про сообщение, а завершение задачи — отдельный статус
• OUTBOX — состояние и событие в одной транзакции • INBOX — UNIQUE(idempotency_key), дубль вернёт прежний результат • ПОВТОРЫ — 0/5/30/120 с + jitter → DLQ
В ОЧЕРЕДИ
ДОСТАВЛЕНО
ОБРАБОТКА
РЕЗУЛЬТАТ СОХРАНЁН
ACK
ТЗ · PYTHON · КОД 3
Тест: падение в любой точке даёт один эффект
13
STACHKA · Суриков Антон · Сбер
Идемпотентность доказывает тест с падением в каждой точке
POINTS = ["before_effect", "after_effect", "after_commit"] @pytest.mark.parametrize("crash_at", POINTS) async def test_crash_then_redelivery(system, crash_at): m = msg(task_id="task-42", key="status:42:v1") system.inject_crash(crash_at) with pytest.raises(Crash): await system.deliver(m) # падение в точке await system.restart() await system.deliver(m) # повторная доставка assert system.effects("status:42:v1") == 1 async def test_parallel_duplicates(system): a, b = await asyncio.gather(deliver(m), deliver(m)) assert a.result_id == b.result_id
Три точки
эффект → commit → ACK; самая опасная — после эффекта до commit
Дальше
тот же ключ с другим payload, таймаут с неизвестным исходом, DLQ
ПРОТОТИП · ЗАМЕРЫ
Сколько стоил один неудачный цикл
14
STACHKA · Суриков Антон · Сбер
Задаче нужны лимиты: вызовы, токены, время, повторы, глубина делегирования
Лимит 65 536 не придуман: сервер запущен с контекстом 131 072 и --parallel 2, то есть по 65 536 на слот.
МетрикаЗначение
Запрос на ревью106 081 токенов
Лимит65 536
Не помещается+40 545
Вход за неудачный цикл46 583
Ответ локальной модели3–5 минут
ТЗ · ОТКРЫТО · ПРАВА
От чьего имени действует агент
15
STACHKA · Суриков Антон · Сбер
Это главная открытая проблема пилота
СОТРУДНИК
СЕССИЯ
АГЕНТ
POLICY
РЕСУРС
Token Exchange / OBO
как передать контекст сотрудника агенту
Сертификаты и CN
кто подтверждает связь агента с сотрудником
90-дневные токены
хранение, ротация, перевыпуск Направление: RFC 8693 — короткий токен, где sub = сотрудник, act = агент, scope сужен.
ПРОТОТИП · БЕЗОПАСНОСТЬ
Шлюз передаёт сообщение, но не читает его
16
STACHKA · Суриков Антон · Сбер
Согласие действует только для той версии, которую видел человек
Открыто: кто раздаёт ключи? Каталог ключей должен подписывать не шлюз. • X25519 — общий секрет · HKDF-SHA256 — ключ из секрета • ChaCha20-Poly1305 — шифр + целостность · Ed25519 — подпись отправителя
ОТПРАВИТЕЛЬ
GATEWAY
ПОЛУЧАТЕЛЬ
ИНЦИДЕНТ · СБОИ ПИЛОТА
Что ломалось — по слоям карты
17
STACHKA · Суриков Антон · Сбер
Инциденты — доказательства того, что обвязка нужна, а не просьба «будьте аккуратнее»
СлойИнцидент
Контракт106 081 > 65 536, запрос не поместился
ИнструментыHTTP 400, XML вместо вызова
Ошибки413 / DNS названы нарушением safety
ДоставкаACK и конфликт 409
Инфраструктураобщая квота, обе модели легли вместе
Релиз6 из 9 тестов падали
ИТОГ · ЧТО УНЕСТИ
Три вывода + открытые вопросы
18
STACHKA · Суриков Антон · Сбер
LLM-агент — ненадёжный компонент. Надёжность даёт инженерия вокруг него.
Открыто: контекст сотрудника и OBO · надёжность шлюза (SQLite на одном узле) · данные для проверки смысла · замер успешного прогона. 1. Состояние задачи — в БД, а не в asyncio.Task 2. Идемпотентность доказывает тест с падением в каждой точке 3. Права, сроки и лимиты проверяет код, а не промпт