Урок курса

Обзор инструментов для LLMOps

LLMOps & Evaluation: из песочницы в продакшн

Чтобы эффективно управлять LLM в продакшене, одной методологии мало – нужны удобные инструменты. К счастью, экосистема LLMOps-инструментов активно развивается. Рассмотрим основные категории и представителей, популярных в 2025 году.

Инструменты наблюдения и мониторинга LLM

LangSmith (от разработчиков LangChain): LangSmith – это платформа для трассировки, отладки и мониторинга приложений на базе LLM. Она позволяет собирать подробные логи взаимодействия модели: шаги агента, вызовы инструментов, промежуточные мысли (chain-of-thought), финальные ответы. Визуальный интерфейс LangSmith помогает разработчикам просматривать шаг за шагом, как агент пришёл к своему ответу, что особенно полезно при отладке сложных цепочек. Кроме наблюдения, LangSmith включает модуль оценки качества (evals): можно сохранять сессии пользователей и затем прогонять их через разные версии модели, сравнивая результаты. Также LangSmith поддерживает сбор фидбэка: эксперты или пользователи могут помечать ответы как корректные/некорректные, что сохраняется в базе для дальнейшей работы. Важная функция – дашборды бизнес-метрик: платформа позволяет настраивать метрики типа стоимости, средней длины ответа, процента неудачных попыток, и отслеживать их в реальном времени. LangSmith интегрируется не только с LangChain-библиотеками, но и может работать как отдельный сервис (SDK на Python/JS) – то есть подходит под любую кастомную LLM-аппликацию. В продакшене с помощью LangSmith команда получает возможность быстро обнаруживать “выпадения” модели (странные ответы, ошибки в цепочках), видеть распределение латентности и в целом уверенно управлять своим LLM-приложением. По сути, LangSmith закрывает пробел стандартных APM (Application Performance Monitoring) систем, предлагая специализированный мониторинг для ИИ-агентов.

Helicone: Helicone – open-source платформа наблюдения за LLM, которая стала популярной благодаря простоте интеграции. Достаточно перенаправить вызовы к API вашей модели через прокси Helicone (буквально поменять базовый URL), и Helicone начнёт логировать каждый запрос и ответ, вместе с метаданными: временем, пользователем, количеством токенов, стоимостью. Helicone ориентирован на разработчиков, фокусируясь на метриках производительности и затрат. Ключевые возможности Helicone:

  • Лёгкая установка: разворачивается локально или в облаке, код открытый – удобно для тех, кто не может отправлять данные внешнему SaaS.

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

  • Аналитика стоимости и использования: интерфейс показывает, сколько токенов и денег “сжёг” каждый пользователь, какая команда чаще всего вызывает длинные ответы и т.п. Это помогает оптимизировать продуктовую стратегию (например, ограничить бесплатным пользователям длину запросов).

  • Версионирование и эксперименты: Helicone позволяет проводить A/B тесты prompt’ов или моделей. Вы можете отправлять часть трафика на одну модель, часть на другую и сравнивать результаты через встроенные метрики.

  • Безопасность и гибкость: поддерживается хранение API-ключей, интеграция с различными LLM-провайдерами (OpenAI, Anthropic, Azure и др.).
    В сравнении с другими, Helicone менее фокусирован на глубокой оценке качества контента, но даёт мощный инструмент для детального логирования и снижения издержек.

HoneyHive: HoneyHive – коммерческая платформа “всё в одном” для LLMOps, особо сильная в части оценки качества и тестирования. Она закрытая (не open-source), но за счёт инвестиций (в 2025 у них крупный seed-раунд) развивается очень активно. HoneyHive предлагает:

  • Масштабируемые evals: можно задать сотни тестовых кейсов (датасеты вопросов) и прогнать свою LLM-систему, получив отчёт по каждому – где ответ корректен, где сбой. Причём есть библиотека готовых метрик и даже LLM-as-a-judge – автоматическая оценка ответов другой моделью по заданным критериям.

  • Наблюдаемость агентов (Tracing): как и LangSmith, HoneyHive визуализирует цепочки размышлений, шаги вызова инструментов. Их SDK совместим с OpenTelemetry стандартом, что позволяет легко встраивать логирование в существующие приложения.

  • Мониторинг и оповещения: настраиваемые дашборды по latency, точности, частоте отказов. Можно задать алерты – например, “если доля успешных ответов падает ниже 90% или средняя задержка растет выше 5 сек, прислать уведомление”.

  • Управление артефактами: HoneyHive имеет интерфейс для совместной работы над prompt-шаблонами, датасетами и инструментами. По сути, это как бы “репозиторий” ваших компонентов LLM-приложения: например, вы можете хранить несколько версий system prompt, сравнивать их; или каталогизировать используемые в агенте инструменты (с описанием, зачем нужны) – полезно для команды из нескольких человек.

  • CI/CD интеграция: одна из фишек – возможность запускать тестовые прогоны (evals) автоматически при каждом обновлении prompt или кода (через webhook в ваш CI). Это реализует концепцию “evaluation-driven development”, когда любое изменение сразу проверяется на наборе кейсов, и вы видите, не ухудшилось ли качество.
    HoneyHive, позиционируясь как enterprise-решение, особенно ценен командам, у которых высокие требования к надежности. С ним можно убедиться, что каждый релиз модели проходит стандартизированные тесты, а в продакшене – что никакой новый тип запроса не выводит систему из строя без вашего ведома.

