LLM в роли судьи, регрессионное тестирование и сквозная прослеживаемость многоагентных систем LLM.
Делиться

По мере усложнения агентов, работающих на основе LLM, традиционные методы ведения журналов и мониторинга оказываются недостаточными. Командам действительно необходима наблюдаемость : возможность отслеживать решения агентов, автоматически оценивать качество ответов и обнаруживать изменения во времени — без написания и поддержки большого объема пользовательского кода для оценки и телеметрии.
Поэтому командам необходимо выбрать подходящую платформу для мониторинга, сосредоточившись на основной задаче — создании и совершенствовании системы управления агентами. При этом интеграция их приложения с платформой мониторинга должна осуществляться с минимальными затратами на функциональный код. В этой статье я продемонстрирую, как можно настроить платформу мониторинга ИИ с открытым исходным кодом для выполнения следующих задач, используя подход с минимальным количеством кода:
- LLM-as-a-Judge : Настройте предварительно созданные системы оценки ответов по критериям правильности, релевантности, несоответствия и другим. Отображайте оценки по всем запускам с подробными журналами и аналитикой.
- Масштабное тестирование : Создайте наборы данных для хранения тестовых примеров регрессии, чтобы измерить точность по сравнению с ожидаемыми истинными значениями. Проактивно выявляйте ошибки LLM и дрейф агента.
- Данные MELT : отслеживание метрик (задержка, использование токенов, дрейф модели), событий (вызовы API, вызовы LLM, использование инструментов), журналов (взаимодействие с пользователем, выполнение инструментов, принятие решений агентом) с подробными трассировками — и все это без подробного кода телеметрии и мониторинга.
Для мониторинга мы будем использовать Langfuse. Это программное обеспечение с открытым исходным кодом, не зависящее от фреймворка и совместимое с популярными системами оркестрации и поставщиками LLM-решений.
Многоагентное приложение
Для этой демонстрации я приложил код приложения службы поддержки клиентов на языке LangGraph. Приложение принимает заявки от пользователей, классифицирует их по категориям: техническая поддержка, поддержка по вопросам выставления счетов или обе категории с помощью агента сортировки, а затем направляет заявку агенту технической поддержки, агенту поддержки по вопросам выставления счетов или обоим. Затем агент финализации синтезирует ответ от обоих агентов в связный, более читабельный формат. Блок-схема выглядит следующим образом:

Код прилагается здесь # ————————————————— # 0. Загрузка .env # ————————————————— from dotenv import load_dotenv load_dotenv(override=True) # ————————————————— # 1. Импорт # ————————————————— import os from typing import TypedDict from langgraph.graph import StateGraph, END from langchain_openai import AzureChatOpenAI from langfuse import Langfuse from langfuse.langchain import CallbackHandler # ————————————————— # 2. Клиент Langfuse (РАБОЧАЯ КОНФИГУРАЦИЯ) # ————————————————— langfuse = Langfuse( host=»https://cloud.langfuse.com», public_key=os.environ[«LANGFUSE_PUBLIC_KEY»] , secret_key=os.environ[«LANGFUSE_SECRET_KEY»] ) langfuse_callback = CallbackHandler() os.environ[«LANGGRAPH_TRACING»] = «false» # ————————————————— # 3. Настройка Azure OpenAI # ————————————————— llm = AzureChatOpenAI( azure_deployment=os.environ[«AZURE_OPENAI_DEPLOYMENT_NAME»], api_version=os.environ.get(«AZURE_OPENAI_API_VERSION», «2025-01-01-preview»), temperature=0.2, callbacks=[langfuse_callback], # 🔑 включает использование токенов ) # ————————————————— # 4. Общее состояние # ————————————————— class AgentState(TypedDict, total=False): ticket: str category: str technical_response: str billing_response: str final_response: str # ————————————————— # 5. Определения агентов # ————————————————— def triage_agent(state: dict) -> dict: with langfuse.start_as_current_observation( as_type=»span», name=»triage_agent», input={«ticket»: state[«ticket»]}, ) as span: span.update_trace(name=»Customer Service Query — LangGraph Demo») response = llm.invoke([ { «role»: «system», «content»: ( «Classify the query as one of: » «Technical, Billing, Both. » «Respond with only the label.» ), }, {«role»: «user», «content»: state[«ticket»]}, ]) raw = response.content.strip().lower() if «both» in raw: category = «Both» elif «technical» in raw: category = «Technical» elif «billing» in raw: category = «Billing» else: category = «Technical» # ✅ безопасный резервный вариант span.update(output={«raw»: raw, «category»: category}) return {«category»: category} def technical_support_agent(state: dict) -> dict: with langfuse.start_as_current_observation( as_type=»span», name=»technical_support_agent», input={ «ticket»: state[«ticket»], «category»: state.get(«category»), }, ) as span: response = llm.invoke([ { «role»: «system», «content»: ( «Вы специалист технической поддержки. » «Предоставьте четкое пошаговое решение.» ), }, {«role»: «user», «content»: state[«ticket»]}, ]) answer = response.content span.update(output={«technical_response»: answer}) return {«technical_response»: answer} def billing_support_agent(state: dict) -> dict: with langfuse.start_as_current_observation( as_type=»span», name=»billing_support_agent», input={ «ticket»: state[«ticket»], «category»: state.get(«category»), }, ) as span: response = llm.invoke([ { «role»: «system», «content»: ( «Вы специалист по поддержке выставления счетов. » «Отвечайте четко о платежах, счетах-фактурах или учетных записях.» ), }, {«role»: «user», «content»: state[«ticket»]}, ]) answer = response.content span.update(output={«billing_response»: answer}) return {«billing_response»: answer} def finalizer_agent(state: dict) -> dict: with langfuse.start_as_current_observation( as_type=»span», name=»finalizer_agent», input={ «ticket»: state[«ticket»], «technical»: state.get(«technical_response»), «billing»: state.get(«billing_response»), }, ) as span: parts = [ f»Technical:n{state['technical_response']}» for k in [«technical_response»] if state.get(k) ] + [ f»Billing:n{state['billing_response']}» for k in [«billing_response»] if state.get(k) ] if not parts: final = «Error: No agent responses available.» else: response = llm.invoke([ { «role»: «system», «content»: ( «Объедините следующие ответы агентов в ОДИН четкий, профессиональный, » «ответ для клиента. Не упоминайте агентов или внутренние метки.» f»Ответьте на запрос пользователя: '{state['ticket']}'.» ), }, {«role»: «user», «content»: «nn».join(parts)}, ]) final = response.content span.update(output={«final_response»: final}) return {«final_response»: final} # ————————————————— # 6. Построение LangGraph # ————————————————— builder = StateGraph(AgentState) builder.add_node(«triage», triage_agent) builder.add_node(«technical», technical_support_agent) builder.add_node(«billing», billing_support_agent) builder.add_node(«finalizer», finalizer_agent) builder.set_entry_point(«triage») # Условная маршрутизация builder.add_conditional_edges( «triage», lambda state: state[«category»], { «Technical»: «technical», «Billing»: «billing», «Both»: «technical», «__default__»: «technical», # ✅ никогда не тупик }, ) # Последовательное разрешение builder.add_conditional_edges( «technical», lambda state: state[«category»], { «Both»: «billing», # Переходим к выставлению счетов, если Both «__default__»: «finalizer», }, ) builder.add_edge(«billing», «finalizer») builder.add_edge(«finalizer», END) graph = builder.compile() # ————————————————— # 9. Main # ————————————————— if __name__ == «__main__»: print(«==============================================») print(«Условная многоагентная система поддержки (готова)») print(«===============================================») print(«Введите 'exit' или 'quit', чтобы остановить программу.n») while True: # Получение ввода пользователя для заявки ticket = input(«Введите ваш запрос в службу поддержки (заявку): «) # Проверка наличия команды выхода if ticket.lower() in [«exit», «quit»]: print(«nВыход из системы поддержки. До свидания!») break if not ticket.strip(): print(«Пожалуйста, введите непустой запрос.» continue try: # — Запуск графа с тикетом пользователя — result = graph.invoke( {«ticket»: ticket}, config={«callbacks»: [langfuse_callback]}, ) # — Вывод результатов — category = result.get('category', 'N/A') print(f»n✅ Классификация сортировки: **{category}**») # Проверка, какие агенты были выполнены на основе наличия ответа executed_agents = [] if result.get(«technical_response»): executed_agents.append(«Technical») if result.get(«billing_response»): executed_agents.append(«Billing») print(f»🛠️ Выполненные агенты: {', '.join(executed_agents) if executed_agents else 'Нет (Сортировка не удалась)'}») print(«n================ ОКОНЧАТЕЛЬНЫЙ ОТВЕТ ================n») print(result[«final_response»]) print(«n» + «=»*60 + «n») except Exception as e: # Это важно для отладки: вывести тип исключения и сообщение print(f»nВо время обработки произошла ошибка ({type(e).__name__}): {e}») print(«nПожалуйста, попробуйте другой запрос.») print(«n» + «=»*60 + «n»)
Конфигурация наблюдаемости
Для настройки Langfuse перейдите по ссылке https://cloud.langfuse.com/ и создайте учетную запись с тарифным планом (доступен тариф для любителей с большими лимитами), затем настройте проект. В настройках проекта вы можете сгенерировать открытый и секретный ключи, которые необходимо указать в начале кода. Вам также необходимо добавить подключение LLM, которое будет использоваться для оценки LLM в качестве судьи.

Схема «магистр права в качестве судьи»
Это основная часть настройки оценки производительности агентов. Здесь вы можете настроить различные предварительно созданные оценщики из библиотеки оценщиков, которые будут оценивать ответы по различным критериям, таким как краткость, правильность, галлюцинаторность, критический анализ ответа и т. д. Для большинства случаев этого должно быть достаточно, в противном случае можно настроить и пользовательские оценщики. Вот как выглядит библиотека оценщиков:

Выберите инструмент оценки, например, «Релевантность», который вы хотите использовать. Вы можете запустить его для новых или существующих трассировок, а также для наборов данных. Кроме того, проверьте запрос оценки, чтобы убедиться, что он соответствует вашей цели оценки. Самое важное — переменные запроса, генерации и другие переменные должны быть правильно сопоставлены с источником (обычно, с входными и выходными данными из трассировки приложения). В нашем случае это будут данные заявки, введенные пользователем, и ответ, сгенерированный агентом финализации, соответственно. Кроме того, для наборов данных вы можете сравнить сгенерированные ответы с эталонными ответами, хранящимися в качестве ожидаемых результатов (подробнее в следующих разделах).
Здесь представлена конфигурация оценки «Точность GT», которую я настроил для новых запусков набора данных, а также сопоставление переменных. Также показан предварительный просмотр запроса на оценку. Большинство оценщиков выставляют оценки в диапазоне от 0 до 1:


Для демонстрации работы службы поддержки клиентов я настроил 3 оценщика: «Релевантность» и «Краткость», которые запускаются для всех новых трассировок, и «Точность GT», который развертывается только для запусков набора данных.

Настройка наборов данных
Создайте набор данных для использования в качестве хранилища тестовых случаев. Здесь вы можете хранить тестовые случаи с входным запросом и идеальным ожидаемым ответом. Для создания набора данных есть 3 варианта: создавать по одной записи за раз, загружать CSV-файл с запросами и ожидаемыми ответами или, что весьма удобно, добавлять входные и выходные данные непосредственно из трассировок приложения, ответы которых экспертами признаны качественными.
Вот набор данных, который я создал для демонстрации. Он представляет собой смесь технических, платежных или «и тех, и других» запросов, и все записи я создал на основе трассировки приложения:

Вот и всё! Настройка завершена, и мы готовы запустить мониторинг.
Результаты наблюдаемости
Домашняя страница Langfuse представляет собой панель управления с несколькими полезными диаграммами. На ней с первого взгляда отображается количество трассировок выполнения, оценки и средние значения, трассировки по времени, использование модели и стоимость и т.д.

Данные MELT
Наиболее полезные данные для мониторинга доступны в опции «Трассировка», которая отображает сводные и подробные сведения обо всех выполнениях. Здесь представлен вид панели мониторинга, отображающий время, имя, входные и выходные данные, а также важные показатели задержки и использования токенов. Обратите внимание, что для каждого выполнения агента нашего приложения генерируются 2 трассировки оценки для настроенных нами оценщиков краткости и релевантности.


Рассмотрим подробности одного из запусков приложения «Служба поддержки клиентов». На левой панели поток работы агента представлен как в виде дерева, так и в виде блок-схемы. Показаны узлы LangGraph (агенты) и вызовы LLM, а также использование токенов. Если бы у наших агентов были вызовы инструментов или этапы с участием человека, они также были бы показаны здесь. Обратите внимание, что сверху также показаны оценки краткости и релевантности, которые для этого запуска составляют 0,40 и 1 соответственно. При нажатии на них отображается причина оценки и ссылка для перехода к трассировке оценщика.
Справа для каждого агента, LLM и вызова инструмента мы видим входные данные и сгенерированные выходные данные. Например, здесь мы видим, что запрос был отнесен к категории «Оба», и поэтому на левой диаграмме показано, что были вызваны как агенты технической поддержки, так и агенты по выставлению счетов, что подтверждает корректную работу нашего процесса.

В верхней части правой панели находится кнопка « Добавить в наборы данных» . На любом этапе дерева при нажатии на эту кнопку откроется панель, подобная изображенной ниже, где вы можете добавить входные и выходные данные этого этапа непосредственно в тестовый набор данных, созданный в предыдущем разделе. Это полезная функция для экспертов, позволяющая добавлять часто встречающиеся запросы пользователей и корректные ответы в набор данных во время обычной работы агента, тем самым создавая репозиторий регрессионных тестов с минимальными усилиями. В будущем, при крупном обновлении или выпуске приложения, набор данных для регрессионного тестирования можно будет запустить, а полученные результаты можно будет оценить по сравнению с ожидаемыми результатами (эталонными значениями), записанными здесь, используя оценщик «Точность GT», который мы создали во время настройки LLM в качестве судьи. Это помогает выявлять дрейф LLM (или дрейф агента) на ранней стадии и принимать корректирующие меры.

Вот один из результатов оценки (краткость) для данного приложения. Оценщик приводит обоснование оценки в 0,4 балла, которую он дал этому ответу.

Баллы
В разделе «Оценки» в Langfuse отображается список всех результатов оценки, проведенной различными активными оценщиками, вместе с их оценками. Более полезной является панель аналитики, где можно выбрать две оценки и просмотреть такие показатели, как среднее значение и стандартное отклонение, а также линии тренда.


Регрессионное тестирование
С помощью наборов данных мы готовы проводить регрессионное тестирование, используя репозиторий тестовых примеров, содержащих запросы и ожидаемые результаты. В нашем наборе данных для регрессионного анализа сохранено 4 запроса, включающих технические, платежные и запросы типа «Оба».
Для этого мы можем запустить прилагаемый код, который получает соответствующий набор данных и запускает эксперимент. Все запуски тестов регистрируются вместе со средними баллами. Результаты выбранного теста с оценками краткости, точности GT и релевантности для каждого тестового случая можно просмотреть на одной панели мониторинга. При необходимости можно получить доступ к подробной трассировке, чтобы увидеть обоснование оценок.
Код можно посмотреть здесь. from langfuse import get_client from langfuse.openai import OpenAI from langchain_openai import AzureChatOpenAI from langfuse import Langfuse import os # Инициализация клиента from dotenv import load_dotenv load_dotenv(override=True) langfuse = Langfuse( host=»https://cloud.langfuse.com», public_key=os.environ[«LANGFUSE_PUBLIC_KEY»] , secret_key=os.environ[«LANGFUSE_SECRET_KEY»] ) llm = AzureChatOpenAI( azure_deployment=os.environ.get(«AZURE_OPENAI_DEPLOYMENT_NAME»), api_version=os.environ.get(«AZURE_OPENAI_API_VERSION», «2025-01-01-preview»), temperature=0.2, ) # Определите функцию задачи def my_task(*, item, **kwargs): question = item.input['ticket'] response = llm.invoke([{«role»: «user», «content»: question}]) raw = response.content.strip().lower() return raw # Получите набор данных из Langfuse dataset = langfuse.get_dataset(«Regression») # Запустите эксперимент непосредственно на наборе данных result = dataset.run_experiment( name=»Production Model Test», description=»Monthly evaluation of our production model», task=my_task # см. выше определение задачи ) # Используйте метод format для отображения результатов print(result.format())


Основные выводы
- Для обеспечения наблюдаемости ИИ не обязательно использовать большой объем кода.
Большинство возможностей оценки, трассировки и регрессионного тестирования для агентов LLM можно включить через конфигурацию, а не с помощью пользовательского кода, что значительно сокращает трудозатраты на разработку и сопровождение. - Сложные алгоритмы оценки могут быть определены декларативно.
Такие возможности, как оценка LLM в качестве судьи (релевантность, краткость, достоверность, соответствие истине), сопоставление переменных и подсказки для оценки, настраиваются непосредственно в платформе мониторинга — без написания специальной логики оценки. - Наборы данных и регрессионное тестирование — это функции, которые в первую очередь настраиваются.
Репозитории тестовых примеров, запуски наборов данных и сравнения с эталонными данными можно настраивать и повторно использовать через пользовательский интерфейс или простую конфигурацию, что позволяет командам запускать регрессионные тесты для разных версий агентов с минимальным добавлением кода. - Полная возможность наблюдения MELT предоставляется «из коробки».
Метрики (задержка, использование токенов, стоимость), события (вызовы LLM и инструментов), журналы и трассировки автоматически собираются и сопоставляются, что исключает необходимость ручной настройки рабочих процессов агентов. - Минимум приборов, максимальная наглядность.
Благодаря интеграции с облегченным SDK, команды получают подробную информацию о путях выполнения нескольких агентов, результатах оценки и тенденциях производительности, что позволяет разработчикам сосредоточиться на логике агентов, а не на механизмах мониторинга.
Заключение
По мере усложнения агентов LLM наблюдаемость перестаёт быть необязательной . Без неё многоагентные системы быстро превращаются в «чёрные ящики», которые трудно оценивать, отлаживать и улучшать.
Платформа для мониторинга ИИ снимает эту нагрузку с разработчиков и кода приложения. Используя подход с минимальным количеством кода и первоочередной конфигурацией , команды могут включить оценку LLM в качестве судьи, регрессионное тестирование и полный мониторинг MELT без создания и поддержки пользовательских конвейеров. Это не только сокращает трудозатраты на разработку, но и ускоряет переход от прототипа к производству.
Внедрение платформы с открытым исходным кодом, не зависящей от фреймворка, такой как Langfuse, позволяет командам получить единый источник достоверной информации о производительности агентов, что упрощает доверие к системам ИИ, их развитие и масштабируемую эксплуатацию.
Хотите узнать больше? Представленное здесь агентское приложение службы поддержки клиентов использует архитектурный шаблон «менеджер-работник», который не работает в CrewAI. Прочитайте о том, как наблюдаемость помогла мне исправить эту известную проблему с иерархическим процессом «менеджер-работник» в CrewAI, отслеживая ответы агентов на каждом этапе и уточняя их, чтобы оркестрация работала должным образом. Полный анализ здесь: Почему архитектура «менеджер-работник» в CrewAI не работает — и как это исправить
Свяжитесь со мной и поделитесь своими комментариями на сайте www.linkedin.com/in/partha-sarkar-lets-talk-AI
Все изображения и данные, использованные в этой статье, сгенерированы синтетическим путем. Рисунки и код созданы мной.
Источник: towardsdatascience.com























