Продакшн-разработка ИИ-агентов · Модуль 6 · Урок 6.1

Глава 12. Стоимость и латентность

Цели главы

После этой главы вы сможете:

  • Разложить счёт за агента на составляющие: токены ввода (input tokens), токены вывода (output tokens) и число обращений к модели в цикле (round-trips).
  • Применять кеширование промпта (prompt caching) для повторно используемого стабильного префикса.
  • Строить каскад моделей (model cascade/routing): дешёвая модель сначала, эскалация на дорогую при сложности или низкой уверенности.
  • Выбирать между синхронным вызовом, батчингом (Message Batches API) и стримингом (streaming) под характер нагрузки.
  • Вводить бюджет токенов (token budget) на запрос и сессию с деградацией при превышении.
  • Понимать, что такое дистилляция (distillation), и когда она оправдана (обзорно).

Что нового (дельта к базовому курсу)

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

Главная дельта: цикл агента (agent loop) умножает затраты. Один пользовательский запрос превращается в N обращений к модели (по числу шагов с инструментами), и на каждом шаге весь предыдущий контекст пересылается заново. Поэтому стоимость растёт не линейно по числу шагов, а квадратично по объёму накопленного контекста. Базовый курс этого не затрагивал.

Новые рычаги, которых не было раньше: кеш стабильного префикса, маршрутизация между моделями разной цены, асинхронный батч для офлайн-нагрузок и явный бюджет токенов как защитный предохранитель.

Куда уходят деньги и время

Счёт за вызов модели складывается из двух статей с разной ценой: input tokens (весь промпт — система, инструменты, история, текущий запрос) и output tokens (то, что сгенерировала модель). Вывод обычно в 4-5 раз дороже ввода за токен (сверьтесь с актуальным прайсом).

В одиночном вызове доминирует вывод. В агентном цикле — наоборот ввод: каждый шаг тащит за собой всю накопленную историю (предыдущие tool_use и tool_result), и эти токены оплачиваются как input снова и снова. Десять шагов по 8000 токенов контекста — это не 8000, а порядка 80000 оплаченных input-токенов.

Латентность (latency) складывается из похожих компонент: время до первого токена (TTFT, time to first token) растёт с размером промпта, а полное время ответа — ещё и с длиной вывода. В цикле задержки шагов суммируются последовательно. Поэтому два независимых рычага — «сколько токенов» и «сколько round-trip'ов» — нужно оптимизировать отдельно.

Prompt caching как рычаг стоимости

Кеширование промпта — это кеш по точному совпадению префикса (prefix match). Если стабильное начало промпта (системная инструкция, список инструментов, неизменные few-shot-примеры) байт-в-байт повторяется между запросами, повторные запросы читают его из кеша по цене около 0.1x от обычного ввода, а не оплачивают полностью.

Порядок рендеринга строгий: tools -> system -> messages. Любое изменение байта в префиксе аннулирует кеш для всего, что идёт после. Отсюда правило: стабильное — в начало, волатильное (текущий вопрос, метки времени, per-request ID) — после последней точки кеширования (cache breakpoint).

Запись в кеш дороже обычного ввода (около 1.25x для 5-минутного TTL), поэтому окупаемость наступает со второго-третьего запроса с тем же префиксом. Проверять попадание нужно по Usage.CacheReadInputTokens: если он стабильно ноль при одинаковом префиксе — где-то тихий инвалидатор (например, time.Now() в системном промпте или несортированный JSON).

Для агента это главный рычаг: системный промпт и набор инструментов кешируются один раз, и каждый шаг цикла читает их из кеша.

Каскад моделей, батчинг и стриминг

Каскад моделей (cheap -> expensive). Не каждый запрос требует самой умной модели. Дешёвая и быстрая модель (например, класса Haiku) обрабатывает простое; при высокой сложности или низкой уверенности запрос эскалируется на более сильную (Sonnet, затем Opus). Решение об эскалации принимает либо отдельный классификатор сложности, либо сам ответ дешёвой модели (низкая уверенность -> повтор на дорогой). Экономия — на массе типовых запросов; риск — двойная оплата эскалированных, поэтому каскад выгоден, когда доля «простых» велика.

Батчинг (Message Batches API). Для офлайн-нагрузок, нечувствительных к задержке (массовая разметка, бэкофилл, ночные отчёты), есть асинхронный батч: до 10000 запросов в пачке, обработка обычно в пределах часа (гарантия — 24 часа), цена примерно вдвое ниже синхронной. Вы отправляете пачку, опрашиваете статус (polling), забираете результаты по готовности. Не путать с интерактивным путём: батч не для онлайн-чата.