Langfuse: Ещё один представитель, заслуживающий упоминания, – Langfuse. Это open-source платформа наблюдаемости и отладки, близкая по идеологии к Helicone, но с функциональностью, приближающейся к HoneyHive. Langfuse позволяет:

  • Логировать запросы, ответы, промежуточные шаги агентов (схоже с LangSmith/HoneyHive).

  • Хранить версии промптов и сравнивать их эффективность.

  • Подключать оценочные метрики и визуализировать их.

  • Самое ценное – Langfuse можно развернуть локально, что важно для команд с повышенными требованиями к приватности данных.
    Langfuse интегрируется с популярными оркестраторами (LangChain, LlamaIndex), что упрощает подключение логирования. Если Helicone – это скорее “чёрный ящик” для логов через прокси, то Langfuse больше подходит, когда вы хотите гибко настроить, какие данные и как собирать, и иметь к ним прямой доступ (SQL или API к журналам). В 2025 Langfuse используется энтузиастами LLMOps, кто хочет мощный инструмент, но без зависимости от SaaS.

Инструменты автоматического тестирования и оценки качества LLM

OpenAI Evals: Это фреймворк от OpenAI с открытым исходным кодом, предназначенный для оценки моделей и их сравнения. OpenAI Evals позволяет создавать так называемые “evals” – тесты, где вы определяете: какие промпты подать на вход, что считать правильным или желаемым выходом, и метрику оценки (точное совпадение, наличие ключевого слова, оценка другим LLM и т.д.). С помощью Evals можно, к примеру, проверить: “Насколько модель правдиво отвечает на вопросы из базы фактов?” или “Как часто модель нарушает заданную инструкцию в таких-то сценариях?”. Фреймворк поставляется с библиотекой готовых оценочных наборов (benchmark’ов) – например, тесты на математические задачи, кодогенерацию, безопасность. В 2024–2025 OpenAI также добавил интеграцию Evals в свой веб-интерфейс: можно запускать оценку прямо через OpenAI dashboard, загружая свои данные. Особенность OpenAI Evals – сообщество: множество инженеров по всему миру делятся своими evals, пополняя общий реестр. Поэтому если вы вводите модель в новую предметную область, шанс есть найти готовый набор вопросов для тестирования. Использование Evals важно в LLMOps цикле при обновлении модели или prompt’ов: прежде чем выкатить в продакшн, вы можете быстро “прогнать” новую версию через десяток ключевых сценариев, убедиться, что она не регрессировала. Конечно, OpenAI Evals не привязан только к моделям OpenAI – им можно тестировать и Claude, и свои модели (но для своих – придётся реализовать адаптер). Фреймворк больше ориентирован на разработчиков: конфигурация теста идёт в коде (Python) или YAML, результаты можно логировать в файлы или даже W&B. Summing up, OpenAI Evals стал de-facto стандартом для создания настраиваемых тестов качества LLM, особенно в тех командах, которые серьёзно следят за метриками при каждой итерации.

Ragas: Ragas – это open-source библиотека (Python), созданная специально для оценки приложений на основе Retrieval-Augmented Generation (RAG). Название намекает: RAG + as (как сервис). Основная идея Ragas: предоставить разработчикам набор метрик и инструментов для измерения качества LLM-ответов в связке с retrieval. Когда LLM отвечает пользователю, опираясь на найденные документы, хочется понимать:

  • Насколько ответ точно отражает информацию из источников?

  • Не упустил ли агент важный документ?

  • Насколько релевантны были найденные пассы в базе знаний?
    Рagas умеет автоматически оценивать такие вещи. В частности, в библиотеке есть метрики на основе LLM (LLM-критики), которые читают ответ и исходный контекст и выставляют оценку “правильности по источникам”. Также есть традиционные метрики типа precision/recall для поиска. Дополнительно Ragas может генерировать тестовые случаи автоматически: например, на основе ваших реальных данных сформировать вопросы, чтобы затем проверить, как система на них ответит. Интеграция Ragas с LangChain позволяет прямо на продакшен-логах запускать оценку: вы экспортируете трассы (включающие запрос, найденные документы, ответ модели), скармливаете их Ragas – и он выдает вам численные показатели качества. Это сильно экономит время, ведь не нужно вручную проверять сотню диалогов – Ragas подсветит самые проблемные. Разработчики позиционируют Ragas как средство внедрения обратной связи в цикл разработки: по сути, вы улучшаете RAG-систему, наблюдая за метриками Ragas до и после изменений. Библиотека молодая, но активно набирает популярность в open-source сообществе RAG-разработчиков.

Promptfoo: Promptfoo – интересный инструмент для тех, кто исповедует подход “test-driven prompt engineering”. Это утилита (CLI и библиотека) для автоматизированного тестирования промптов и моделей. С помощью Promptfoo вы можете задавать сценарии теста в виде простого YAML или JSON: перечисляете примеры запросов, указываете, какой ответ ожидается (или хотя бы ключевые слова/фрагменты, которые должны присутствовать или отсутствовать), и запускаете проверку. Утилита сама подставит ваши промпты к модели (поддерживает разные API: OpenAI, локальные модели и др.), получит ответы и сравнит с ожиданием. Например, можно протестировать: “Модель не должна раскрывать конфиденциальные данные при таком-то запросе” – ожидание: ответ содержит фразу отказа. Или “Для математического вопроса ответ должен содержать числовое значение”. Promptfoo хорош тем, что позволяет быстро проверять регрессии: сохранили набор из 50 тест-промптов, и каждый раз перед релизом новой версии прогоняете – видите, все ли проходят. Кроме того, Promptfoo поддерживает сравнение нескольких моделей или вариантов промптов параллельно. Можно одновременно протестировать GPT-4.5 vs Claude 3 vs локальный Mistral на одном и том же наборе вопросов и получить таблицу, где видны сильные и слабые стороны каждого (по вашим критериям). Интеграция в CI тоже возможна: например, если какой-то тест не прошёл, билд провалится – значит, нужно править prompt или логику, прежде чем выпускать обновление. Promptfoo – открытый проект, который стал довольно популярным среди разработчиков маленьких LLM-продуктов и энтузиастов, потому что он лёгковесный и понятный. Он учит тому, что качество LLM-ответов можно и нужно проверять автоматически, насколько это возможно, а не полагаться только на ручное прогоны и удачу.

Платформы для комплексного управления LLM (LLMOps-платформы)

Weights & Biases (W&B): W&B давно известен в мире MLOps как инструмент для трекинга экспериментов и менеджмента моделей. В контексте LLMOps W&B нашёл новое применение: он помогает организовывать работу над prompt-инжинирингом, тонкой настройкой LLM и контролировать метрики качества. С помощью W&B можно:

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

  • Отслеживать эксперименты с промптом визуально: например, вы попробовали 10 разных формулировок подсказки – W&B может сгруппировать результаты (ответы модели) и предоставить интерфейс, где легко увидеть, какой prompt дал лучший результат по выбранной метрике. В 2025 W&B даже добавил специальный UI для LLM, позволяющий сравнивать цепочки: вы видите, на каком шаге цепочки размышлений модель ошиблась в одном варианте и как повелась в другом.

  • Логировать метрики качества: W&B интегрируется с фреймворками типа OpenAI Evals или собственными скриптами, так что результаты автоматических тестов (например, “точность 85% на таком-то наборе вопросов”) можно автоматически отправлять в W&B. Таким образом, у вас в проекте будет история метрик по датам/коммитам – удобно для отчётности и анализа прогресса.

  • Следить за расходом ресурсов: хоть W&B в первую очередь про эксперименты, но есть возможность логировать и инфраструктурные показатели – скажем, время выполнения каждого запроса, нагрузку на GPU, использование памяти. Это помогает в процессе оптимизации модели: сразу видно, что, допустим, модель X дала +2% качества, но потребляет в 3 раза больше памяти – стоит ли игра свеч?

  • Сотрудничество команды: W&B Reports позволяют формировать отчёты или дашборды, где инженеры и менеджеры вместе анализируют результаты. В случае LLMOps это может быть отчёт “Сравнение моделей GPT-4.5 и Gemini на наших пользовательских данных”, с графиками, примерами ответов, выводами – всё в одном месте.
    Всё это делает W&B своеобразным “центром управления полётами” для ваших экспериментов с LLM. Хотя W&B – не специализирован исключительно под LLM (он универсален для ML), его гибкость и уже знакомая многим интеграция делает его полезным и в этом домене. Многие продвинутые команды, выводящие LLM-продукты, подключают W&B, чтобы не утонуть в версиях, метриках и выводах модели при быстром цикле итераций.

Klu.ai: Klu – это пример комплексной LLMOps-платформы “под ключ”, которая предлагает множество возможностей из одного окна. Она ориентирована на команды, строящие генеративные приложения, и стремится дать всё необходимое для быстрого цикла от идеи до продакшена. Что предлагает Klu на август 2025:

  • Поддержка множества LLM-провайдеров: прямо в платформе можно подключить OpenAI (GPT-4/4.5), Anthropic (Claude 2/3), Google Vertex AI (Gemini), свои модели (например, Llama2, Mistral) через облачные сервисы или локально. То есть Klu агностичен к тому, чей LLM вы используете – цель, чтобы вы могли сравнить и выбрать лучшее.

  • Конструктор AI-функционала: интерфейс, где можно проектировать свой AI-модуль – будь то чат-бот, поисковый ассистент, система проверки текста. Можно задавать prompt-шаблоны, добавлять контекст (например, подключить свою базу данных знаний), настраивать пост-обработку ответов. Это похоже на low-code среду для LLM-продуктов.

  • Оптимизация и оценка моделей: Klu имеет встроенные средства для сравнения моделей между собой. Допустим, вы хотите понять, стоит ли fine-tune вашей собственной модели или использовать GPT-4. В Klu можно прогнать оба варианта на одном наборе данных и получить наглядное сравнение метрик, качества ответов. Кроме того, Klu предлагает рекомендации по оптимизации – например, если видит, что можно чуть изменить prompt для лучшего результата, или что ваш датасет перекошен, то подскажет.

  • Непрерывный мониторинг: развернув решение через Klu, вы получаете дашборды с ключевыми параметрами (как и в других инструментах: доля успешных ответов, скорость, пользовательская активность). Особый акцент делается на Trust & Safety – Klu контролирует, чтобы модель не выдавала нежелательный контент. Есть модуль риск-менеджмента: например, проверка на утечку персональных данных или токсичность, с отчетами, насколько ваша система этому подвержена.

  • CI/CD интеграция: Klu поддерживает среду для тестирования изменений – вы можете иметь dev-окружение, где пробуете новую версию модели/настроек, и только убедившись в её качестве, развернуть в прод. Для enterprise есть фичи вроде управления доступом, audit log (кто что изменил), что важно для крупного бизнеса.

  • Примеры готовых кейсов: на Klu есть шаблоны популярных приложений (копилоты для кода, чат для customer support, генерация контента), что ускоряет старт – вы берёте шаблон, адаптируете под свои данные и сразу получаете более-менее рабочий прототип, а затем уже донастраиваете.
    В итоге Klu можно рассматривать как интегрированное решение, которое сочетает в себе частично функции и мониторинга, и оценки, и управления версиями, и даже хостинга моделей. Такие платформы особенно привлекательны для команд, у которых нет выделенных DevOps на ML – Klu берёт часть этой роли, позволяя сфокусироваться на логике приложения. Однако, как и со всеми “все-в-одном” решениями, гибкость может быть ниже, чем при сочетании отдельных инструментов. Выбор между использованием отдельных утилит (типа LangSmith + Ragas + Promptfoo) или единой платформы (Klu, Baseten, Bedrock и т.п.) зависит от ресурсов команды и требований проекта.

Прочие примечательные инструменты и тенденции

Помимо перечисленного, в экосистеме LLMOps к августу 2025 стоит отметить:

  • LangChain и LlamaIndex как фреймворки разработки LLM-приложений – они не столько про оперейшнз, сколько про удобство создания цепочек и RAG, но вокруг них формируются плагины для LLMOps (например, LangChain имеет LangSmith, а LlamaIndex – интеграции с телеметрией).

  • Arize AI (Phoenix) – платформа мониторинга данных ML, которая адаптировалась под LLM: позволяет отслеживать дрейф в эмбеддингах, кластеризовать ответы модели и находить аномалии (например, вдруг распределение тем изменилось – сигнал, что модель начала иначе себя вести).

  • Evidently AI – инструмент для мониторинга метрик и данных, тоже расширил поддержку под LLM-данные, например, метрики токсичности, перплексия и прочее, чтобы детектировать деградацию модели со временем.

  • Guardrails (защитные ограждения) – библиотеки (например, Guardrails.ai) для валидации и пост-обработки ответов. Они позволяют задать схему, формат или ограничения и автоматически проверять выход модели, превращая его к нужному виду или отбрасывая, если не проходит. Это можно считать частью LLMOps-инфраструктуры по обеспечению качества в режиме реального времени.

  • MLOps-платформы, поддерживающие LLM: классические платформы вроде MLflow, Databricks, SageMaker интегрировали возможности работы с LLM. Например, Databricks выпустил Dolly и инструменты для fine-tuning, а также MLflow Evaluation – модуль, куда можно вписать LLM-модель и автоматически получать отчеты по её ответам на наборы данных. То есть граница между MLOps и LLMOps размылась: многие существующие решения обросли специфичными функциями для LLM.

  • CI/CD практики для LLM: появляются скрипты и открытые решения, как вставить LLM-тесты в pipeline. Например, open-source проект Promptfoo (упомянутый выше) легко интегрируется с GitHub Actions – можно настроить workflow, который запускает promptfoo test при каждом Pull Request и не даст смержить, если какие-то важные use-case начали фейлиться. Также некоторые компании делятся шаблонами, как они ставят в CI генерацию отчёта об изменении качества перед деплоем (что-то вроде: “Новая версия: средняя оценка ответов 4.3/5 против старой 4.5/5 – нужно разобраться прежде чем деплоить”). Эта культура только формируется, но явно за ней будущее, т.к. регулярные релизы требуют автоматизации контроля качества.

Резюмируя урок 3: инструменты LLMOps – ваш помощник, чтобы сделать невидимое поведение LLM видимым, а нестабильное – контролируемым. Каждый проект может комбинировать разные утилиты под свои задачи: где-то хватит простого логирования через Helicone и пары eval-тестов, а где-то требуется комплексная платформа. Но пренебрегать этими инструментами уже не роскошь, а риск – без них поддерживать качество и эффективность LLM-систем крайне сложно.

Локальный eval-контур: «LLM-judge» без внешних сервисов + отчёт

Цель. Поднять у себя минимальный контур оценки качества ответов (для RAG/FAQ): датасет из кейсов → генерация ответов вашей моделью → локальный «судья» (правила) → метрики и отчёт (CSV/Markdown). Всё самодостаточно, без зависимостей.

1) Что и зачем мы мерим

  • pass@1 — доля кейсов, где ответ проходит рубрику.
  • groundedness — доля числовых фактов в ответе, найденных в выданном контексте (числа «не с потолка»).
  • citations_ok — есть ссылки вида [doc_id] и они принадлежат разрешённым источникам.
  • must_include — обязательные слова/фразы по кейсу.

Результат — простые, но «острые» сигналы, которые поймают явные регрессы без сложных внешних инструментов.

2) Формат датасета (JSONL)

Создайте файл rag_cases.jsonl (5 кейсов для разгона):

{"id":"r1","question":"Сколько дней базового отпуска?", "context":[{"id":"d1","text":"Политика отпуска: базовый отпуск 28 дней. Регион: РФ."},{"id":"d2","text":"Дополнительный отпуск за переработки по согласованию."}],"rubric":{"must_include":["отпуск","дней"],"allow_cites":["d1","d2"]}}
{"id":"r2","question":"Оплачивается ли больничный?", "context":[{"id":"d3","text":"Больничный (лист временной нетрудоспособности) оплачивается по нормам."}],"rubric":{"must_include":["больничный"],"allow_cites":["d3"]}}
{"id":"r3","question":"Есть ли отпуск без содержания?", "context":[{"id":"d4","text":"FAQ: отпуск без сохранения заработной платы предоставляется по заявлению."}],"rubric":{"must_include":["без","содерж"],"allow_cites":["d4"]}}
{"id":"r4","question":"Какие правила командировок?", "context":[{"id":"d5","text":"Политика командировок: суточные, авиа/жд, отчёт. Изменений в 2024 нет."}],"rubric":{"must_include":["командиров"],"allow_cites":["d5"]}}
{"id":"r5","question":"Назовите все числа из политики отпуска.", "context":[{"id":"d1","text":"Политика отпуска: базовый отпуск 28 дней. Регион: РФ."},{"id":"d6","text":"Отпускные рассчитываются по среднему, коэффициенты 1.00."}],"rubric":{"must_include":["отпуск"],"allow_cites":["d1","d6"]}}

3) Каркас eval-харнеса (готов к запуску)

Сохраните как rag_eval.py. По умолчанию используется mock-генератор; для реальной модели замените функцию call_model().

# rag_eval.py
import json, re, csv
from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any, Tuple

DATA_FILE = Path("rag_cases.jsonl")

# ------------------ ВСТАВЬТЕ СВОЮ МОДЕЛЬ ЗДЕСЬ ------------------
def call_model(question: str, context: List[Dict[str,str]]) -> str:
    """
    Замените на реальный вызов (RAG-пайплайн):
    - сформируйте prompt с фрагментами context
    - верните ТЕКСТ ответа, включая цитаты вида [doc_id]
    По умолчанию — "умный" мок для демонстрации.
    """
    ctx_map = {c["id"]: c["text"] for c in context}
    q = question.lower()
    if "базов" in q and "отпуск" in q:
        return "Базовый отпуск составляет 28 дней [d1]."
    if "больнич" in q:
        return "Да, больничный оплачивается по нормам [d3]."
    if "без содерж" in q or "без содержания" in q:
        return "Да, предусмотрен отпуск без сохранения заработной платы [d4]."
    if "командиров" in q:
        return "Суточные, авиа/жд и обязательный отчёт, изменений в 2024 нет [d5]."
    if "все числа" in q or "числа" in q:
        return "Политика содержит числа 28 и 1.00 [d1][d6]."
    # "плохой" ответ без цитат/не из контекста
    return "Вероятно 30 дней. Уточните у HR."
# ----------------------------------------------------------------

# ------------------ JUDGE: правила-оценки ------------------
_Num = re.compile(r"\d+(?:[.,]\d+)?")

def extract_citations(answer: str) -> List[str]:
    return re.findall(r"\[([A-Za-z0-9_-]+)\]", answer)

def numbers_in_text(text: str) -> List[str]:
    return [x.replace(",", ".") for x in _Num.findall(text)]

def grounded_numbers(answer: str, context_texts: List[str]) -> Tuple[float, List[str]]:
    """Доля чисел из ответа, которые встречаются в объединённом контексте (как строки)."""
    nums_ans = numbers_in_text(answer)
    if not nums_ans: 
        return 1.0, []  # нет чисел — считаем ок
    blob = " ".join(context_texts)
    nums_ctx = set(numbers_in_text(blob))
    unsupported = [n for n in nums_ans if n not in nums_ctx]
    ok = (len(nums_ans) - len(unsupported)) / max(1, len(nums_ans))
    return ok, unsupported

def contains_all(answer: str, need: List[str]) -> bool:
    text = answer.lower()
    return all(any(n in text for n in (w if isinstance(w, list) else [w])) for w in need)

def citations_ok(answer: str, allow: List[str]) -> Tuple[bool, List[str], List[str]]:
    cites = extract_citations(answer)
    bad = [c for c in cites if c not in allow]
    return (len(cites) > 0 and not bad), cites, bad

# ------------------ Загрузка и прогон ------------------
@dataclass
class Case:
    id: str
    question: str
    context: List[Dict[str,str]]
    rubric: Dict[str, Any]

def load_cases(p: Path) -> List[Case]:
    out=[]
    for line in p.read_text(encoding="utf-8").splitlines():
        if not line.strip(): continue
        r = json.loads(line)
        out.append(Case(id=r["id"], question=r["question"], context=r["context"], rubric=r["rubric"]))
    return out

def judge_case(ans: str, case: Case) -> Dict[str, Any]:
    allow = case.rubric.get("allow_cites", [])
    must = case.rubric.get("must_include", [])
    ok_cites, cites, bad_cites = citations_ok(ans, allow)
    g_score, unsupported = grounded_numbers(ans, [c["text"] for c in case.context])
    ok_must = contains_all(ans, must)
    passed = ok_cites and ok_must and g_score >= 1.0  # все числа в ответе должны быть из контекста
    reasons=[]
    if not ok_cites: reasons.append("CITATIONS_FAIL")
    if not ok_must: reasons.append("MUST_FAIL")
    if g_score < 1.0: reasons.append(f"NUM_UNSUPPORTED:{','.join(unsupported[:3])}")
    return {
        "pass": passed,
        "citations_ok": ok_cites,
        "cites": cites,
        "bad_cites": bad_cites,
        "must_ok": ok_must,
        "groundedness": round(g_score, 3),
        "unsupported_numbers": unsupported,
        "reasons": reasons
    }

def run_eval(data_file: Path = DATA_FILE) -> Dict[str, Any]:
    cases = load_cases(data_file)
    rows=[]
    for cs in cases:
        ans = call_model(cs.question, cs.context)
        res = judge_case(ans, cs)
        rows.append({
            "id": cs.id,
            "pass": res["pass"],
            "groundedness": res["groundedness"],
            "cit_ok": res["citations_ok"],
            "bad_cites": ";".join(res["bad_cites"]),
            "reasons": ";".join(res["reasons"]),
            "answer": ans
        })
    pass_rate = sum(1 for r in rows if r["pass"]) / len(rows)
    return {"pass_rate": pass_rate, "rows": rows}

def save_reports(report: Dict[str,Any], csv_path="rag_eval_report.csv", md_path="rag_eval_report.md"):
    rows = report["rows"]
    # CSV
    with open(csv_path, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        w.writeheader(); w.writerows(rows)
    # Markdown (краткий)
    lines = [f"# RAG Eval Report", f"pass@1 = {report['pass_rate']:.2f}", "", "| id | pass | grounded | cit_ok | reasons |", "|---|---:|---:|---:|---|"]
    for r in rows:
        lines.append(f"| {r['id']} | {'✅' if r['pass'] else '❌'} | {r['groundedness']:.2f} | {'✅' if r['cit_ok'] else '❌'} | {r['reasons']} |")
    Path(md_path).write_text("\n".join(lines), encoding="utf-8")
    print(f"CSV → {csv_path}\nMD  → {md_path}")

if __name__ == "__main__":
    rpt = run_eval()
    print(f"pass@1 = {rpt['pass_rate']:.2f}")
    save_reports(rpt)

4) Как запускать

  1. Сохраните два файла рядом: rag_cases.jsonl и rag_eval.py.
  2. Запустите:
    python rag_eval.py
    Вы увидите pass@1 и получите два отчёта: rag_eval_report.csv и rag_eval_report.md.
  3. Подключите свою модель: замените в rag_eval.py функцию call_model() на реальный вызов (ваш RAG/FAQ эндпоинт).

5) Автопроверки (микро-лаба)

  • Ответы содержат цитаты [doc_id], и они входят в allow_cites кейса.
  • Все числа ответа встречаются в контексте (иначе «галлюцинация» по числам).
  • В ответе есть обязательные слова/части слов из must_include.

6) Практические подсказки

  • Добавьте ещё один сигнал length_ok (например, ≤ 60 слов) и правило «если нет цитат — мягкий отказ».
  • Ночной прогон на реальных логах (50–200 вопросов) даст стабильную базу для регресс-тестов.
  • В отчёте храните версию промпта/модели и хеш датасета, чтобы сравнивать релизы честно.

Трейс одной сессии: trace_id/span_id в JSON-логах

Цель. Научиться логировать «сквозной» запрос с trace_id и иерархией span_id → быстро видеть, что делали retriever, llm.generate и postprocess, сколько это заняло и чем закончилось.

1) Мини-теория

  • trace_id — общий идентификатор для всех шагов одного запроса.
  • span — отдельная операция (например, retriever), имеет span_id, parent_span_id, start/end, атрибуты и статус.
  • Что логируем: события span_start/span_end в структурном JSON: имя, атрибуты, задержка, статус.

2) Готовый код (без внешних зависимостей)

Скрипт ниже реализует простый трассер на contextvars. Он печатает JSON-строки (подходит для последующей загрузки в ClickHouse/ELK/BigQuery).

# trace_demo.py
from __future__ import annotations
import json, time, uuid, random, contextvars
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

# --- контекст запроса (сквозной) ---
trace_id_var = contextvars.ContextVar("trace_id", default="")
span_stack_var = contextvars.ContextVar("span_stack", default=[])

LOGS: List[Dict[str, Any]] = []  # буфер для автопроверок

def now_iso() -> str:
    import datetime as dt
    return dt.datetime.utcnow().isoformat(timespec="milliseconds") + "Z"

def log(event: str, **fields):
    rec = {"ts": now_iso(), "trace_id": trace_id_var.get(""), "event": event, **fields}
    line = json.dumps(rec, ensure_ascii=False)
    print(line)
    LOGS.append(rec)

# --- примитивный span-контекст ---
@dataclass
class _SpanCtx:
    span_id: str
    parent_span_id: Optional[str]
    name: str
    t0: float

class span:
    def __init__(self, name: str, **attrs):
        self.name = name
        self.attrs = attrs
        self.ctx: Optional[_SpanCtx] = None
    def __enter__(self):
        stack = list(span_stack_var.get())
        parent = stack[-1].span_id if stack else None
        self.ctx = _SpanCtx(span_id=uuid.uuid4().hex[:16], parent_span_id=parent, name=self.name, t0=time.perf_counter())
        span_stack_var.set(stack + [self.ctx])
        log("span_start", name=self.name, span_id=self.ctx.span_id, parent_span_id=self.ctx.parent_span_id, **self.attrs)
        return self
    def __exit__(self, exc_type, exc, tb):
        assert self.ctx is not None
        elapsed_ms = int((time.perf_counter() - self.ctx.t0) * 1000)
        # pop
        stack = list(span_stack_var.get())
        span_stack_var.set(stack[:-1])
        status = "ERROR" if exc_type else "OK"
        log("span_end", name=self.name, span_id=self.ctx.span_id, parent_span_id=self.ctx.parent_span_id, latency_ms=elapsed_ms, status=status)
        # не подавляем исключения
        return False

# --- «бизнес»-функции пайплайна (симуляция) ---
def retriever(query: str, k=3) -> List[str]:
    with span("retriever", k=k):
        time.sleep(random.uniform(0.01, 0.03))
        ids = ["d1", "d3", "d5"][:k]
        log("retriever.result", retrieved_ids=",".join(ids), k=k)
        return ids

