Инженерия качества: Как перестать надеяться на удачу и начать измерять своих ИИ-агентов [Часть 1]

от автора

Доброго времени суток!

Хочется поговорить об одной из самых «больных» тем в современной AI-разработке — как проверить, что система работает правильно. 🙂

Удивительно, но текущий хайп вокруг LLM привел к довольно значительной деградации инженерной культуры в этой области («в среднем по больнице»). В эпоху первых трансформеров (да и более ранние эпохи) ни у кого не возникало сомнений: нужен «Golden Set», ручная разметка и жесткий контроль метрик. NLP был уделом специалистов по машинному обучению.

С приходом LLM порог входа упал. Теперь любой может написать промпт и получить ответ. Проверка качества превратилась в «vibe-check»: разработчик задает три вопроса, видит, что агент ответил «вроде нормально», и считает задачу решенной.

Но тут есть проблема: LLM вероятностна. Если она ответила правильно 5 (или даже 500) раз подряд, это не значит, что на 6-й (или 501-й) раз она не улетит в галлюцинации. Без продуманного процесса непрерывной оценки вы строите крайне хрупкую конструкцию.

Первое, что разумно ввести в ваши практики — это вспомнить про Golden Set. 🙂

Golden Set — это эталонный набор данных «вопрос-ответ», на котором вы гоняете свою систему. Но для агентов старый формат пар «запрос-текст» уже не всегда достаточен.

Агент — это система, которая совершает действия. Поэтому современный Golden Set должен содержать траектории:

  • Эталонные рассуждения (Chain-of-Thought).

  • Эталонные вызовы инструментов (Function Calling): какие функции и с какими аргументами должны быть вызваны.

  • Эталонные данные (Ground Truth): не просто текст ответа, а факты, которые должны в нем присутствовать.

Где взять данные?

Собирать такие данные вручную — это долго, дорого и больно (именно поэтому об этом так любят забывать :-)). Но у некоторых типов ИИ-систем, скажем самой популярной агентской топологии RAG, есть «читерское» преимущество: ваши документы сами по себе — идеальный источник данных для тестов.

Генерация GoldenSet для RAG системы

Один из самых эффективных способов получить Golden Set для RAG — при помощи LLM построить Граф Знаний (Knowledge Graph) на основе ваших же документов.

Есть отличная реализация в рамках популярной библиотеки оценки ИИ-систем RAGAS. Используя её и Конституцию России, взятую в качестве примера PDF-документа, давайте рассмотрим, как это работает. 🙂

Процесс выглядит так:

  1. Построение графа (Knowledge Graph): Мы разбиваем документы на иерархические узлы (Document -> Section -> Chunk). К каждому узлу LLM добавляет метаданные:

    • Summary: Краткое резюме контента.

    • Entities: Имена, даты, специфические термины.

  2. Связи (Relationships): Узлы связываются на основе структуры (следующий/предыдущий) или семантической близости извлеченных сущностей.

  3. Синтез вопросов: На основе структуры графа мы запускаем «синтезаторы», которые обходят получившийся граф и создают вопросы разной сложности.

Как синтезируются вопросы?

Ragas предлагает довольно гибкую систему синтезаторов, позволяющую проверить RAG под разными углами:

Simple (Single-Hop): Проверяет базовый поиск. Вопрос касается одного конкретного факта в одном документе. Пример: «В каком году была принята текущая Конституция РФ?»

Multi-Hop: Самый важный тест. Требует сопоставления фактов из разных частей документа или даже разных файлов. Это проверяет способность ретривера собирать разрозненный контекст. Пример: «Какие ограничения накладываются на президента, если он одновременно является главой совета безопасности?»

Comparative: Заставляет модель сравнивать сущности. Пример: «Чем полномочия Государственной Думы отличаются от полномочий Совета Федерации в вопросе принятия федеральных законов?»

Specific vs Abstract: Мы можем генерировать как очень конкретные вопросы (фактология), так и абстрактные (обобщение темы).

Давайте рассмотрим, как выглядит в коде:

Я буду использовать в качестве LLM qwen/qwen3.6-35b-a3b, а в качестве модели эмбеддинга text-embedding-qwen3-embedding-0.6b. И одна и вторая запущенна локально в LM_Studio.

import argparseimport asyncioimport loggingimport osimport instructorfrom langchain_community.document_loaders import PyMuPDFLoaderfrom langchain_openai import OpenAIEmbeddingsfrom openai import AsyncOpenAIfrom ragas.embeddings.base import LangchainEmbeddingsWrapperfrom ragas.llms.base import InstructorLLM, InstructorModelArgsfrom ragas.testset import TestsetGeneratorfrom ragas.testset.graph import KnowledgeGraphfrom ragas.testset.synthesizers.multi_hop.specific import MultiHopSpecificQuerySynthesizerfrom ragas.testset.synthesizers.single_hop.specific import SingleHopSpecificQuerySynthesizer# Настройка логированияlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')logger = logging.getLogger(__name__)async def main():    parser = argparse.ArgumentParser()    parser.add_argument("--pdf", type=str, default="constitution.pdf", help="Путь к PDF")    parser.add_argument("--num-pages", type=int, default=5, help="Сколько страниц использовать (по умолчанию 5, -1 — все)")    parser.add_argument("--output", type=str, default="golden_set.csv", help="Путь для сохранения Golden Set")    args = parser.parse_args()    # 1. Конфигурация из переменных окружения    base_url = os.getenv("LLM_BASE_URL", "http://127.0.0.1:1234/v1")    api_key = os.getenv("LLM_API_KEY", "lm-studio")    model = os.getenv("LLM_MODEL", "qwen/qwen3.6-35b-a3b")    emb_model = os.getenv("LLM_EMBEDDING_MODEL", "text-embedding-qwen3-embedding-0.6b")    # Путь к PDF    pdf_path = args.pdf    if not os.path.exists(pdf_path):        logger.error(f"Файл {pdf_path} не найден. Положите constitution.pdf рядом со скриптом.")        return    # 2. Загрузка страниц    logger.info(f"Загрузка PDF: {pdf_path}")    loader = PyMuPDFLoader(pdf_path)    all_documents = loader.load()    if args.num_pages != -1:        documents = all_documents[:args.num_pages]        logger.info(f"Ограничение: используем первые {args.num_pages} страниц из {len(all_documents)}")    else:        documents = all_documents        logger.info(f"Используем все страницы: {len(documents)}")    # 3. Сооружаем обертки которые ожидает Ragas, заодно чиня ряд проблем LM_Studio :)    client = AsyncOpenAI(base_url=base_url, api_key=api_key)    # Используем MD_JSON для корректной работы структурного вывода с моделью, запущенной в LM_Studio    patched_client = instructor.from_openai(client, mode=instructor.Mode.MD_JSON)    # Провайдер "custom" предотвращает некоторые проблемы библиотеки с попыткой ходить не локально, а в облачный OpenAI    ragas_llm = InstructorLLM(        client=patched_client,        model=model,        provider="custom",        model_args=InstructorModelArgs(temperature=0.2)    )    # Эмбеддинги через LangChain-обертку, снова чиним особенности LM_Studio :)    emb_lc = OpenAIEmbeddings(        base_url=base_url,        api_key=api_key,        model=emb_model,        check_embedding_ctx_length=False    )    ragas_emb = LangchainEmbeddingsWrapper(emb_lc)    # 4. Генерация Knowledge Graph и Golden Set    logger.info(f"--- Запуск генерации Knowledge Graph (страниц: {len(documents)}) ---")    try:        kg = KnowledgeGraph()        generator = TestsetGenerator(llm=ragas_llm, embedding_model=ragas_emb, knowledge_graph=kg)        # Настройка синтезаторов вопросов        distribution = [            (SingleHopSpecificQuerySynthesizer(llm=ragas_llm), 0.5),            (MultiHopSpecificQuerySynthesizer(llm=ragas_llm), 0.5),        ]        # Адаптация промптов под русский язык...        # Это не исключит генерации на английском на 100%, но минимизирует такие случаи        for query, _ in distribution:            prompts = await query.adapt_prompts("russian", llm=ragas_llm, adapt_instruction=True)            query.set_prompts(**prompts)        # Генерируем 10 вопросов, настоящий GoldenSet обычно содержит больше 100...        testset = generator.generate_with_langchain_docs(            documents,            testset_size=10,            query_distribution=distribution        )        df = testset.to_pandas()        logger.info("Генерация успешно завершена!")        # Сохранение в файл        output_file = args.output        df.to_csv(output_file, index=False)        logger.info(f"Golden Set сохранен в: {output_file}")    except Exception as e:        logger.error(f"Ошибка при генерации: {e}")if __name__ == "__main__":    asyncio.run(main())

Обратите внимание на конструкцию query.adapt_prompts(…). Библиотека оперирует для генерации англоязычными промптами, что может приводить к тому, что генерируемые вопросы и ответы, будут на английском, это предусмотренный авторами способ минимизировать такие случаи (хотя иногда она все равно будет писать транслитом) 🙂

В реальных кейсах, довольно часто используют, логику деления на документы таким же способом, что и при индексации, условно используете RecursiveCharacterTextSplitter, его же чанки и используйте для построения графа документов и вопросов по ним.

Благодарю за внимание!

В следующей, части статьи поговорим, что вообще разумно измерять в агенсткой ИИ системе, а затем перейдем к Е-Е как совместить такой Golden Set с процессом непрерывной проверки.

ссылка на оригинал статьи https://habr.com/ru/articles/1034050/