Треугольник: скорость, цена, качество

Первое, что нужно принять: универсальной «оптимизации» не существует. Три величины связаны, и улучшение одной обычно ухудшает другую.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
Треугольник компромиссов + где уходит время Качество Скорость Дёшево выбирай осознанно сильная модель + reflection = умно, но медленно и дорого · слабая = наоборот Из чего складывается ответ агента LLM-вызовы (обычно 80–95% времени) инструменты RAG свой код ← оптимизировать свой код почти бесполезно Внутри LLM-вызова: prefill (вход) ↓ лечится prompt caching генерация (выход, токен за токеном) ↓ короче ответ ГЛАВНЫЙ РЫЧАГ: число ПОСЛЕДОВАТЕЛЬНЫХ LLM-вызовов 5 шагов × 3с = 15с · убрать лишний шаг или распараллелить = минус секунды выходные токены дороже входных и генерируются последовательно → краткость = скорость + деньги

Ключевой вывод из анатомии: у агента почти всё время съедают LLM-вызовы, а внутри вызова — генерация выходных токенов (она идёт последовательно, токен за токеном). Значит, оптимизировать свой Python-код бессмысленно, а по-настоящему работают три рычага: меньше последовательных вызовов, короче выход, дешевле/быстрее модель там, где можно.

⚠️ Сначала измерь — потом оптимизируй

Оптимизация «на глаз» почти всегда бьёт мимо: разработчик ускоряет свой код, который занимает 2% времени, пока 90% уходит в лишний reasoning-шаг. У тебя уже есть инструменты из раздела Observability: открой трейс и посмотри, сколько заняли LLM-вызовы, сколько инструменты, сколько шагов сделал агент, и p95 из дашборда. Оптимизируй самый жирный кусок, а не тот, который проще починить.

Ускорение: меньше шагов и короче ответ

Раз время = (число последовательных вызовов) × (время вызова), атакуем оба множителя.

  • Параллелизация независимых шагов. Если агент делает три поиска, не зависящих друг от друга, — запусти их одновременно (asyncio.gather), а не по очереди. Это паттерн parallelization из модуля 05.
  • Убрать лишние шаги. Каждый дополнительный «подумай ещё раз» стоит секунд. Reflection-цикл полезен, но не для каждого запроса — включай его только там, где он реально повышает качество.
  • Короче выход. Выходные токены генерируются последовательно и стоят дороже входных: попроси модель отвечать без воды, ограничь max_tokens.
  • Меньше вход. Prefill тоже время — это прямая выгода от context engineering и prompt caching из прошлых уроков.
  • Стриминг. Не сокращает общее время, но радикально улучшает воспринимаемую скорость (TTFT) — самый дешёвый «прирост» из всех.
Параллелизация независимых вызовов
python
import asyncio

# ❌ последовательно: 1.5 + 1.2 + 1.8 = 4.5с
async def gather_slow(topic):
    docs    = await search_docs(topic)
    prices  = await get_prices(topic)
    reviews = await get_reviews(topic)
    return docs, prices, reviews

# ✅ параллельно: max(1.5, 1.2, 1.8) = 1.8с — втрое быстрее
async def gather_fast(topic):
    docs, prices, reviews = await asyncio.gather(
        search_docs(topic), get_prices(topic), get_reviews(topic),
        return_exceptions=True,        # один упал — не рушим всё (graceful degradation)
    )
    return docs, prices, reviews

# работает ТОЛЬКО для независимых шагов: если запрос B зависит
# от результата A — параллелить нечего, останется последовательность

Удешевление: правильная модель и кэш

Стоимость агента = токены × цена модели × число вызовов. Самый большой рычаг — не платить премиальной моделью за простую работу.

