Реализация кодирования для внедрения строгого управления версиями запросов и регрессионного тестирования для больших языковых моделей с использованием MLflow

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

Установка среды выполнения

Мы устанавливаем все необходимые зависимости и импортируем основные библиотеки, используемые в руководстве. Мы безопасно загружаем ключ API OpenAI во время выполнения, гарантируя, что учётные данные никогда не будут жёстко закодированы в ноутбуке. Мы также инициализируем основные ресурсы НЛП, чтобы обеспечить надёжную работу конвейера оценки в разных средах.

«`python
!pip -q install -U «openai>=1.0.0» mlflow rouge-score nltk sentence-transformers scikit-learn pandas

import os, json, time, difflib, re
from typing import List, Dict, Any, Tuple

import mlflow
import pandas as pd
import numpy as np

from openai import OpenAI
from rougescore import rougescorer
import nltk
from nltk.translate.bleuscore import sentencebleu, SmoothingFunction
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

nltk.download(«punkt», quiet=True)
nltk.download(«punkt_tab», quiet=True)

if not os.getenv(«OPENAIAPIKEY»):
try:
from google.colab import userdata # type: ignore
k = userdata.get(«OPENAIAPIKEY»)
if k:
os.environ[«OPENAIAPIKEY»] = k
except Exception:
pass

if not os.getenv(«OPENAIAPIKEY»):
import getpass
os.environ[«OPENAIAPIKEY»] = getpass.getpass(«Enter OPENAIAPIKEY (input hidden): «).strip()

assert os.getenv(«OPENAIAPIKEY»), «OPENAIAPIKEY is required.»
«`

Конфигурации эксперимента

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

«`python
MODEL = «gpt-4o-mini»
TEMPERATURE = 0.2
MAXOUTPUTTOKENS = 250

ABSSEMSIM_MIN = 0.78
DELTASEMSIMMAXDROP = 0.05
DELTAROUGELMAXDROP = 0.08
DELTABLEUMAX_DROP = 0.10

mlflow.settrackinguri(«file:/content/mlruns»)
mlflow.setexperiment(«promptversioningllmregression»)

client = OpenAI()
embedder = SentenceTransformer(«all-MiniLM-L6-v2»)

EVAL_SET = [
{
«id»: «q1»,
«input»: «Summarize in one sentence: MLflow tracks experiments, runs, parameters, metrics, and artifacts.»,
«reference»: «MLflow helps track machine learning experiments by logging runs with parameters, metrics, and artifacts.»
},
{
«id»: «q2»,
«input»: «Rewrite professionally: ‘this model is kinda slow but it works ok.'»,
«reference»: «The model is somewhat slow, but it performs reliably.»
},
{
«id»: «q3»,
«input»: «Extract key fields as JSON: ‘Order 5531 by Alice costs $42.50 and ships to Toronto.'»,
«reference»: ‘{«orderid»:»5531″,»customer»:»Alice»,»amountusd»:42.50,»city»:»Toronto»}’
},
{
«id»: «q4»,
«input»: «Answer briefly: What is prompt regression testing?»,
«reference»: «Prompt regression testing checks whether prompt changes degrade model outputs compared to a baseline.»
},
]

PROMPTS = [
{
«version»: «v1_baseline»,
«prompt»: (
«You are a precise assistant.\n»
«Follow the user request carefully.\n»
«If asked for JSON, output valid JSON only.\n»
«User: {user_input}»
)
},
{
«version»: «v2_formatting»,
«prompt»: (
«You are a helpful, structured assistant.\n»
«Respond clearly and concisely.\n»
«Prefer clean formatting.\n»
«User request: {user_input}»
)
},
{
«version»: «v3_guardrailed»,
«prompt»: (
«You are a rigorous assistant.\n»
«Rules:\n»
«1) If user asks for JSON, output ONLY valid minified JSON.\n»
«2) Otherwise, keep the answer short and factual.\n»
«User: {user_input}»
)
},
]
«`

Оценка запросов

Мы реализуем основной вызов LLM и метрики оценки, используемые для оценки качества запросов. Мы вычисляем оценки BLEU, ROUGE-L и семантического сходства, чтобы учесть как поверхностные, так и семантические различия в выходных данных модели.