Стриминг (streaming). Снижает не реальную, а воспринимаемую латентность: токены текут по мере генерации, пользователь видит первые слова почти сразу (низкий TTFT), хотя полный ответ готов в то же время. Кроме UX, стриминг обязателен для длинных ответов (большой MaxTokens): без него HTTP-запрос рискует упереться в таймаут SDK.

Бюджет токенов и дистилляция

Бюджет токенов (token budget). Это предохранитель против разорительного цикла. Вы задаёте лимит токенов на запрос или на сессию и считаете фактический расход по Usage после каждого вызова. При приближении к лимиту — деградация (degrade gracefully): сократить историю (компактация), снизить уровень усилий (effort), переключиться на дешёвую модель или вернуть пользователю честный отказ вместо неконтролируемого продолжения. Без бюджета один зациклившийся агент может сжечь дневной лимит.

Дистилляция (distillation), обзорно. Дорогая, сильная модель используется как учитель: на наборе типовых запросов она генерирует эталонные ответы, на которых дообучается дешёвая модель-ученик. Цель — приблизить качество дешёвой модели к дорогой на узком домене, сократив стоимость инференса. Это уже территория адаптации моделей (глава 14): прибегают к ней, только когда промптинг, кеш и каскад выжаты, а объём однотипной нагрузки оправдывает затраты на обучение и поддержку.

Семантический кеш ответов (semantic cache)

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

Механика: запрос превращается в эмбеддинг (embedding) — числовой вектор смысла. Векторы прежних запросов и их ответы лежат в хранилище. Для нового запроса считаем эмбеддинг, ищем ближайший по косинусной близости (cosine similarity) и, если близость выше порога (например, > 0.95), отдаём кешированный ответ. Иначе — вызываем модель и кладём пару «эмбеддинг -> ответ» в кеш. Экономия здесь радикальнее, чем у prompt caching: на попадании вы не платите ни за input, ни за output, ни за латентность генерации.

Риски и оговорки:

  • Ложные попадания (false positives). Два запроса бывают близки по эмбеддингу, но требуют разных ответов («отмени заказ 123» vs «отмени заказ 124»). Слишком низкий порог -> выдача неверного ответа. Порог подбирайте консервативно и тестируйте на реальных парах.
  • Устаревание (staleness). Ответ на «какой у меня баланс?» нельзя кешировать надолго: мир изменился. Кешируйте только то, что стабильно во времени, и ставьте TTL/инвалидацию.
  • Скоуп по пользователю и правам. Это критично: нельзя отдать ответ одного пользователя другому. Кеш обязан быть разделён по пользователю/тенанту/правам доступа — иначе утечка чужих данных. Ключ кеша включает идентификатор скоупа, поиск ближайшего идёт только внутри скоупа.

Вывод: семантический кеш — мощный, но острый инструмент. Он уместен для повторяющихся типовых вопросов с устойчивыми ответами (FAQ, справочные запросы), а не для персонального или быстро меняющегося.

Reasoning-модели под прод: thinking-токены как статья расходов

Если агент использует режим расширенного мышления (extended thinking, см. базовый курс), у стоимости появляется ещё одна статья — thinking-токены. Модель тратит их на внутреннее рассуждение перед ответом, и они тарифицируются как output (дорогая сторона). Под нагрузкой это меняет расчёт бюджета:

  • Цена. К input + output добавляется бюджет размышления. На простых задачах он не окупается — это первый кандидат на отключение (ср. каскад моделей: «думающую» конфигурацию включаем только на сложной ветке).
  • Латентность. Размышление увеличивает время до первого полезного токена: модель сначала «думает», и TTFT растёт. Для интерактивного UX это ощутимо — здесь особенно важен стриминг (показать, что агент думает) и разумный потолок бюджета размышления.
  • Prompt caching. Длинные цепочки рассуждения раздувают контекст последующих шагов. Кешируйте стабильный префикс как обычно, но помните: thinking-блоки прошлых ходов — это дополнительные токены в истории.
  • Interleaved thinking + tools. Модель может чередовать размышление и вызовы инструментов в пределах хода: подумала → вызвала инструмент → подумала над результатом. Это повышает качество многошаговых задач, но множит и thinking-токены, и round-trip'ы — закладывайте это в бюджет шага.

