作者对免费模型服务器做了90次调用测试,评估JSON结构化输出的可靠性,发现免费层在嵌套任务上普遍失败。
延迟只能告诉你服务器慢不慢,不能告诉你答案对不对。我花了几周时间测量免费模型服务器的响应时间。这一次,我测量了另一个指标:输出质量。
一个免费模型服务器能产生可靠的结构化输出吗?真实应用需要的是 JSON,而不是散文。我建了一个小型基准测试来找出答案。
大多数免费服务器测试只停留在速度层面。它们测量首次 token 延迟、方差、超时时间。这些我都做过。但这些都不能告诉你 JSON 能不能正确解析。
我的问题更具体。如果我要一个扁平对象,它能返回有效结果吗?如果我要嵌套数据,它能完整返回吗?免费层级到底在什么地方会出问题?
三个任务,难度递增,每个任务跑 30 遍,总共 90 次调用。
任务 A — 平面提取。从收据文本中提取 customer_name、date 和 amount。5 个字段的指令,没有嵌套。
任务 B — 枚举分类。读取一条客服工单,从固定列表中返回 category 并附上置信度分数。模型必须遵守枚举约束。
任务 C — 嵌套订单。提取一个包含 order_id、customer 和 line_items 数组的订单。这是免费服务器通常会崩溃的地方。
我将测试套件指向 MonkeyCode 的免费模型端点。客户端从 MonkeyCode 的免费服务器选项运行,所以网络路径模拟的是真实用户,而不是我本地笔记本。
声明:本文是 MonkeyCode 产品推广的一部分。
脚本很小。它从 JSON 读取测试用例、调用端点、对每个响应打分,然后写 CSV 并打印摘要。
评分规则很严格。有效的 JSON 是不够的。每个期望的键必须存在,每个值必须匹配。部分得分只在 schema 完整时才计入。
# benchmark_free_server.py
import asyncio, csv, json, os, time
from dataclasses import dataclass
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url=os.getenv("LLM_BASE_URL", "https://your-endpoint/v1"),
api_key=os.getenv("LLM_API_KEY", "demo-key"),
)
@dataclass
class Case:
task: str
name: str
prompt: str
expected: dict
def load_cases(path: str) -> list[Case]:
with open(path) as f:
return [Case(**c) for c in json.load(f)]
def validate(text: str, expected: dict) -> dict:
score = {"valid_json": False, "schema_ok": False,
"exact_match": False, "fields_correct": 0.0}
try:
data = json.loads(text)
except json.JSONDecodeError:
return score
score["valid_json"] = True
score["schema_ok"] = all(k in data for k in expected)
if not score["schema_ok"]:
return score
correct = sum(1 for k, v in expected.items() if data.get(k) == v)
score["fields_correct"] = correct / len(expected)
score["exact_match"] = score["fields_correct"] == 1.0
return score
async def run_case(case: Case) -> dict:
t0 = time.perf_counter()
try:
resp = await client.chat.completions.create(
model=os.getenv("LLM_MODEL", "free-model"),
messages=[
{"role": "system",
"content": "Return only valid JSON. No markdown fences."},
{"role": "user", "content": case.prompt},
],
temperature=0, # drop this line if your endpoint rejects it
max_tokens=500,
)
text = resp.choices[0].message.content or ""
score = validate(text, case.expected)
return {"task": case.task, "name": case.name,
"latency_s": round(time.perf_counter() - t0, 2),
"error": "", **score}
except Exception as e:
return {"task": case.task, "name": case.name,
"latency_s": round(time.perf_counter() - t0, 2),
"error": type(e).__name__, "valid_json": False,
"schema_ok": False, "exact_match": False,
"fields_correct": 0.0}
async def main():
cases = load_cases("cases.json")
results = [await run_case(c) for c in cases]
with open("results.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(results[0].keys()))
writer.writeheader()
writer.writerows(results)
by_task = {}
for r in results:
by_task.setdefault(r["task"], []).append(r)
for task, rs in by_task.items():
n = len(rs)
print(f"{task}: n={n} valid={sum(r['valid_json'] for r in rs)} "
f"exact={sum(r['exact_match'] for r in rs)} "
f"avg_latency={sum(r['latency_s'] for r in rs)/n:.2f}s")
if __name__ == "__main__":
asyncio.run(main())
测试用例文件是纯 JSON,每个任务一条记录:
[
{
"task": "A",
"name": "receipt_01",
"prompt": "Extract customer_name, date, and amount from: 'Cafe Luna, 2026-08-23, latte 4.50, pastry 3.25, total 7.75.'",
"expected": {"customer_name": "Cafe Luna", "date": "2026-08-23", "amount": "7.75"}
}
]
用三个环境变量运行:
export LLM_BASE_URL="https://your-endpoint/v1"
export LLM_API_KEY="your-key"
export LLM_MODEL="free-model"
python benchmark_free_server.py
这个测试套件并非 MonkeyCode 专用。你可以把它指向任何 OpenAI 兼容的端点。结论会不同,但方法不会变。
一个下午,一个端点,90 次调用。以下是汇总结果。
任务 A 可用。任务 B 接近。任务 C 靠运气。这个模式在每批测试中都会重复。
三种失败模式占主导,你会看到同样的问题。
Markdown 围栏。模型把 JSON 包在 ```json 里,即便系统提示说了不要。我的验证器把这些判为无效。加一个剥离步骤可以恢复大部分。
截断。任务 C 的响应触发了 token 上限。JSON 在数组中间就断了。没有闭合括号,无法恢复,没有任何警告。
键名漂移。line_items 变成了 items。customer_name 变成了 name。即使数据正确,schema 检查也会失败。
看着眼熟吗?如果你在生产环境中解析 LLM 输出,这三种你肯定都见过。
以下是我得出的规则。把它当作起点,而不是圣经。
不要相信第一次解析。运行一个三步修复循环。
解析。如果 json.loads 成功,停止。
剥离。移除 Markdown 围栏和尾部逗号。重试一次。
让模型修复。把错误信息发回去。一次重试,不要更多。
def repair(text: str):
candidates = [text]
cleaned = text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.strip("`").removeprefix("json").strip()
candidates.append(cleaned)
for c in candidates:
try:
return json.loads(c)
except json.JSONDecodeError:
continue
return None
修复循环拯救了任务 C 中大部分的围栏失败。有效 JSON 从 19/30 提升到 26/30。Schema-OK 几乎没有变化。截断无法修复。
在复制我的结论之前,先读一下注意事项。
这是一次会话、一个端点、一个下午的结果。免费服务器随时可能变更。我的数字是一个观察结果,不是产品基准。自己跑一下这个测试套件。
提示词比模型更重要。微小的措辞变化能让精确匹配率移动十个点。你的结果会不同。
不要向免费端点发送敏感数据。提示词可能会被记录。不要包含任何 PII、密钥或患者记录。
谁应该跳过这种方法?有严格 schema 保证的团队。不能容忍重试的应用。任何有合规官在场的人。
延迟只是故事的一半。正确性是另一半。免费模型服务器处理扁平 JSON 很好,但在嵌套结构上会跟你较劲。
在信任之前先测量。测试套件在上面。跑一下,然后做决定。
如果你用自己的端点跑了这个测试,我想看你的数字。把 CSV 贴在评论区。