1. なぜ ChatGPT App にロードテストが必要なのか?
従来のウェブでは、ロードテストは「何百万 RPS、巨大クラスタ、SRE にピザ」といったイメージで語られがちです。ChatGPT App と MCP サーバーの現実はもっと素朴で、幸いコストも低めです。すでに SLO の基礎は学びましたが、SLO/オブザーバビリティとフィード品質が負荷下でどう相互作用するかを見ていきましょう。
最大の特徴は、ChatGPT が回答生成を続ける前に tool call の完了を待つという点です。ユーザーはトークンの美しいストリームを見ますが、モデルがツール呼び出しを決めた瞬間にその魔法は終わり——バックエンドの応答待ちになります。もし MCP や ACP サーバーが、目標の 2〜4秒の代わりにときどき 8〜10秒かけて応答してしまうと、UX は「魔法のアシスタント」から「また遅いサイト」へと変わってしまいます。
さらに、厳しいタイムアウト・バジェットがあります。 ツール呼び出しに対して OpenAI は上限を数十秒程度に設定しています(正確な値はモードに依存しますが、30〜60秒の範囲で考えるべきで、UX 的には 5〜10秒以内が望ましい)。ピーク時に tool call が 25〜30秒に収まっていたとしても、形式的にはリミット内でも、ユーザー視点ではすでに「壊れている」状態です。
2つ目のポイント:重要なのは抽象的な RPS ではなく同時実行数です。Store に出る App であれば、50〜100 の同時アクティブユーザーは十分現実的です。検証したいのはまさにそこです。「合成の GET /health で 50k RPS を耐えられるか」ではありません。
そして最後に、ChatGPT App はスタックです。
flowchart LR User --> ChatGPT ChatGPT -->|tools/call| MCP["MCP サーバー GiftGenius"] MCP --> DB["ギフトフィードのデータベース"] MCP --> ACP["チェックアウト / ACP バックエンド"] ACP --> PSP["決済 / Stripe"]
このスタックが小さくても現実的な負荷でどう動くかを確認しておかないと、ちょっとしたプロモ配信や Store の特集入りが、たちまち「LLM プロダクトの反面教師」スライド行きになりかねません。
この講義でいう「軽量ロードテスト」とは、短時間(通常 1〜10 分)で次を確認するランのことです。
- 想定ピークの同時接続をシステムが耐えられるか。
- p95/p99 のレイテンシが SLO を超えて悪化していないか。
- 外部 API からのエラー、タイムアウト、レートリミットが多発していないか。
同時に、品質のもう一つの側面——商品フィード(product feed。以下「フィード」)のデータについても見ていきます。フィードが悪ければ、GiftGenius は「Gift」にも「Genius」にもなりません。
前半では MCP/ACP 向けの軽量ロードテスト(何をどう負荷するか、どのメトリクスを見るか)を解説し、それをオブザーバビリティ(レイテンシ、エラー、リソース、webhook、ログ)に結び付けます。後半ではフィード品質について、負荷下で意外と問題化するポイントを扱います。
2. 何に負荷をかけるべきか:ChatGPT ではなく自分たちの API
後で混乱しないよう、ここははっきりさせておきます。ロードテストは自分たちのバックエンド——MCP サーバー、ACP のエンドポイント、webhook——に対して直接行います。ChatGPT の UI 経由では行いません。
理由はいくつかあります。
- 第一に、コスト。ChatGPT 経由で実際の tool call を回すとトークン課金が発生し、同時に ChatGPT の各種リミットにも突き当たります。しかしテスト対象は自分たちのコードです。
- 第二に、予測可能性。 /mcp や /api/checkout を直接呼べば、モデルがその時ツールを呼ぶかどうかといった判断に左右されず、シナリオを自分でコントロールできます。
- 第三に、可視性。 負荷下では「5 分で MCP に 2000 リクエスト来た」「レイテンシの分布」「CPU のグラフ」を明確に見たいのです。ChatGPT 経由で負荷を流すと、余分なノイズや制約の層が増え、状況把握が難しくなります.
GiftGenius のロードテストで典型的に叩くエンドポイント:
- JSON‑RPC の tools を実装している MCP サーバーのエンドポイント(/mcp など);
- チェックアウトの作成と完了用に 1〜2 個の ACP エンドポイント(決済は sandbox モード);
- 必要に応じて、決済サービスからの webhook を処理するエンドポイント(イベント急増時の挙動を見る)。
ここでは、Next.js 16 のバックエンド上で MCP サーバーが /api/mcp に、ACP のエンドポイントが /api/checkout/create にあるものとします。
3. GiftGenius 向けミニスモークロードシナリオ
プロダクトマネージャーが「現実的なピークは同時 50 ユーザー。各ユーザーは来訪してギフトを探し、ときどき決済まで進む」と想定しているとしましょう。
軽量ロードテストでは、たとえば 30〜50 の「仮想ユーザー」(VU)を用意し、それぞれが次のシーケンスを実行すれば十分です。
- giftgenius.search_gifts ツールを呼び出す(プロフィールと予算に基づくギフト検索)。
- 結果から 2 商品について giftgenius.get_gift_details を呼び出す。
- (ときどき)1 商品について ACP の create_checkout_session を呼ぶ。
これらはすべて ChatGPT を介さず、HTTP で MCP/ACP に直接送ります。
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"],
},
},
};
実プロジェクトでは多少構造が異なるかもしれませんが、原理は同じです。1 つの JSON‑RPC メソッドの中にツール名と引数が入ります。
4. TypeScript で簡単なロードスクリプトを書く
まずはシナリオの最小部分、giftgenius.search_gifts を MCP に投げる処理を実装します。最初に、/api/mcp にリクエストを送りレイテンシを計測する最小限の Node.js/TypeScript スクリプトを作り、その後でチェックアウトや分岐を足します。
基本の 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); // エラーを示すために -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));
これで MCP に約 200 回の呼び出しが行われ、その一部は平行で実行されるため、十分な同時実行状態を作れます。
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)}%`);
これで最低限のスモークロードスクリプトができました。ローカルやステージングでリリース前に実行できます。ChatGPT に触らず、トークンも消費せず、MCP に集中できます。
ACP とチェックアウトはどうするか
同様に、ACP エンドポイントを叩く callCreateCheckoutSession ヘルパーを追加できます。このときは必ずテスト/サンドボックスの決済モードを使い、実注文が積み上がらないようにします。呼び出しは通常の JSON POST です。
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 回」といったパターンを入れて、検索数が購入数より多いファネルを模擬します。
5. もう少し本格的なツール:k6(でもシンプルに)
Node スクリプトは「最小限の入口」として良いのですが、k6 のような専門ツールも便利です。シナリオは JavaScript で書けて、ランタイムは Go(高速)です。
MCP 用の小さな k6 スクリプト例:
// 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 などへエクスポートも可能です。
ツールを替えても目標は同じです。100 万 RPS を耐えることではなく、想定ピークの 5〜10倍でもシステムが崩れず、p95 が SLO 内に収まっていることを確かめるのが目的です。
6. ロード実行中(および後)に見るべきもの
メトリクスと SLO はすでに扱いましたが、ここではロードの文脈に落とし込みます。
第一に、レイテンシ。 search_gifts のような MCP ツールでは「p95 は 2〜3 秒未満」といった目標を先に置いているはずです。スモークロード中に p95/p99 が 2〜3 倍に伸びていないかを見ます。またベースライン比較も重要です。変更前は p95 が 400ms だったのに、変更後に 1500ms になったなら、形式的に SLO 内でも要注意です。
第二に、error rate。 負荷下では、DB のコネクションプール枯渇、外部 API からの 429、決済へのアクセスでのタイムアウトなど、意外な問題が露呈します。通常の負荷では error rate はほぼゼロであるべきです。スモークロードでは散発的な失敗は許容しても、5〜10% は明らかに異常です。
第三に、リソースメトリクス。 CPU、メモリ、場合によっては開いているファイルディスクリプタと接続数。これはインフラによって差はありますが、要点はシンプルです。30 VU で CPU が 100% に張り付いて GC が半分の時間を消費する——そんな状況は避けたいのです。
第四に、webhook。 コマース系のシナリオでは、注文の最終確定が決済システムからの webhook を正しく処理できるかに依存することが多いです。ACP のリクエスト時間だけでなく、「webhook 到着 → 正常処理完了」までの遅延にも注目しましょう。
最後に、ログ。 trace_id/checkout_session_id を含む構造化ログがあれば、負荷後に最も遅い(または失敗した)数件を拾い、MCP → 外部 API → ACP → webhook というチェーンを辿れます。負荷下で p99 の「尻尾」が不自然に長いときに特に有用です。
7. フィードデータ品質:構造から意味へ
負荷下でのレイテンシ、エラー、リソースの挙動を見てきました。これらの SLO を満たしていても、データが悪ければ UX は崩れます。
二つ目の大きな話題——データです。GiftGenius のようなコマース系 App における product feed(商品フィード)は、単なる「どこかのディスク上の何か」ではなく、文字通り LLM とエージェントの燃料です。フィードがゴミなら、モデルは価格や在庫を「でっち上げてはくれません」。
フィード品質は 3 層で考えると整理しやすいです。
構造レイヤー
これはデータの基本的な妥当性です。
- JSON が正しくパースできる。
- 必須フィールドが揃っている:id、name、price、currency、imageUrl、availability など。
- 値の型が期待どおり:価格は数値、availability は enum、categories は文字列配列。
- id の重複がない。
この一部はすでにコントラクトテストでカバーしているはずです(フィード用の JSON Schema/Zod スキーマを定義)。あとはそれらのスキーマを実データに適用するだけです。
GiftGenius のフィード要素向けの簡単な Zod スキーマ例:
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。
- 通貨が市場に合っていない(EUR だけで販売しているのに USD)。
- 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("ベストセラーだが在庫なし");
}
return problems;
};
これらのチェックは nightly ジョブとして、または新しいフィード生成時に回せます。
LLM レイヤー
モデルは賢いですが、苦手もあります。
- HTML や余分なタグ、技術的テキストだらけの説明文。
- ロケールの指定なしに言語が混在(フィードの半分が英語、半分が別言語)。
- 「今すぐ格安で買える最高のスーパーギフト」みたいな過剰な SEO 風タイトル。
このレイヤーでは、データをモデルに優しい形に整えるのが重要です。
- HTML タグを除去するか、プレーンテキスト化する。
- 説明文の言語を正規化する(少なくとも locale を明示する)。
- 過度に長いタイトルや重複情報を削る。
これらは前処理スクリプトで自動化できる部分もありますし、フィードを作成するチームとの運用で解消できる部分もあります。
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。その部分のフィードが汚れていると、バリデーションの連発や例外、フォールバック処理の多発で、エラーも性能劣化も同時に起きます。
- フィードがノイジー(巨大な説明文や HTML、無意味なタグだらけ)だと、MCP サーバーはより多くのデータを取得・シリアライズする必要があり、tool-call の処理時間や応答サイズに直接影響します。
- コマース側でフィードが悪いと、「空振り」のチェックアウト試行が増えます。ユーザーが選んだ商品が実は在庫切れ、というケースです。これは UX にも ACP のメトリクス(不成功 intent の増加)にも悪影響です。
この関係はマトリクスで考えるとわかりやすいです。
| フィードの問題 | 負荷時の症状 | 確認場所 |
|---|---|---|
| 価格/通貨の不整合 | ACP でのエラーや決済拒否 | ACP ログ + チェックアウトの SLO |
| 商品の重複 | 不自然なレコメンド結果、不要な呼び出し増加 | MCP ログ、UX メトリクス |
| 画像/説明の欠落 | モデルの提案が薄味になる | App ログ + UX フィードバック |
| 説明文の HTML/ノイズ | シリアライズの遅延、ペイロード膨張 | MCP のレイテンシ |
ロードランは懐中電灯のような役割を果たします。普段はほとんど触られないフィードの箇所を照らし、トラフィックが増えると顕在化する問題点を浮き彫りにします。
10. これを GiftGenius のリリースプロセスに組み込む
プロセスの観点では、ここまでの内容は「初回リリース前に一度だけ」ではいけません。モジュール 16(「本番、ネットワーク、スケーリング」)と 17(「オブザーバビリティと品質」)の学習計画では、これらを定期的なリリースチェックリストの一部として組み込むことを意図しています。リリース前に unit/contract/E2E を回すだけでなく、短いスモークロードとフィード検証も回します。
新バージョンのデプロイ前に妥当な最小パイプライン:
- Unit + Contract + 統合テストがグリーン。
- 重要なコード(検索ロジック、DB 処理、チェックアウト)を変更した場合、ステージングで MCP/ACP に対する短いスモークロードを実行。
- フィードバリデータがエラーなく完走し、フィードの基本メトリクス(壊れたレコード数、画像なしの割合など)が許容範囲。
- 新しいエンドポイントや SLO を反映してダッシュボードとアラートを更新。
- 失敗時のロールバック計画を用意(フラグで機能を無効化、またはビルドをロールバック)。
これで GiftGenius は「デモ」から、Store とトラフィック急増に耐えられるサービスへと進化します。
11. ロードテストとフィード検証での典型的なミス
ミス1: ChatGPT 経由でロードテストを行い、自分たちのバックエンドを直接叩かない。
「実運用に近く」と称して ChatGPT の UI 経由でスクリプトを流すと、OpenAI のリミットに突き当たり、トークンも消費し、結果がノイジーになります。MCP/ACP の問題は、/mcp と /api/checkout を直接叩く方が桁違いに安く、明確に捕捉できます。
ミス2: 平均応答時間だけに注目する。
「平均レイテンシは 500ms。大丈夫」——でも p95 が 5 秒ならどうでしょう。すでに SLO の回で述べたとおり、実 UX を決めるのは分布の「尻尾」(p95/p99)です。負荷下では平均が保たれていても、尻尾が 2〜3 倍に伸びがちです。
ミス3: 実用的なスモークロードではなく「エンタープライズ級ロード」を目指す。
何万ユーザーを模した複雑な環境を何カ月もかけて作るのは、GiftGenius クラスの ChatGPT App ではほぼ不要です。50〜100 VU のシンプルなスモークロードを定期実行し、意味のあるメトリクスを持つ方が遥かに有益です。
ミス4: 非現実的な負荷シナリオ。
スクリプトが同じリクエストだけを送り続け、ユーザーや言語、商品タイプのバリエーションを持たず、ACP や webhook に触れもしない。これではホットなハッピーパスだけをテストしているに過ぎず、実際の「角」は影のままです。最低限でも、予算や興味を変え、チェックアウトに到達するユーザーもいれば到達しないユーザーもいる、といった現実的なフローを模擬しましょう。
ミス5: フィードを「目視だけ」または本番だけで検証する。
フィードを組んで本番に流し、モデルの妙な推薦を見て首をかしげる——そんな事態を避けましょう。Zod/JSON Schema の簡単なスクリプトなら 1 分で、「画像なしが 10%、価格 0 が 5%、通貨が XXX が 3%」といった基本問題を示せます。自動検証のないフィードは、コマースアプリで最頻の恥ポイントです。
ミス6: フィードが悪くても LLM が「察してくれる」と期待する。
モデルは多くをこなしますが、正しい価格や在庫を生成してはくれません。同じ商品が異なる価格で載っていたり、「在庫あり/なし」が同時に存在していれば、エージェントは幻覚を起こし、ユーザーに不整合な体験を与えかねません。データの清潔さの責任はモデルではなくあなたにあります。
ミス7: フィードメトリクスと全体の SLO が結び付いていない。
MCP と ACP が完璧に速くても、フィードの 30% が「壊れている」なら UX は悪化します。多くのチームは技術的 SLO(レイテンシ、error rate)だけを見て、データ品質の SLO(有効 SKU の最小割合、重複の最大数など)を見落とします。その結果、「数字上は良い」が「体感は悪い」という状態に陥ります。
ミス8: 準備なしに本番でロードテストを実行する。
金曜の夜に誰かが「ちょっと k6 を本番 MCP に流してみるか」と思い立つ——告知もなく。良くて実トラフィックのメトリクスを乱し、当番エンジニアを困惑させます。悪ければ外部 API や決済システムのレートリミットに引っかかります。最初のシナリオは常にステージングで。どうしても本番が必要なら、時間帯と通知を含めて計画的に行いましょう。
GO TO FULL VERSION