Token 生成速度只是 AI 推理链中最快的一环;实际用户体验的是从点击到渲染完成的完整往返时间,包含浏览器、网络、认证、队列等所有环节的耗时。
上周我在为一个团队提供咨询时,他们向我展示了一个引以为豪的基准测试:在一个全新的模型上达到每秒四十个 token,测试环境是一个干净的 notebook。然后他们部署了同样的功能,却眼睁睁看着单个用户请求从点击到答案渲染完成用了十一秒。模型很快,但整个往返却是一场灾难。这个差距不是调优问题,而是一个测量问题,它会一直困扰你,直到你改变测量方式。
每个 AI 功能都是一连串的交接:浏览器、边缘节点、认证层、你的 API、队列、服务器、模型提供商、持久化层,以及回传路径。每秒 token 数只测量了这根链条中的一个环节,而且往往是最快的那个环节。延迟、成本和失败实际发生的地方都在链条的其余部分。那为什么我们一直在为那个从不出问题的环节发布基准测试呢?
目前在 DEV 上有一个关于 AI 徽章实际衡量什么的健康讨论,同样的疑问也适用于模型基准测试:它们用极高的精度测量了错误的东西。用户感受到的数字是往返时间,而唯一诚实的方式是在你能找到的最弱的合法环境中运行整条链条进行测量。在那里,一个免费服务器变成了你技术栈中最有价值的工具,也正是在那里,大多数团队停止了认真测量。
披露:本文是 MonkeyCode 产品推广的一部分。
这就是为什么我一直针对 MonkeyCode 运行我的往返测试——一个将免费模型访问和免费服务器选项捆绑在一起的开源项目。模型访问给你一个真实的推理端点,服务器给你一个真实的部署目标,这意味着你可以测量整条链条而不是 notebook 单元格。由于它是开源的,你可以阅读代理代码而不是信任某个仪表盘,这在调试服务器和模型之间的交接时很重要。在撰写本文时,免费的额度宣传的是一千万 token,但不要基于这个数字来构建业务;配额会变化,练习的目的是测量,而不是额度。
以下是我在任何 AI 端点信任之前对其运行的可复现测试,你可以在大约十分钟内针对 MonkeyCode 免费服务器运行它。首先用单个 curl 感受冷启动:
curl -s -o /dev/null -w "cold start: %{time_total}s\n" \
-X POST https://your-free-server.example/api/chat \
-H "Content-Type: application/json" \
-d '{"prompt":"Say hello in one sentence.","stream":false}'
然后运行完整的脚本,它测量冷启动、热启动 p50 和 p95、并发 p95,以及针对你定义的预算的错误率:
# roundtrip.py — measure the contract, not the model
import asyncio
import statistics
import time
import httpx
URL = "https://your-free-server.example/api/chat"
BUDGET = {"p95_seconds": 3.0, "error_rate": 0.01}
PROMPT = {"prompt": "Summarize this repository in three sentences.", "stream": False}
def p95(values: list[float]) -> float:
ordered = sorted(values)
return ordered[min(len(ordered) - 1, int(len(ordered) * 0.95))]
async def one_call(client: httpx.AsyncClient) -> tuple[float, int]:
start = time.perf_counter()
try:
response = await client.post(URL, json=PROMPT, timeout=30)
return time.perf_counter() - start, response.status_code
except httpx.HTTPError:
return time.perf_counter() - start, 0
async def main() -> None:
async with httpx.AsyncClient() as client:
cold, cold_code = await one_call(client) # first call after idle
warm = [await one_call(client) for _ in range(20)] # sequential warm calls
concurrent = await asyncio.gather(*(one_call(client) for _ in range(10)))
warm_times = [t for t, _ in warm]
all_codes = [code for _, code in warm + list(concurrent)]
error_rate = sum(1 for code in all_codes if code >= 400 or code == 0) / len(all_codes)
print(f"cold start: {cold:.2f}s (status {cold_code})")
print(f"warm p50: {statistics.median(warm_times):.2f}s")
print(f"warm p95: {p95(warm_times):.2f}s")
print(f"concurrent p95: {p95([t for t, _ in concurrent]):.2f}s")
print(f"error rate: {error_rate:.1%}")
print(f"budget met: {p95(warm_times) <= BUDGET['p95_seconds'] and error_rate <= BUDGET['error_rate']}")
if __name__ == "__main__":
asyncio.run(main())
针对本地模型运行一次,针对 MonkeyCode 免费服务器运行一次,再针对你付费的生产端点运行一次。第一次运行什么都告诉不了你,第二次运行告诉你用户实际会感受到什么,第三次运行告诉你你在为什么付费。如果免费服务器达到了预算,你暂时不需要付费 tier。如果没达到,你有两个选择:购买更多算力来掩盖问题,或者修复链条。
以我的经验,失败几乎从来不是模型的问题。是你为了避免超时而加入的队列、是那个在发送第一个字节之前就缓冲整个响应的流式包装器、是在返回答案之前发生的数据库写入。免费服务器会暴露所有这些问题,因为它没有多余的容量来掩盖它们,而这正是关键所在。免费服务器是最坏情况的生产预演,而不是演示环境。
一千万 token 的额度不是演示配额,而是预演预算。每次运行这个脚本都会消耗几千个 token 来换取关于你架构的信息,这使其成为你做过的最便宜的负载测试。当额度用完时,那是再次测量的信号,而不是恐慌的理由。如果你把全部额度花在 notebook 基准测试上,你什么都没学到,除了知道模型很快——这一点你早就知道了。
谁不应该使用这种方法?如果你正在构建一个硬性两百毫秒预算的实时语音功能,免费服务器不是你的预演环境,那是不同的产品类别。如果你的持续并发已经超过共享免费 tier 能承受的范围,你已经过了这个测试能帮助你的阶段。如果你的合规规则禁止将 prompt 发送到第三方端点,这些都不适用于你。免费服务器是一个预演舞台,而不是永久居所,约束条件才是老师。
所以停止为模型做基准测试,开始为往返时间做基准测试。这周针对 MonkeyCode 免费服务器运行这个脚本,告诉我链条的哪一层最先失败。我要的是响应代码,不是感觉。