Практическое правило: рассматривайте «бюджет размышления» как отдельный рычаг наравне с выбором модели и кешем — включайте reasoning адресно (через routing/каскад), а не глобально. Конкретные параметры и тарификация thinking-токенов версионно-зависимы — сверяйтесь с актуальными доками.

Атрибуция стоимости и unit-экономика агента

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

Что для этого нужно:

  • Тегирование расхода. На каждый вызов модели из Usage (input/output/cache/thinking-токены) считайте стоимость и пишите её со измерениями (dimensions): tenant, user, feature, agent, версия модели, request_id. Это те же спаны трейсинга (гл.9) — стоимость становится ещё одним атрибутом шага.
  • Unit-экономика. Главная метрика — не «стоимость вызова», а стоимость на успешно решённую задачу (cost per resolved task): суммарные токены всех шагов и ретраев одного запроса, делённые на факт успеха (из eval/трейсов). Один агент с дешёвой моделью, но 12 шагами, может стоить дороже, чем дорогая модель за 2 шага.
  • Агрегация и алерты. Катите стоимость по тем же измерениям в дашборд; ставьте алерты на аномалии (всплеск cost-per-task у фичи = регресс промпта или зацикливание). Связывайте с мультитенантными квотами (гл.13).

Мысленная модель: стоимость — это метрика продукта, а не строка в счёте провайдера. Атрибуция превращает «дорого вообще» в «дорого вот здесь и вот почему» — и только тогда оптимизация (кэш, каскад, сокращение шагов) бьёт в цель.

Атрибуция стоимости: считаем цену вызова и пишем её с измерениями
package costlatency

// Usage — то, что вернул провайдер по одному вызову (сверяйтесь с актуальными полями).
type Usage struct {
    InputTokens    int
    OutputTokens   int
    CacheReadTokens int
    ThinkingTokens int // тарифицируется как output
}

// Price — прайс модели за 1K токенов (значения сверяйте с актуальным прайсом).
type Price struct{ In, Out, CacheRead float64 }

// cost считает стоимость одного вызова. Thinking-токены идут по цене output.
func cost(u Usage, p Price) float64 {
    return float64(u.InputTokens)/1000*p.In +
        float64(u.OutputTokens+u.ThinkingTokens)/1000*p.Out +
        float64(u.CacheReadTokens)/1000*p.CacheRead
}

// CostEvent — запись о стоимости с ИЗМЕРЕНИЯМИ для последующей агрегации.
// Те же поля удобно класть как атрибуты в спан трейсинга (Глава 9).
type CostEvent struct {
    RequestID string
    Tenant    string
    User      string
    Feature   string
    Model     string
    USD       float64
}

// record накапливает стоимость по всем шагам одного запроса. Unit-экономику
// (cost per resolved task) считают, поделив сумму запроса на факт успеха задачи.
func record(emit func(CostEvent), reqID, tenant, user, feature, model string, u Usage, p Price) {
    emit(CostEvent{
        RequestID: reqID, Tenant: tenant, User: user,
        Feature: feature, Model: model, USD: cost(u, p),
    })
}
Каскад моделей: классификатор сложности выбирает дешёвую или дорогую модель
package costlatency

import (
	"context"
	"strings"

	"github.com/anthropics/anthropic-sdk-go"
)

// Complexity — оценка сложности запроса, по ней выбираем модель.
type Complexity int

const (
	Simple Complexity = iota // короткий вопрос, фактический ответ
	Hard                     // рассуждение, многошаговость, анализ
)

// classify — дешёвая эвристика «на входе». В проде роль классификатора
// может играть отдельный быстрый вызов модели класса Haiku, но эвристика
// бесплатна и часто достаточна как первый фильтр.
func classify(prompt string) Complexity {
	p := strings.ToLower(prompt)
	long := len(prompt) > 600
	reasoning := strings.Contains(p, "почему") ||
		strings.Contains(p, "сравни") ||
		strings.Contains(p, "проанализируй")
	if long || reasoning {
		return Hard
	}
	return Simple
}

// pickModel — каскад cheap -> expensive: простое отдаём быстрой дешёвой
// модели, сложное эскалируем на более сильную. Слаги сверяйте с доками.
func pickModel(c Complexity) anthropic.Model {
	if c == Hard {
		return anthropic.ModelClaudeSonnet4_6
	}
	return anthropic.ModelClaudeHaiku4_5_20251001
}

