Инженер AI-агентов для BDD: интенсивная подготовка к техническому интервью

Курс систематизирует знания от архитектуры LLM до проектирования сложных ReAct-агентов с RAG-контуром. Вы научитесь обосновывать технические решения для автоматизации тестирования в банковском контуре, используя Python, pgvector и принципы интеграции AI в SDLC.

Архитектура современных LLM и специфика работы с контекстным окном

Архитектура современных LLM и специфика работы с контекстным окном

Представьте ситуацию: вы загружаете в нейросеть 50-страничную спецификацию банковского продукта и просите сгенерировать для нее BDD-сценарии на Gherkin. Модель думает минуту, а затем выдает ошибку превышения лимита, либо генерирует отличный код, но полностью забывает или искажает правила из начала документа. Почему так происходит? Ответ кроется в фундаментальной архитектуре современных больших языковых моделей (LLM) и жестких математических ограничениях их «краткосрочной памяти».

Чтобы успешно пройти техническое интервью на позицию инженера AI-агентов, недостаточно уметь отправлять запросы к API. Нужно понимать, как модель обрабатывает текст под капотом, почему контекстное окно стоит дорого и почему мы не можем просто передать всю корпоративную базу знаний в одном запросе.

Токенизация: как модель видит текст

LLM не читают текст по буквам или словам. Текст разбивается на базовые единицы — токены.

Токен может быть целым словом, слогом или даже отдельным символом (особенно для редких языков или спецсимволов в коде). В среднем для английского языка 1 токен равен примерно 0.75 слова. Для русского языка или специфичного синтаксиса Gherkin (например, Given, When, Then) токенизация может быть менее эффективной: одно слово часто разбивается на 2-3 токена.

Это критически важно для инженерии AI-агентов по двум причинам:

  1. Биллинг: Провайдеры (OpenAI, Anthropic) тарифицируют API по количеству токенов, а не символов.
  2. Лимиты: Контекстное окно модели измеряется строго в токенах.

Сердце LLM: Трансформеры и механизм Self-Attention

Современные LLM (от GPT-3 до Llama 3) построены на архитектуре Transformer. Главное нововведение этой архитектуры, позволившее совершить революцию в AI, — механизм Self-Attention (внимание к себе).

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

Например, в предложении «Банк одобрил кредит, потому что у него была хорошая история», механизм Self-Attention помогает модели понять, что слово «него» относится к клиенту, а не к банку, выстроив сильную математическую связь между этими токенами.

Квадратичная сложность: цена понимания

Вычисление связей «каждый с каждым» имеет свою цену. Вычислительная сложность механизма Self-Attention составляет O(N2)O(N^2), где NN — количество токенов в контекстном окне.

Что это значит на практике для инженера? Если вы увеличиваете объем входного промпта в 2 раза (например, передаете не 10, а 20 существующих BDD-шагов в качестве примеров), количество вычислений, которые должна сделать видеокарта (GPU), вырастает в 4 раза. Если увеличиваете в 10 раз — вычисления растут в 100 раз.

Именно поэтому обработка длинных контекстов требует огромных объемов видеопамяти (VRAM) и занимает больше времени (растет метрика Time-to-First-Token — время до генерации первого слова ответа).

Анатомия контекстного окна

Контекстное окно — это вся оперативная память модели на один цикл взаимодействия. Это жесткий лимит WW, который делится на две части: входные данные (Input) и выходные данные (Output).

W=I+OW = I + O

В контексте создания AI-агента для генерации BDD-сценариев, ваш Input (II) будет состоять из:

  1. System Prompt: Инструкции для модели («Ты — QA-инженер в банке...»).
  2. Context (База знаний): Существующие шаги Gherkin (step_definitions), которые модель должна переиспользовать.
  3. User Input: Функциональные требования, которые нужно покрыть тестами.

Output (OO) — это сгенерированный моделью .feature файл.

Если лимит модели составляет 8000 токенов, и вы передали в Input 7500 токенов (загрузив туда сотни существующих шагов), у модели останется всего 500 токенов на генерацию ответа. Сценарий просто оборвется на середине.

Проблема длинного контекста: "Lost in the Middle"

Современные модели заявляют огромные контекстные окна (128k, 200k и даже 1M токенов). Казалось бы, проблема решена? Можно просто выгрузить весь банковский BDD-фреймворк в промпт и попросить написать тест.

На практике это плохой архитектурный паттерн. Помимо огромной стоимости каждого такого запроса и высокой задержки (latency), возникает эффект Lost in the Middle (потеря в середине).

Исследования показывают, что LLM отлично извлекают информацию из самого начала промпта и из его самого конца. Но точность извлечения фактов, спрятанных в середине длинного текста, резко падает — модель начинает «галлюцинировать» или просто игнорировать эти данные. Если нужный BDD-шаг для переиспользования оказался в середине огромного промпта, модель с высокой вероятностью его проигнорирует и придумает свой собственный, нарушив стандартизацию.

Мост к RAG

Мы приходим к инженерному противоречию:

  • Нам нужно передать модели знания о существующих шагах Gherkin в кодовой базе банка, чтобы она их переиспользовала.
  • Мы не можем передать всю кодовую базу в промпт из-за лимитов окна, стоимости, квадратичной сложности O(N2)O(N^2) и эффекта Lost in the Middle.

Решение этой проблемы — передавать в контекстное окно не все шаги, а только релевантные для конкретной задачи. Именно эту задачу решает архитектура RAG (Retrieval-Augmented Generation), которую мы детально разберем на следующем этапе.

Механика RAG: от эмбеддингов до векторного поиска в PostgreSQL с pgvector

Механика RAG: от эмбеддингов до векторного поиска в PostgreSQL с pgvector

Контекстное окно нейросети, как мы выяснили ранее, строго ограничено. Если в корпоративной базе банка хранится 50 000 автоматизированных BDD-шагов, мы физически не сможем поместить их все в промпт из-за лимита токенов и риска феномена Lost in the Middle. Языковой модели нужно передавать только те 5-10 шагов, которые действительно нужны для написания конкретного тестового сценария. Но как понять, какие именно шаги из десятков тысяч релевантны текущей задаче, еще до того, как мы обратимся к LLM?

Решением этой архитектурной задачи является RAG.

RAG (Retrieval-Augmented Generation) — это паттерн проектирования AI-систем, при котором приложение сначала извлекает (Retrieve) релевантную информацию из внешней базы данных, а затем обогащает (Augment) ей контекст запроса перед генерацией (Generation) ответа языковой моделью.

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

Эмбеддинги: математический смысл текста

Классический полнотекстовый поиск (например, через оператор LIKE в SQL) здесь работает плохо. Если тестировщик просит сгенерировать сценарий для фразы «клиент заходит в личный кабинет», а в базе есть готовый шаг Given user logs in with valid credentials, поиск по ключевым словам не найдет совпадений. Нам нужен поиск по смыслу — семантический поиск.

Для этого текст необходимо перевести на язык математики. Этот процесс называется построением эмбеддингов.

Эмбеддинг (Embedding) — это представление текста в виде плотного вектора (массива чисел с плавающей точкой) фиксированной длины, где геометрическое расположение вектора отражает семантический смысл текста.

Модели-энкодеры (например, text-embedding-ada-002 от OpenAI) читают текст и возвращают массив чисел. Для упомянутой модели это всегда массив из 1536 чисел, независимо от того, передали вы одно слово или целый абзац.