«`python
def callllm(formattedprompt: str) -> str:
resp = client.responses.create(
model=MODEL,
input=formatted_prompt,
temperature=TEMPERATURE,
maxoutputtokens=MAXOUTPUTTOKENS,
)
out = getattr(resp, «output_text», None)
if out:
return out.strip()
try:
texts = []
for item in resp.output:
if getattr(item, «type», «») == «message»:
for c in item.content:
if getattr(c, «type», «») in («output_text», «text»):
texts.append(getattr(c, «text», «»))
return «\n».join(texts).strip()
except Exception:
return «»

smooth = SmoothingFunction().method3
rouge = rougescorer.RougeScorer([«rougeL»], usestemmer=True)

def safe_tokenize(s: str) -> List[str]:
s = (s or «»).strip().lower()
if not s:
return []
try:
return nltk.word_tokenize(s)
except LookupError:
return re.findall(r»\b\w+\b», s)

def bleu_score(ref: str, hyp: str) -> float:
r = safe_tokenize(ref)
h = safe_tokenize(hyp)
if len(h) == 0 or len(r) == 0:
return 0.0
return float(sentencebleu([r], h, smoothingfunction=smooth))

def rougeL_f1(ref: str, hyp: str) -> float:
scores = rouge.score(ref or «», hyp or «»)
return float(scores[«rougeL»].fmeasure)

def semantic_sim(ref: str, hyp: str) -> float:
embs = embedder.encode([ref or «», hyp or «»], normalize_embeddings=True)
return float(cosine_similarity([embs[0]], [embs[1]])[0][0])
«`

Логика оценки и регрессии

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

«`python
def evaluateprompt(prompttemplate: str) -> Tuple[pd.DataFrame, Dict[str, float], str]:
rows = []
for ex in EVAL_SET:
p = prompttemplate.format(userinput=ex[«input»])
y = call_llm(p)
ref = ex[«reference»]

rows.append({
«id»: ex[«id»],
«input»: ex[«input»],
«reference»: ref,
«output»: y,
«bleu»: bleu_score(ref, y),
«rougeLf1″: rougeLf1(ref, y),
«semanticsim»: semanticsim(ref, y),
})

df = pd.DataFrame(rows)
agg = {
«bleu_mean»: float(df[«bleu»].mean()),
«rougeLf1mean»: float(df[«rougeL_f1»].mean()),
«semanticsimmean»: float(df[«semantic_sim»].mean()),
}
outputsjsonl = «\n».join(json.dumps(r, ensureascii=False) for r in rows)
return df, agg, outputs_jsonl

def logtextartifact(text: str, artifact_path: str):
mlflow.logtext(text, artifactpath)

def prompt_diff(old: str, new: str) -> str:
a = old.splitlines(keepends=True)
b = new.splitlines(keepends=True)
return «».join(difflib.unifieddiff(a, b, fromfile=»previousprompt», tofile=»current_prompt»))

def computeregressionflags(baseline: Dict[str, float], current: Dict[str, float]) -> Dict[str, Any]:
dsem = baseline[«semanticsimmean»] — current[«semanticsim_mean»]
drouge = baseline[«rougeLf1mean»] — current[«rougeLf1_mean»]
dbleu = baseline[«bleumean»] — current[«bleu_mean»]

flags = {
«abssemanticfail»: current[«semanticsimmean»] < ABSSEMSIM_MIN,
«dropsemanticfail»: dsem > DELTASEMSIMMAX_DROP,
«droprougefail»: drouge > DELTAROUGELMAX_DROP,
«dropbleufail»: dbleu > DELTABLEUMAXDROP,
«deltasemantic»: float(dsem),
«deltarougeL»: float(drouge),
«deltableu»: float(dbleu),
}
flags[«regression»] = any([flags[«abssemanticfail»], flags[«dropsemanticfail»], flags[«droprougefail»], flags[«dropbleufail»]])
return flags
«`

Запуск тестирования версий запросов с помощью MLflow

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