// Answer выполняет запрос на модели, выбранной каскадом.
func Answer(ctx context.Context, client anthropic.Client, prompt string) (string, error) {
	model := pickModel(classify(prompt))

	resp, err := client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     model,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		},
	})
	if err != nil {
		return "", err
	}

	var sb strings.Builder
	for _, block := range resp.Content {
		if t, ok := block.AsAny().(anthropic.TextBlock); ok {
			sb.WriteString(t.Text)
		}
	}
	return sb.String(), nil
}
Трекер бюджета токенов: учёт расхода и деградация при превышении
package costlatency

import (
	"context"
	"errors"
	"sync"

	"github.com/anthropics/anthropic-sdk-go"
)

// ErrBudgetExceeded возвращается, когда сессия исчерпала бюджет токенов.
var ErrBudgetExceeded = errors.New("token budget exceeded")

// Budget — потокобезопасный счётчик токенов на сессию.
// Считаем суммарно input + cache + output как «использованный контекст».
type Budget struct {
	mu    sync.Mutex
	limit int64
	used  int64
}

func NewBudget(limit int64) *Budget {
	return &Budget{limit: limit}
}

// Remaining — сколько токенов ещё доступно.
func (b *Budget) Remaining() int64 {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.limit - b.used
}

// record прибавляет фактический расход из Usage ответа.
func (b *Budget) record(u anthropic.Usage) {
	b.mu.Lock()
	defer b.mu.Unlock()
	// Полный объём промпта = uncached input + запись в кеш + чтение из кеша.
	b.used += u.InputTokens +
		u.CacheCreationInputTokens +
		u.CacheReadInputTokens +
		u.OutputTokens
}

// Step делает один вызов модели под контролем бюджета.
// Если бюджет уже исчерпан — отказываем заранее (деградация), не тратя деньги.
func (b *Budget) Step(
	ctx context.Context,
	client anthropic.Client,
	params anthropic.MessageNewParams,
) (*anthropic.Message, error) {
	if b.Remaining() <= 0 {
		return nil, ErrBudgetExceeded
	}

	resp, err := client.Messages.New(ctx, params)
	if err != nil {
		return nil, err
	}

	// Учитываем расход постфактум: лимит — мягкий предохранитель, а не
	// жёсткий потолок на отдельный вызов (для этого есть MaxTokens).
	b.record(resp.Usage)
	return resp, nil
}
Стриминг ответа: низкий TTFT и сбор финального сообщения
package costlatency

import (
	"context"
	"fmt"
	"io"

	"github.com/anthropics/anthropic-sdk-go"
)

// StreamAnswer стримит ответ в w по мере генерации (низкий TTFT для UX)
// и параллельно собирает полное сообщение через Accumulate.
func StreamAnswer(
	ctx context.Context,
	client anthropic.Client,
	w io.Writer,
	prompt string,
) (anthropic.Message, error) {
	stream := client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
		Model: anthropic.ModelClaudeOpus4_8,
		// Для длинных ответов даём простор и обязательно стримим,
		// иначе синхронный запрос рискует упереться в таймаут SDK.
		MaxTokens: 64000,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		},
	})

	// В Go нет GetFinalMessage() на стриме — аккумулируем вручную.
	message := anthropic.Message{}
	for stream.Next() {
		event := stream.Current()
		message.Accumulate(event)

		// Печатаем дельты текста сразу — пользователь видит первые слова,
		// не дожидаясь конца генерации.
		if delta, ok := event.AsAny().(anthropic.ContentBlockDeltaEvent); ok {
			if td, ok := delta.Delta.AsAny().(anthropic.TextDelta); ok {
				fmt.Fprint(w, td.Text)
			}
		}
	}
	if err := stream.Err(); err != nil {
		return anthropic.Message{}, err
	}
	return message, nil
}
Семантический кеш: эмбеддинг запроса -> поиск ближайшего -> кеш или вызов модели
package costlatency

import (
	"context"
	"math"
	"sync"

	"github.com/anthropics/anthropic-sdk-go"
)

// Embedder превращает текст в вектор смысла. Реализация — любой провайдер
// эмбеддингов; здесь нас интересует только контракт.
type Embedder interface {
	Embed(ctx context.Context, text string) ([]float32, error)
}

// cacheEntry — запомненная пара «вектор запроса -> ответ».
type cacheEntry struct {
	vec    []float32
	answer string
}

