Всем привет! На связи команда Take it easy. Название говорит само за себя: мы упрощаем жизнь другим командам в релизном цикле и повышаем эффективность производственного процесса. 

В любой разработке много времени отнимает тестирование. Поэтому мы решили автоматизировать создание тестовых сценариев API, чтобы помочь тестировщикам. Применили ИИ-инструмент APISpecGen для анализа спецификаций новых API-требований, генерации соответствующих тестовых сценариев, обезличенных тестовых данных по схемам запрос/ответ и select-запросов с помощью GigaChat.

Как работает APISpecGen

На вход мы подаём требования в любом формате (ссылка на Confluence, текстовый файл). На выходе получаем чек-лист идей для проверок в следующем формате:

  • название;

  • цель;

  • предусловия;

  • шаги (с ожидаемым результатом).

Почему именно API-тесты

Мы выбрали API-требования, потому что UI-требования нередко включают в себя диаграммы, макеты и т. д. Нам было проще создать инструмент и изучить работоспособность ИИ с помощью GigaChat. Тогда наша команда была далека от этой темы: мы, как слепые котята, двигались на ощупь, методом проб и ошибок. 

Слишком много текста и токенов

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

Эмбеддинги

Представьте себе два вектора в пространстве. Когда они равны? Когда их длина и направление одинаковы. Так вот, эмбеддинги — это способ представить слова в виде числовых векторов. Чтобы не только снизить количество токенов при поиске по тексту, но и помочь модели лучше улавливать смысл слов и фраз при работе с контекстом.

Вот простейший пример использования эмбеддингов:

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader

loader = TextLoader('требования.txt')
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)

from langchain_community.vectorstores import Chroma
from langchain_gigachat.embeddings.gigachat import GigaChatEmbeddings

embedding_model = GigaChatEmbeddings(
    base_url="https://gigachat-ift.sberdevices.delta.sbrf.ru/v1",
    ca_bundle_file="certs/chain_pem.txt",
    cert_file="certs/published_pem.txt", 
    key_file="certs/TakeItEasy.key",
    verify_ssl_certs=False
)

from langchain_gigachat.chat_models import GigaChat

llm = GigaChat(
    base_url="https://gigachat-ift.sberdevices.delta.sbrf.ru/v1",
    ca_bundle_file="certs/chain_pem.txt",
    cert_file="certs/published_pem.txt", 
    key_file="certs/TakeItEasy.key",
    temperature=0.2,
    model='GigaChat',
    timeout=10)

vectorestore = Chroma.from_documents(
    texts,
    embedding=embedding_model,
)

retriever = vectorestore.as_retriever()

# rs = vectorestore.similarity_search('Какой Path и method для отправки запроса на сервис')

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

system_prompt = (
    "Ты должен ответить на вопрос пользователя с использованием данных из текста.\n"
    "Отвечаай коротко, не более 2-3 предложений.\n"
    "Вот части текста для ответа:"
    "\n\n"
    "{context}"
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{input}"),
    ]
)

question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)

rag = rag_chain.invoke({"input": 'Какой Path и method для отправки запроса на сервис'})["answer"]
print(f"RAG: {rag}")

Ответ:

RAG: для отправки запроса на сервис SearchElectionProduct необходимо использовать метод POST и endpoint /v2/oc/products/search.

С помощью модели для векторного представления текста мы превращаем требования в векторы и кладём их в векторную БД (например, Chroma). Когда мы делали это первый раз, не до конца понимали, как оно работает. Поэтому загрузили в БД целое требование. Эффекта BOOM не вышло ☹ 

Потом поняли, что надо не просто бездумно передавать разделённый на фрагменты текст, а структурировать его. Ведь у каждого API-сервиса есть:

  • название и описание;

  • path;

  • method;

  • схема запроса;

  • схема ответа;

  • логика.

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

Агенты и RAG

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

  • придумать название ТК;

  • выделить цель проверки; 

  • выделить предусловия (если требуются) и подготовить тестовые данные;

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

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

«Танцы с бубнами» начались на последних двух шагах. Как все знают, не всегда и не все требования хорошо описаны. Порой даже непонятно, куда отправить запрос и с каким телом. Стандарта написания требований нет, и каждый аналитик пишет требования на основе собственного опыта. 

Каждый слышит, как он дышит.

Как он дышит, так и пишет,

не стараясь угодить...

Булат Окуджава

На входе был набор требований от разных аналитиков: где-то данные были представлены в виде JSON-схемы, где-то был Swagger (API-studio), где-то таблица с большим количеством colspan.

Надо было не только структурировать информацию, но и проверять её наличие. Если требования неполные — запрашивать необходимые данные или предусмотреть сценарий, как обходиться без них. 

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

Это был один из первых написанных нами агентов. Вот его алгоритм:

  1. На вход подаётся схема запроса (в любом виде).

  2. Агент преобразовывает её в JSON-схему и заполняет синтетическими данными, максимально опираясь на контекст схемы (описание поля, обязательность, тип, паттерн и т. д.).

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

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

RAG и structured_output помог нам добиться более стабильного результата. Мы получали цельную картину. Алгоритм несложный: «Покажи, что подаётся на вход и что хочешь получить, уточни детали — и получишь более точный результат».

К сожалению, with_structure_output не всегда стабильно работает с GigaChat, поэтому мы парсили ответ с помощью PydanticOutputParser.

from typing import List
from pydantic import BaseModel, Field

#Описываем модель

class CheckList(BaseModel):
    """Список проверок логики работы сервиса"""
    check_list: List[str] = Field( description="Список проверок")
    
from typing import Optional

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser

parser = PydanticOutputParser(pydantic_object=CheckList)

prompt = ChatPromptTemplate.from_messages(
    [
         (
            "system",
            "Твоя задача придумать и требований список проверок."
            "Список нужно вернуть в формате JSON\n{format_instructions}\n"
            "В ответе верни только JSON со списком проверок без ```json ... ```"
        ),
        ("human", "ТРЕБОВАНИЯ: {query}"),
    ]
).partial(format_instructions=parser.get_format_instructions())

chain = prompt | llm |parser
query = "Придумай список проверок логики для требований и верни ответ в виде json"

rs = chain.invoke({"query": doc_file})
print("\n".join(rs.check_list))

Цепочки вызовов и агенты — это самое крутое, что есть в LLM, поэтому мы познакомились с LangGraph. Он позволяет с лёгкостью сделать всё, о чём мы рассказали!

Внедрив APISpecGen в наш производственный процесс, мы значительно увеличили покрытие требований тестовыми сценариями (до 70 % в некоторых случаях). Это повысило качество выводимого в пром ПО с той же длительностью тестирования. Команды Сбера с удовольствием пользуются этим инструментом. Уже сформирован бэклог задач на доработку APISpecGen по требованиям от различных систем.

Выводы

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

Команда AIKIBTeam:

  • Владимир Казурин

  • Александра Якунина

  • Мария Лаврентьева

Комментарии (1)


  1. Ranger21
    02.06.2025 06:36

    Запрос и промпт простейшие, а качество ответа зависит от модели, чем сейчас GigaChat страдает...