1. Навіщо ChatGPT App узагалі навантажувальні тести?
У класичному вебі навантажувальне тестування часто асоціюється з картинкою «мільйони RPS, гігантський кластер, піца для SRE». Для ChatGPT App і MCP‑серверів реальність простіша й, на щастя, дешевша. Загалом ви вже знайомі з SLO, тож подивімося, як SLO/observability і якість фіду взаємодіють під навантаженням.
Головна особливість така: ChatGPT чекає завершення tool call, щоб продовжити генерувати відповідь. Користувач бачить гарний потік токенів, але щойно модель вирішує викликати інструмент — магія стріму закінчується, доки бекенд не відповість. Якщо ваш MCP або ACP‑сервер іноді відповідає за 8–10 с замість цільових 2–4 с, UX перетворюється з «чарівного асистента» на «ще один повільний сайт».
До того ж є жорсткий бюджет таймаутів: для викликів інструментів OpenAI тримає верхню межу на рівні десятків секунд (точні числа залежать від режиму, але орієнтуватися варто на 30–60 с; а з погляду UX — узагалі на 5–10 с). Якщо на піку навантаження ваші tool calls раптом починають укладатися в 25–30 с, ви формально ще в ліміті, але з погляду користувача система вже «зламалася».
Другий момент: нам важлива не стільки абстрактна RPS, скільки одночасність. Для застосунку зі Store цілком реалістично мати 50–100 активних користувачів одночасно; саме це й варто перевіряти, а не «чи витримає 50k RPS синтетичного GET /health».
І нарешті, ChatGPT App — це цілий стек:
flowchart LR User --> ChatGPT ChatGPT -->|tools/call| MCP["Сервер MCP GiftGenius"] MCP --> DB["База з фідом подарунків"] MCP --> ACP["Checkout / бекенд ACP"] ACP --> PSP["Платіжка / Stripe"]
Якщо ми не перевіримо, як цей стек живе під невеликим, але реалістичним навантаженням, то будь-яка промо‑розсилка або потрапляння в добірку Store може швидко перетворити його на слайд «як не треба робити LLM‑продукти».
У цій лекції під «легкими навантажувальними тестами» ми розумітимемо короткі прогони (зазвичай 1–10 хв), які перевіряють:
- чи витримує система очікуваний піковий онлайн;
- чи не виходять затримки p95/p99 за межі SLO;
- чи не «сиплються» помилки, таймаути та обмеження rate limit від зовнішніх API.
Паралельно подивимося й на інший бік якості — дані товарного фіду (product feed, далі просто «фід»), без яких жоден GiftGenius не буде ні «Gift», ні «Genius».
Спочатку розберемося з легкими навантажувальними тестами для MCP/ACP (що саме й як навантажувати, на які метрики дивитися). Потім «приземлимо» це на спостережуваність: затримки, помилки, ресурси, вебхуки й логи. А в другій половині поговоримо про якість фіду й те, як вона під навантаженням інколи несподівано «вистрілює».
2. Що саме навантажувати: не ChatGPT, а власні API
Важливо зафіксувати одну думку, щоб потім не плутатися: навантажувальне тестування ми проводимо безпосередньо на наш бекенд — MCP‑сервер, ACP‑ендпойнти, вебхуки — а не через інтерфейс ChatGPT.
Причин кілька.
- По‑перше, економія. Якщо ганяти реальні tool calls через ChatGPT, ви платитимете за токени й водночас упиратиметеся в ліміти ChatGPT, хоча насправді тестуєте власний код.
- По‑друге, передбачуваність. Під час прямих викликів /mcp або /api/checkout ви контролюєте сценарій і не залежите від рішень моделі — викликатиме вона цей інструмент зараз чи ні.
- По‑третє, прозорість. Під навантаженням ви хочете чітко бачити: ось 2000 запитів у MCP за 5 хв, ось розподіл затримок, ось графік CPU. Якщо проганяти навантаження через ChatGPT, додатковий шар шуму й обмежень лише ускладнить картину.
Типовий набір ендпойнтів для навантажувального тесту GiftGenius:
- ендпойнт MCP‑сервера, що реалізує JSON‑RPC tools (/mcp або аналогічний);
- один‑два ACP‑ендпойнти для створення й завершення checkout (у sandbox‑режимі платіжної системи);
- можливо — ендпойнт, що обробляє вебхуки від платіжної системи, щоб подивитися, як він поводиться на піку подій.
Припустімо, що в нас є бекенд на Next.js 16, на якому живе MCP‑сервер, доступний за /api/mcp, і ACP‑сервер з ендпойнтом /api/checkout/create.
3. Міні‑сценарій smoke‑load для GiftGenius
Уявімо, що наші product‑менеджери вірять у світле майбутнє й кажуть: «Реалістичний пік — 50 одночасних користувачів: кожен заходить, обирає подарунок і час від часу доходить до оплати».
Для легкого навантажувального тесту нам достатньо змоделювати, скажімо, 30–50 «віртуальних користувачів» (VU). Кожен із них виконує послідовність:
- Виклик інструмента giftgenius.search_gifts (пошук подарунків за профілем і бюджетом).
- Виклик giftgenius.get_gift_details для кількох товарів із результату.
- (Іноді) виклик ACP‑ендпойнта create_checkout_session для одного товару.
Усе це — безпосередньо через HTTP до нашого MCP/ACP, без ChatGPT.
JSON‑RPC виклик до MCP
Приклад тіла запиту до MCP (спрощено):
const body = {
jsonrpc: "2.0",
id: "test-" + Math.random(),
method: "tools/call",
params: {
toolName: "giftgenius.search_gifts",
arguments: {
occasion: "birthday",
budget: 50,
interests: ["sport", "books"],
},
},
};
У реальному проєкті структура може трохи відрізнятися, але принцип той самий: один JSON‑RPC метод, а всередині — tool і аргументи.
4. Пишемо простий навантажувальний скрипт на TypeScript
Як перший крок реалізуємо найпростішу частину нашого сценарію — виклик giftgenius.search_gifts до MCP. Спершу зробимо мінімальний Node.js‑скрипт на TypeScript, який надсилає такі запити до /api/mcp і вимірює затримку. А вже потім додамо checkout і складніші гілки.
Базовий HTTP‑клієнт
Припустімо, у нас є .env з MCP_URL=http://localhost:3000/api/mcp.
// scripts/loadTest.ts
import "dotenv/config";
const MCP_URL = process.env.MCP_URL!;
async function callSearchGifts() {
const body = {
jsonrpc: "2.0",
id: `search-${Date.now()}-${Math.random()}`,
method: "tools/call",
params: {
toolName: "giftgenius.search_gifts",
arguments: { occasion: "birthday", budget: 50 },
},
};
const started = Date.now();
const res = await fetch(MCP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const latencyMs = Date.now() - started;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return latencyMs;
}
За потреби тут можна додати найпростіший парсинг JSON‑відповіді, але для оцінювання затримки й error rate цього достатньо.
Конкурентний запуск кількох запитів
Нам потрібно керувати кількістю одночасних запитів. Для простоти візьмемо фіксовану кількість «віртуальних користувачів» і попросимо кожного зробити N запитів поспіль.
async function runVirtualUser(iterations: number) {
const latencies: number[] = [];
for (let i = 0; i < iterations; i++) {
try {
const ms = await callSearchGifts();
latencies.push(ms);
} catch (e) {
console.error("Error in VU:", e);
latencies.push(-1); // позначимо помилку
}
}
return latencies;
}
Тепер можна запустити, скажімо, 20 таких віртуальних користувачів:
async function main() {
const users = 20;
const iterations = 10;
const tasks = Array.from({ length: users }, () =>
runVirtualUser(iterations),
);
const results = await Promise.all(tasks);
const all = results.flat();
// ...обчислення метрик
}
main().catch((e) => console.error(e));
Це вже забезпечить приблизно 200 викликів MCP; частина з них виконуватиметься паралельно — тобто з достатньо високою одночасністю.
Розрахунок p95 і error rate
Додамо маленьку утиліту для розрахунку перцентиля й частки помилок. Нагадаємо: p95 — це значення, нижче якого вкладається 95 % запитів.
function percentile(values: number[], p: number) {
const sorted = values.filter(v => v >= 0).sort((a, b) => a - b);
if (!sorted.length) return 0;
const idx = Math.floor((p / 100) * (sorted.length - 1));
return sorted[idx];
}
function errorRate(values: number[]) {
const total = values.length;
const errors = values.filter(v => v < 0).length;
return (errors / total) * 100;
}
І в main додамо виведення:
const p95 = percentile(all, 95);
const p99 = percentile(all, 99);
const errRate = errorRate(all);
console.log(`Total: ${all.length}`);
console.log(`p95: ${p95} ms, p99: ${p99} ms`);
console.log(`Error rate: ${errRate.toFixed(2)}%`);
Тепер у вас є мінімальний smoke‑load‑скрипт, який можна запускати локально або на staging перед релізом. При цьому ви жодним чином не чіпаєте ChatGPT, не «спалюєте» токени, а вся увага — на вашому MCP.
Що робити з ACP і checkout
Так само можна додати ще один helper callCreateCheckoutSession, який звертатиметься до ACP‑ендпойнта. Важливо використовувати тестовий (пісочний) режим платежів, щоб не створювати реальних замовлень. Типовий виклик виглядатиме як звичайний POST із JSON:
async function callCreateCheckoutSession(productId: string) {
const started = Date.now();
const res = await fetch("http://localhost:3000/api/checkout/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId, test: true }),
});
const latencyMs = Date.now() - started;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return latencyMs;
}
Потім ви можете в runVirtualUser зробити патерн: 3 рази пошук → 1 раз checkout, щоб змоделювати воронку «пошуків більше, ніж покупок».
5. Інструменти серйозніше: k6 (але просто)
Node‑скрипт хороший як «мінімальний вхід», але інколи зручніше використати спеціалізований інструмент на кшталт k6. Там сценарії пишуться на JavaScript, а сам рантайм — на Go (тобто швидкий).
Приклад невеликого k6‑скрипта для MCP:
// loadtest-mcp.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 30 },
{ duration: "2m", target: 30 },
],
};
export default function () {
const payload = JSON.stringify({
jsonrpc: "2.0",
id: `search-${Math.random()}`,
method: "tools/call",
params: {
toolName: "giftgenius.search_gifts",
arguments: { occasion: "birthday", budget: 50 },
},
});
const res = http.post(__ENV.MCP_URL, payload, {
headers: { "Content-Type": "application/json" },
});
check(res, { "status is 200": (r) => r.status === 200 });
sleep(1);
}
Команда запуску:
MCP_URL=http://localhost:3000/api/mcp k6 run loadtest-mcp.js
k6 сам порахує p95/p99 і error rate, побудує зрозумілі звіти — далі ви можете експортувати їх у Grafana та інші системи.
Важливо, що навіть із такими інструментами наша мета лишається колишньою: не «витримати мільйон RPS», а переконатися, що за навантаження в 5–10× від очікуваного піку система не розвалюється, а p95 лишається в межах SLO.
6. Що дивитися під час (і після) навантажувального прогона
Ми вже обговорювали метрики й SLO, а зараз просто «приземлимо» їх у контекст навантажувального тесту.
По‑перше, latency. Для MCP‑інструментів типу search_gifts ви заздалегідь задавали собі ціль на кшталт «p95 < 2–3 с». Під час smoke‑load ви дивитеся, чи не зросли p95/p99 у 2–3 рази. Важливо порівнювати з базовим рівнем: якщо до зміни коду p95 була 400 мс, а після — 1500 мс, то навіть якщо ви формально ще в SLO, це вже привід замислитися.
По‑друге, error rate. Під навантаженням часто «вилазять» несподівані речі: вичерпаний пул зʼєднань до БД, неочікувані 429 від зовнішнього API, таймаути під час звернення до платіжної системи. За нормального навантаження error rate має бути близьким до нуля; у smoke‑load допускаються поодинокі збої, але аж ніяк не 5–10 %.
По‑третє, ресурсні метрики: CPU, памʼять, іноді — кількість відкритих файлових дескрипторів і зʼєднань. Вони вже залежать від вашої інфраструктури, але ключова ідея проста: ви не хочете бачити, як за 30 VU CPU тримається на 100 % і GC «зʼїдає» половину часу.
По‑четверте, вебхуки. Якщо у вас commerce‑сценарій, то фінальна точка замовлення часто залежить від успішної обробки вебхука від платіжної системи. Важливо дивитися не лише на швидкість запиту в ACP, а й на затримку «вебхук прийшов → ми його успішно обробили».
І нарешті, логи. Структуровані логи з trace_id/checkout_session_id дозволяють після навантажувального прогона взяти кілька найповільніших або «падаючих» запитів і пройти ланцюжком: MCP → зовнішнє API → ACP → вебхук. Це особливо корисно, якщо під навантаженням ви бачите дивні «хвости» p99.
7. Якість даних фіду: від структури до змісту
Ми подивилися, як під навантаженням поводяться затримки, помилки й ресурси. Але навіть якщо за всіма цими SLO ви вкладаєтеся в цілі, користувацький досвід однаково може «сипатися» через погані дані.
Переходимо до другої великої теми — даних. У commerce‑застосунку на кшталт GiftGenius product feed (фід товарів) — це не «щось на диску», а буквально пальне для LLM і агентів. Якщо у фіді сміття, модель не «вигадає» за вас ціну й наявність.
Зручно мислити якість фіду трьома шарами.
Структурний рівень
Це базова валідність даних:
- JSON коректно парситься.
- Усі обовʼязкові поля присутні: id, name, price, currency, imageUrl, availability тощо.
- Типи значень відповідають очікуванням: ціна — число, availability — enum, categories — масив рядків.
- Немає дублікатів id.
Частину цього ви вже покрили контрактними тестами, коли описували JSON Schema/Zod‑схему для фіду. Тепер ці схеми потрібно застосовувати до реальних обсягів даних.
Приклад простої Zod‑схеми для елемента фіду GiftGenius:
import { z } from "zod";
export const giftItemSchema = z.object({
id: z.string().min(1),
name: z.string().min(3),
description: z.string().optional(),
price: z.number().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
imageUrl: z.string().url(),
inStock: z.boolean(),
tags: z.array(z.string()).default([]),
});
А схема всього фіду — просто z.array(giftItemSchema).
Бізнес‑рівень (семантика)
Структурно товар може бути валідним, але з погляду бізнесу — абсурдним:
- Ціна 0 або 0.01 для дорогого товару.
- Валюта не відповідає ринку (USD для товарів, що продаються лише в EUR).
- inStock = true, але дата останнього оновлення — пів року тому.
- Категорії з 1000 варіантів без уніфікації.
Для цього рівня корисно додати додаткові перевірки й «правила здорового глузду». Наприклад:
const businessRules = (item: GiftItem) => {
const problems: string[] = [];
if (item.price > 10000) {
problems.push("підозріло висока ціна");
}
if (!item.inStock && item.tags.includes("bestseller")) {
problems.push("bestseller, але нема в наявності");
}
return problems;
};
Ці перевірки можна запускати як частину нічного завдання (nightly job) або під час генерації нового фіду.
Рівень LLM
Модель — дуже розумна, але має свої типові хиби:
- Опис, забитий HTML, зайвими тегами й технічним текстом.
- Змішані мови (пів фіду українською/англійською) без вказання locale.
- Дуже довгі «SEO‑назви» у стилі «Купити найкращий супер‑пупер подарунок терміново дешево».
На цьому рівні важливо привести дані до дружнього формату:
- Прибрати HTML‑теги або звести їх до plaintext.
- Нормалізувати мову описів (або принаймні явно вказувати locale).
- Обрізати надто довгі назви й дубльовану інформацію.
Ці завдання можна частково автоматизувати (наприклад, через pre‑processing‑скрипти), а частково — домовитися з командою, яка наповнює фід.
8. Практика: валідатор фіду для GiftGenius
Додамо до нашого проєкту простий скрипт validateFeed.ts, який читатиме JSON із фідом, валідуватиме його через Zod і рахуватиме базові метрики якості.
// scripts/validateFeed.ts
import { readFile } from "fs/promises";
import { giftItemSchema } from "../src/schema/giftItem";
async function main() {
const raw = await readFile("data/gift-feed.json", "utf-8");
const data = JSON.parse(raw);
const items = giftItemSchema.array().parse(data);
console.log(`Усього товарів: ${items.length}`);
const missingImages = items.filter(i => !i.imageUrl).length;
console.log(`Без зображень: ${missingImages}`);
}
main().catch((e) => {
console.error("Feed validation failed:", e);
process.exit(1);
});
Тут ми використовуємо той самий контракт, що й MCP‑сервер. Тобто контрактні тести та перевірка фіду спираються на одну схему — це суттєво зменшує ймовірність розбіжностей.
Далі можна додати перевірки бізнес‑правил і метрики типу:
- частка товарів без опису;
- частка товарів із підозріло низькою/високою ціною;
- кількість дублікатів id або повторюваних name + price.
Ці цифри вже можна надсилати в систему метрик (Prometheus, Datadog тощо) і тримати під ними окремі SLO на якість даних — так само, як ви задаєте SLO для коду.
9. Як навантаження і фід повʼязані між собою
Іноді здається, що «продуктивність» і «якість даних» — дві не надто повʼязані теми. На практиці вони доволі щільно переплетені.
Приклади таких звʼязків:
- Під навантаженням частина запитів починає йти «рідкісними» гілками логіки, які раніше майже не траплялися. Наприклад, товари з особливими типами знижок або нестандартним shipping. Якщо фід у цих місцях брудний, ви можете отримати і помилки, і помітну деградацію продуктивності (багато валідацій, винятків, fallback‑логіки).
- Якщо фід дуже «шумний» (величезні описи з HTML, беззмістовні теги), MCP‑серверу доводиться тягнути й серіалізувати більше даних. Це напряму впливає на час обробки tool call та розмір відповіді.
- У commerce‑частині поганий фід може призвести до великої кількості «порожніх» checkout‑спроб, коли користувач обирає товар, якого раптово нема в наявності. Це бʼє і по UX, і по метриках ACP (зростає кількість неуспішних intentʼів).
Зручно дивитися на це як на матрицю:
| Проблема фіду | Симптом під навантаженням | Де дивитися |
|---|---|---|
| Неконсистентні ціни/валюти | Помилки в ACP, відхилені платежі | Логи ACP + SLO checkout |
| Дублікати товарів | Дивні результати рекомендацій, зайві виклики | Логи MCP, UX‑метрики |
| Відсутні зображення/описи | Модель дає «плоскі» рекомендації | Логи App + UX‑відгуки |
| HTML/сміття в описах | Повільна серіалізація, великі payloadʼи | Затримки MCP |
Навантажувальний прогін тут відіграє роль ліхтаря: він підсвічує ті ділянки фіду, до яких у звичайному житті рідко доходять руки. Але за активного трафіку саме вони починають «вистрілювати».
10. Вбудовуємо це в процес релізу GiftGenius
З погляду процесу все описане вище не має бути «одного разу перед першим продакшном». У навчальному плані модулів 16 («Production, мережа і масштабування») та 17 («Спостережуваність і якість») цей підхід закладено саме як частина регулярного release checklistʼа: перед релізом ви не лише проганяєте unit/contract/E2E, а й короткий smoke‑load плюс перевірку фіду.
Розумний мінімальний пайплайн перед викладенням нової версії:
- Unit + contract + інтеграційні тести зелені.
- Короткий smoke‑load проти MCP/ACP на staging, якщо змінювався критичний код (логіка пошуку, робота з БД, checkout).
- Валідатор фіду відпрацьовує без помилок, а базові метрики фіду (кількість «битих» записів, частка без зображень тощо) — у допустимих межах.
- Дашборди й алерти оновлені з урахуванням нових ендпойнтів і SLO.
- На випадок збою підготовлено план відкату (rollback): або вимкнення фічі прапорцем, або відкат білда.
Так ваш GiftGenius перестає бути «демкою для DevDay» і перетворюється на сервіс, готовий до життя в Store та до сплесків трафіку.
11. Типові помилки під час навантажувальних тестів і перевірки фіду
Помилка №1: навантажувальний тест «через ChatGPT», а не безпосередньо по своєму бекенду.
Іноді намагаються «протестувати все як у реальності» й запускають скрипти, які ходять саме через інтерфейс ChatGPT. У підсумку впираються в ліміти OpenAI, спалюють токени й отримують дуже «шумні» результати. Водночас проблеми MCP/ACP можна було б упіймати в сто разів дешевше, звертаючись безпосередньо до /mcp і /api/checkout.
Помилка №2: фокус лише на середньому часі відповіді.
«У нас середня затримка 500 мс, усе чудово» — а те, що p95 при цьому 5 с, чомусь ігнорують. Ми вже обговорювали в темі SLO, що саме «хвіст» розподілу (p95/p99) визначає реальний UX. Під навантаженням середнє часто лишається пристойним, а хвіст зростає вдвічі‑втричі.
Помилка №3: спроба влаштувати «enterprise‑навантаження» замість практичного smoke‑load.
Місяцями будувати складний стенд, що імітує десятки тисяч користувачів, для ChatGPT App рівня GiftGenius — майже завжди зайве. Набагато корисніше мати простий, але такий, який регулярно запускають, smoke‑load на 50–100 VU зі зрозумілими метриками.
Помилка №4: нереалістичний сценарій навантаження.
Скрипт надсилає один і той самий запит, без варіацій користувача, мови, типу товару, і водночас не чіпає ACP та вебхуки. У результаті ви тестуєте один «гарячий» основний сценарій (happy path), а реальні «кути» системи так і лишаються в тіні. Краще моделювати хоча б спрощений, але правдоподібний флоу: різні бюджети, різні інтереси, частина користувачів доходить до checkout, частина — ні.
Помилка №5: перевірка фіду тільки «на око» або вже в продакшні.
Фід зібрали, вивантажили в продакшн, побачили дивні рекомендації від моделі — і почали чухати потилицю. Водночас простий скрипт на Zod/JSON Schema міг би за хвилину показати, що 10 % товарів без зображень, у 5 % ціна 0, а в 3 % валюта XXX. Відсутність автоматичної валідації фіду — одне з найчастіших джерел сорому в commerce‑застосунках.
Помилка №6: надія, що LLM «сам усе зрозуміє» за поганого фіду.
Так, модель уміє багато, але вона не вигадуватиме коректну ціну або наявність товару. Якщо той самий товар у фіді трапляється з різними цінами або одночасно «в наявності» й «нема в наявності», агент може видати і галюцинації, і неконсистентний досвід для користувача. Відповідальність за чистоту даних — на вас, а не на моделі.
Помилка №7: відсутність звʼязку між метриками фіду й загальними SLO.
Можна мати ідеально швидкі MCP і ACP, але якщо 30 % товарів у фіді «биті», користувацький досвід усе одно буде жахливим. Часто команди відстежують тільки технічні SLO (затримки, error rate) і ігнорують SLO щодо якості даних (мінімальний відсоток валідних SKU, максимум дублікатів тощо). У результаті «за цифрами все добре», а за відчуттями — ні.
Помилка №8: запуск навантажувальних тестів прямо в бойовому продакшні без підготовки.
Іноді хтось у пʼятницю ввечері вирішує «швиденько прогнати k6 на бойовому MCP», нікого не попередивши. У кращому разі ви зібʼєте реальні метрики й здивуєте on‑call‑інженера сплеском трафіку, у гіршому — натрапите на rate limit зовнішнього API або платіжної системи. Завжди проганяйте перші сценарії на staging, а якщо потрібен тест на проді — робіть це свідомо: з вікнами та попередженнями.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