// SemanticCache — кеш ответов по эмбеддинг-похожести. Живёт на стороне
// приложения и матчит ПОХОЖИЕ запросы (в отличие от prompt caching,
// который матчит ТОЧНЫЙ префикс на сервере).
type SemanticCache struct {
	mu        sync.RWMutex
	embedder  Embedder
	client    anthropic.Client
	threshold float32 // порог косинусной близости, напр. 0.95

	// Скоуп по пользователю: ответы одного пользователя НЕ должны
	// возвращаться другому. Поиск ближайшего идёт только внутри скоупа.
	byUser map[string][]cacheEntry
}

func NewSemanticCache(e Embedder, c anthropic.Client, threshold float32) *SemanticCache {
	return &SemanticCache{
		embedder:  e,
		client:    c,
		threshold: threshold,
		byUser:    make(map[string][]cacheEntry),
	}
}

// cosine — косинусная близость двух векторов одинаковой длины.
func cosine(a, b []float32) float32 {
	var dot, na, nb float32
	for i := range a {
		dot += a[i] * b[i]
		na += a[i] * a[i]
		nb += b[i] * b[i]
	}
	if na == 0 || nb == 0 {
		return 0
	}
	// Векторы эмбеддингов обычно нормированы, но делим на нормы честно.
	return dot / float32(math.Sqrt(float64(na))*math.Sqrt(float64(nb)))
}

// lookup ищет ближайший кешированный ответ ВНУТРИ скоупа пользователя.
func (sc *SemanticCache) lookup(userID string, vec []float32) (string, bool) {
	sc.mu.RLock()
	defer sc.mu.RUnlock()
	var best float32
	var answer string
	for _, e := range sc.byUser[userID] {
		if s := cosine(vec, e.vec); s > best {
			best, answer = s, e.answer
		}
	}
	if best >= sc.threshold {
		return answer, true
	}
	return "", false
}

// Answer возвращает кешированный ответ при близком попадании,
// иначе вызывает модель и запоминает результат для будущих запросов.
func (sc *SemanticCache) Answer(
	ctx context.Context,
	userID, prompt string,
) (string, bool, error) {
	vec, err := sc.embedder.Embed(ctx, prompt)
	if err != nil {
		return "", false, err
	}

	if answer, ok := sc.lookup(userID, vec); ok {
		// Попадание: ни input, ни output, ни латентность генерации не платим.
		return answer, true, nil
	}

	// Промах: вызываем модель.
	resp, err := sc.client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeHaiku4_5_20251001,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
		},
	})
	if err != nil {
		return "", false, err
	}

	var answer string
	for _, block := range resp.Content {
		if t, ok := block.AsAny().(anthropic.TextBlock); ok {
			answer += t.Text
		}
	}

	// Записываем в кеш под скоупом пользователя. В проде — с TTL/инвалидацией,
	// и только для запросов, чьи ответы устойчивы во времени.
	sc.mu.Lock()
	sc.byUser[userID] = append(sc.byUser[userID], cacheEntry{vec: vec, answer: answer})
	sc.mu.Unlock()

	return answer, false, nil
}

Anti-patterns

Грабли стоимости и латентности
ГрабляПочему плохоКак избегать
Метка времени или per-request ID в системном промптеПрефикс меняется каждый запрос, кеш аннулируется полностью, каждый вызов оплачивается по полнойДержать system/tools байт-стабильными; волатильное выносить в конец messages, после cache breakpoint
Несортированный JSON или итерация по map в стабильном префиксеНедетерминированный порядок ключей меняет байты префикса — тихий промах кеша без ошибкиСериализовать детерминированно (сортировать ключи), проверять CacheReadInputTokens
Все запросы идут на самую сильную модельПлатите Opus-цену за тривиальные запросы, которые решает Haiku в разы дешевле и быстрееКаскад cheap->expensive с классификатором сложности и эскалацией по уверенности
Синхронный вызов для массовой офлайн-разметкиПолная цена и удержание соединений там, где задержка не важнаMessage Batches API: асинхронно, примерно вдвое дешевле, до 10000 запросов в пачке
Цикл агента без бюджета токеновЗациклившийся или разросшийся контекст незаметно сжигает дневной лимитToken budget на сессию: учёт по Usage, деградация (компактация/дешёвая модель/отказ) при превышении
Большой MaxTokens без стримингаДлинная генерация упирается в HTTP-таймаут SDK, запрос падает и требует повтораДля длинных ответов всегда стримить и аккумулировать через Accumulate
Общий семантический кеш без скоупа по пользователюПохожий запрос другого пользователя получает чужой кешированный ответ — утечка данных и правКлючевать кеш по пользователю/тенанту/правам; искать ближайший только внутри скоупа; TTL и консервативный порог близости

Практическое задание (pro-m6-cost-latency)

  • Соберите шлюз агента, который перед вызовом классифицирует запрос (эвристика или быстрый вызов Haiku) и выбирает модель каскадом cheap->expensive.
  • Закешируйте стабильный префикс: системный промпт и набор инструментов с CacheControl на последнем system-блоке; убедитесь по CacheReadInputTokens, что кеш читается.
  • Оберните цикл в Budget: учитывайте input+cache+output из Usage, задайте лимит на сессию и реализуйте деградацию при ErrBudgetExceeded.
  • Для онлайн-ответов включите стриминг с Accumulate; измерьте TTFT до и после.
  • Вынесите офлайн-нагрузку (массовую разметку) в отдельный путь через Message Batches API и сравните стоимость с синхронным путём.
  • Залогируйте на каждый запрос: выбранную модель, разбивку Usage (input/cache_read/cache_creation/output) и оценку стоимости в долларах.
  • Добавьте семантический кеш для повторяющихся типовых запросов: эмбеддинг -> поиск ближайшего по cosine -> при превышении порога вернуть кеш; обязательно скоупьте кеш по пользователю и задайте TTL.

Проверка знаний

Агент делает 10 шагов с инструментами, на каждом шаге в контексте накапливается вся предыдущая история (tool_use и tool_result).

Какая статья затрат доминирует в таком цикле и почему?

  • A Output tokens, потому что вывод дороже за токен
  • B Input tokens, потому что вся накопленная история пересылается заново на каждом шаге
  • C Стоимость стриминга, пропорциональная числу дельт
  • D Плата за хранение кеша между шагами

Команде нужно разметить 50000 документов к утру; задержка отдельного запроса не важна.

Какой путь обработки оптимален по стоимости?

  • A Синхронные вызовы в цикле с максимальной параллельностью
  • B Стриминг каждого документа для низкого TTFT
  • C Message Batches API: асинхронно, примерно вдвое дешевле
  • D Каскад моделей с эскалацией каждого документа на Opus

При повторных запросах с одинаковым системным промптом CacheReadInputTokens стабильно равен нулю.

Что наиболее вероятно происходит?

  • A Кеш работает, просто метрика не заполняется
  • B В стабильный префикс попал тихий инвалидатор — например, time.Now() или несортированный JSON
  • C Модель слишком сильная для кеширования
  • D Нужно увеличить MaxTokens, чтобы кеш активировался

Команда добавила семантический кеш ответов: новый запрос сравнивается по эмбеддингу с прежними, и при cosine выше порога возвращается готовый ответ. Кеш общий для всех пользователей.

В чём главная опасность этой реализации и чем семантический кеш отличается от prompt caching?

  • A Опасности нет: семантический кеш — это то же самое, что серверный prompt caching по префиксу
  • B Общий кеш без скоупа может вернуть похожему запросу чужой ответ; в отличие от prompt caching (точный префикс на сервере, модель всё равно вызывается), семантический кеш на стороне приложения матчит похожие запросы и отдаёт ответ без вызова модели
  • C Семантический кеш всегда дороже, потому что хранит эмбеддинги
  • D Prompt caching матчит похожие запросы, а семантический — точные

Команда глобально включила режим extended thinking для всех запросов агента, включая короткие справочные. Счёт вырос сильнее ожидаемого, а интерактивный чат стал заметно дольше отвечать первым словом.

Как объяснить рост стоимости/латентности и что сделать?

  • A Thinking-токены бесплатны; дело только в сети — увеличить таймаут
  • B Thinking-токены тарифицируются как output и увеличивают TTFT; включать reasoning адресно (routing/каскад) только на сложных задачах, ограничивать бюджет размышления, на простых — отключить
  • C Нужно отключить prompt caching, он мешает thinking
  • D Перейти на модель побольше — она думает дешевле

Счёт за агента вырос на 40%. В дашборде видна только общая сумма по провайдеру — без разбивки по фичам, тенантам и задачам.

Чего не хватает и какая метrика важнее всего для оптимизации?

  • A Ничего: общий счёт — достаточный сигнал, нужно просто включить кэш
  • B Не хватает атрибуции стоимости (тегирование Usage по tenant/user/feature/model на каждом вызове); ключевая метрика — стоимость на успешно решённую задачу (cost per resolved task), а не цена одного вызова
  • C Достаточно перейти на самую дешёвую модель для всего
  • D Нужно просто понизить бюджет токенов на запрос