11.3 · Трейсинг и стоимость одного ответа
Неделя 11 · Продакшен · ⏱ ~4 ч · Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»Клиент пишет «бот ответил ерунду», и без трейса не узнать, что пошло не так: поиск не нашёл нужный чанк, модель проигнорировала контекст, инструмент вернул пустоту или сработал фолбэк на слабую модель. С деньгами то же: «почему вчера потратили втрое больше» без трейсов не объяснить. На собеседованиях observability — обязательный пункт system design: «как вы узнаёте, что качество упало?», «сколько стоит один ответ?», «какой у вас p95?».
Логи vs метрики vs трейсы
Заголовок раздела «Логи vs метрики vs трейсы»Кусочки наблюдаемости у нас уже есть: в Проекте 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) часто есть готовое.
Как инструментировать
Заголовок раздела «Как инструментировать»Чтобы такое дерево появилось, код нужно инструментировать — отметить, где начинается и заканчивается каждый шаг. Три уровня, от простого к гибкому:
- Обёртка клиента.
from langfuse.openai import openai— drop-in замена OpenAI SDK, каждый вызов становится generation с токенами и стоимостью. Работает и для Ollama (гл. 11.4), и для LiteLLM, если они идут через OpenAI-клиент. - Декоратор
@observeна своих функциях — span на функцию, вложенность строится автоматически по стеку вызовов (в том числе в async-коде). - Ручные 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 statisticslat = [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 — ассистент продолжает отвечать.
LLM CLI: эксперименты из терминала
Заголовок раздела «LLM CLI: эксперименты из терминала»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 пишет их в файл в профиле пользователя) — можно вместо этого передавать через переменные окружения.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Трейсинг от провайдера почти не зависит: Langfuse всё равно, кто ответил. Зависит от провайдера usage и стоимость — у каждого свои поля, и именно на них ломается «стоимость одного ответа». Факты проверены по документации DeepSeek, Ollama и OpenRouter в ноябре 2026.
Откуда брать цифры для generation
Заголовок раздела «Откуда брать цифры для generation»| Что нужно в трейсе | 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). Это удобно, но с собственным расчётом стоит сверять хотя бы выборочно.
Стоимость DeepSeek в Langfuse
Заголовок раздела «Стоимость DeepSeek в Langfuse»Встроенной таблице цен 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 costfrom datetime import datetime, timezonefrom 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 metadataimport ollama # official Python library; pin the version
r = ollama.chat(model=MODEL, messages=messages, options={"num_ctx": 16384})ns = 1e9meta = { "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.
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Langfuse docs → раздел Observability: обзор, data model (traces, observations, generations, sessions, scores) и Python SDK (≈45 мин). Обращаем внимание на
@observe, вложенность,flush()и на то, как задаютсяuser_id/session_id/tags. - В тех же docs → раздел про model usage and cost: как Langfuse считает стоимость, как задать свою цену модели и как передать готовую стоимость (≈20 мин). Запоминаем, что делать с кэш-токенами.
- В docs → интеграция с OpenAI SDK (
from langfuse.openai import openai) — если наши проекты (или Ollama/LiteLLM) идут через OpenAI-клиент (≈10 мин). - В docs → раздел про дашборды/metrics и про маскирование данных (≈20 мин). Смотрим, какие графики есть из коробки (cost, latency, по моделям) и как строится свой.
- LLM CLI → установка, «Usage», «Logging to SQLite», плагины (≈25 мин). Ставим и делаем 5 запросов, затем
llm logs -n 5 -u. - DeepSeek: Create Chat Completion → схема объекта
usageв ответе (≈10 мин). Выписываем все поля, включая кэш иreasoning_tokens, и находим, где они лежат в реальном ответе (print(resp.usage.model_dump())). - Ollama: Usage → метрики времени и токенов родного API (≈10 мин). Важно: длительности в наносекундах.
- (Если используется OpenRouter) Usage Accounting →
usage.cost,cached_tokens,reasoning_tokens(≈10 мин). - В docs LLM CLI находим, как подключить OpenAI-совместимую модель (DeepSeek) и плагин
llm-ollamaдля локальной (≈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, generationanswerна 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). Где проблема и что дальше?
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 подключаем локальную модель и сравниваем её с облачной по качеству, латентности и стоимости — эти же метрики попадут в таблицу экспериментов.