Проблема: агент — чёрный ящик

В обычном бэкенде упавший запрос оставляет стектрейс, и по логам видно, что произошло. С LLM-агентом всё иначе: он не «падает», он тихо ошибается — уверенно выдаёт неправильный ответ. И причина спрятана в цепочке невидимых шагов: какой промпт реально ушёл в модель после подстановки, что вернул retriever, как агент решил вызвать инструмент, что тот ответил, как модель это проинтерпретировала.

Три вопроса, на которые обычные логи не отвечают, а observability для LLM — да:

  • Что пошло не так? На каком шаге reasoning свернул не туда — плохой retrieval, кривой tool-ответ, галлюцинация?
  • Сколько это стоит? Токены и деньги на каждый вызов, латентность каждого шага — где узкое место.
  • Стало ли лучше? Поменял промпт — качество выросло или деградировало? Без измерения это гадание.
ℹ️ Observability = «видеть внутренности системы по её выходам»

Термин из SRE: способность понять внутреннее состояние системы, наблюдая её внешние сигналы (трейсы, метрики, логи). Для LLM-агентов ключевой сигнал — трейс: полное дерево вложенных вызовов одного запроса. LangSmith специализируется именно на LLM-трейсах: он понимает промпты, токены, tool-вызовы, а не просто строки.

Что такое LangSmith

LangSmith — платформа для наблюдаемости и оценки LLM-приложений от команды LangChain. Три роли, вокруг которых построен этот урок:

  • Трейсинг (observe). Записывает каждый запуск агента как дерево runs — с промптами, ответами, токенами, латентностью, ошибками.
  • Дебаг (debug). Даёт разобрать любой трейс по шагам, найти сбойный узел, переиграть промпт в Playground.
  • Оценка (evaluate). Прогоняет систему на датасете и измеряет качество автоматическими и человеческими оценщиками — до и после изменений.
⚠️ LangSmith ≠ только для LangChain

Частое заблуждение: «LangSmith работает лишь с LangChain/LangGraph». Нет — трейсить можно любой Python/JS-код: голые вызовы OpenAI/Anthropic SDK, кастомные функции, что угодно. С LangChain/LangGraph трейсинг включается автоматически, а для остального есть декоратор @traceable. Это отдельный SaaS-продукт (есть облако и self-hosted), а не часть фреймворка.

В модуле 03 ты уже подключал LangSmith к LangGraph-агенту ради отладки графа. Здесь смотрим шире — как на production-инструмент: не только «посмотреть трейс», но и построить измеримый процесс контроля качества.

Модель данных: runs, trace, spans

Чтобы читать трейсы, надо понять их структуру. Всё строится на одной сущности — run (запуск). Run — это одна единица работы: вызов LLM, вызов инструмента, шаг цепочки. Runs вкладываются друг в друга, образуя дерево. Всё дерево одного запроса — это trace.

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
TRACE — одно дерево одного запроса пользователя AgentExecutor (root run) chain · вход: вопрос пользователя · 3.9s · 4 210 токенов · $0.021 LLM call — решает вызвать инструмент llm · 0.8s · 1 120 токенов · выход: tool_call search(...) Tool: web_search tool · 1.6s · вход: "рынок EV 2025" · выход: 5 сниппетов Retriever (вложенный вызов) retriever · 0.3s · вернул 5 документов LLM call — формулирует финальный ответ llm · 1.2s · 1 890 токенов · выход: текст ответа Каждый run хранит: • тип (chain / llm / tool / retriever) • inputs — что пришло на вход • outputs — что вернул • latency — время шага • tokens + cost (для llm) • error — если упал • metadata + tags родитель ← ссылка → дети складываются в дерево (trace) → видно, ГДЕ именно сбой

Такое дерево сразу отвечает на «где сломалось»: раскрываешь трейс, идёшь по узлам и видишь, что, например, web_search вернул пустоту → следующий LLM-call галлюцинировал на пустом контексте. В плоских логах это заняло бы час; в трейсе — 20 секунд.

Роль 1: трейсинг — как подключить

Хорошая новость: включение трейсинга почти не требует кода. LangSmith читает переменные окружения и начинает записывать автоматически.

Включаем трейсинг через переменные окружения
bash
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="ls-..."         # ключ из настроек LangSmith
export LANGSMITH_PROJECT="my-agent-prod"  # проект = группа трейсов
# (в старых версиях переменные назывались LANGCHAIN_TRACING_V2 и т.д.)

Дальше — два сценария. С LangChain/LangGraph ничего писать не надо: трейсы полетят сами. Для любого другого кода — оборачиваешь функции декоратором @traceable, и они появляются в дереве как runs.

Трейсинг произвольного кода (без LangChain) через @traceable
python
from langsmith import traceable
from openai import OpenAI

client = OpenAI()   # вызовы OpenAI SDK LangSmith подхватит автоматически

@traceable                       # эта функция станет run в трейсе
def research(topic: str) -> str:
    docs = search_web(topic)     # если тоже @traceable — вложится как дочерний run
    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": f"Суммаризируй: {docs}"}],
    )
    return resp.choices[0].message.content

@traceable
def search_web(topic: str) -> list[str]:
    ...

# просто вызываешь — трейс с деревом runs появится в LangSmith
research("рынок EV 2025")
✅ Теги и метаданные — чтобы находить нужные трейсы

В проде трейсов тысячи. Навешивай tags (например, версия промпта, тип запроса) и metadata (user_id, session_id) — по ним потом фильтруешь: «покажи все медленные трейсы версии prompt-v3 у пользователей из группы B». Без этого observability в проде превращается в стог сена. Это ровно то, что ты делал в LangGraph-уроке модуля 03, — здесь тот же приём для любых систем.

Роль 2: дебаг по трейсу

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

1. Фильтр   → находим трейс: по ошибке / высокой латентности / фидбеку юзера
2. Дерево   → раскрываем runs, идём по шагам сверху вниз
3. Узел     → находим сбойный: пустой retrieval? кривой tool-ответ? плохой промпт?
4. Inspect  → смотрим ТОЧНЫЙ вход/выход этого run (промпт после подстановки!)
5. Playground → правим промпт прямо в UI, прогоняем, сравниваем результат
6. Fix      → переносим исправление в код, проверяем на новом трейсе

Что именно смотреть в узлах — по типам:

Тип runНа что смотреть при дебаге
llmФинальный промпт после подстановки переменных, ответ, токены. Часто баг — в самом промпте, а не в модели
toolЧто реально передали инструменту и что он вернул. Мусор на входе LLM часто отсюда
retrieverКакие документы вернулись и релевантны ли. Пустой/нерелевантный retrieval → галлюцинация
chainЛатентность и стоимость поддерева — где узкое место по времени и деньгам
⚠️ Смотри промпт ПОСЛЕ подстановки

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

Роль 3: оценка качества

Трейсинг и дебаг отвечают на «что случилось сейчас». Но production-агента надо улучшать, не ломая. Здесь ключевой вопрос: «я поменял промпт — стало лучше или хуже?» Глазами на трёх примерах это не проверить. Нужна систематическая оценка на датасете.

Три сущности:

  • Dataset — набор примеров {вход → ожидаемый выход} (golden examples). Можно собрать вручную, синтетически или из реальных трейсов прода — прямо в LangSmith добавить интересный трейс в датасет.
  • Evaluator — функция, ставящая оценку выходу: точное совпадение, кастомная логика или LLM-as-judge (модель оценивает по критериям).
  • Experiment — прогон твоей системы на датасете со всеми оценщиками. Результаты разных версий сравниваются бок о бок.
Оценка на датасете: experiment + evaluators
python
from langsmith import Client, evaluate

client = Client()

# 1. Датасет golden-примеров (создаётся раз; можно пополнять из прод-трейсов)
dataset = client.create_dataset("qa-golden")
client.create_examples(
    dataset_id=dataset.id,
    inputs=[{"question": "Столица Франции?"}],
    outputs=[{"answer": "Париж"}],
)

# 2. Оценщик: LLM-as-judge — правильный ли ответ по смыслу
def correctness(run, example) -> dict:
    predicted = run.outputs["answer"]
    expected  = example.outputs["answer"]
    score = llm_judge(predicted, expected)   # модель судит: 0..1
    return {"key": "correctness", "score": score}

# 3. Эксперимент: прогоняем систему на всём датасете
evaluate(
    lambda inputs: my_agent(inputs["question"]),   # тестируемая система
    data="qa-golden",
    evaluators=[correctness],
    experiment_prefix="prompt-v3",                 # версия для сравнения
)
# → в UI: таблица оценок, сравнение prompt-v3 vs prompt-v2 по метрикам
🎯 Так observability закрывает петлю улучшения

Круг замыкается: прод-трейсы → находишь плохие кейсы → добавляешь их в датасет → меняешь промпт → эксперимент показывает, что метрика выросла и старые кейсы не сломались (это и есть regression testing) → катишь в прод → снова трейсишь. Без оценки ты улучшаешь вслепую; с ней — по числам. Детали оценщиков (LLM-as-judge, RAGAS, A/B) — в разделе «Оценка качества».

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

❌ Ошибка 1: трейсинг «на потом»

Подключать observability, когда прод уже горит, — поздно: сбойные запросы уже не воспроизвести. Включай трейсинг с первого дня, это две переменные окружения.

❌ Ошибка 2: трейсы без тегов и метаданных

Тысячи безымянных трейсов невозможно фильтровать. Без user_id, версии промпта, типа запроса нужный кейс не найти. Размечай сразу.

❌ Ошибка 3: «улучшил промпт» на глаз

Правка промпта без прогона на датасете — рулетка: чинишь один кейс, ломаешь пять. Любое изменение → эксперимент с оценщиками до/после.

❌ Ошибка 4: чувствительные данные в трейсах

Трейсы содержат реальные вход/выход — там могут оказаться PII, токены, секреты. Настраивай маскирование/фильтрацию и учитывай приватность (для строгих требований — self-hosted или Langfuse).

Шпаргалка

LangSmith — всё в одном месте
text
ЗАЧЕМ: агент — чёрный ящик, тихо ошибается. Нужна observability под LLM.
  3 вопроса: что сломалось? · сколько стоит? · стало ли лучше?

LANGSMITH = трейсинг + дебаг + оценка (от LangChain, но НЕ только для него)
  работает с любым кодом: LangChain/LangGraph — авто, остальное — @traceable

МОДЕЛЬ ДАННЫХ:
  run   = одна единица работы (llm / tool / retriever / chain)
  trace = дерево runs одного запроса
  каждый run: type · inputs · outputs · latency · tokens+cost · error · tags/meta

① ТРЕЙСИНГ (подключение):
  env: LANGSMITH_TRACING=true · LANGSMITH_API_KEY · LANGSMITH_PROJECT
  @traceable на функции · теги+метаданные (user_id, версия промпта) — ОБЯЗАТЕЛЬНО

② ДЕБАГ: фильтр → дерево → сбойный узел → inspect (промпт ПОСЛЕ подстановки!) → Playground → fix
  llm: финальный промпт+ответ · tool: вход/выход · retriever: релевантность · chain: latency/cost

③ ОЦЕНКА: dataset (golden, из прод-трейсов) + evaluator (LLM-as-judge/custom) + experiment
  меняешь промпт → эксперимент до/после → метрика выросла, регрессий нет → в прод

ПЕТЛЯ: прод-трейс → плохой кейс → в датасет → правка → эксперимент → прод → трейс

ГРАБЛИ: трейсинг «на потом» · без тегов · правка промпта на глаз · PII в трейсах

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

Задание: подключи observability к агенту

  1. Заведи бесплатный проект в LangSmith, включи трейсинг тремя переменными окружения и прогони любого агента из модулей 03–05. Открой трейс, разверни дерево runs.
  2. Найди в трейсе самый долгий и самый «дорогой» (по токенам) шаг. Где узкое место — LLM-вызов, инструмент, retrieval?
  3. Оберни в @traceable кусок кода без LangChain (голый вызов OpenAI/Anthropic SDK) и убедись, что он появился в дереве.
  4. Навесь на трейсы теги (версия промпта) и метаданные (user_id). Отфильтруй трейсы по тегу в UI.
  5. Сломай что-нибудь специально (испорти промпт-шаблон, чтобы переменная не подставлялась) и найди это по трейсу. Что показывает узел llm — промпт до или после подстановки?
  6. Собери мини-датасет из 5 примеров, напиши простой evaluator (точное совпадение или LLM-as-judge) и запусти evaluate для двух версий промпта. Какая победила по метрике?
  7. Со звёздочкой: добавь интересный трейс из «прода» (своего теста) в датасет прямо из UI и включи его в regression-набор.

Что дальше

LangSmith — облачный SaaS. Если нужны self-hosted, полный контроль над данными и open-source — есть Langfuse с той же идеологией трейсов и оценки. Следующий урок — про него и про то, когда какой инструмент выбрать.