Каждое число в векторе — это координата в 1536-мерном пространстве. Тексты со схожим смыслом получают векторы, которые в этом многомерном пространстве находятся близко друг к другу. Таким образом, фразы «клиент заходит в систему» и user logs in окажутся соседями, даже если не имеют общих букв.

Характеристика Лексический поиск (Ключевые слова) Семантический поиск (Эмбеддинги)
Принцип работы Точное совпадение символов или корней слов Близость векторов в многомерном пространстве
Устойчивость к синонимам Низкая (нужно вручную прописывать словари) Высокая (модель сама понимает синонимы)
Мультиязычность Требует отдельного поиска для каждого языка Векторы разных языков с одним смыслом лежат рядом

Как измерить близость смыслов: косинусное сходство

Если у нас есть два вектора, нам нужна метрика, чтобы понять, насколько они близки. В машинном обучении для текстов стандартом де-факто является косинусное сходство (Cosine Similarity).

Вместо того чтобы измерять физическое расстояние между концами векторов (Евклидово расстояние), мы измеряем угол между ними. Это важно, потому что длина вектора может зависеть от длины текста, а нас интересует направление — то есть смысл.

Формула косинусного сходства:

cos(θ)=ABAB\cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\| \|\mathbf{B}\|}

Где:

  • A\mathbf{A} и B\mathbf{B} — сравниваемые векторы (эмбеддинги двух текстов).
  • AB\mathbf{A} \cdot \mathbf{B} — скалярное произведение векторов.
  • A\|\mathbf{A}\| и B\|\mathbf{B}\| — длины (нормы) этих векторов.
  • θ\theta — угол между векторами в многомерном пространстве.

Результат вычисления всегда лежит в диапазоне от -1 до 1:

  • cos(θ)1\cos(\theta) \approx 1: векторы сонаправлены (угол близок к нулю). Тексты семантически идентичны.
  • cos(θ)0\cos(\theta) \approx 0: векторы ортогональны (угол 90 градусов). Тексты вообще не связаны по смыслу.
  • cos(θ)1\cos(\theta) \approx -1: векторы противоположны. В работе с текстовыми эмбеддингами встречается редко.

PostgreSQL + pgvector: хранение и поиск в реальном проекте

Векторы нужно где-то хранить и уметь быстро вычислять их косинусное сходство с вектором запроса. Существуют специализированные векторные базы данных (Pinecone, Milvus, Qdrant). Однако в корпоративной банковской среде внедрение новой СУБД — это долгий процесс согласований.

Поскольку в стеке уже заявлен PostgreSQL, идеальным решением становится pgvector — официальное расширение, превращающее классическую реляционную базу в полноценное векторное хранилище.

Чтобы начать работу, расширение нужно активировать и создать таблицу со специальным типом данных vector:

-- Включаем расширение в базе данных
CREATE EXTENSION IF NOT EXISTS vector;

-- Создаем таблицу для хранения BDD-шагов
CREATE TABLE bdd_steps (
    id serial PRIMARY KEY,
    step_type varchar(50), -- Given, When, Then
    step_text text,        -- Исходный текст шага
    embedding vector(1536) -- Векторное представление (размерность 1536)
);

Когда пользователь (или AI-агент) хочет найти существующий шаг для автоматизации, система работает по следующему алгоритму:

  1. Бэкенд получает текстовый запрос (например, «проверка баланса счета»).
  2. Запрос отправляется в embedding-модель, которая возвращает вектор Q\mathbf{Q}.
  3. Бэкенд делает SQL-запрос к PostgreSQL, используя оператор <=>, который в pgvector означает вычисление косинусного расстояния (чем меньше значение, тем ближе векторы).
-- Поиск топ-3 самых похожих шагов
SELECT step_text, step_type
FROM bdd_steps
ORDER BY embedding <=> '[0.12, -0.05, 0.88, ...]' -- Сюда подставляется вектор запроса
LIMIT 3;

Сборка контура: от запроса к генерации

Теперь мы можем собрать весь процесс воедино.

Когда QA-инженер пишет в IDE функциональное требование, наш плагин отправляет его на бэкенд. Бэкенд превращает требование в вектор и делает запрос в PostgreSQL. База данных моментально вычисляет косинусное сходство между вектором запроса и десятками тысяч векторов существующих BDD-шагов, возвращая топ-5 самых подходящих.

Только после этого формируется финальный промпт для LLM. В него попадает исходная задача и блок контекста с найденными шагами. Модель, опираясь на механизм Self-Attention в пределах своего контекстного окна, связывает задачу с предоставленными готовыми шагами и генерирует .feature файл на Gherkin.

Таким образом, мы решаем главную бизнес-задачу: LLM не выдумывает новые шаги (что привело бы к дублированию кода и удорожанию поддержки), а переиспользует существующую кодовую базу банка. Однако, чтобы система работала безупречно, простого векторного поиска бывает недостаточно — требуются более сложные стратегии извлечения и ранжирования, которые мы рассмотрим далее.

Стратегии Retrieval: семантический поиск и ранжирование существующих BDD-шагов

Стратегии Retrieval: семантический поиск и ранжирование существующих BDD-шагов

Векторный поиск находит шаг Then баланс дебетового счета равен {amount} с показателем косинусного сходства 0.92, когда вы ищете «Проверка баланса кредитного счета». Для нейросети слова «дебетовый» и «кредитный» находятся в одном семантическом кластере — это финансовые термины, описывающие тип счета. Но для тестировщика банка подстановка дебетового шага вместо кредитного означает сломанный автотест и проваленный пайплайн. Это главная ловушка базового векторного поиска: семантическая близость не гарантирует функциональной релевантности.

Чтобы AI-агент мог надежно собирать Gherkin-сценарии из существующей кодовой базы, простого сравнения эмбеддингов в pgvector недостаточно. Нам предстоит выстроить многоступенчатый конвейер извлечения (Retrieval), который компенсирует слепые зоны семантического поиска.

Проблема плотных векторов и спасение в виде BM25

Эмбеддинги, которые мы сохранили в PostgreSQL, представляют собой так называемые «плотные» векторы (Dense vectors). Они отлично улавливают общий смысл, синонимы и контекст. Если в функциональном требовании написано «юзер залогинился», плотный вектор легко найдет BDD-шаг Given пользователь успешно авторизован.

Однако плотные векторы плохо справляются с точными совпадениями, специфичными идентификаторами и артикулами. Если нам нужен шаг, взаимодействующий с кнопкой btn-submit-form-42, семантический поиск может выдать шаг с кнопкой btn-cancel-form-42, так как контекст вокруг них идентичен.

Для решения этой проблемы применяется гибридный поиск (Hybrid Search), который объединяет плотные векторы с «разреженными» (Sparse vectors), основанными на лексическом поиске. Индустриальным стандартом лексического поиска является алгоритм BM25.

BM25 (Best Matching 25) — это функция ранжирования, которая оценивает релевантность документа поисковому запросу на основе частоты совпадения слов, учитывая длину документа и редкость слова во всей базе.

Характеристика Векторный поиск (Dense / Embeddings) Лексический поиск (Sparse / BM25)
Сильная сторона Понимание синонимов, абстрактных концепций и опечаток Точное совпадение ID, названий переменных, специфичных терминов
Слабая сторона Игнорирует важные детали (отрицания, точные числа) Не понимает синонимы («авторизация» \neq «логин»)
Применение в BDD Поиск шагов по смыслу бизнес-требования Поиск шагов по конкретным локаторам или названиям систем

Гибридный поиск и алгоритм RRF

Когда мы выполняем запрос к базе BDD-шагов, мы запускаем оба механизма параллельно: pgvector ищет по смыслу, а полнотекстовый поиск PostgreSQL (или отдельный движок вроде Elasticsearch) ищет по BM25.

