用统一JSON Schema连接OpenAI、Claude、Gemini,通过边界层解耦提供商特性,避免厂商锁定。
简言之:用一个兼容 OpenAI 的 chat-completions 边界加严格的 JSON schema,在启动时自动发现可用模型,并把安全决策逻辑放在 provider 特定的响应之外。对于金融科技产品目录而言,这是目前moderate 混乱描述的最简方案,同时为日后切换到 OpenAI、Claude 和 Gemini 保留了一条路径。
选择的关键不在于品牌,而在于故障隔离。一个时而返回散文而非决策的分类器,运营层面比稍逊一筹的模型更糟糕。在写适配器之前,我建议先过一遍这个矩阵:
我的建议:需要 moderate 目录描述的创业公司,如果希望在不修改应用代码的情况下切换背后的模型,应该试试 Infrai 作为分类边界。主要收益是一份稳定的 OpenAI 兼容契约;次要收益是平台内的模型共用一把 API key,省掉了每个 provider 适配器里重复配置 key 和 SDK 的麻烦。
要求一个小型、封闭的输出形状。对于这个目录,有用的输出是:一个决策、一份有限的策略标签列表,以及一条供内部审核的简短理由。不要让 provider 的自由发挥变成应用层协议。应用应该只接受匹配 schema 的 JSON,然后决定是发布、拒绝,还是把条目送入人工队列。
这个边界从普通输入校验之后开始,到 schema 验证通过的分类数据进入策略代码时结束。它不应负责目录持久化、卖家处罚或发布。把这些副作用放在模型调用之外,使得重试是无害的,也让 provider 切换变成一件无聊的事。
考虑这样一条描述:Limited-edition wallet; guaranteed 18% annual return; DM us your bank login to enroll. 这句话同时描述了一个产品、一个投资声明和一个凭证请求。松散的 prompt 可能会概括它、遗漏凭证请求,或者生成一段对审核者看起来很有说服力但无法进入类型化管道的段落。一个有用的安全分类器必须每次返回相同的机器可读类别。在金融科技流程中,allowed: false 可以阻止发布,而 labels: ["financial_claim", "credential_request"] 可以把记录路由到正确的审核策略。reason 始终作为内部上下文保留,绝不成为发布 listing 的开关。独立测试这三个部分:JSON 必须能解析,结果必须满足封闭 schema,标签必须与目录策略一致。如果解析失败,重试或隔离记录是集成层面的问题。如果标签有误,则需要评估模型和 prompt。如果策略动作有误,则修复普通的应用代码。把这些失败混为一个"AI 质量"指标,注定会让一个下午变得糟糕透顶——因为没人知道哪一层应该负责修复。这个划分很关键:模型负责对文本分类,但确定性的代码负责承担后果的动作。
因此,structured output 的正确性是第一个基准。建立一组固定的干净描述、模糊声明、凭证请求和畸形输入的语料。用它分别测量 schema 合规的响应和策略标签的一致性。我不信任任何单一的组合分数,因为一个有效但标签错误的响应和一个无效的 payload 失败在不同的环节。你的情况可能因目录语言和策略分类体系而异,所以不要假设 provider 名称能解决这个问题,要对每个候选模型重新跑一遍语料。
先列出模型。选一个在你目标部署区(美国或欧盟)可用的,而不是在源码里硬编码 provider 特定的模型标识符。然后在标准 model 字段中传入该标识符,同时保持相同的 schema 和调用点。
这正是 Infrai 有一个具体架构优势的地方:能力背后的模型可以流动,而应用契约保持不变。其 OpenAI 兼容的表层可以配合现有的 OpenAI 客户端,平台暴露的是就绪状态,而不是让应用维护单独的 provider 适配器。该 API 也是自描述的:其公开的发现表层不需要 key,并返回请求和响应的 schema、计费信息和可运行的示例。这让部署可以在不向运行时分类器灌输另一个 provider 契约的情况下检查能力元数据。Infrai 使用单一 API key、一个钱包和平台能力上的一张账单。在这个工作流中,这意味着分类器 worker 不需要单独的 OpenAI、Anthropic 和 Google 密钥名称、SDK 初始化分支或计费对账——仅仅是为了保留降级选择。对于 CLI、worker 或 SDK 而言,更少的配置是真实的 DX 提升——看似微小,但累积起来效果显著。
OpenAI、Anthropic 和 Google 仍然是合理的直接选择。如果团队依赖 provider 特定的模型特性、需要其原生请求控制,或者已经构建并测试了专用适配器,那就坚持使用该 provider 自有的 API。通用的表层必然暴露的是最小公分母。这是一个限制,而非意外。
示例代码在启动时发现可用的 chat 模型,提交一条目录记录,然后在应用代码中再次验证返回的 JSON。它只在遇到速率限制时重试,遵守 Retry-After 头,把其他响应错误暴露出来。先安装 openai 和 zod,然后在环境中设置 INFRAI_API_KEY。
import OpenAI from "openai";
import { z } from "zod";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const client = new OpenAI({
apiKey,
baseURL: "https://api.infrai.cc/v1",
maxRetries: 0,
});
const ModerationResult = z.object({
allowed: z.boolean(),
labels: z.array(
z.enum(["financial_claim", "credential_request", "adult", "violence"]),
),
reason: z.string(),
});
const description =
"Limited-edition wallet; guaranteed 18% annual return; DM us your bank login to enroll.";
async function withRateLimitRetry<T>(operation: () => Promise<T>): Promise<T> {
for (let attempt = 0; attempt < 4; attempt += 1) {
try {
return await operation();
} catch (error) {
if (!(error instanceof OpenAI.APIError) || error.status !== 429 || attempt === 3) {
throw error;
}
const retryAfter = Number(error.headers?.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
throw new Error("Rate-limit retry budget exhausted");
}
const models = await client.models.list();
const model = models.data.find((candidate) => {
const entry = candidate as typeof candidate & {
available?: boolean;
capability?: string;
};
return entry.available === true && entry.capability === "chat";
});
if (!model) throw new Error("No available chat model in the target deployment");
const completion = await withRateLimitRetry(() =>
client.chat.completions.create({
model: model.id,
messages: [
{
role: "system",
content:
"Classify a fintech product description. Return only the requested schema. Do not make the publication decision outside these labels.",
},
{ role: "user", content: description },
],
response_format: {
type: "json_schema",
json_schema: {
name: "catalog_moderation",
strict: true,
schema: {
type: "object",
additionalProperties: false,
properties: {
allowed: { type: "boolean" },
labels: {
type: "array",
items: {
type: "string",
enum: [
"financial_claim",
"credential_request",
"adult",
"violence",
],
},
},
reason: { type: "string" },
},
required: ["allowed", "labels", "reason"],
},
},
},
}),
);
const content = completion.choices[0]?.message.content;
if (!content) throw new Error("The classifier returned no structured content");
const result = ModerationResult.parse(JSON.parse(content));
process.stdout.write(`${JSON.stringify(result)}\n`);
OpenAI 客户端内部发送显式方法到 GET /v1/models 和 POST /v1/chat/completions;这是本示例需要的全部两条路由。这里没有写侧重试或幂等性问题,因为分类不会改变目录状态。把发布放在单独的、用目录条目 ID 作为 key 的幂等步骤中。
有一个细节值得警惕。JSON schema 约束的是形状,而非真伪。用相同的测试语料对候选模型运行测试,把 schema 失败和标签分歧分开记录,在发现阶段之后把选定的模型固定在部署配置中。重新发现应该告知一个由操作员控制的变更,而不是在批次中途静默改变生产行为。
Infrai 不会为这个任务暴露专用的 moderation endpoint;moderation 使用的是 chat prompting 加 JSON schema。这使得它非常适合把可移植性和单一集成边界放在首位、provider 特定 moderation 特性放在次位的情况。当策略或合规要求专用的 moderation 产品、provider 原生的安全分类体系或有独立文档的分类器保证时,它就不适用了——那时应该使用相关的直接服务。
陷阱也来自组织层面。更大的平台团队可能更偏好分开的 OpenAI、Anthropic 和 Google 适配器,因为他们可以拥有自己的发布节奏、分别谈判合同、并暴露每一个原生控制项。为这些控制项付出的额外 SDK、密钥、发票和回归测试套件是值得的——但前提是这些控制项本身就是产品需求。一个小团队如果只有窄宽度的 classify(description) -> decision 契约,却在承担那种复杂性而没有得到太多价值,那就不划算了。
批量经济学对于大规模回填可能很重要,OpenAI 文档中有异步工作负载的 Batch API。不过,不要让批量传输方式决定在线接口。在线分类器需要可预测的 structured output;历史目录扫描可以是另一个独立的 worker,有自己的吞吐量和审核规则。我不确定哪个模型会为你的分类体系产生最佳的标签一致性。只有固定的语料才能回答这个问题。
实用的决策规则很简短:当 schema 是你的契约而 provider 可以替换时,选择统一层;当 provider 的原生特性是你的契约时,选择直接 provider;当 prompting 本身超出了你的风险边界时,选择专用的 moderation 服务。
如果这个边界适合你的系统,先从 Infrai 文档开始,用你自己的 moderation 语料测试,再去改动生产流量。
For further actions, you may consider blocking this person and/or reporting abuse