CodeGym /課程 /ChatGPT Apps /輕量負載測試與 feed 資料品質

輕量負載測試與 feed 資料品質

ChatGPT Apps
等級 17 , 課堂 4
開放

1. 為什麼 ChatGPT App 需要負載測試?

在傳統 Web 世界,負載測試常被聯想到「上百萬 RPS、巨型叢集、SRE 吃披薩」的圖景。對 ChatGPT App 與 MCP 伺服器而言,現實更簡單、也更省錢。你基本已熟悉 SLO,我們來看看 SLO/可觀測性與 feed 品質在負載下如何互動。

關鍵特性:ChatGPT 會等待 tool call 完成後才繼續產生回覆。使用者會看到漂亮的 token 串流,但一旦模型決定呼叫工具,串流的魔法就結束了——直到後端回應。如果你的 MCP 或 ACP 伺服器有時回應 8–10 秒,而目標是 2–4 秒,UX 會從「神奇助理」變成「又一個慢網站」。

另外還有硬性的逾時預算: 對工具呼叫,OpenAI 設有大約數十秒的上限(確切數字依模式而異,但應該以 30–60 秒思考;就 UX 而言甚至希望在 5–10 秒內)。如果在尖峰負載時你的 tool calls 突然落在 25–30 秒,你雖然形式上仍在上限內,但對使用者而言已經「壞掉」。

第二點:我們在乎的不是抽象的 RPS,而是併發度。對來自 Store 的 App,50–100 位同時活躍使用者相當現實;這正是你要驗證的,而不是「能否撐住 50k RPS 的合成 GET /health」。

最後,ChatGPT App 是一個堆疊(stack):

flowchart LR
  User --> ChatGPT
  ChatGPT -->|tools/call| MCP["GiftGenius 的 MCP 伺服器"]
  MCP --> DB["禮品 feed 資料庫"]
  MCP --> ACP["Checkout / ACP 後端"]
  ACP --> PSP["金流 / Stripe"]

如果我們不檢查這個堆疊在小而真實的負載下如何運作,任何一次行銷推播或被收錄進 Store 精選,都可能把它快速變成「LLM 產品反面教材」的投影片。

在本講次裡,「輕量負載測試」指的是短時程(通常 1–10 分鐘)的壓測,用來檢查:

  • 系統是否承受預期的尖峰同時在線;
  • p95/p99 延遲是否飆破 SLO;
  • 是否出現大量錯誤、逾時,以及外部 API 的 rate limit 限制。

同時也看看品質的另一面——產品 feed(以下簡稱「feed」)的資料;沒有它,再好的 GiftGenius 也不會既「Gift」又「Genius」。

本講次前半先處理 MCP/ACP 的輕量負載測試(該壓哪裡、怎麼壓、看哪些指標),再把它落到可觀測性(延遲、錯誤、資源、webhook 與日誌);後半則談 feed 的品質,以及它如何在負載下意外出槌。

2. 該壓什麼:不是 ChatGPT,而是你自己的 API

先釘牢一件事,避免之後混淆:負載測試要直接針對我們的後端——MCP 伺服器、ACP endpoint、webhook——而不是透過 ChatGPT 的 UI。

原因有幾個。

  • 其一,省錢。如果透過 ChatGPT 發送真實的 tool calls,你會為 token 付費,還同時撞上 ChatGPT 的限制,但你其實在測你自己的程式碼。
  • 其二,可預測。直接呼叫 /mcp/api/checkout,你能完全控制腳本情境,而不依賴模型此刻會不會呼叫該工具。
  • 其三,透明。在負載下你想清楚看見:5 分鐘內打了 2000 個 MCP 請求、延遲分佈、CPU 圖表。如果把負載繞經 ChatGPT,額外的雜訊與限制只會讓畫面更複雜。

GiftGenius 負載測試的典型 endpoint 清單:

  • MCP 伺服器中實作 JSON‑RPC tools 的 endpoint(/mcp 或同類);
  • 一到兩個 ACP endpoint,用於建立與完成 checkout(在金流的 sandbox 模式中);
  • 或許再加上處理金流 webhook 的 endpoint,看看在事件尖峰時的行為。

