文章揭示了免费模型服务与物理机器的根本区别:TTFB测的是队列位置而非容量,quota是天花板而非消除方差,curl测试会掩盖客户端连接池问题。附实际可用的性能测试方法。
我在代码审查中经常看到同一个问题。
"这个能放在免费层吗?"
你实际上怎么知道答案?
大多数回答从 curl 和秒表开始。这是错误的工具。
免费模型服务器不是一台更小的机器。它是一个带有软承诺的共享队列。把这两者混淆会产生流传多年的误解。
以下是我不再相信的六个误解,外加一个可以得出结论的适配测试。
TTFB 测量的是队列位置,而不是容量。
你在队列中很幸运。下一个请求可能等待十倍的时间。
测量持续吞吐量,而不是第一个数据块。每秒 token 数能告诉你更多信息。完整响应比一个延迟数字更有价值。
这是我用惨痛教训学到的。我的演示在台上卡住了。
配额是一个上限。它不会消除方差。
简单的重试逻辑会把更大的配额变成更多失败。
按地板设计,而不是天花板设计。带抖动的指数退避在任何时候都比更高的限制更好。
有时是。有时不是。
提供商可能会限制并发、更改批处理,或服务量化变体。模型卡说是一回事。端点可能做的是另一回事。
永远不要假设一致性。探测实际行为。
Curl 隐藏了你客户端的问题。
没有连接池。没有超时处理。没有流式缓冲逻辑。
你的应用可能会串行化请求、丢弃连接或在读取时阻塞。端点是好的。你的客户端才是瓶颈。
一次运行只是一个案例。
免费层与陌生人共享容量。你邻居的批处理作业会改变你的延迟。
在同一时间段内至少运行十次相同的探测。方差才是信号,而不是平均值。平均值会掩盖尾部。尾部才是用户感受到的。
我曾经连续几周指责提供商。
真正的问题是我的代码。我每个请求都打开一个新连接。我在流式传输之前读取完整 body。
在指责队列之前先检查你的客户端。仅仅连接复用通常就能使吞吐量翻倍。
免费层是一个队列,不是一台机器。
你借用的是带有软 SLA 的共享容量。问题不是"它快吗?"问题是"它的方差适合我的工作负载吗?"
这改变了一切。你不再寻找更快的端点。你开始将工作负载与层级相匹配。
我写了一个小型探测工具,可以按工作负载适配度对端点进行分类。
它在三个并发级别上运行重复调用。记录 TTFB、每秒 token 数和错误率。然后将结果映射到决策表。
# fitcheck.py — 按工作负载适配度对免费模型端点进行分类
# 假设是 OpenAI 兼容的流式端点。根据你的提供商调整解析逻辑。
import asyncio
import statistics
import time
import httpx
ENDPOINT = "https://your-free-endpoint.example/v1/chat/completions"
PROMPT = "Explain idempotency in 150 words."
ROUNDS = 10
CONCURRENCY_LEVELS = [1, 4, 8]
def estimate_tokens(text: str) -> int:
# 粗略估计:约每 4 个字符一个 token
return max(1, len(text) // 4)
async def one_call(client: httpx.AsyncClient) -> dict:
payload = {
"model": "your-model",
"messages": [{"role": "user", "content": PROMPT}],
"stream": True,
}
start = time.perf_counter()
try:
first_chunk = None
text = ""
async with client.stream("POST", ENDPOINT, json=payload) as response:
async for chunk in response.aiter_text():
if first_chunk is None:
first_chunk = time.perf_counter()
text += chunk
return {
"ok": True,
"ttfb": first_chunk - start if first_chunk else None,
"total": time.perf_counter() - start,
"tokens": estimate_tokens(text),
}
except Exception as exc: # noqa: BLE001
return {"ok": False, "error": str(exc)}
async def run_level(concurrency: int) -> list[dict]:
limits = httpx.Limits(max_connections=concurrency)
async with httpx.AsyncClient(limits=limits, timeout=60) as client:
tasks = [one_call(client) for _ in range(ROUNDS)]
return await asyncio.gather(*tasks)
def classify(results: list[dict]) -> dict:
ok = [r for r in results if r.get("ok")]
errors = [r for r in results if not r.get("ok")]
ttfb = [r["ttfb"] for r in ok if r.get("ttfb")]
tps = [
r["tokens"] / r["total"]
for r in ok
if r.get("total") and r["tokens"]
]
cv = statistics.pstdev(tps) / statistics.mean(tps) if len(tps) > 1 else 1.0
return {
"error_rate": len(errors) / len(results),
"median_ttfb": statistics.median(ttfb) if ttfb else None,
"median_tps": statistics.median(tps) if tps else None,
"throughput_cv": round(cv, 2),
}
async def main() -> None:
for level in CONCURRENCY_LEVELS:
results = await run_level(level)
print(f"concurrency={level}: {classify(results)}")
if __name__ == "__main__":
asyncio.run(main())
在三个不同时间段运行它。然后将各行映射到以下表格。
各信号含义:
中位 TTFB 告诉你队列等待时间。
中位 TPS 告诉你吞吐量。
CV 告诉你稳定性。
错误率告诉你可信度。
示例输出(你的会不同):
concurrency=1: {'error_rate': 0.0, 'median_ttfb': 1.1, 'median_tps': 19.2, 'throughput_cv': 0.28}
concurrency=4: {'error_rate': 0.0, 'median_ttfb': 1.9, 'median_tps': 22.5, 'throughput_cv': 0.41}
concurrency=8: {'error_rate': 0.1, 'median_ttfb': 3.9, 'median_tps': 16.0, 'throughput_cv': 0.61}
并发 1 和 4 通过了批处理适配。并发 8 降级为演示适配。结论:低并发的批处理适配。不是生产适配。
诚实地读表格。演示适配意味着只适合演示。批处理适配意味着离线任务。生产适配意味着你有证据,而不是希望。
我用这个脚本评估了 MonkeyCode 的免费模型访问和免费服务器选项。
披露:本文是作为 MonkeyCode 产品推广的一部分准备的。
工作流程与任何提供商相同。将脚本指向端点。在三个不同时间段运行。比较三行结果。
我没有测量生产级延迟。那不是这个层级的承诺。我测量的是适配度。这是唯一诚实的问题。
脚本不关心你指向哪个提供商。这才是重点。
输出告诉你端点属于哪里。演示。批处理。或者说,与你的流量完全不符。
这个适配测试不能替代 SLA。
如果你的工作负载是突发性的、面向用户的或受监管的,你需要的是合同,而不是队列。免费层可以随时更改形态。你的探测结果只是一个快照,而不是保证。
如果你不能容忍任何错误,也跳过此测试。测试测量的是方差。它不会消除方差。
如果你需要数据驻留保证,也跳过。探测无法验证 token 在哪里处理。
不要再问免费层是否快。
问它是否适合你的工作负载。然后用重复测量来证明。
在你的下一次架构辩论之前运行适配测试。结论会平息争论。
队列不是机器。你的工作是知道你在用哪一个。