Перейти к содержимому

11.3 · Трейсинг и стоимость одного ответа

Неделя 11 · Продакшен · ⏱ ~4 ч · Практика

Клиент пишет «бот ответил ерунду», и без трейса не узнать, что пошло не так: поиск не нашёл нужный чанк, модель проигнорировала контекст, инструмент вернул пустоту или сработал фолбэк на слабую модель. С деньгами то же: «почему вчера потратили втрое больше» без трейсов не объяснить. На собеседованиях observability — обязательный пункт system design: «как вы узнаёте, что качество упало?», «сколько стоит один ответ?», «какой у вас p95?».

Кусочки наблюдаемости у нас уже есть: в Проекте 1 бот пишет стоимость каждого ответа в лог (гл. 3.4), в главе 6.5 (гл. 6.5) мы подключили Langfuse к ассистенту поддержки, а в главе 11.2 (гл. 11.2) цифры кэша и бюджетов легли в jsonl-файлы bench/. Теперь собираем это в одну систему для всех четырёх проектов. Начнём со словаря: наблюдаемость складывается из трёх видов данных, и каждый отвечает на свой вопрос.

Что отвечает Пример
Лог что произошло в момент 12:03:11 tool=get_open_orders rows=14
Метрика сколько / как часто, агрегат p95 латентности = 4.2 с, ошибки 1.3%
Трейс как прошёл один запрос по всем шагам дерево: поиск 180 мс → реранк 300 мс → LLM 2.1 с ($0.004)

В обычном бэкенде трейсинг — это «где тормозит». В LLM-приложении трейс ещё и содержательный: в нём лежат промпт, найденные документы, аргументы инструментов и ответ. Поэтому трейс — главный инструмент разбора ошибок (гл. 8.4): открываем плохой ответ про гарантию на «Свет мини», видим, что в контекст попали чанки со страницы лампы «Свет», — проблема в retrieval, а не в модели.

Вот трейс одного ответа ассистента поддержки — пайплайн глав 5.1–5.3: дешёвая модель переформулирует вопрос (plan_query), поиск достаёт кандидатов, реранкер оставляет лучшие пять, основная модель пишет ответ. Каждый шаг виден со своим временем, а вызовы моделей — ещё и с токенами и ценой:

trace "support-answer" user=demo session=tg-123 tags=[support, answer_v3] total: 3.1 s, $0.0061
├─ span "plan_query" 0.6 s
│ └─ generation model=small in=220 out=40 0.6 s $0.0002
├─ span "retrieve" top_k=8 0.2 s
│ input: "возврат вскрытого чая" output: [8 chunk ids]
├─ span "rerank" top=5 0.3 s
└─ generation "answer" model=main in=4100 (cache_read=1800) out=380 2.0 s $0.0059
output: "Нет: чай во вскрытой упаковке возврату не подлежит [1] ..."
  • Trace — один запрос пользователя целиком. Атрибуты: user_id, session_id (диалог в Telegram), tags (версия промпта, проект), metadata.
  • Span — любой шаг с началом и концом (поиск, вызов инструмента, парсинг).
  • Generation — span, который является вызовом модели: модель, параметры, usage (токены, включая кэш), стоимость.
  • Score — оценка, прикреплённая к трейсу: от пользователя (👍/👎 в боте), от LLM-судьи, из evals (гл. 6.4).

Langfuse построен на OpenTelemetry (OTel) — открытом стандарте трейсинга. Для LLM у OTel есть семантические соглашения GenAI (имена атрибутов вроде модели и токенов). Практический смысл: трейсы можно отправлять в любой совместимый бэкенд, а инструментирование библиотек (OpenAI SDK, LangChain, LlamaIndex) часто есть готовое.

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

  1. Обёртка клиента. from langfuse.openai import openai — drop-in замена OpenAI SDK, каждый вызов становится generation с токенами и стоимостью. Работает и для Ollama (гл. 11.4), и для LiteLLM, если они идут через OpenAI-клиент.
  2. Декоратор @observe на своих функциях — span на функцию, вложенность строится автоматически по стеку вызовов (в том числе в async-коде).
  3. Ручные observations — когда нужно точно контролировать, что записать, например usage с кэшем Anthropic.

Иллюстративный пример (Langfuse Python SDK; у SDK бывают мажорные изменения — сигнатуры стоит сверить с документацией своей версии):

from langfuse import observe, get_client
langfuse = get_client() # reads LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL
@observe(name="retrieve")
def retrieve(question: str) -> list[dict]:
...
@observe(name="support-answer")
def answer(question: str, user_id: str) -> str:
chunks = retrieve(question)
with langfuse.start_as_current_observation(
as_type="generation", name="answer", model=MODEL
) as gen:
resp, usage, cost = call_llm(question, chunks) # your llm_utils from AIL-11/45
gen.update(
output=resp.text,
usage_details=usage, # e.g. {"input": ..., "output": ..., "cache_read_input_tokens": ...}
cost_details=cost, # your own computed cost, so cache discounts are right
)
return resp.text

Про стоимость: Langfuse умеет считать её сам по таблице цен моделей, но для новых моделей, кэша и Ollama цены может не быть или она будет неточной. Надёжнее передавать свою стоимость, посчитанную формулой из гл. 11.2, и сверять с инвойсом провайдера. Для локальной модели стоимость токенов — ноль, но в README честно пишем, что это не «бесплатно» (железо, электричество, время).

Короткоживущие скрипты (evals, bench_cost.py) должны вызывать langfuse.flush() перед выходом — SDK отправляет данные в фоне пачками, и без flush последние трейсы теряются.

Трейс отвечает на вопрос про один запрос; дашборд — про все запросы за день или неделю. Метрики не собираются отдельно: Langfuse агрегирует их из тех же трейсов, поэтому всё, что нужно на дашборде, должно сначала попасть в трейс (стоимость в generation, причина отказа в metadata). Нужны три группы.

Стоимость: средняя на ответ, сумма за день, разбивка по моделям (фолбэк виден сразу) и по шагам (plan_query дешёвый, answer дорогой).

Латентность — перцентили, а не среднее. Распределение латентности LLM длиннохвостое: большинство ответов за 2 с, но некоторые 15 с (длинный ответ, ретрай, агент сделал 6 шагов). Среднее прячет хвост.

  • p50 (медиана) — половина ответов быстрее этого значения: «типичный» опыт.
  • p95 — 95% ответов быстрее: то, что видит каждый двадцатый запрос. Обычно именно на p95 ставят цели (SLO).
  • Для стриминга отдельно — TTFT (time to first token): клиент видит, что бот «пишет», и ждать готов дольше.
import statistics
lat = [r["latency_s"] for r in rows]
p50 = statistics.median(lat)
p95 = statistics.quantiles(lat, n=100)[94] # needs enough data points to be meaningful

Как и с evals в главе 8.1 (гл. 8.1), маленькая выборка даёт шумную оценку: на 20 запросах p95 — это практически максимум. Рядом с цифрой пишем, на скольких запросах она посчитана.

Ошибки: доля запросов, закончившихся исключением, таймаутом, отказом по бюджету, фолбэком. Отдельно — «мягкие» ошибки: ответ без ссылок, невалидный JSON, отказ «В базе знаний нет ответа на этот вопрос.» (это может быть и правильный отказ, и провал retrieval — разбираем по трейсам). Передачу оператору (handoff с причиной refused, personal_data или complaint) кладём в metadata трейса, чтобы считать её долю.

Для агента (Проект 4): число шагов на задачу, число вызовов инструментов, доля задач, упёршихся в лимит шагов. Стоимость агента — это сумма всех итераций, и она растёт сверхлинейно, потому что каждая итерация повторяет всю историю.

Метрики одни и те же, но «один трейс» в каждом проекте означает своё: сообщение владельца, вопрос клиента, прогон кейса или целое расследование агента.

Проект Трейс = Ключевые spans Особое
1. Telegram-бот «аналитик магазина» (гл. 3.1–16) одно сообщение владельца выбор инструмента, вызовы read-only функций к SQLite (get_sales, get_open_orders), ответ session_id = чат; данные заказов в трейсах
2. Ассистент поддержки по kb/ (гл. 5.3) один вопрос клиента plan_query, retrieve, rerank, answer url чанков в output retrieve; сообщения клиентов в трейсах
3. Evals/эксперименты (гл. 8.2–35) один прогон кейса как в проекте 2 + score tag = имя эксперимента, чтобы сравнивать
4. Агент-разборщик заказов (гл. 10.2) одно расследование каждая итерация, каждый tool call лимит шагов, стоимость всей цепочки