В результате мы получаем два независимых списка топ-результатов. Векторный поиск поставил нужный нам шаг на 3-е место, а BM25 — на 1-е. Как их объединить? Для этого используется алгоритм Reciprocal Rank Fusion (RRF).

RRF не смотрит на абсолютные значения метрик (потому что косинусное сходство и баллы BM25 лежат в разных шкалах). Он смотрит только на позицию (ранг) документа в каждом из списков.

RRFscore=1k+rankdense+1k+ranksparseRRF_{score} = \frac{1}{k + rank_{dense}} + \frac{1}{k + rank_{sparse}}

Где:

  • RRFscoreRRF_{score} — итоговый балл документа, по которому будет отсортирован финальный список.
  • kk — константа сглаживания (в индустрии обычно принимается равной 60), которая предотвращает слишком сильное влияние первых мест.
  • rankdenserank_{dense} — позиция документа в выдаче векторного поиска (1, 2, 3...).
  • ranksparserank_{sparse} — позиция документа в выдаче лексического поиска.

Если наш идеальный шаг занял 3-е место в векторной выдаче и 1-е в лексической, при k=60k = 60 его итоговый балл будет: 160+3+160+10.0158+0.0163=0.0321\frac{1}{60 + 3} + \frac{1}{60 + 1} \approx 0.0158 + 0.0163 = 0.0321. Тот шаг, который наберет максимальный RRFscoreRRF_{score}, отправится в контекстное окно LLM.

Предварительная фильтрация: Метаданные в PostgreSQL

Математика векторов и RRF требует вычислительных ресурсов. Нет смысла искать шаг валидации ответа API среди шагов, предназначенных для кликов по UI-элементам.

Поскольку мы используем PostgreSQL, мы можем применять жесткую фильтрацию по метаданным перед тем, как вычислять математическую близость. Каждый BDD-шаг в нашей базе должен сопровождаться тегами.

SELECT
    step_text,
    embedding <=> '[0.12, -0.45, ...]' AS distance
FROM
    bdd_steps
WHERE
    step_type = 'Then'
    AND domain = 'payments'
ORDER BY
    distance ASC
LIMIT 10;

В этом примере оператор WHERE отсекает 90% нерелевантной базы (оставляя только проверки Then из домена payments), и оператор <=> вычисляет косинусное расстояние только для оставшихся строк. Это радикально ускоряет поиск и снижает вероятность галлюцинаций LLM, так как в выборку физически не могут попасть шаги-действия (Given или When).

Re-ranking: Глубокая оценка с помощью Cross-Encoder

Даже после гибридного поиска и фильтрации порядок шагов может быть не идеальным. Модели эмбеддингов, которые мы обсуждали ранее, относятся к классу Bi-encoder. Они превращают запрос в вектор А, документ в вектор Б, и быстро сравнивают их. Это быстро, но поверхностно.

Для финальной полировки топ-результатов применяется Cross-encoder.

Cross-encoder — это архитектура нейросети, которая принимает на вход одновременно и запрос, и документ (через специальный токен-разделитель), пропуская их вместе через все слои внимания (Self-Attention).

Bi-encoder можно сравнить с HR-специалистом, который за секунду сопоставляет название вашей должности с названием вакансии. Cross-encoder — это часовое техническое интервью, где выясняется реальная совместимость.

Cross-encoder работает медленно, поэтому его нельзя применять ко всей базе PostgreSQL. Паттерн использования выглядит так:

  1. Retrieval (Извлечение): Гибридный поиск + фильтрация метаданных быстро достают Топ-50 кандидатов из базы в 10 000 шагов.
  2. Re-ranking (Ранжирование): Cross-encoder попарно оценивает связку «Запрос + Кандидат» для этих 50 шагов и выдает точную оценку релевантности от 0 до 1.
  3. Context Injection (Инъекция): Топ-5 лучших шагов после ре-ранжирования вставляются в промпт для LLM.

Именно на этапе Cross-encoder нейросеть окончательно поймет, что шаг с «дебетовым счетом» не подходит для запроса про «кредитный счет», так как механизм Self-Attention напрямую свяжет эти слова-антонимы внутри контекста проверки.

Построенный таким образом конвейер (Metadata Filter \rightarrow Hybrid Search \rightarrow Cross-encoder Re-ranking) гарантирует, что в ограниченное контекстное окно языковой модели попадут исключительно те BDD-шаги, которые существуют в кодовой базе банка и точно соответствуют функциональному сценарию. Следующая задача — научить AI-агента самостоятельно принимать решение о том, какие именно запросы отправлять в этот поисковый конвейер.

Агенты и Reasoning: реализация паттерна ReAct для планирования тестовых сценариев

Агенты и Reasoning: реализация паттерна ReAct для планирования тестовых сценариев

Даже самая совершенная поисковая система выдаст мусор, если задать ей неправильный вопрос. Представьте: бизнес-аналитик просит систему «Сгенерировать тест для одобрения корпоративного кредита». Если мы напрямую отправим эту фразу в наш гибридный конвейер поиска, построенный ранее, векторная база попытается найти один шаг (step definition), семантически близкий ко всему этому сложному процессу. Результат будет нулевым или нерелевантным, потому что в кодовой базе нет единого шага «одобрить кредит» — там лежат атомарные кирпичики вроде «пользователь авторизован», «нажата кнопка X» и «статус заявки изменился на Y».

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

Паттерн ReAct: от генерации к рассуждению

Для решения многосоставных задач используется архитектурный паттерн ReAct (Reasoning and Acting). Он объединяет способность LLM к логическому рассуждению (формированию плана) со способностью выполнять действия во внешней среде (например, обращаться к базе данных).

ReAct — это парадигма, в которой агент решает задачу через циклический процесс: он формулирует мысль о том, что нужно сделать сейчас, выполняет конкретное действие, получает результат этого действия из внешнего мира и на его основе формулирует следующую мысль.

Этот цикл состоит из трех обязательных компонентов, которые повторяются до достижения цели:

  1. Thought (Мысль): Агент анализирует текущий контекст и решает, какой следующий шаг предпринять.
  2. Action (Действие): Агент вызывает внешний инструмент (Tool) с конкретными параметрами.
  3. Observation (Наблюдение): Агент получает сырой ответ от инструмента и добавляет его в свой контекст.

Сравнение подходов

Характеристика Стандартный RAG (One-shot) Агентный подход (ReAct)
Количество обращений к БД Одно (весь промпт целиком) Несколько (под каждый логический шаг)
Реакция на отсутствие данных Галлюцинация или отказ отвечать Смена стратегии поиска, переформулирование запроса
Роль LLM Синтез ответа на основе одного куска контекста Планировщик, маршрутизатор и оценщик результатов

Инструменты (Tools) и Function Calling

Чтобы LLM могла совершить Action, ей нужно дать «руки». В контексте разработки агентов это реализуется через механизм Function Calling (вызов функций).

Мы не учим модель писать SQL-запросы к PostgreSQL. Вместо этого мы описываем для нее доступные инструменты в виде строгой JSON-схемы. Модель читает описание и, когда решает, что инструмент нужен, генерирует не обычный текст, а структурированный JSON с аргументами для вызова.

Для нашего BDD-агента мы оборачиваем сложный конвейер поиска (с BM25, кросс-энкодерами и фильтрацией) в один понятный инструмент:

{
  "name": "search_bdd_steps",
  "description": "Ищет существующие шаги Gherkin в кодовой базе. Используй это для поиска точных формулировок шагов.",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "Короткая фраза для поиска одного конкретного действия, например 'ввод пин-кода' или 'проверка баланса'."
      },
      "step_type": {
        "type": "string",
        "enum": ["Given", "When", "Then"],
        "description": "Тип шага для фильтрации."
      }
    },
    "required": ["query"]
  }
}

Когда агент решает использовать search_bdd_steps, выполнение на стороне LLM приостанавливается. Наш Python-бэкенд перехватывает JSON, реально выполняет поиск по векторной базе и возвращает текст найденных шагов обратно модели в виде Observation.

Трассировка цикла: блокировка скомпрометированной карты

Посмотрим, как этот цикл разворачивается в реальности на примере запроса: «Напиши сценарий блокировки банковской карты при подозрении на компрометацию».

Итерация 1: Погружение в контекст

  • Thought: Чтобы заблокировать карту, пользователь должен сначала находиться в системе и иметь активную карту. Мне нужно найти шаги предусловий.
  • Action: search_bdd_steps(query="пользователь авторизован", step_type="Given")
  • Observation: ["Given клиент авторизован в мобильном приложении", "Given клиент находится на главном экране"]

Итерация 2: Основное действие

  • Thought: Отлично, предусловия найдены. Теперь нужно найти шаг, где клиент инициирует блокировку карты из-за компрометации.
  • Action: search_bdd_steps(query="заблокировать карту причина компрометация", step_type="When")
  • Observation: ["When клиент выбирает опцию 'Блокировка карты'", "When клиент указывает причину 'Украли/Потерял'"]

Итерация 3: Проверка результата

  • Thought: Шаги действий есть. Осталось найти шаг проверки, что статус карты изменился.
  • Action: search_bdd_steps(query="статус карты заблокирована", step_type="Then")
  • Observation: ["Then статус карты в системе меняется на 'Заблокирована'"]

Итерация 4: Завершение

  • Thought: Я собрал все необходимые шаги из существующей базы. Теперь я могу сформировать итоговый feature-файл.
  • Action: finish(final_answer="Feature: Блокировка... Scenario: ...")

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

Сила Reasoning: обработка ошибок и тупиков

Главная ценность ReAct проявляется не тогда, когда всё идет по плану, а когда система сталкивается с неопределенностью. На технических интервью часто спрашивают: «Что будет, если ваш RAG ничего не найдет?».

Предположим, на 3-й итерации агент искал шаг отправки СМС:

  • Action: search_bdd_steps(query="отправлено СМС о блокировке", step_type="Then")
  • Observation: [] (Пустой список, шаг не найден).

Стандартный скрипт на этом бы сломался или LLM придумала бы несуществующий шаг. ReAct-агент реагирует иначе:

  • Thought: Шаг с отправкой СМС не найден в базе. Возможно, в этом банке используются push-уведомления. Попробую поискать другой канал связи.
  • Action: search_bdd_steps(query="отправлено push уведомление", step_type="Then")
  • Observation: ["Then клиент получает push-уведомление с текстом {string}"]

Агент адаптировался к реальности кодовой базы благодаря этапу Thought. Он проанализировал неудачу и скорректировал свое поведение.

Ограничение автономности: защита от бесконечных циклов

Автономность агента таит в себе опасность. Если нужного шага действительно нет ни в каком виде, агент может бесконечно перебирать синонимы, сжигая бюджет на токены.

Для контроля этого процесса вводится жесткое математическое ограничение на уровне бэкенда:

iNmaxi \leq N_{max}

Где ii — номер текущей итерации цикла (Thought-Action-Observation), а NmaxN_{max} — максимально допустимое количество шагов агента (обычно от 5 до 10 для задач генерации тестов).

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

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

Проектирование Python-бэкенда на FastAPI: асинхронность и интеграция с Kafka

Проектирование Python-бэкенда на FastAPI: асинхронность и интеграция с Kafka

Один вызов современной LLM занимает от 1 до 5 секунд. Если наш AI-агент, построенный на базе паттерна ReAct, не находит нужный шаг с первого раза, он меняет стратегию, делает новый запрос к векторной базе и снова обращается к LLM. Время выполнения одного пользовательского запроса на генерацию BDD-сценария описывается формулой Ttotal=i=1N(Tthought+Taction)T_{total} = \sum_{i=1}^{N} (T_{thought} + T_{action}), где NN — количество итераций агента. При N=5N = 5 ответ может формироваться 25–30 секунд. Если мы попытаемся обрабатывать такие запросы в традиционном синхронном веб-фреймворке, первый же десяток пользователей, одновременно нажавших кнопку «Сгенерировать тест» в своих IDE, полностью парализует сервер корпоративного банка.

Чтобы построить надежный внутренний инструмент, нам необходимо спроектировать бэкенд, способный выдерживать высокую конкурентность при длительном времени ожидания ответа от внешних систем.

I/O-bound природа AI-агентов и роль GIL

В Python существует механизм глобальной блокировки интерпретатора (GIL).

GIL (Global Interpreter Lock) — это мьютекс, который защищает доступ к объектам Python, предотвращая одновременное выполнение байт-кода несколькими нативными потоками.

На технических интервью часто спрашивают, как GIL мешает параллельным вычислениям. Однако в контексте разработки AI-агентов GIL не является узким местом.

Процесс работы нашего агента — это классическая I/O-bound задача (ограниченная вводом-выводом). Агент почти не тратит процессорное время сервера (CPU-bound) на математические вычисления. Вместо этого он постоянно ждет: ждет ответа от API OpenAI или локальной LLM, ждет результатов поиска из PostgreSQL с pgvector, ждет ответа от корпоративной базы знаний.

Когда поток в Python выполняет I/O-операцию (например, сетевой запрос к LLM), он освобождает GIL. Это означает, что пока один агент ждет генерации токенов, сервер может обрабатывать запросы других пользователей. Проблема синхронных серверов не в GIL, а в том, что каждый ожидающий запрос блокирует целый поток ОС, которых в пуле сервера (например, Gunicorn с синхронными воркерами) обычно немного.

FastAPI и магия Event Loop

Для решения проблемы блокировки потоков мы используем асинхронное программирование на базе библиотеки asyncio и фреймворка FastAPI.

В основе asyncio лежит Event Loop (цикл событий). Это бесконечный цикл, который управляет выполнением асинхронных задач. Когда FastAPI получает запрос и доходит до строки с обращением к LLM, он не блокирует поток. Ключевое слово await сигнализирует циклу событий: «Эта задача уходит в режим ожидания сети. Сохрани её состояние и переключись на обслуживание следующего входящего HTTP-запроса».

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.post("/generate-bdd")
async def generate_bdd_scenario(feature_description: str):
    # Ключевое слово await возвращает управление в Event Loop
    # Пока LLM думает, сервер принимает другие запросы
    agent_response = await react_agent.run(feature_description)
    return {"scenario": agent_response}

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

Архитектурный барьер: проблема HTTP-таймаутов

Асинхронный FastAPI решает проблему выживаемости сервера. Но он не решает проблему клиента и сетевой инфраструктуры.

Представим, что тест-автоматизатор вызывает наш эндпоинт из IDE-плагина. Запрос проходит через корпоративный балансировщик нагрузки (например, Nginx). Балансировщики настроены на обрыв соединения, если сервер не отвечает в течение заданного времени (обычно 30 или 60 секунд).

Если ReAct-агент застрял в сложном цикле поиска и генерации, на 31-й секунде балансировщик принудительно закроет соединение и вернет клиенту ошибку 504 Gateway Timeout. Сервер FastAPI при этом честно доделает работу на 45-й секунде, но результат будет отправлять некуда — клиент уже отключен.

Для долгих и непредсказуемых задач парадигма Request-Response (запрос-ответ) поверх HTTP перестает работать. Нам нужен переход к событийно-ориентированной архитектуре (Event-Driven Architecture).

Apache Kafka: шина для долгоживущих задач

Чтобы отвязать время работы AI-агента от времени жизни HTTP-запроса, мы вводим в стек Apache Kafka — распределенный брокер сообщений.

Вместо того чтобы заставлять клиента ждать завершения генерации, мы разделяем процесс на два независимых этапа с помощью очередей (топиков).

Механика асинхронной делегации

  1. Producer (FastAPI): IDE отправляет HTTP POST запрос с описанием функционала. FastAPI мгновенно валидирует запрос, генерирует уникальный task_id, упаковывает данные в сообщение и публикует его в Kafka-топик bdd_generation_requests.
  2. Ответ клиенту: FastAPI сразу же возвращает клиенту HTTP-статус 202 Accepted и task_id. Соединение закрывается за миллисекунды. Никаких таймаутов.
  3. Consumer (Worker): В фоне работает отдельный пул Python-воркеров. Они подписаны на топик bdd_generation_requests. Воркер берет сообщение, запускает тяжелый многошаговый ReAct-цикл (который может длиться хоть минуту) и по завершении публикует готовый Gherkin-сценарий в топик bdd_generation_results.

Топик (Topic) в Kafka — это именованный канал, в который производители пишут сообщения, а потребители их читают. Сообщения в топике упорядочены и сохраняются на диск, что гарантирует их сохранность даже при падении воркера.

Сравнение подходов к маршрутизации запросов

Характеристика Синхронный HTTP Асинхронный HTTP (FastAPI) Event-Driven (FastAPI + Kafka)
Блокировка потока Да (сервер быстро падает под нагрузкой) Нет (сервер выдерживает нагрузку) Нет
Риск 504 Timeout Высокий Высокий (если агент думает долго) Исключен (мгновенный ответ 202)
Масштабирование Добавление серверов Добавление серверов Независимое масштабирование API и AI-воркеров
Устойчивость к сбоям Запрос теряется при падении Запрос теряется при падении Запрос сохраняется в Kafka и будет переобработан

Масштабирование через Consumer Groups

Интеграция Kafka дает нам мощный инструмент для управления ресурсами, критически важный при работе с LLM.

Если утром 50 тестировщиков одновременно запросят генерацию сценариев, в топик упадет 50 сообщений. Если бы мы обрабатывали их сразу, мы могли бы упереться в лимиты (Rate Limits) API корпоративной LLM.

Используя механизм Consumer Group (группа потребителей) в Kafka, мы можем запустить, например, ровно 5 воркеров. Они будут плавно, один за другим, разбирать очередь из 50 задач. Если нам нужно ускорить процесс и лимиты LLM позволяют — мы просто запускаем еще 5 контейнеров с воркерами. Kafka автоматически перераспределит нагрузку между ними (через механизм партиций), не требуя изменений в коде FastAPI.

Таким образом, FastAPI берет на себя роль быстрого и легкого шлюза, обеспечивающего интерфейс для внешних систем, а Kafka выступает надежным буфером, защищающим тяжелую логику AI-агента от сетевых таймаутов и пиковых нагрузок.

Интеграция AI-агента в BDD-фреймворк: генерация Gherkin и маппинг step definitions

Интеграция AI-агента в BDD-фреймворк: генерация Gherkin и маппинг step definitions

Представьте, что ReAct-агент успешно завершил свой цикл поиска. В топик Kafka падает готовый сценарий: Then the account balance should be updated. Выглядит логично, но при запуске тестов Python BDD-фреймворк (например, behave или pytest-bdd) мгновенно падает с ошибкой UndefinedStepError. Причина проста: в кодовой базе банка этот шаг реализован как @then('the balance is {amount}'). Свободная генерация текста — главный враг автоматизации. Наша задача — заставить LLM говорить строго на языке существующих функций.

Доставка результата: от Kafka к IDE

В прошлой главе мы остановились на том, что тяжеловесный процесс генерации делегирован в Kafka, а клиент получил лишь task_id. Чтобы замкнуть этот контур и вернуть готовый Gherkin-сценарий инженеру в IDE, нам нужен механизм асинхронной доставки.

Поскольку конечным потребителем является плагин для IDE (IntelliJ IDEA или VS Code), мы стоим перед выбором паттерна коммуникации.

Паттерн Принцип работы Применимость для IDE-плагина
Long Polling Клиент периодически спрашивает: «Готово? Готово?» Высокий оверхед на сеть, задержки в получении результата.
Server-Sent Events (SSE) Однонаправленный поток данных от сервера к клиенту. Хорошо подходит для стриминга текста, но не поддерживает двустороннюю связь.
WebSockets Постоянное двустороннее TCP-соединение. Идеально. Позволяет не только вернуть итоговый файл, но и транслировать логику рассуждений агента (Thoughts) в реальном времени.

Для интеграции мы вводим компонент WebSocket Gateway. Это легковесный сервис на FastAPI, который подписывается на топик Kafka bdd_generation_results. Когда плагин IDE инициирует генерацию, он открывает WebSocket-соединение, передавая свой task_id. Gateway читает Kafka, находит сообщение с совпадающим идентификатором и пушит его прямо в сокет клиента. O(1)O(1) маршрутизация без блокировки потоков.

Иллюзия свободного текста

Получив канал связи, мы должны решить, что именно по нему передавать. Естественный порыв — попросить LLM сгенерировать готовый .feature файл. Это фатальная архитектурная ошибка.

Большие языковые модели вероятностны и склонны к перифразу. BDD-фреймворки детерминированы и требуют посимвольного совпадения регулярных выражений.

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

Структурированная генерация и Step Registry

В основе любого Python BDD-фреймворка лежит Step Registry — глобальный реестр, где текстовые паттерны связаны с Python-функциями.

Вместо генерации текста мы заставляем агента (через механизм Function Calling) возвращать структурированный JSON. Агент использует контекст, полученный от векторной базы данных, чтобы выбрать точные идентификаторы шагов.

{
  "scenario_name": "Успешный перевод между своими счетами",
  "steps": [
    {
      "step_id": "given_user_auth_12",
      "keyword": "Given",
      "parameters": {"user_id": "105"}
    },
    {
      "step_id": "when_transfer_funds_08",
      "keyword": "When",
      "parameters": {"amount": "500", "currency": "USD"}
    }
  ]
}

В этом подходе LLM выступает в роли планировщика. Она не придумывает формулировки, она вызывает существующие функции тестирования, передавая им аргументы.

Математика извлечения параметров

Самый сложный этап маппинга — правильное извлечение переменных (плейсхолдеров). Допустим, в базе знаний найден шаг с сигнатурой: @when('пользователь переводит {amount} {currency} на счет {target_account}')

Агент анализирует исходное функциональное требование («Клиент перекидывает 1000 руб на счет 40817...») и должен извлечь параметры. Для успешного маппинга должно строго выполняться условие: Nargs=NparamsN_{args} = N_{params}

Где NargsN_{args} — количество аргументов, переданных агентом в JSON, а NparamsN_{params} — количество плейсхолдеров в сигнатуре шага. Если NargsNparamsN_{args} \neq N_{params}, BDD-фреймворк не сможет внедрить переменные в функцию, и тест упадет с ошибкой TypeError.