假設我們有一個 Next.js 16 後端,裡面跑著 MCP 伺服器,對外提供 /api/mcp,以及一個 ACP 伺服器,提供 /api/checkout/create

3. GiftGenius 的 smoke‑load 迷你情境

假設產品經理相當樂觀,說:「合理的尖峰是 50 位同時使用者,每個人會進來、選個禮物,偶爾走到付款。」

對輕量負載測試而言,模擬 30–50 位「虛擬使用者」(VU)就足夠;每位依序執行:

  1. 呼叫工具 giftgenius.search_gifts(依個人設定與預算搜尋禮物)。
  2. 對結果中的兩個商品呼叫 giftgenius.get_gift_details
  3. (偶爾)針對其中一個商品呼叫 ACP endpoint create_checkout_session

以上全部透過 HTTP 直接打我們的 MCP/ACP,不經 ChatGPT。

對 MCP 的 JSON‑RPC 呼叫

對 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 寫一個簡單的負載腳本

第一步實作最簡單的情境——對 MCP 呼叫 giftgenius.search_gifts。先寫一個最小的 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 回應的解析,但就量測延遲與錯誤率而言,這樣已足夠。

並行啟動多個請求

我們需要控制同時請求的數量。為簡單起見,固定一個「虛擬使用者」數量,讓每個人連續做 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 與錯誤率

加上一個小工具來計算分位數與錯誤。提醒: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、不會燒 token,注意力全部放在你的 MCP。

如何處理 ACP 與 checkout

同樣可以加上一個 helper callCreateCheckoutSession,打 ACP endpoint。務必使用金流的測試/沙箱模式,避免灌入真單。典型呼叫是帶 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 次 → checkout 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 與錯誤率,並產生漂亮的報告——之後你可以匯出到 Grafana 等系統。

重點是,即便使用這些工具,我們的目標仍一樣:不是撐住百萬 RPS,而是確保在預期尖峰的 5–10 倍下系統也不會崩壞,且 p95 仍在 SLO 範圍內。

6. 在負載前後應該關注什麼

我們已談過指標與 SLO,現在把它們「落地」到負載情境。

第一,延遲(latency)。 對像是 search_gifts 的 MCP 工具,你先前應設定過目標,例如「p95 < 2–3 秒」。在 smoke‑load 時要看:p95/p99 是否上飄到原本的 2–3 倍。同時要與 baseline 比較:若改動前 p95 是 400 ms,改動後變 1500 ms,即使形式上仍在 SLO,也該警覺。

第二,錯誤率(error rate)。 在負載下常會冒出意外:資料庫連線池用罄、外部 API 回 429、打金流時逾時。正常負載下錯誤率應趨近於 0;在 smoke‑load 可以接受零星失敗,但絕不是 5–10%。

第三,資源指標: CPU、記憶體,有時還有開啟的檔案描述元與連線數。這些取決於你的基礎設施,但核心想法很簡單:你不希望在 30 VU 時 CPU 100% 且 GC 吃掉一半時間。

第四,webhook。 如果是 commerce 情境,訂單的終點常仰賴金流系統的 webhook 成功處理。要看的不僅是 ACP 請求速度,還有「webhook 抵達 → 我們成功處理」的延遲。

最後,日誌。 帶有 trace_id/checkout_session_id 的結構化日誌,能讓你在負載測試後抓兩三個最慢或失敗的請求,沿著鏈路追下去:MCP → 外部 API → ACP → webhook。當你在負載下看到奇怪的 p99 尾巴時,這尤其有用。

7. feed 資料品質:從結構到語義

我們已看過延遲、錯誤、資源在負載下的表現。但即便上述 SLO 全都達標,使用者體驗仍可能因資料品質不佳而「崩壞」。

進入第二個大主題:資料。對 GiftGenius 這類 commerce App 而言,product feed(商品 feed)不是「放在硬碟上的東西」,而是 LLM 與代理的燃料。若 feed 充滿雜訊,模型不會幫你「捏造」正確價格與庫存。