def llm_generate(query: str, context_ids: List[str], model="demo-pro", prompt_version="v1") -> str:
    with span("llm.generate", model=model, prompt_version=prompt_version):
        toks = random.randint(80, 140)
        time.sleep(0.005 * toks / 10)  # имитация работы
        txt = f"Ответ из модели {model} с цитатами [{context_ids[0]}]."
        log("llm.tokens", tokens_out=toks, model=model)
        return txt

def postprocess(answer: str) -> str:
    with span("postprocess.validate", schema="faq_v1"):
        time.sleep(random.uniform(0.002, 0.006))
        ok = answer.endswith(".")
        log("validator.result", ok=ok)
        return answer if ok else (answer + ".")

# --- обработчик запроса ---
def handle_request(user_query: str, prompt_version="v1") -> str:
    # установить trace_id и корневой span
    trace_id_var.set(uuid.uuid4().hex)
    with span("request", user_query=user_query):
        ids = retriever(user_query, k=3)
        ans = llm_generate(user_query, ids, model="demo-pro", prompt_version=prompt_version)
        final = postprocess(ans)
        log("request.done", bytes=len(final.encode("utf-8")))
        return final

# --- автопроверки шага ---
def _self_test():
    LOGS.clear()
    out = handle_request("Сколько дней базового отпуска?")
    # 1) один trace_id у всех записей
    tids = {r["trace_id"] for r in LOGS}
    assert len(tids) == 1 and next(iter(tids)), "Должен быть единый непустой trace_id"
    # 2) есть 4 спана (request, retriever, llm.generate, postprocess.validate)
    names = [r["name"] for r in LOGS if r["event"] == "span_end"]
    for need in ("request", "retriever", "llm.generate", "postprocess.validate"):
        assert need in names, f"Нет завершающего спана {need}"
    # 3) все span_end имеют latency_ms > 0
    assert all((r.get("latency_ms", 1) > 0) for r in LOGS if r["event"] == "span_end")
    # 4) у дочерних спанов parent_span_id совпадает с span_id корня
    root_span = next(r for r in LOGS if r["event"]=="span_start" and r["name"]=="request")
    children = [r for r in LOGS if r.get("parent_span_id")==root_span["span_id"] and r.get("name") in ("retriever","llm.generate","postprocess.validate") and r["event"]=="span_start"]
    assert len(children) == 3, "Все дочерние спаны должны ссылаться на корень"
    print("\nАвтотесты пройдены ✓")

if __name__ == "__main__":
    _self_test()
    # пример: можно вызвать несколько раз
    # handle_request("Оплачивается ли больничный?")

3) Как запускать

  1. Сохраните код в файл trace_demo.py.
  2. Запустите:
    python trace_demo.py
    Скрипт выполнит автопроверки и напечатает JSON-логи с событиями span_start/span_end и тех. событиями (retriever.result, llm.tokens, validator.result, request.done).

4) Что проверить (микро-лаба)

  • У всех строк один и тот же trace_id (одна сессия).
  • Есть четыре спана: request → retriever → llm.generate → postprocess.validate (у дочерних верный parent_span_id).
  • У всех span_end есть поле latency_ms и статус OK.

5) Мини-чеклист для продакшна

  • Структурные логи (JSON), не свободный текст.
  • Обязательные поля: trace_id, span_id, parent_span_id, name, latency_ms, status, а также model, prompt_version, tokens_out для LLM-спана.
  • Сэмплинг: 100% ошибок + хвостовые по latency/cost; остальное — долями.
  • PII: перед записью редактируйте e-mail/телефоны; не логируйте секреты/ключи.

6) (Опционально) Апгрейд до OpenTelemetry

Когда будете готовы, замените кастомные спаны на OTel (opentelemetry-sdk + ConsoleSpanExporter или OTLP-экспорт в ваш бэкенд). Имена спанов и атрибуты сохраняйте такими же, чтобы не ломать анализ.

Задача 1

Какое определение корректнее всего описывает назначение LangSmith?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Какую «уникальную» пользу Helicone чаще всего выбирают на старте?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Какое утверждение точнее всего характеризует HoneyHive?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

За что чаще всего ценят Langfuse?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 5

Что из перечисленного — прежде всего «фреймворк для автоматических оценок»?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 6

Инструменты, которые нативно «заточены» под RAG-оценку (faithfulness/groundedness, контекстная релевантность):

Выберите все подходящие варианты
Без регистрации · результат не сохраняется

Задача 7

Что W&B даёт команде LLMOps «из коробки»?

Выберите все подходящие варианты
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку