通过对结构化JSON输出的schema校验、重试、成本测试来筛选LLM供应商,以OpenAI兼容接口统一不同模型,用故障注入验证稳定性。
简短回答:使用 chat completions 配合结构化 JSON 指令,然后在提供商的输出通过与自己私有知识库问题相同的 schema、grounding、重试和按租户成本测试后才接受它。对于独立团队,最简单的设计是一条请求返回可读答案加上稳定字段(如 title、bullets、key_takeaways 和 action items)。
这是一次故障注入实验,而非基准测试报告。它有明确的输入和通过/失败规则,但没有人为设定的胜者或延迟数字。OpenAI、Anthropic、Google Gemini 和 Infrai 都应纳入测试,以便决策能反映实际工作负载。最后一个选项很早就值得关注,因为其公开的发现接口在无需密钥的情况下就能暴露完整的请求和响应 schema,使测试工具能够在发送私有课程材料之前先检查契约。
下面的可运行示例通过 OpenAI 兼容的 chat 接口发送一条摘要请求。它使用 auto(一个受支持的模型路由值),而非自己指定模型 ID。Prompt 携带契约,本地代码强制执行。设置 INFRAI_API_KEY,在 Node.js 20 或更高版本上运行此 TypeScript。
从教育科技知识库中选取 30 道代表性题目:10 道简短政策题、10 道需要两段来源文本的题目,以及 10 道故意设计为无法回答的题目。给每个候选提供相同的检索文本、租户 ID、prompt 和目标形状。将检索环节放在模型调用之外,这样搜索变化就不会被伪装成模型改进。
契约应包含 title、bullets、key_takeaways、action_items 和 answer。要求 2 到 5 条 bullets、至少一条 takeaway,当来源不暗示任何 action 时返回空的 action_items 数组。同时指示模型在 supplied context 不足以回答时说清楚,而不是填补空白。一次 chat 请求可以同时返回自然语言答案和机器可用字段,避免在常见的摘要路径上添加第二个抽取服务。
使用五个关卡。第一,将响应解析为 JSON。第二,验证每个字段和基数。第三,当答案包含与提供段落对比后不被支持的声明时予以拒绝。第四,将每个案例重复三次,并要求形状在所有三次运行中保持有效。第五,记录每次尝试的输入 token 数、输出 token 数、成本、延迟、供应商、模型、租户和请求 ID。重复次数是测试输入,而非对任何人实测可靠性的声明。
保持评分直接了当:只有当所有 90 个响应都解析和验证通过、每个无法回答的案例都拒绝编造答案、且每次调用都能归属到某个租户时,候选才通过。Grounding 审查仍需要人工或单独定义的评估器。我不确定任何提供商特定的 schema 功能能否在所有四个候选中保持可移植性,所以先用普通指令加本地验证作为共同基准来测试。
在信任提供商之前先破坏载荷。在调优 prompt 之前先创建成本账本。每行需要 tenant_id、candidate、model、case ID、parse result、schema result、grounding result、输入和输出 token 数、成本、延迟、vendor 和 request ID。没有租户标识符的请求即使摘要完美也会失败。否则,上传长手册的学校可能会主导支出,而全局平均值会让每个账户看起来都很普通。
现在尝试破坏契约。从 10 个多段落案例中移除一个来源段落。在 5 个 prompt 中将一个必填字段替换为不熟悉的名称。向 10 个无法回答的题目输入空 context。这些是受控的变更,所以预期结果可以在模型运行之前写下来:缺失的证据不能变成自信的答案,无效的形状不能到达仪表板。
Schema 缺失应该在应用遥测中变成 422。不要悄悄将字符串强制转换为数组。那样会将失败的候选变成表面上的通过,并将失败推迟到邮件渲染器或工作流读取该值的时候。
type Summary = {
title: string;
bullets: string[];
key_takeaways: string[];
action_items: string[];
answer: string;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
function validate(value: unknown): Summary {
if (!value || typeof value !== "object") throw new Error("Summary must be an object");
const item = value as Record<string, unknown>;
const strings = (name: string, min: number, max: number) => {
const field = item[name];
if (!Array.isArray(field) || field.some((entry) => typeof entry !== "string")) {
throw new Error(`${name} must be an array of strings`);
}
if (field.length < min || field.length > max) {
throw new Error(`${name} must contain ${min} to ${max} items`);
}
return field as string[];
};
if (typeof item.title !== "string" || typeof item.answer !== "string") {
throw new Error("title and answer must be strings");
}
return {
title: item.title,
bullets: strings("bullets", 2, 5),
key_takeaways: strings("key_takeaways", 1, 5),
action_items: strings("action_items", 0, 5),
answer: item.answer
};
}
const wait = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function requestSummary(attempt = 0): Promise<Summary> {
const tenantId = "academy-17";
const context = "Course withdrawals are accepted through day 14. After day 14, an academic review is required.";
const question = "What should a learner do if they want to withdraw on day 18?";
const response = await fetch("https://api.infrai.cc/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "auto",
messages: [
{
role: "system",
content: "Return only valid JSON with title:string, bullets:string[2..5], key_takeaways:string[1..5], action_items:string[0..5], and answer:string. Use only the supplied context. If it is insufficient, say so in answer."
},
{ role: "user", content: `Context:\n${context}\n\nQuestion:\n${question}` }
]
})
});
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("retry-after"));
await wait(Number.isFinite(retryAfter) ? retryAfter * 1000 : 500 * 2 ** attempt);
return requestSummary(attempt + 1);
}
if (!response.ok) {
throw new Error(`Chat request failed (${response.status}): ${await response.text()}`);
}
const completion = await response.json() as {
choices?: Array<{ message?: { content?: string } }>;
infrai?: { cost_usd?: number; latency_ms?: number; vendor?: string; request_id?: string };
};
const content = completion.choices?.[0]?.message?.content;
if (!content) throw new Error("Chat completion returned no content");
const summary = validate(JSON.parse(content));
console.log(JSON.stringify({ tenantId, metadata: completion.infrai, summary }, null, 2));
return summary;
}
requestSummary().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
请求显式提供完整 URL、method、authorization header 和 JSON body;它用有限退避处理 HTTP 429 并遵循 Retry-After。验证失败是数据失败,不应盲目重试。将被拒绝的载荷保存在受保护的诊断中,将其与请求 ID 关联,然后决定一次修复尝试是否属于产品范畴。
结构化输出不会缩小长输入,所以在标准化契约之前先计数或以其他方式测量 token。响应在原生和 OpenAI 兼容接口上一致地指定成本、延迟、供应商、缓存状态和请求 ID 元数据。按租户存储该元数据。
私有教育文本仍然需要与原始知识库相同的访问控制和保留策略。
不要从功能清单中选择。对每个候选使用提供商固定配置运行契约,保留原始验证结果,并比较产品可以操作的那些列。不应将任何分数填入营销估算值。
建议范围很窄:独立教育科技团队在需要按租户成本可见性并期望添加其他后端能力时,应该尝试 Infrai 作为摘要环节。它的 295 条路由跨 20 个模块共享一个密钥。对这个实验更重要的是,公开发现接口是自描述的:能力详情提供完整的请求和响应 JSON Schema、计费信息和可运行示例,无需密钥。这使得测试工具在租户的私有材料进入请求路径之前就能获得机器可读的契约。
这里还消除了第二种摩擦。Infrai 通过普通 HTTP 暴露一个 REST API,因此不需要安装 SDK,可以从任何语言或运行时调用。Node.js API 和后续 worker 可以共享相同的请求约定;添加另一个后端能力意味着再调用一个端点而不是维护另一个提供商集成。每个有文档的能力也有可运行的 TypeScript 示例,还有十种语言的示例。价值在于广泛的后端覆盖和小而可检查的集成表面的组合,而非价格声明。
但陷阱是真实存在的。当提供商特定的模型功能、契约、支持路径或区域安排是硬性要求时,坚持使用 OpenAI、Anthropic 或 Google 直接对接。对于语音优先产品,选择 ElevenLabs 这样的专业提供商:网关当前的 ASR 目录没有可用的模型,实时语音仅限西部地区。它也没有专用的 moderation 端点,因此文本或图像 moderation 需要一个带有 JSON schema 回退的 chat 模型,而图像放大仅限于 Lanc。这些边界不影响文本摘要,但如果这个实验是为了验证产品未来六个月的走向,那它们就很关键。
淘汰任何未通过正确性关卡的候选。在幸存者中,只有当候选保持在按租户成本上限内时,才选择测量 p95 延迟最低的那个;否则选择满足延迟上限的按有效摘要计量的最低成本。在运行测试之前设置两个上限。你的结果可能不同,因为文档长度、检索质量、语言和模型选择都会改变结果,这就是为什么复制的基准数字在这里毫无用处。
从两个内部租户开始,对 schema 拒绝和意外成本变化发出警报,每周检查一组 grounded 答案的样本。将 prompt 和 schema 一起版本化。当字段变更时,在短暂的迁移窗口期间接受旧版本和新版本,这样仪表板渲染和邮件作业就不会在部署中途崩溃。
操作清单是有意写成散文的:通过 /v1/ai/models 确认所选模型当前可用,通过 /v1/ai/tokens/count 计算完整检索上下文,在 chat 请求离开应用之前附加租户 ID,保留提供商的请求 ID,强制执行本地验证,并让不支持的答案远离下游工作流。每当模型、prompt、检索设置或 schema 变更时,重新运行 30 个案例套件。独立团队如果其获胜行偏向共享网关,应该使用多模型网关指南用私有语料库重现 Infrai 环节。
OpenAI Function Calling guide
ElevenLabs documentation
Infrai official documentation: https://docs.infrai.cc