將 feed 品質分成三層來思考很實用。

結構層

這是資料的基本有效性:

  • JSON 能正確解析。
  • 所有必填欄位齊全:idnamepricecurrencyimageUrlavailability 等。
  • 值的型別符合預期:價格是數字、availability 是 enum、categories 是字串陣列。
  • 沒有重複的 id

這部分你可能已用契約式測試覆蓋過,透過 JSON Schema/Zod 描述 feed 結構。現在要把這些 schema 套在真實的資料量上。

GiftGenius 單一 feed 項目的簡單 Zod schema 範例:

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([]),
});

整個 feed 的 schema 就是 z.array(giftItemSchema)

商業層(語義)

結構上商品可能「合法」,但在商業邏輯上卻很荒謬:

  • 昂貴商品的價格是 0 或 0.01。
  • 幣別不符市場(只在 EUR 銷售的商品卻標 USD)。
  • inStock = true,但最後更新日期是半年前。
  • 類別多達上千種但未統一。

在這一層,加上一些額外檢查與「常識規則」很有用。例如:

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 的一部分,或在生成新 feed 時執行。

LLM 層

模型很強,但也有它的「小毛病」:

  • 描述塞滿 HTML、多餘標籤與技術性文字。
  • 語言混雜(半個 feed 是俄文、半個是英文)卻未標示 locale。
  • 過長的「SEO 名稱」,像「快來買最棒超級無敵禮物便宜又划算」。

這一層要把資料整理成對模型友善的格式:

  • 移除 HTML 標籤或轉為純文字。
  • 正規化描述的語言(至少明確標註 locale)。
  • 截斷過長的名稱與重複資訊。

部分可以自動化(例如透過前處理腳本),另一些則需要與負責填充 feed 的團隊達成約定。

8. 實作:GiftGenius 的 feed 驗證器

為專案加入一個簡單腳本 validateFeed.ts,讀取 feed 的 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 伺服器相同的契約,也就是說,契約式測試與 feed 檢查共用同一份 schema——這能大幅降低不一致的機率。

之後可以加上商業規則檢查與指標,例如:

  • 無描述的商品占比;
  • 價格異常偏低/偏高的占比;
  • 重複的 id 或重複的 name + price 數量。

這些數字可以上報到指標系統(如 Prometheus、Datadog 等),並針對資料品質訂定獨立的 SLO——就像你為程式碼訂定 SLO 一樣。

9. 負載與 feed 之間如何互相影響

有時看似「效能」與「資料品質」是兩個不太相關的主題。實際上,它們緊密交織。

一些關聯例子:

  • 在負載下,部分請求會走到以往少見的邏輯分支。例如有特殊折扣類型或非標準 shipping 的商品。如果這些位置的 feed 很髒,你可能同時遇到錯誤與嚴重效能劣化(大量驗證、例外、fallback 邏輯)。
  • 若 feed 噪音很大(超長描述含 HTML、無意義標籤),MCP 伺服器必須拉與序列化更多資料,直接拉長 tool-call 的處理時間與回應大小。
  • 在 commerce 流程中,糟糕的 feed 會導致許多「空的」checkout 嘗試——使用者選到突然缺貨的商品。這同時傷害 UX 與 ACP 指標(未成功 intent 增加)。

把它看成一張矩陣會很實用:

Feed 問題 在負載下的症狀 觀察位置
價格/幣別不一致 ACP 出錯、付款被拒 ACP 日誌 + checkout SLO
商品重複 推薦結果怪異、冗餘呼叫 MCP 日誌、UX 指標
缺少圖片/描述 模型給出「扁平」的推薦 App 日誌 + UX 回饋
描述含 HTML/雜訊 序列化變慢、payload 變大 MCP 延遲

負載測試在此就像手電筒:它能照亮那些平時少被觸及、但在活躍流量下會出槍的 feed 區段。

10. 把這些嵌入 GiftGenius 的 release 流程