ПриёмСутьЭффект
Модель под задачуКлассификация/роутинг/извлечение — дешёвой моделью, сложный reasoning — сильнойКратное снижение счёта
LLM-роутингДешёвая модель определяет сложность и отдаёт сильной только трудноеЭкономия на массе простых запросов
Prompt cachingНе платить за повторяющийся префикс (прошлый урок)До ~90% на входе
Семантический кэшПохожий вопрос уже задавали → отдать сохранённый ответНоль токенов на попадании
Batch APIНеспешные задачи — пакетом со скидкойДешевле, но с задержкой
✅ Не одна модель на всё — а разные под разные шаги

Самая частая незамеченная переплата: агент использует топовую модель для всех вызовов, включая «определи язык запроса», «выбери инструмент», «переформулируй запрос». Это как возить хлеб на грузовике. Раздели шаги: рутинные — дешёвая быстрая модель, финальный ответ и сложный reasoning — сильная. Только проверяй качество на своём датасете (раздел оценки): деградация должна быть измерена, а не предположена.

⚠️ Семантический кэш может отдать неверный ответ

Кэш «по смыслу» (эмбеддинги вопроса, поиск похожего — модуль 04) даёт мгновенный бесплатный ответ, но у него есть опасность: при слишком мягком пороге похожести «Какая погода в Москве?» и «Какая погода в Минске?» могут схлопнуться в один кэш-хит. Ставь строгий порог, учитывай пользователя/контекст в ключе кэша и никогда не кэшируй персонализированные или быстро устаревающие ответы. Неверный ответ из кэша хуже, чем медленный правильный.

Пропускная способность

Третий вопрос — сколько параллельных пользователей мы выдержим. Здесь важно понять специфику: агент почти всё время ждёт сеть, а не считает. Значит, узкое место обычно не CPU твоего сервера.

Что НЕ ограничивает throughput агента:
  CPU сервера — агент 95% времени просто ЖДЁТ ответа LLM

Что РЕАЛЬНО ограничивает:
  1. Лимиты провайдера (RPM/TPM) ← чаще всего именно это, «потолок» снаружи
  2. Блокирующий код в async → event loop встал, конкурентность = 1
  3. Синхронная архитектура (поток на запрос) → упор в память/потоки
  4. Общие ресурсы: пул соединений к БД, локи

→ Масштабирование агента = async + больше квоты у провайдера,
  а НЕ «добавить процессорных ядер»

Отсюда практический порядок действий: сначала убедись, что весь путь асинхронный (нет блокирующих вызовов — урок про FastAPI), затем что ты не упираешься в лимиты провайдера (rate limiting из раздела надёжности) и при необходимости подними tier. Горизонтальное масштабирование (больше реплик) помогает только после этого — иначе десять реплик просто быстрее упрутся в тот же общий лимит провайдера.

🎯 Порядок оптимизации, который работает

1) Измерь по трейсам, где время и деньги. 2) Убери лишние шаги и распараллель независимые. 3) Включи prompt caching и подрежь контекст. 4) Раздели модели по сложности шагов. 5) Добавь кэш там, где безопасно. 6) Только потом масштабируй инфраструктуру. И после каждого шага — прогон offline-оценки: оптимизация не должна тихо съесть качество.

Типичные ошибки

❌ Ошибка 1: оптимизировать без измерения

Ускоряют свой код (2% времени), пока 90% уходит в лишний LLM-шаг. Сначала трейс и p95, потом оптимизация самого жирного куска.

❌ Ошибка 2: одна топовая модель на все шаги

Премиальной моделью выполняются «выбери инструмент» и «определи язык». Дели шаги по сложности — экономия кратная.

❌ Ошибка 3: последовательные независимые вызовы

Три поиска по очереди вместо asyncio.gather — время складывается вместо максимума. Параллель всё, что не зависит друг от друга.

❌ Ошибка 4: оптимизация без проверки качества

Урезали шаги и модель — стало быстро и дёшево, но агент отвечает хуже, и это заметили пользователи, а не вы. После каждой оптимизации — offline-оценка.

❌ Ошибка 5: масштабировать железом вместо async и квот

Агент ждёт сеть, а не считает: реплики не помогут, если код блокирующий или упёрлись в RPM/TPM провайдера. Сначала async и лимиты, потом масштаб.

Шпаргалка

Оптимизация агента — всё в одном месте
text
ТРЕУГОЛЬНИК: скорость ↔ цена ↔ КАЧЕСТВО — улучшение одного ухудшает другое
  универсальной оптимизации НЕТ, выбирай точку осознанно под сценарий

АНАТОМИЯ ЗАДЕРЖКИ:
  LLM-вызовы = 80–95% времени · инструменты · RAG · свой код (~0 → оптимизировать бесполезно)
  внутри вызова: prefill (вход, лечится caching) + ГЕНЕРАЦИЯ (выход, последовательно)
  ГЛАВНЫЙ РЫЧАГ = число ПОСЛЕДОВАТЕЛЬНЫХ LLM-вызовов (5 шагов × 3с = 15с)

⚠️ СНАЧАЛА ИЗМЕРЬ (трейс + p95), потом оптимизируй — жирный кусок, а не удобный

УСКОРЕНИЕ: параллелить независимые шаги (asyncio.gather) · убрать лишние шаги
  (reflection не на каждый запрос) · короче выход (max_tokens) · меньше вход
  (context engineering + caching) · СТРИМИНГ (не сокращает время, но чинит TTFT)

УДЕШЕВЛЕНИЕ: стоимость = токены × цена модели × число вызовов
  ★ МОДЕЛЬ ПОД ЗАДАЧУ: рутина (роутинг/классификация) — дешёвой, reasoning — сильной
  LLM-роутинг по сложности · prompt caching (~90% входа) · семантический кэш · Batch API
  ⚠️ семантический кэш: строгий порог, юзер в ключе, НЕ кэшировать персональное/устаревающее
     (Москва/Минск схлопнутся при мягком пороге — неверный ответ хуже медленного)

THROUGHPUT: агент 95% времени ЖДЁТ сеть → узкое место НЕ CPU
  реально ограничивают: лимиты провайдера (RPM/TPM) · блокирующий код в async ·
  синхронная архитектура · общие пулы/локи
  → async + квоты, а не «больше ядер» (10 реплик упрутся в тот же лимит)

ПОРЯДОК: измерь → убрать/параллелить шаги → caching+контекст → модели по сложности →
  кэш → масштаб · ПОСЛЕ КАЖДОГО ШАГА — offline-оценка качества

ГРАБЛИ: без измерения · одна топ-модель на всё · последовательные независимые ·
        без проверки качества · железо вместо async/квот

Практическое задание

Задание: оптимизируй агента по числам

  1. Открой трейс агента из модулей 03–05 и разложи время: сколько на LLM-вызовы, инструменты, свой код? Сколько последовательных LLM-вызовов на один запрос?
  2. Найди независимые шаги, которые сейчас идут по очереди. Распараллель через asyncio.gather — насколько упало общее время?
  3. Найди шаг, где используется топовая модель, но задача рутинная (роутинг, классификация, извлечение). Замени на дешёвую и прогони offline-оценку: просело ли качество?
  4. Посчитай стоимость одного запроса до и после включения prompt caching и подрезки контекста. Во сколько раз изменилась?
  5. Прикинь throughput: что упрётся первым — твой сервер или лимиты провайдера? Есть ли в коде блокирующие вызовы, ограничивающие конкурентность?
  6. Для своего продукта выбери точку в треугольнике: что важнее — скорость, цена или качество? Какие компромиссы ты готов принять и почему?
  7. Со звёздочкой: добавь семантический кэш для частых запросов. Подбери порог похожести так, чтобы не склеивались разные вопросы, и замерь hit rate и экономию.

Что дальше

Агент оптимизирован по скорости, цене и пропускной способности. Остался последний шаг — вывести его в мир: упаковать, развернуть, настроить окружение и автоматизировать выкатку. Финальный урок раздела и модуля — деплой: Docker, Railway, Modal, AWS Lambda и как выбрать площадку под свой агент.