Для контроля этого условия на стороне Python-бэкенда реализуется валидатор. Он парсит сигнатуру шага по идентификатору step_id, подсчитывает количество переменных в фигурных скобках и сверяет их с ключами словаря parameters из JSON-ответа агента.

Сборка Gherkin и Dry Run валидация

Имея валидный JSON и проверенные параметры, бэкенд приступает к финальной сборке. Он берет оригинальные текстовые паттерны из Step Registry, подставляет в них параметры и формирует итоговый текст:

Feature: Внутренние переводы
  Scenario: Успешный перевод между своими счетами
    Given пользователь "105" авторизован в системе
    When пользователь переводит "500" "USD" на счет "40817810..."

Но перед тем как отправить этот текст через WebSocket Gateway в IDE, система выполняет критический шаг — Dry Run (холостой прогон).

Dry Run — это программный запуск BDD-фреймворка в режиме парсинга, без фактического выполнения Python-кода тестов. Бэкенд скармливает сгенерированный сценарий behave или pytest-bdd. Если фреймворк возвращает статус passed (все шаги распознаны и слинкованы с кодом) — сценарий отправляется пользователю. Если возникает UndefinedStepError — текст не доходит до клиента. Ошибка оборачивается в новый промпт и отправляется обратно ReAct-агенту как Observation, запуская цикл самокоррекции.

Таким образом, мы гарантируем, что пользователь в IDE получает не просто правдоподобный текст, а 100% исполняемый автоматизированный тест, полностью интегрированный с существующей кодовой базой банка.

Оценка качества AI-системы: метрики Retrieval, Accuracy и борьба с галлюцинациями

Оценка качества AI-системы: метрики Retrieval, Accuracy и борьба с галлюцинациями

Представьте, что наш AI-агент сгенерировал идеально валидный сценарий. Система Dry Run, встроенная в конвейер, подтвердила: все шаги существуют в Step Registry, синтаксис Gherkin безупречен. Но вместо проверки «блокировки учетной записи после трех неверных вводов ПИН-кода» агент собрал тест для «восстановления забытого пароля». С технической точки зрения код работает, с точки зрения бизнеса — система автоматизирует создание бесполезного мусора. Как доказать, что внедряемый AI-инструмент действительно решает задачу, а не просто генерирует правдоподобный текст?

Качество RAG-системы и ReAct-агента невозможно измерить одной цифрой. Ошибка в финальном ответе может быть следствием двух совершенно разных проблем: либо векторная база не нашла нужный шаг в библиотеке (провал извлечения), либо агент нашел правильный шаг, но передал в него неверные параметры (провал генерации). Чтобы управлять качеством, мы должны разделить оценку на два независимых контура.

Оценка контура извлечения (Retrieval Metrics)

Когда агент формирует запрос к нашему гибридному поиску, база возвращает массив потенциально подходящих шагов. Наша задача — математически оценить, насколько этот массив полезен. Для этого в индустрии поиска используют метрики, оценивающие выдачу на заданной глубине KK (обычно топ-3 или топ-5 результатов).

Базовые метрики: Precision и Recall

Два фундаментальных показателя отвечают на вопрос о соотношении найденного и релевантного.

Precision@K=RKPrecision@K = \frac{R}{K}

Где RR — количество действительно подходящих шагов в выданном топе, а KK — размер оцениваемой выдачи. Эта метрика показывает, насколько выдача «чистая». Если из 5 возвращенных базой шагов только 2 подходят для нашего теста, точность составит 0.4.

Recall@K=RTRecall@K = \frac{R}{T}

Где RR — количество найденных подходящих шагов в топе KK, а TT — общее количество подходящих шагов во всей корпоративной библиотеке. Полнота показывает, не упустили ли мы что-то важное.

Сравним их поведение в контексте автотестов:

Ситуация Precision@5 Recall@5 Диагноз системы
Выдано 5 шагов. 1 идеальный, 4 мусорных. В базе больше нет подходящих. 0.2 (Низкая) 1.0 (Идеальная) База находит всё, но забивает контекстное окно агента лишним шумом.
Выдано 5 шагов. Все 5 отличные. Но в базе есть еще 15 вариаций этого действия. 1.0 (Идеальная) 0.25 (Низкая) Выдача чистая, но мы рискуем упустить специфический шаг (например, для мобильной платформы).

Оценка ранжирования: MRR (Mean Reciprocal Rank)

Для LLM критически важен порядок данных в контексте. Чем выше правильный ответ, тем меньше вероятность, что агент его проигнорирует. Метрики выше не учитывают позицию: шаг на 1-м месте и на 5-м месте дает одинаковый вклад в Precision. Чтобы оценить качество ранжирования, используется MRR.

MRR=1Qi=1Q1rankiMRR = \frac{1}{|Q|} \sum_{i=1}^{|Q|} \frac{1}{rank_i}

Здесь Q|Q| — общее количество тестовых запросов к базе, а rankirank_i — позиция (ранг) первого релевантного шага в выдаче для конкретного запроса.

Если агент ищет шаг «клиент подписывает документ USB-токеном», и гибридный поиск ставит идеальное совпадение на 4-е место, то показатель для этого запроса будет 14=0.25\frac{1}{4} = 0.25. Если на 1-е место — 1.01.0. Усредняя этот показатель по сотням тестовых запросов, мы получаем единую метрику качества работы нашего поискового пайплайна (эмбеддингов, BM25 и Re-ranking).

Оценка контура генерации (Generation Metrics)

Допустим, MRR нашей базы близок к 1.0. Агент получает идеальные шаги. Теперь он должен сопоставить требования бизнеса с плейсхолдерами в коде и собрать JSON. Здесь классические метрики точного совпадения не работают: агент может сформулировать параметры разными словами, которые будут одинаково верны.

Для оценки семантической правильности применяется фреймворк RAG Triad (Триада RAG), который оценивает три вектора связей внутри системы.

  1. Context Relevance (Релевантность контекста): Насколько найденные шаги соответствуют исходному функциональному требованию? (Оценивает стык «Требование \to База знаний»).
  2. Groundedness (Обоснованность): Насколько финальный JSON-сценарий опирается только на предоставленные шаги и требования, не придумывая отсебятины? (Оценивает стык «Контекст \to Ответ»).
  3. Answer Relevance (Релевантность ответа): Покрывает ли итоговый Gherkin-файл изначальную бизнес-задачу целиком? (Оценивает стык «Требование \to Ответ»).

Обоснованность (Groundedness) — ключевая метрика борьбы с галлюцинациями в корпоративных данных. Она требует, чтобы каждый сгенерированный параметр можно было проследить до конкретного слова в исходном документе.

LLM-as-a-Judge

Кто должен вычислять эти метрики? Человек физически не способен просматривать тысячи логов генерации. Решением выступает паттерн LLM-as-a-Judge (LLM в роли судьи).

Мы берем отдельную, более мощную модель (например, с температурой 0 для максимальной детерминированности) и даем ей системный промпт оценщика. На вход судье подаются три элемента: исходное требование, найденные базой шаги и итоговый результат нашего агента. Судья анализирует их и возвращает бинарную оценку (1 или 0) по каждому критерию триады, а также текстовое обоснование оценки.

Специфика галлюцинаций в BDD и трассируемость

В контексте генерации автотестов галлюцинации принимают специфические формы. Агент редко выдумывает несуществующие слова. Его галлюцинации — это логические ошибки:

  • Использование шага UI вместо API для бэкенд-теста.
  • Подстановка параметра currency="USD" в сценарий, где валюта не была указана явно (модель додумала дефолтное значение из своих весов).
  • Пропуск обязательного шага авторизации, потому что в бизнес-требовании о нем не упоминалось напрямую («очевидные» для человека вещи).

Главным оружием против таких галлюцинаций становится Traceability (Трассируемость).

Мы модифицируем промпт агента так, чтобы при формировании JSON он не просто выдавал step_id и parameters, но и добавлял поле source_quote. Для каждого параметра агент обязан скопировать точную цитату из исходного требования, которая послужила основанием для выбора этого значения.

Если агент пытается подставить USD, но не может найти цитату про доллары в исходном тексте, механизм внутреннего контроля (Thought в цикле ReAct) заставит его остановиться и запросить уточнение, либо оставить параметр пустым.

Практический пример: пайплайн оценки

Сведем все метрики в единый процесс оценки качества для сценария «Подписание документа USB-токеном».

  1. Этап Retrieval: Агент извлекает шаги. В топ-3 попадают:

    • Позиция 1: Шаг: Пользователь подписывает документ по СМС.
    • Позиция 2: Шаг: Корпоративный клиент подписывает документ USB-токеном.
    • Позиция 3: Шаг: Загрузка сертификата токена. Оценка: Правильный шаг на втором месте. rank=2rank = 2. MRR для этого запроса равен 0.5. Precision@3 = 0.33.
  2. Этап Generation: Агент формирует JSON, подставляя параметр role="Корпоративный клиент".

  3. Этап Evaluation (LLM-as-a-Judge):

    • Context Relevance: 1 (Контекст содержит нужный шаг).
    • Groundedness: 1 (Роль «Корпоративный клиент» взята прямо из текста шага, есть цитата-трассировка).
    • Answer Relevance: 1 (Сценарий решает поставленную задачу).

Внедрив такой пайплайн оценки, мы переводим разговоры о качестве AI из плоскости «мне кажется, он пишет плохие тесты» в плоскость «MRR базы упал до 0.4, а Groundedness агента составляет 82%». Это дает инженерный фундамент для безопасного обновления моделей, изменения системных промптов и тюнинга поисковых алгоритмов. Дальше предстоит интегрировать этот измерительный комплекс в процессы непрерывной интеграции.

Промышленная эксплуатация: Docker, CI/CD для LLM-сервисов и мониторинг агентов

Промышленная эксплуатация: Docker, CI/CD для LLM-сервисов и мониторинг агентов

Традиционное программное обеспечение при критической ошибке падает с исключением или возвращает HTTP 500. AI-агент при ошибке не падает — он уверенно генерирует синтаксически безупречный, но логически абсурдный Gherkin-сценарий, попутно сжигая 50 000 токенов в бесконечном цикле поиска. Переход от локальной разработки к промышленной эксплуатации требует полного пересмотра того, как мы упаковываем, тестируем и отслеживаем работу системы.

Упаковка AI-бэкенда: специфика Docker-контейнеризации

Наш бэкенд состоит из FastAPI, асинхронных консьюмеров Kafka и интеграции с PostgreSQL (pgvector). Главная проблема при контейнеризации Python-приложений в сфере AI — раздувание образа. Библиотеки для работы с векторными базами, фреймворки маршрутизации промптов и драйверы баз данных могут увеличить размер контейнера до нескольких гигабайт, что замедляет деплой и масштабирование.

Для решения этой проблемы применяется паттерн Multi-stage build (многоэтапная сборка). Мы разделяем процесс на этап компиляции зависимостей и этап формирования финального легковесного образа.

# Этап 1: Сборка (Builder)
FROM python:3.11-slim as builder

WORKDIR /app
RUN apt-get update && apt-get install -y gcc libpq-dev
COPY requirements.txt .
# Устанавливаем зависимости в отдельную директорию
RUN pip install --prefix=/install -r requirements.txt

# Этап 2: Продакшен (Runner)
FROM python:3.11-slim

WORKDIR /app
# Копируем только готовые бинарники и библиотеки, оставляя gcc и кэши позади
COPY --from=builder /install /usr/local
COPY ./src /app/src

# Запускаем FastAPI воркер
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

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

CI/CD для AI: от проверки кода к Continuous Evaluation

В классическом CI/CD пайплайн проверяет синтаксис, прогоняет юнит-тесты и собирает образ. Но для AI-систем изменение одной строки в системном промпте или корректировка веса лексического поиска в гибридной выдаче не ломает код. Это ломает поведение.

Чтобы автоматизировать контроль качества, метрики измерительного комплекса (RAG Triad, Traceability) встраиваются в пайплайн развертывания. Этот процесс называется Continuous Evaluation (CE).

Continuous Evaluation (CE) — практика непрерывной оценки качества ответов AI-модели на этапах CI/CD с использованием фиксированных наборов данных и автоматизированных метрик, блокирующая релиз при деградации семантики.

Основой CE является Golden Dataset (Золотой датасет). Это тщательно собранная эталонная выборка, состоящая из пар: «Исходное функциональное требование» \rightarrow «Идеальный набор BDD-шагов».

Как выглядит пайплайн развертывания AI-агента

  1. Static Analysis & Unit Tests: Проверка Python-кода, линтеры, тесты бизнес-логики (например, правильность сборки JSON в текст Gherkin).
  2. Integration Tests: Поднятие тестовых контейнеров базы с pgvector и Kafka, проверка успешности записи и чтения.
  3. Continuous Evaluation (Golden Dataset Run):
    • Пайплайн запускает ReAct-агента с новой версией промпта или кода на 100 примерах из Golden Dataset.
    • Паттерн LLM-as-a-Judge оценивает результаты по метрикам Groundedness и Context Relevance.
    • Проверяется наличие ссылок на исходные требования (source_quote).
  4. Quality Gate: Если средний балл метрик падает более чем на 5% по сравнению с текущей версией в production (main ветка), пайплайн завершается с ошибкой, блокируя слияние кода (Merge/Pull Request).
Характеристика Традиционный CI/CD AI-ориентированный CI/CD (LLMOps)
Объект тестирования Детерминированный код Вероятностное поведение и промпты
Триггер ошибки Упавший assert (ожидаемое \neq фактическое) Падение семантической оценки ниже порога (например, Score<0.8Score < 0.8)
Время прохождения Минуты (быстрое выполнение) Десятки минут (ожидание ответов от LLM-судьи)
Откат релиза При багах в логике При галлюцинациях и потере контекста

Observability: мониторинг ReAct-агентов в production

Когда агент успешно проходит CI/CD и попадает в production, стандартных метрик (RPS, таймауты, загрузка CPU) становится недостаточно. Нам необходимо внедрить LLM Observability — наблюдаемость внутренних процессов языковой модели и инструментов.

Ключевые оси мониторинга AI-агента:

  1. Экономика и квоты (Token Usage): Каждый запрос стоит денег и приближает систему к лимитам провайдера (Rate Limits). Мониторинг должен агрегировать количество входящих (Prompt) и исходящих (Completion) токенов в разрезе каждого пользователя или команды.
  2. Глубина рассуждений (Agentic Loop Depth): Сколько итераций Thought-Action-Observation потребовалось агенту для финального ответа? Если среднее количество шагов растет, значит, база BDD-шагов стала хуже искаться, и агент вынужден перебирать разные запросы. Если количество итераций регулярно упирается в жесткий лимит (например, N=5N = 5), это сигнал о зацикливании (Infinite Loop).
  3. Задержка поиска (Retrieval Latency): Разделение времени ответа на время генерации LLM и время выполнения векторного поиска в PostgreSQL. Если база разрастается, а индексы pgvector (например, HNSW) не оптимизированы, поиск станет узким горлышком.

Трассировка мыслей агента (Tracing)

Для отладки инцидентов в production логирования текста недостаточно. Используются системы распределенной трассировки (например, OpenTelemetry), которые визуализируют весь жизненный цикл запроса в виде дерева (Spans).

В таком дереве корневой спан — это получение сообщения из Kafka. Вложенные спаны — это каждый шаг ReAct-цикла:

  • Спан 1: Генерация плана (Thought).
  • Спан 2: Вызов инструмента поиска (Action).
  • Спан 3: SQL-запрос к pgvector (Observation).
  • Спан 4: Финальная валидация через Dry Run.

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

Практический кейс: защита от деградации системы

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

Разработчик отправляет код в репозиторий. Запускается CI/CD. Юнит-тесты проходят успешно (код не сломан). Поднимается интеграционная база — всё работает. Начинается этап Continuous Evaluation на Golden Dataset.

Пайплайн прогоняет 100 тестовых сценариев. Агент успешно генерирует Gherkin, но LLM-as-a-Judge фиксирует, что в 40% случаев пропало поле source_quote. Метрика Traceability падает с 0.95 до 0.50. Quality Gate краснеет, пайплайн блокирует деплой. Система спасла компанию от внедрения агента, который генерирует непроверяемые тесты, предотвратив деградацию доверия пользователей к инструменту.

Построив надежный бэкенд, внедрив метрики качества и обеспечив бесперебойную эксплуатацию через Docker и CI/CD, мы подготовили прочный фундамент. Теперь система готова к тому, чтобы передать результаты своей работы конечному пользователю.

Проектирование интерфейсов: от Streamlit-прототипа к архитектуре IDE Plugin

Проектирование интерфейсов: от Streamlit-прототипа к архитектуре IDE Plugin

Переключение контекста обходится инженеру в среднем в 23 минуты потерянной продуктивности. Если для генерации BDD-сценария тестировщику нужно свернуть IDE, открыть корпоративный браузер, авторизоваться в веб-интерфейсе AI-агента, вставить требования, дождаться ответа, скопировать результат и вернуться обратно в код — инструмент будет заброшен в первый же месяц. Идеальный AI-бэкенд не имеет ценности, если он ломает состояние потока. Наша задача — сделать систему невидимой, встроив ее прямо в кончики пальцев разработчика.

Streamlit как полигон для гипотез

Прежде чем инвестировать недели в разработку сложного плагина для сред разработки (IDE), архитектуру и качество генерации необходимо валидировать. Для этого в Python-экосистеме стандартом де-факто стал Streamlit.

Streamlit позволяет за считанные часы обернуть наш асинхронный FastAPI-бэкенд в интерактивный веб-интерфейс. На этом этапе мы не решаем проблему контекстного переключения, мы решаем проблему наблюдаемости (Observability) для самой команды разработки AI.

В Streamlit-прототипе мы подключаемся к нашему WebSocket Gateway, чтобы визуализировать работу ReAct-агента в реальном времени:

import streamlit as st
import websockets
import asyncio

async def listen_to_agent(task_id):
    uri = f"ws://api.bank.local/ws/agent/{task_id}"
    async with websockets.connect(uri) as websocket:
        while True:
            message = await websocket.recv()
            st.write(message) # Вывод Thought, Action и финального JSON

# Запуск асинхронного слушателя в UI

Прототип выполняет три критические функции:

  1. Отладка промптов: инженеры видят сырой вывод модели до того, как он будет отформатирован для конечного пользователя.
  2. Стресс-тест инфраструктуры: проверка того, как WebSocket-соединения держатся при длительных сессиях (Twait>30T_{wait} > 30 секунд).
  3. Сбор Golden Dataset: тестировщики могут вручную оценивать сгенерированные в веб-интерфейсе шаги, формируя эталонную базу для CI/CD контура.

Однако, как только качество генерации достигает целевых метрик, Streamlit становится узким местом. Он изолирован от локальной файловой системы пользователя и не знает, какой файл сейчас открыт.

Переход в рабочую среду: Архитектура IDE Plugin

Чтобы AI-агент стал полноценным напарником, он должен жить внутри IDE (например, PyCharm или VS Code). Плагин меняет саму парадигму взаимодействия: система больше не ждет, пока пользователь принесет ей текст, она сама читает локальный контекст.

Сравним две парадигмы взаимодействия:

Характеристика Веб-интерфейс (Streamlit) IDE Plugin
Осведомленность о контексте Нулевая (только то, что ввел пользователь) Полная (текущий файл, выделенный текст, соседние файлы)
Доставка результата Ручное копирование (Copy-Paste) Программная вставка (AST-модификация или Diff)
Жизненный цикл Сессия в браузере Фоновый процесс (Daemon) в IDE
Сетевое взаимодействие HTTP + WebSockets (браузер) Нативные TCP/WebSocket клиенты среды разработки

Плагин выступает в роли умного клиента. Когда автоматизатор выделяет блок функциональных требований в комментариях и нажимает горячую клавишу, плагин собирает локальный контекст: выделенный текст, импорты в текущем файле и путь к файлу. Этот пакет данных отправляется в наш FastAPI-бэкенд, инициируя работу ReAct-агента.

Локальный контекст (Local Context) — набор метаданных и фрагментов кода из текущего рабочего пространства среды разработки, неявно передаваемый AI-модели для повышения релевантности генерации без дополнительных усилий со стороны пользователя.

Управление асинхронным UX: Streaming и Diff View

Главная UX-проблема LLM-агентов — время ожидания. В банковской среде сложный поиск по базе шагов и несколько итераций самокоррекции могут занимать десятки секунд. Если IDE в это время просто покажет крутящийся лоадер, пользователь решит, что система зависла.

Здесь применяется паттерн Streaming UX.

Streaming UX — подход к проектированию интерфейсов, при котором пользователю непрерывно транслируются промежуточные состояния длительной операции (например, логика рассуждений агента), что психологически снижает воспринимаемое время ожидания.

Через WebSocket Gateway плагин получает не только финальный результат, но и события жизненного цикла агента. В боковой панели IDE разработчик видит:

  • 🔍 Ищу существующие шаги для "авторизация по СМС"...
  • ⚙️ Найдено 3 совпадения. Формирую Gherkin-сценарий...
  • 🧪 Проверяю синтаксис (Dry Run)...

Безопасное применение кода

Когда бэкенд наконец возвращает готовый JSON со структурой Gherkin-сценария, плагин не имеет права просто вставить его в файл. Неконтролируемая модификация кода подрывает доверие к инструменту.

Вместо этого используется механизм Diff View (Shadow Document).

  1. Плагин создает виртуальный, скрытый от прямого редактирования документ в памяти IDE.
  2. В этот документ применяется сгенерированный сценарий.
  3. IDE открывает встроенное окно сравнения (Diff), где слева — текущий код пользователя, а справа — предложенный AI вариант.
  4. Разработчик визуально проверяет изменения и нажимает кнопку «Accept» (Применить) или «Reject» (Отклонить).

Эта механика замыкает петлю обратной связи. Если пользователь нажимает «Reject» и вносит правки вручную, плагин может отправить телеметрию (какие именно шаги были исправлены) обратно в Kafka. Эти данные в дальнейшем используются для дообучения модели ранжирования (Re-ranking) или корректировки системного промпта, обеспечивая непрерывное улучшение AI-системы на основе реальных действий инженеров в IDE.