就流程而言,以上不該只是「上線前的一次性」。在模組 16(「Production、網路與擴充」)與 17(「可觀測性與品質」)的課綱裡,這套做法就是例行 release‑checklist 的一部分:上線前不只跑 unit/contract/E2E,還要短時的 smoke‑load 加上 feed 檢查。

上線前的一個合理最小 pipeline:

  1. Unit + contract + 整合測試皆為綠燈。
  2. 在 staging 對 MCP/ACP 跑一次短的 smoke‑load(若關鍵碼有變:搜尋邏輯、資料庫處理、checkout)。
  3. feed 驗證器無錯誤,feed 的基礎品質指標(壞資料筆數、缺圖占比等)在可接受範圍內。
  4. 儀表板與警報已更新,涵蓋新 endpoint 與 SLO。
  5. 若發生失敗有回滾方案:功能旗標關閉或回退建置版本。

如此一來,你的 GiftGenius 就不再是「DevDay 的 demo」,而是準備好進 Store、能承受流量波峰的服務。

11. 負載測試與 feed 檢查的常見錯誤

錯誤 №1:對 ChatGPT 做負載,而不是你的後端。
有人想「像真實情境那樣」測試,於是寫腳本從 ChatGPT UI 走。結果撞上 OpenAI 的限制、燒 token、得到極度雜訊的結果。同時,MCP/ACP 的問題本來可以便宜 100 倍就抓到,直接打 /mcp/api/checkout 就好。

錯誤 №2:只關注平均回應時間。
「我們平均延遲 500 ms,一切良好」——但 p95 是 5 秒這件事卻被忽略。我們在 SLO 主題已談過,真正決定 UX 的是分佈尾端(p95/p99)。在負載下,平均值常維持體面,但尾巴會長到兩三倍。

錯誤 №3:搞「企業級壓測」而非務實的 smoke‑load。
為 ChatGPT App(如 GiftGenius)花數月打造模擬數萬使用者的複雜場景,幾乎都是多餘的。更有價值的是一個簡單、但能定期執行的 smoke‑load:50–100 個 VU,指標清楚。

錯誤 №4:不真實的負載情境。
腳本一直送同一個請求,沒有使用者、語言、商品類型的變化,也不碰 ACP 與 webhook。結果你測的是單一熱門 happy‑path,系統真正的「角落」仍在陰影裡。至少要模擬一個簡化但可信的流程:不同預算、不同興趣,有些使用者會走到 checkout,有些不會。

錯誤 №5:feed 只「用眼看」或直接在 prod 檢查。
feed 生好了、丟進 prod,看到模型給出奇怪的推薦才摸頭。如果有個簡單的 Zod/JSON Schema 腳本,早在 1 分鐘內就能指出:10% 商品沒圖片、5% 價格是 0、3% 幣別是 XXX。缺少自動化的 feed 驗證,是 commerce 應用中最常見的「糗點」之一。

錯誤 №6:在 feed 很差時指望 LLM「自己會懂」。
模型很厲害,但不會替你編出正確價格或庫存。同一商品若在 feed 中同時出現不同價格,或「有庫存」/「無庫存」並存,代理可能給出幻覺與不一致的體驗。資料乾淨的責任在你,不在模型。

錯誤 №7:feed 指標與整體 SLO 脫節。
MCP 與 ACP 再快也沒用,如果 30% 的商品在 feed 裡是「壞的」,使用者體驗仍然糟。團隊常只追蹤技術 SLO(延遲、錯誤率),卻忽略資料品質的 SLO(有效 SKU 的最低比例、重複上限等)。於是「數字上都很好」,體感卻不好。

錯誤 №8:未準備就直接在生產環境跑負載測試。
有時有人在星期五晚上決定「快跑一個 k6 打生產 MCP」,也沒通知任何人。最好情況下你會干擾真實指標、讓 on‑call 工程師被流量尖峰嚇到;最糟是踩到外部 API 或金流系統的 rate limit。先在 staging 跑首批情境;若需要 prod 測試——要有計畫、有時段、有通知。

1
問卷/小測驗
可觀測性與品質,等級 17,課堂 4
未開放
可觀測性與品質
可觀測性與品質
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION