Урок курса
Обзор инструментов для 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) Как запускать
- Сохраните два файла рядом:
rag_cases.jsonlиrag_eval.py. - Запустите:
Вы увидитеpython rag_eval.pypass@1и получите два отчёта:rag_eval_report.csvиrag_eval_report.md. - Подключите свою модель: замените в
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) Как запускать
- Сохраните код в файл
trace_demo.py. - Запустите:
Скрипт выполнит автопроверки и напечатает JSON-логи с событиямиpython trace_demo.pyspan_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 «из коробки»?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
