Продакшн-разработка ИИ-агентов · Модуль 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
}
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).
Какая статья затрат доминирует в таком цикле и почему?
Верный ответ: B
Хотя за токен вывод дороже, в цикле доминирует ввод: на каждом шаге весь предыдущий контекст оплачивается как input снова. Именно поэтому кеширование стабильного префикса даёт основную экономию. Платы за «хранение кеша» как таковой нет — есть разовая запись.
Команде нужно разметить 50000 документов к утру; задержка отдельного запроса не важна.
Какой путь обработки оптимален по стоимости?
Верный ответ: C
Нагрузка офлайновая и нечувствительна к задержке — это классический случай для Message Batches API: асинхронно, около вдвое дешевле синхронного пути. Стриминг улучшает воспринимаемую латентность, которая здесь не важна; синхронный путь дороже без выгоды.
При повторных запросах с одинаковым системным промптом CacheReadInputTokens стабильно равен нулю.
Что наиболее вероятно происходит?
Верный ответ: B
Нулевой cache read при идентичном (как кажется) префиксе — признак тихого инвалидатора: меняющаяся метка времени, per-request ID или недетерминированная сериализация делают байты префикса разными. Кеш — это совпадение по точным байтам; MaxTokens и сила модели тут ни при чём.
Команда добавила семантический кеш ответов: новый запрос сравнивается по эмбеддингу с прежними, и при cosine выше порога возвращается готовый ответ. Кеш общий для всех пользователей.
В чём главная опасность этой реализации и чем семантический кеш отличается от prompt caching?
Верный ответ: B
Семантический кеш живёт в приложении и матчит ПОХОЖИЕ запросы по cosine, возвращая ответ вообще без вызова модели; prompt caching — серверный кеш ТОЧНОГО префикса, при котором модель всё равно генерирует ответ. Главный риск общего кеша — отдать ответ одного пользователя другому: кеш обязан скоупиться по пользователю/правам. Плюс ложные попадания и устаревание — отсюда консервативный порог и TTL.
Команда глобально включила режим extended thinking для всех запросов агента, включая короткие справочные. Счёт вырос сильнее ожидаемого, а интерактивный чат стал заметно дольше отвечать первым словом.
Как объяснить рост стоимости/латентности и что сделать?
Верный ответ: B
B верно. Размышление — отдельная статья расходов (оплачивается как output) и удлиняет время до первого токена. Под прод reasoning включают избирательно через routing/каскад и ограничивают бюджет размышления, а на простых задачах выключают. A неверно (токены платные), C не связано с проблемой, D обычно лишь увеличит стоимость.
Счёт за агента вырос на 40%. В дашборде видна только общая сумма по провайдеру — без разбивки по фичам, тенантам и задачам.
Чего не хватает и какая метrика важнее всего для оптимизации?
Верный ответ: B
B верно. Без атрибуции вы не знаете, какая фича/тенант формирует счёт, и оптимизируете вслепую. Тегируйте стоимость каждого вызова измерениями (это те же спаны, что в гл.9) и считайте unit-экономику — токены всех шагов и ретраев одной задачи, делённые на факт успеха. Дешёвая модель с 12 шагами может стоить дороже дорогой с 2 шагами. A/C/D — действия вслепую, которые без атрибуции легко бьют мимо или режут качество.
Дальше почитать
- Prompt caching
- OpenTelemetry — GenAI semantic conventions (атрибуты стоимости/токенов на спанах)
- Anthropic — Extended thinking (тарификация thinking-токенов; сверяйтесь с актуальными доками)
- Batch processing (Message Batches API)
- Streaming Messages
- Pricing
- anthropic-sdk-go
- Embeddings (эмбеддинги для семантического кеша)