Последний столбец таблицы — самый важный: трейс содержит всё, что видела модель. Для Проектов 1 и 4 это данные из снимка базы магазина: заказы, суммы, оплаты, а в events.message — комментарии клиентов. Для Проекта 2 — настоящие сообщения клиентов, в которых бывают телефон, e-mail и номер заказа. Данные выдуманы, но обращаемся с ними как с настоящими. Правила:

  • Трейсы со снимком базы и с настоящими сообщениями клиентов — только в self-hosted Langfuse на своей машине (гл. 11.1), не в облаке. Или маскирование: у Langfuse есть механизм маскирования данных перед отправкой; в него подключаем contains_personal_data() из rag/privacy.py, а суммы и контакты заменяем на ***.
  • Выдуманные вопросы из eval/golden.jsonl по публичной базе знаний можно трейсить и в Langfuse Cloud.
  • Публичная демо-версия и скриншоты дашборда для README — только на демо-базе data/demo_shop.db и вопросах из набора.
  • Ключи и токены никогда не попадают в metadata трейсов. Проверяем, что не логируется целиком объект конфигурации или заголовки.

Трейсинг — вспомогательная система. SDK Langfuse отправляет данные асинхронно и не должен бросать исключения в наш код, но собственные обёртки (подсчёт стоимости, запись в SQLite) — должны быть в try/except с логированием. Проверка из гл. 11.1: останавливаем контейнер Langfuse — ассистент продолжает отвечать.

Langfuse трейсит приложение. Но часть работы идёт вне приложения: сравнить три формулировки промпта, проверить новую модель на паре вопросов. Писать для этого код с инструментированием — долго, а без журнала результаты теряются. Для таких случаев есть лёгкий инструмент.

LLM Саймона Уиллисона — CLI для разговора с моделями: llm "вопрос", llm -m <model> -s "system prompt" < file.md, шаблоны, эмбеддинги. Провайдеры подключаются плагинами (llm-anthropic, llm-ollama и др.). Чем полезен в контексте этой главы: каждый запрос автоматически пишется в SQLite — промпт, ответ, модель, токены (llm logs -u, llm logs --json, путь к базе — llm logs path). Это мини-трейсинг для быстрых экспериментов: три формулировки промпта, сравненные из терминала, оставляют журнал с токенами, который можно открыть в Datasette или запросить SQL.

Плагины LLM — сторонний код, который получает наши ключи. Сам llm ставим через uv tool install llm, плагины — только нужные и после обычной проверки стороннего кода (автор, репозиторий, что делает установка). Стоит учесть, как он хранит ключи (llm keys set пишет их в файл в профиле пользователя) — можно вместо этого передавать через переменные окружения.

Трейсинг от провайдера почти не зависит: Langfuse всё равно, кто ответил. Зависит от провайдера usage и стоимость — у каждого свои поля, и именно на них ломается «стоимость одного ответа». Факты проверены по документации DeepSeek, Ollama и OpenRouter в ноябре 2026.

Что нужно в трейсе Claude OpenAI DeepSeek Ollama
Input / output токены usage.input_tokens / output_tokens usage.prompt_tokens / completion_tokens как у OpenAI родной API: prompt_eval_count / eval_count; OpenAI-совместимый — нужно проверить, что usage заполнен (в странице совместимости это явно не описано)
Кэш cache_creation_input_tokens, cache_read_input_tokens prompt_tokens_details.cached_tokens prompt_cache_hit_tokens / prompt_cache_miss_tokens (в примере — на верхнем уровне usage, в схеме — в prompt_tokens_details; читаем оба) prompt_eval_cached_count
Токены размышления в output_tokens (thinking) completion_tokens_details.reasoning_tokens completion_tokens_details.reasoning_tokens; размышление включено по умолчанию зависит от модели
Стоимость в ответе нет, считаем сами нет, считаем сами нет, считаем сами; цена зависит от пиковых часов UTC $0 API
Время считаем сами считаем сами считаем сами total_duration, load_duration, prompt_eval_duration, eval_duration в наносекундах
Usage при стриминге в финальном message_delta stream_options={"include_usage": True} stream_options поддерживается include_usage поддерживается

Через OpenRouter (глава 2.4 · OpenRouter: один API ко многим моделям, гл. 2.4) стоимость приходит готовой в usage.cost — без дополнительных параметров (Usage Accounting). Это удобно, но с собственным расчётом стоит сверять хотя бы выборочно.

Встроенной таблице цен Langfuse не стоит доверять для DeepSeek: у него цена промаха и попадания в кэш различаются в десятки раз, а вне пиковых часов (01:00–04:00 и 06:00–10:00 UTC по будням — пик) всё стоит вдвое дешевле. Ни того, ни другого автоматический расчёт может не знать — это стоит проверить на своей версии, а надёжнее передавать свою стоимость функцией deepseek_cost из главы 11.2 (гл. 11.2):

# illustrative: one DeepSeek call as a Langfuse generation with your own cost
from datetime import datetime, timezone
from common.llm_utils import deepseek, deepseek_cost, ds_cache_tokens, is_peak
with langfuse.start_as_current_observation(as_type="generation", name="answer",
model="deepseek-flash") as gen:
ts = datetime.now(timezone.utc)
resp = deepseek.chat.completions.create(model="deepseek-flash", messages=messages,
extra_body={"thinking": {"type": "disabled"}})
hit, miss = ds_cache_tokens(resp.usage)
details = resp.usage.completion_tokens_details
gen.update(
output=resp.choices[0].message.content,
usage_details={"input_cache_hit": hit, "input_cache_miss": miss,
"output": resp.usage.completion_tokens,
"reasoning": getattr(details, "reasoning_tokens", 0) or 0},
cost_details={"total": deepseek_cost("deepseek-flash", resp.usage, ts)},
metadata={"provider": "deepseek", "peak": is_peak(ts)},
)

Две вещи, которые надо проверить на своих ответах, а не принять на веру: входят ли reasoning_tokens в completion_tokens (в OpenAI-конвенции входят; DeepSeek в документации пишет, что биллинг — по сумме input и output токенов) — иначе рассуждение посчитается дважды или не посчитается вовсе; и совпадает ли сумма наших cost за день с расходом в личном кабинете DeepSeek.

Ollama: стоимость ноль, время — главная метрика

Заголовок раздела «Ollama: стоимость ноль, время — главная метрика»

Для локальной модели стоимость токенов — $0, а интересное — время. Родной API Ollama (Usage) отдаёт его по фазам, и из этого получаются метрики главы 11.4 (гл. 11.4):

# illustrative: Ollama native /api/chat metrics -> trace metadata
import ollama # official Python library; pin the version
r = ollama.chat(model=MODEL, messages=messages, options={"num_ctx": 16384})
ns = 1e9
meta = {
"load_s": r.load_duration / ns, # >0 = cold start, exclude from latency stats
"prefill_s": r.prompt_eval_duration / ns, # ~ TTFT without network
"decode_tok_per_s": r.eval_count / (r.eval_duration / ns),
"prompt_tokens": r.prompt_eval_count, # compare with num_ctx: truncation?
"cached_prompt_tokens": getattr(r, "prompt_eval_cached_count", None),
}

load_duration заметно больше нуля — значит, модель загружалась (холодный старт после keep_alive); такие запросы помечаем тегом и не смешиваем с остальными в p95.

Приватность: какой провайдер в каком проекте

Заголовок раздела «Приватность: какой провайдер в каком проекте»

Правило курса: личные данные клиентов и снимок базы магазина никогда не уходят в облачные модели, их обрабатывает локальная модель. Серверы DeepSeek в Китае, но правило действует для любого облачного провайдера.

Проект Провайдер Где трейсы
1. Бот «аналитик магазина» снимок data/shop.db — только Ollama; DeepSeek — только с демо-базой data/demo_shop.db снимок — только self-hosted Langfuse (глава 11.1, гл. 11.1)
2. Ассистент поддержки вопросы из набора по публичной kb/ — DeepSeek, запасной Ollama; настоящие сообщения клиентов — только Ollama набор — можно облако Langfuse; сообщения клиентов — self-hosted или с маскированием
3. Evals DeepSeek для генерации и судьи на публичных данных можно облако
4. Агент-разборщик заказов только Ollama на снимке; DeepSeek — только на демо-базе снимок — только self-hosted

Если в трейсах Проекта 1 или 4 встретится provider=deepseek при работе со снимком базы или в Проекте 2 — на настоящем сообщении клиента, это инцидент, а не метрика. Делаем это проверкой: тест или алерт по metadata.

Нагрузочные прогоны Проектов 2 и 3 — на DeepSeek (deepseek-flash) по вопросам из eval/golden.jsonl: база знаний публичная, вопросы выдуманы, цифры дешёвые и реалистичные для облака. Проекты 1 и 4 — на Ollama со снимком базы локально или на DeepSeek с демо-базой (тогда в README пишем, на чём меряно). Для сравнения можно повторить 20 запросов Проекта 2 на Claude и сравнить стоимость и p95 — разница в цене будет на порядок и больше, и это хорошая строка для README.

  1. Langfuse docs → раздел Observability: обзор, data model (traces, observations, generations, sessions, scores) и Python SDK (≈45 мин). Обращаем внимание на @observe, вложенность, flush() и на то, как задаются user_id/session_id/tags.
  2. В тех же docs → раздел про model usage and cost: как Langfuse считает стоимость, как задать свою цену модели и как передать готовую стоимость (≈20 мин). Запоминаем, что делать с кэш-токенами.
  3. В docs → интеграция с OpenAI SDK (from langfuse.openai import openai) — если наши проекты (или Ollama/LiteLLM) идут через OpenAI-клиент (≈10 мин).
  4. В docs → раздел про дашборды/metrics и про маскирование данных (≈20 мин). Смотрим, какие графики есть из коробки (cost, latency, по моделям) и как строится свой.
  5. LLM CLI → установка, «Usage», «Logging to SQLite», плагины (≈25 мин). Ставим и делаем 5 запросов, затем llm logs -n 5 -u.
  6. DeepSeek: Create Chat Completion → схема объекта usage в ответе (≈10 мин). Выписываем все поля, включая кэш и reasoning_tokens, и находим, где они лежат в реальном ответе (print(resp.usage.model_dump())).
  7. Ollama: Usage → метрики времени и токенов родного API (≈10 мин). Важно: длительности в наносекундах.
  8. (Если используется OpenRouter) Usage Accounting → usage.cost, cached_tokens, reasoning_tokens (≈10 мин).
  9. В docs LLM CLI находим, как подключить OpenAI-совместимую модель (DeepSeek) и плагин llm-ollama для локальной (≈10 мин); перед установкой плагина — обычная проверка стороннего кода.
  10. (Справочно) В документации OpenTelemetry находим страницу про семантические соглашения GenAI — только просмотреть, какие атрибуты стандартизованы (≈10 мин).
  • Плоские трейсы. Если всё записано одним generation без spans, не понять, поиск виноват или модель. Минимум: retrieve и answer — отдельно.
  • Нет flush() в скриптах — evals «прошли», а в Langfuse пусто или половина.
  • Двойной учёт стоимости: обёртка клиента посчитала, и вручную записана ещё одна generation на тот же вызов. Сумма в дашборде удвоится.
  • Стоимость без кэша. Если Langfuse считает по прайсу без учёта cache_read, экономия из гл. 11.2 на дашборде не видна, а стоимость завышена.
  • Среднее вместо перцентилей и перцентили на 15 точках.
  • Трейсы со снимком базы или настоящими сообщениями клиентов в облаке или на скриншотах в README.
  • Высокая кардинальность тегов: не кладём в tags уникальные значения (id запроса, текст вопроса) — это для metadata.
  • В async-коде контекст трейса передаётся через contextvars; если задача запускается в отдельном потоке или пуле, вложенность может потеряться — проверяем дерево в UI.
  • Сэмплирование: при большом трафике трейсят не всё. На нашем масштабе — трейсим 100%.
  • DeepSeek по прайсу Langfuse — без hit/miss и пиковых часов стоимость окажется неверной в обе стороны. Передаём свою.
  • Ollama: наносекунды в длительностях родного API и холодный старт (load_duration) внутри p95.
  • Трейс с provider=deepseek на снимке базы или на сообщении клиента — утечка, а не просто запись.
  • Поднять Langfuse (self-hosted из гл. 11.1 или облако для демо-данных и вопросов из набора). Создать проект на каждый из четырёх учебных проектов или один проект с тегом project:<name> — выбрать и записать почему.
  • Проект 2 (ассистент поддержки): spans plan_query, retrieve (в output — url и score чанков), rerank, generation answer на DeepSeek со своей стоимостью (deepseek_cost из главы 11.2: hit/miss + пиковые часы) и usage_details с кэшем и reasoning. Задать 5 вопросов из набора (например, «можно ли вернуть вскрытый чай?», «какая гарантия на LAMP-LIGHT-MINI?»), открыть трейсы: дерево правильно вложено, стоимость совпадает с нашим расчётом.
  • Проект 2, настоящие сообщения: в metadata трейса — результат contains_personal_data и причина handoff, если она была. Такие трейсы создаёт только Ollama и пишет только self-hosted Langfuse или маскирование до отправки.
  • Проект 1 (Telegram-бот): trace на сообщение, session_id = id чата, span на каждый read-only инструмент (аргументы и число строк, но не сами суммы — маскирование). Снимок базы — только Ollama и только self-hosted Langfuse; DeepSeek — только с демо-базой. В metadata — provider, load_s, decode_tok_per_s.
  • Добавить 👍/👎 в бот как score на трейс.
  • Проект 3 (evals): каждый прогон кейса — trace с тегом эксперимента; результат проверки — score. flush() в конце скрипта.
  • Проект 4 (агент): span на итерацию, generation на каждый вызов модели, span на tool call; в metadata трейса — число шагов и причина остановки.
  • В каждый трейс — фактически ответивший провайдер и модель (видно фолбэки DeepSeek → OpenRouter → Ollama из главы 11.2).
  • Сверить сумму cost за день с расходом в личном кабинете DeepSeek; расхождение больше нескольких процентов — разобраться (кэш, пиковые часы, reasoning).
  • Проверка приватности: тест или запрос к Langfuse, который находит трейсы проектов 1 и 4 со снимком базы и трейсы Проекта 2 с настоящими сообщениями клиентов, где provider != "ollama". Должно быть ноль.
  • Нагрузка для цифр: прогнать по 50–100 запросов в каждом проекте (вопросы из eval-наборов; для агента — 20 случаев из гл. 10.3).
  • Дашборд в Langfuse: средняя стоимость ответа, p50/p95 латентности, доля ошибок, разбивка стоимости по моделям — по каждому проекту.
  • Сверка: посчитать p50/p95 и среднюю стоимость самим — скрипт trace_metrics.py, который берёт данные из своего лога (или экспорта Langfuse) и печатает таблицу. Цифры должны совпасть с дашбордом в пределах округления.
  • Найти в трейсах один плохой ответ и по дереву определить причину (retrieval / промпт / модель / инструмент). Записать в журнал ошибок (гл. 8.4).
  • Поставить LLM CLI, подключить DeepSeek как OpenAI-совместимую модель и Ollama через плагин, сравнить из терминала две формулировки системного промпта на 3 вопросах из набора на обеих, посмотреть llm logs -u.
  • Таблица в README каждого проекта:
    | Метрика | Значение | На скольких запросах |
    |---|---|---|
    | Средняя стоимость ответа | $0.0042 | 100 |
    | Латентность p50 / p95 | 2.1 с / 5.8 с | 100 |
    | Доля ошибок | 1% | 100 |
  • Во всех четырёх проектах каждый запрос даёт трейс с вложенными шагами, моделью, токенами и стоимостью.
  • Для каждого проекта известны средняя стоимость ответа, p50/p95 латентности и доля ошибок, с указанием объёма выборки; цифры в README.
  • Трейсы со снимком базы и настоящими сообщениями клиентов не покидают нашу машину и созданы только Ollama; публичные скриншоты — на демо-базе и вопросах из набора.
  • Стоимость DeepSeek в трейсах считается с кэшем и пиковыми часами и сходится с личным кабинетом.

Вопрос 1

Чем generation отличается от span в Langfuse?

Span — любой шаг с длительностью; generation — span, представляющий вызов модели, с моделью, параметрами, usage и стоимостью. Стоимость и токены агрегируются по generations.

Вопрос 2

Почему для латентности LLM-ответов смотрят p95, а не среднее?

Распределение длиннохвостое: среднее прячет медленные ответы, которые видит заметная доля пользователей. p95 показывает опыт «каждого двадцатого».

Вопрос 3

В каких проектах трейсы содержат чувствительные данные, и что с этим делать?

В Проектах 1 и 4 — данные из снимка базы (заказы, суммы, оплаты, комментарии клиентов в events); в Проекте 2 — настоящие сообщения клиентов с телефонами, e-mail и номерами заказов. Только self-hosted Langfuse локально или маскирование (contains_personal_data); демо и скриншоты — на демо-базе и вопросах из набора.

Вопрос 4

Что не так в этом скрипте прогона evals?

from langfuse import observe
@observe(name="eval-case")
def run_case(case): ...
for case in cases:
run_case(case)
print("done")

Нет langfuse.flush() в конце: SDK отправляет данные в фоне, скрипт завершается, и последние трейсы теряются.

Вопрос 5

Что лучше положить в tags, а что в metadata: имя эксперимента chunking-B, id запроса, версия промпта v3, текст вопроса?

В tags — значения с малой кардинальностью для фильтрации: chunking-B, v3. Id запроса и текст вопроса — в metadata/input.

Вопрос 6

Дашборд показывает, что средняя стоимость ответа в 2 раза выше ручного расчёта. Какие две причины могут к этому привести?

Двойная запись generation (обёртка + ручная); автоматическая стоимость без учёта кэш-скидки; неверная цена модели в таблице Langfuse; фолбэк на более дорогую модель.

Вопрос 7

Что такое TTFT и почему это важно для Telegram-бота со стримингом?

Time to first token — время до первого токена ответа. При стриминге клиент видит, что ответ пошёл, и общая длительность воспринимается терпимее; TTFT — главный показатель «отзывчивости».

Вопрос 8

По трейсу видно: вопрос был про гарантию на «Свет мини», retrieve вернул 8 чанков, все со страницы лампы «Свет», ответ неверный (12 месяцев вместо 6). Где проблема и что дальше?

  • a) в модели — сменить на более сильную
  • b) в retrieval — смотреть запрос, plan_query, чанкинг и метаданные
  • c) в промпте — добавить «будь внимательнее»
  • d) в трейсинге

b. Модель ответила по тому, что ей дали; надо разбирать запрос к поиску, переформулировку, фильтры по метаданным (гл. 5.1) и чанкинг — похожие товары различаются гарантией, и поиск должен распознать нужный товар.

Вопрос 9

p95 посчитан по 12 запросам. Насколько этой цифре можно верить?

Почти никак: на 12 точках p95 практически равен максимуму и сильно зависит от одного выброса. Нужно больше запросов и указание размера выборки.

Вопрос 10

Зачем LLM CLI, если есть Langfuse?

Для быстрых экспериментов из терминала без кода: промпт против нескольких моделей, всё автоматически логируется в SQLite с токенами. Langfuse — для приложения.

Вопрос 11

Почему свой подсчёт стоимости лучше передавать в Langfuse явно, а не полагаться на автоматический?

Автоматический расчёт может не знать цену новой модели, кэш-множителей или локальной модели. Своя формула (гл. 11.2) согласована с инвойсом и учитывает кэш.

Вопрос 12

В трейсе DeepSeek prompt_tokens=5000, prompt_cache_hit_tokens=4000. Почему стоимость нельзя считать как 5000 × цена_input, и что ещё влияет на цену этого запроса, кроме токенов?

4000 токенов — попадание в кэш, у DeepSeek оно в десятки раз дешевле промаха; считать надо hit и miss отдельно. Ещё на цену влияет время запроса: вне пиковых часов UTC всё вдвое дешевле.

Вопрос 13

У запроса к Ollama eval_count=300, eval_duration=6000000000. Сколько tokens/s? А если load_duration равен 4 секундам — что это значит для статистики латентности?

300 / 6 с = 50 tokens/s (длительности в наносекундах). load_duration в 4 с — холодный старт: модель загружалась в память; такой запрос помечают и исключают из p95 или считают отдельно.

Вопрос 14

В Проекте 4 на снимке базы в Langfuse видна generation с model=deepseek-flash. Что это значит и что делать?

Данные снимка базы (заказы, оплаты, комментарии клиентов) ушли в облако DeepSeek — инцидент приватности: где-то фолбэк или конфиг пустил приватный путь в облако. Остановить, найти причину (цепочка фолбэка, переменная окружения), добавить тест, что приватный путь только локальный.

Теперь есть линейка, которой можно мерить любую замену модели. В главе 11.4 · Локальная модель через Ollama + свои evals подключаем локальную модель и сравниваем её с облачной по качеству, латентности и стоимости — эти же метрики попадут в таблицу экспериментов.