«`python
print(«Running prompt versioning + regression testing with MLflow…»)
print(f»Tracking URI: {mlflow.gettrackinguri()}»)
print(f»Experiment: {mlflow.getexperimentbyname(‘promptversioningllmregression’).name}»)

run_summary = []
baseline_metrics = None
baseline_prompt = None
baseline_df = None
baselinemetricsname = None

with mlflow.startrun(runname=f»promptregressionsuite{int(time.time())}») as parentrun:
mlflow.settag(«task», «promptversioningregressiontesting»)
mlflow.log_param(«model», MODEL)
mlflow.log_param(«temperature», TEMPERATURE)
mlflow.logparam(«maxoutputtokens», MAXOUTPUT_TOKENS)
mlflow.logparam(«evalsetsize», len(EVALSET))

for pv in PROMPTS:
ver = pv[«version»]
prompt_t = pv[«prompt»]

with mlflow.startrun(runname=ver, nested=True) as child_run:
mlflow.logparam(«promptversion», ver)
logtextartifact(prompt_t, f»prompts/{ver}.txt»)

if baselineprompt is not None and baselinemetrics_name is not None:
diff = promptdiff(baselineprompt, prompt_t)
logtextartifact(diff, f»promptdiffs/{baselinemetricsname}to_{ver}.diff»)
else:
logtextartifact(«BASELINEPROMPT (no diff)», f»promptdiffs/{ver}.diff»)

df, agg, outputsjsonl = evaluateprompt(prompt_t)

mlflow.logdict(agg, f»metrics/{ver}agg.json»)
logtextartifact(outputsjsonl, f»outputs/{ver}outputs.jsonl»)

mlflow.logmetric(«bleumean», agg[«bleu_mean»])
mlflow.logmetric(«rougeLf1mean», agg[«rougeLf1_mean»])
mlflow.logmetric(«semanticsimmean», agg[«semanticsim_mean»])

if baseline_metrics is None:
baseline_metrics = agg
baselineprompt = promptt
baseline_df = df
baselinemetricsname = ver
flags = {«regression»: False, «deltableu»: 0.0, «deltarougeL»: 0.0, «delta_semantic»: 0.0}
mlflow.set_tag(«regression», «false»)
else:
flags = computeregressionflags(baseline_metrics, agg)
mlflow.logmetric(«deltableu», flags[«delta_bleu»])
mlflow.logmetric(«deltarougeL», flags[«delta_rougeL»])
mlflow.logmetric(«deltasemantic», flags[«delta_semantic»])
mlflow.set_tag(«regression», str(flags[«regression»]).lower())
for k in [«abssemanticfail»,»dropsemanticfail»,»droprougefail»,»dropbleufail»]:
mlflow.set_tag(k, str(flags[k]).lower())

run_summary.append({
«prompt_version»: ver,
«bleumean»: agg[«bleumean»],
«rougeLf1mean»: agg[«rougeLf1mean»],
«semanticsimmean»: agg[«semanticsimmean»],
«deltableuvsbaseline»: float(flags.get(«deltableu», 0.0)),
«deltarougeLvsbaseline»: float(flags.get(«deltarougeL», 0.0)),
«deltasemanticvsbaseline»: float(flags.get(«deltasemantic», 0.0)),
«regression_flag»: bool(flags[«regression»]),
«mlflowrunid»: childrun.info.runid,
})

summarydf = pd.DataFrame(runsummary).sortvalues(«promptversion»)
print(«\n=== Aggregated Results (higher is better) ===»)
display(summary_df)
«`

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

1. Какие инструменты и библиотеки используются в статье для реализации управления версиями запросов и регрессионного тестирования?

В статье используются следующие инструменты и библиотеки: MLflow для отслеживания экспериментов, OpenAI для взаимодействия с языковой моделью, NLTK для обработки естественного языка, Sentence-Transformers для создания эмбедингов предложений, Scikit-learn для метрик качества, Pandas для работы с данными.

2. Какие метрики качества используются для оценки выходных данных модели в статье?

Для оценки выходных данных модели в статье используются метрики BLEU, ROUGE-L и семантического сходства.

3. Как в статье реализована логика оценки и регрессии для больших языковых моделей?

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

4. Какие параметры модели и пороги регрессии определены в статье?

В статье определены следующие параметры модели: модель `gpt-4o-mini`, температура `0.2`, максимальное количество токенов `250`. Пороговые значения для регрессии включают минимальное семантическое сходство `0.78`, максимальное допустимое падение семантического сходства `0.05`, максимальное допустимое падение ROUGE-L `0.08` и максимальное допустимое падение BLEU `0.10`.

5. Как в статье обеспечивается воспроизводимость экспериментов?

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

Источник