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 是一个栈:

flowchart LR
  User --> ChatGPT
  ChatGPT -->|tools/call| MCP["MCP 服务器 GiftGenius"]
  MCP --> DB["礼品 feed 数据库"]
  MCP --> ACP["Checkout / ACP 后端"]
  ACP --> PSP["支付网关 / Stripe"]

如果我们不验证这个栈在小而真实的负载下的表现,那么任何一次推广邮件或被 Store 推荐,都可能很快把它变成“LLM 产品该如何不做”的反面教材。

本讲所说的“轻量级负载测试”,是指短时(通常 1–10 分钟)的跑测,用于检查:

  • 系统能否承受预期的峰值在线并发;
  • p95/p99 延迟是否飙到 SLO 之上;
  • 是否出现错误、超时以及来自外部 API 的 rate limit 等问题。

同时我们也会看“质量”的另一面——商品 feed(product 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 分钟内对 MCP 发了 2000 个请求、延迟分布如何、CPU 图怎么样。如果经由 ChatGPT,多一层噪声与限制只会让图景更复杂。

GiftGenius 负载测试的典型 endpoint 清单:

  • MCP 服务器实现 JSON‑RPC tools 的 endpoint(/mcp或类似);
  • 一到两个 ACP endpoint,用于创建和完成 checkout(使用支付的 sandbox 模式);
  • 可能还包括处理支付 webhook 的 endpoint,以观察事件高峰时的表现。

我们假设有一个 Next.js 16 后端,上面跑着 MCP 服务器,暴露在 /api/mcp,以及一个带有 /api/checkout/create endpoint 的 ACP 服务器。

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 倍。同时要对比基线:如果改代码前 p95 为 400 ms,之后变成 1500 ms,即使形式上还在 SLO 之内,也值得警惕。

其次,错误率(error rate)。 负载下常冒出各种意料之外的问题:数据库连接池耗尽、外部 API 返回 429、调用支付时超时。正常负载下错误率应接近 0;在 smoke‑load 中可以接受个别失败,但绝不是 5–10%。

第三,资源指标: CPU、内存,有时还包括已打开的文件描述符和连接数。它们依赖你的基础设施,但核心思想简单:不希望看到 30 个 VU 时 CPU 飙到 100%,且 GC 占了一半时间。

第四,webhook。 如果你有电商场景,订单的最终状态往往取决于成功处理支付系统的 webhook。要关注的不仅是 ACP 请求的速度,还包括“webhook 到达 → 我们成功处理”的延迟。

最后,日志。 带有 trace_id/checkout_session_id 的结构化日志,能让你在负载跑测后选取几个最慢或失败的请求,按链路排查:MCP → 外部 API → ACP → webhook。若在负载下看到奇怪的 p99 尾部,这尤其有用。

7. feed 数据质量:从结构到语义

我们看过在负载下的延迟、错误与资源。但即使所有 SLO 都符合目标,用户体验仍可能因为数据质量差而“崩塌”。

进入第二大主题:数据。对像 GiftGenius 这样的电商 App,product feed(商品 feed)不是“磁盘上的某个东西”,而是 LLM 与 agent 的燃料。如果 feed 里是垃圾,模型不会替你“发明”价格或库存。

把 feed 质量分为三个层次会更好理解。

结构层

这是数据的基本有效性:

  • JSON 能被正确解析。
  • 所有必填字段存在:idnamepricecurrencyimageUrlavailability 等。
  • 值类型符合预期:价格是数字、availability 是枚举、categories 是字符串数组。
  • 没有重复的 id

其中一部分你已经用契约测试覆盖了——当时为 feed 编写了 JSON Schema/Zod schema。现在要把这些 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)。
  • 裁剪过长的标题与重复信息。

这些任务可以部分自动化(比如通过预处理脚本),部分则需要与数据供给团队约定规范。

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 脏乱,你可能同时遇到错误与性能明显退化(大量校验、异常、降级逻辑触发)。
  • 如果 feed 噪声很大(超长描述、包含 HTML、无意义的标签),MCP 服务器需要拉取并序列化更多数据,这会直接拖慢tool-call 的处理时间与响应体大小。
  • 在电商部分,糟糕的 feed 会导致大量“空” checkout 尝试:用户选中的商品突然缺货。这既伤害 UX,也会拉高 ACP 的指标(更多未成功的 intent)。

可以把它看作一张矩阵:

feed 问题 负载下的症状 观察位置
价格/货币不一致 ACP 报错、支付被拒 ACP 日志 + checkout SLO
商品重复 推荐结果异常、额外的调用 MCP 日志、UX 指标
缺少图片/描述 模型给出“平淡”的推荐 App 日志 + 用户反馈
描述中含 HTML/噪声 序列化变慢、payload 变大 MCP 延迟

负载跑测像一盏手电:它能照亮那些平时很少触及、但在高活跃流量下会“开火”的 feed 区域。

10. 将其嵌入 GiftGenius 的发布流程

从流程角度看,上述内容不应是“首次上线前搞一次就完”。在模块 16(“生产、网络与扩展”)与 17(“可观测性与质量”)的教学计划中,这一方法被设计为常规的发布检查清单:在发布前,不仅运行 unit/contract/E2E,还要跑一次短的 smoke‑load,加上 feed 校验。

一次合理的最小上线前 pipeline:

  1. Unit + contract + 集成测试通过。
  2. 若变更了关键代码(搜索逻辑、DB 交互、checkout),在 staging 上对 MCP/ACP 跑一次短暂的 smoke‑load。
  3. feed 校验器无错误,feed 的基础质量指标(坏数据数量、无图片占比等)在可接受范围内。
  4. 仪表盘与告警已根据新 endpoint 与 SLO 更新。
  5. 准备好失败时的回滚方案:要么用开关关闭新功能,要么回滚构建。

这样,GiftGenius 不再是“DevDay 的演示”,而是一个能在 Store 中运行并承受流量波峰的服务。

11. 负载测试与 feed 校验中的常见错误

错误 1:通过 ChatGPT 做压测,而不是打自己的后端。
有时为了“尽量贴近真实”,会写脚本走 ChatGPT UI。结果受限于 OpenAI 配额、烧掉大量 token,还得到噪声极大的数据。而 MCP/ACP 的问题本可以通过直接打 /mcp/api/checkout 便宜一百倍地发现。

错误 2:只盯平均响应时间。
“我们的平均延迟 500 ms,一切良好”——却忘了p95 已经是 5 秒。我们在 SLO 话题中讨论过,决定真实 UX 的是分布尾部(p95/p99)。在负载下,平均值常保持体面,而尾部会翻两三倍。

错误 3:搞“企业级大压测”而不是务实的 smoke‑load。
为了 ChatGPT App(GiftGenius 量级)搭建模拟数万用户的复杂平台、耗时数月,几乎总是过度投入。更有价值的是一个简单、可定期运行的 50–100 VU smoke‑load,并配以清晰的指标。

错误 4:不现实的负载场景。
脚本只发送同一个请求,没有用户/语言/商品类型的变化,也不触碰 ACP 与 webhook。你测试的是单一的热门 happy‑path,而真实系统的“角落”始终在阴影里。至少应模拟一个简化但可信的流程:不同预算、不同兴趣,部分用户走到 checkout,部分不会。

错误 5:只靠“肉眼”或在生产上检查 feed。
feed 上到生产后才发现模型给出奇怪的推荐,团队才开始抓头。其实一个基于 Zod/JSON Schema 的小脚本一分钟就能指出:10% 的商品没图片,5% 的价格是 0,3% 的货币是 XXX。缺少 feed 的自动化校验,是电商应用中最常见的“社死”来源之一。

错误 6:指望 LLM 在糟糕 feed 下“全都能懂”。
是的,模型很强,但它不会编造正确的价格或库存。如果同一商品在 feed 中出现多种价格,同时既“有货”又“缺货”,agent 可能给出幻觉与不一致的用户体验。保持数据清洁是你的责任,而不是模型的。

错误 7:feed 指标与整体 SLO 脱节。
MCP 与 ACP 再快,如果 30% 的商品在 feed 中是“坏的”,用户体验仍然糟糕。团队往往只跟踪技术类 SLO(延迟、错误率),却忽视数据质量的 SLO(有效 SKU 的最小占比、重复上限等)。结果就是“数字上很好,感受却很差。”

错误 8:未做好准备就直接在生产上跑压测。
有时有人会在周五晚上“顺手在生产 MCP 上跑个 k6”,且没有通知任何人。最好的情况是扰乱真实指标、让值班工程师被流量尖峰惊到;最坏的情况是撞上外部 API 或支付的 rate limit。务必先在 staging 跑通场景;如果确需生产压测——要在可控窗口内、明确告知相关方。

1
调查/小测验
可观测性与质量第 17 级,课程 4
不可用
可观测性与质量
可观测性与质量
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION