文章主张将模型路由变更视为数据迁移:分类结果必须记录 taxonomy、prompt、route 和 model 版本及来源,不能因切换供应商直接覆盖历史标签。还建议通过明确的晋升规则和弃答状态,隔离格式正确但语义不兼容的输出。
把每次模型变更都视为一次数据迁移:如果使用一个应用凭证,并统一采用 chat-completions 形式的边界能简化 Node.js 服务,那就这样做;但如果没有版本化 schema、来源信息和明确的晋升规则,绝不能让一次路由决策覆盖此前已经接受的分类结果。
这就是简短的答案。用一个 API key 取代多个 provider 集成,可以减少调用方暴露的凭证;但这并不意味着不同模型是等价的,也不意味着可信路由服务不再需要持有上游凭证。真正困难的是:当 prompt、模型和 adapter 发生变化时,如何保持标签含义不变。即使响应能够被顺利解析,它仍然可能与下游查询所依赖的分类体系不兼容。
一旦写入存储,这种错误就会成为持久数据。
对于标签分类工作负载,真正有用的抽象比通用 AI client 更小。定义一条分类命令,其中包含不可变的条目标识符、输入文本或对输入文本的受控引用、分类体系版本以及截止时间类别。定义可接受的结果时,应包含同一个条目标识符、唯一一个允许的标签或明确的弃权结果,以及生成该结果时使用的分类体系、prompt、路由和模型版本。
应用负责管理条目和业务截止时间。路由服务使用应用的单一 key 完成身份验证,根据策略选择 adapter,并将上游凭证保存在彼此隔离的 secret 作用域中。adapter 将通用请求转换成 provider 特定的请求。这些层中的任何一层都无权擅自认定某个未知标签与已知标签“足够接近”。
这个边界是有意设计得很窄。Chat completions 可以作为传输层的请求形式,受 schema 约束的生成也能减少格式错误的响应,但在确定性 validator 完成检查之前,生成的 JSON 仍然是不可信输入。Structured Outputs 文档说明了如何让响应遵循给定的 JSON Schema;但这并不能免除消费者检查条目标识、当前启用的分类体系版本或提交前置条件的责任。
因此,单 key 设计是一种安全与运维选择,而不是语义可移植性的证明。当多个服务端调用方需要共享同一套策略和审计边界时,这种设计很有用。但如果团队没有能力为这个额外服务承担凭证轮换、配额管理、adapter 一致性和审计记录保留,那么它并不适用。在这种情况下,应继续通过本地接口封装直接集成,即使这意味着服务器需要持有多个 key。
可以,前提是 Node.js 发送的是 provider-neutral 命令,并且系统将路由元数据视为需要保存的证据,而不是用完即弃的 telemetry。路由应根据明确声明的属性来选择,例如支持的分类体系、数据区域、输入大小、延迟类别,以及同步执行还是 batch 执行。一旦某次尝试开始,就应锁定其路由。超时也许可以成为发起另一次尝试的理由,但绝不能因此抹去第一次尝试由哪个模型处理的信息。
真正危险的是完成状态不明确的情况。假设尝试 a-1041 已经到达上游服务,但调用方没有收到响应,于是策略立即将 a-1042 发送到另一个地方。两个答案在语法上可能都完全有效,却在某个难以判断的分类边界上产生分歧。如果数据层接受最后到达的那个答案,那么网络时序就成了分类策略。这种设计毫无正当性可言。
为逻辑命令使用 idempotency key,并为每次尝试分配不同的 ID。以 append-only 方式保存候选结果。晋升操作应基于当前分类体系和结果版本进行条件写入;发生冲突时,应将其视为正常的并发结果并上报,而不是通过盲目覆盖来“修复”。一小段代码就足以说明这个边界;生产实现可以使用事务数据库、compare-and-swap 对象,或其他具备等价条件写入语义的存储系统。
from dataclasses import dataclass
from typing import Literal
Label = Literal["billing", "defect", "feature", "other", "abstain"]
@dataclass(frozen=True)
class Candidate:
item_id: str
command_id: str
attempt_id: str
label: Label
taxonomy_version: str
prompt_version: str
route_id: str
model_id: str
def promote(
candidate: Candidate,
*,
expected_item_id: str,
active_taxonomy: str,
current_result_version: int,
) -> dict[str, object]:
if candidate.item_id != expected_item_id:
raise ValueError("IDENTITY_MISMATCH")
if candidate.taxonomy_version != active_taxonomy:
raise ValueError("TAXONOMY_VERSION_REJECTED")
if candidate.label == "abstain":
raise ValueError("ABSTENTION_REQUIRES_REVIEW")
return {
"condition": {"result_version": current_result_version},
"next_result_version": current_result_version + 1,
"candidate": candidate,
}
这些符号化错误是有意设计的。TAXONOMY_VERSION_REJECTED 可以在不记录源文本的情况下,告诉运维人员究竟是哪一项契约失败了;而一个笼统的 invalid payload 会把身份、schema 和策略错误混在一起,变成一个毫无用处的分类桶。我特意没有在示例中加入 SDK 调用,因为 wire protocol 才是可替换的部分,提交不变量并不是。
不要自动重试所有失败。如果连接在请求被接受之前中断,那么在命令截止时间内或许可以重试。身份验证被拒绝,需要修复 secret 或配置。反复发送同一个请求,并不能让不符合 schema 的答案变得安全。内容策略拒绝或弃权是一种结果,需要明确的产品规则来处理。应在 adapter 边界统一这些类别;如果 provider 提供了 correlation identifier,则将其保存在受限的 telemetry 中;同时不要让原始输入进入常规日志。
只有在模型路由通过一套固定且经过审核的评估集后,才值得纳入考虑。这套评估集必须能够代表已存储的数据总体,包括常见情况、少数类别、模糊边界、空内容、Unicode、超大输入、嵌入文档中的指令式文本,以及预期应该弃权的案例。结果需要按照标签和 cohort 分组。一项总体准确率指标可能掩盖这样的事实:某条路由虽然改善了主流类别,却损害了会触发高成本工作流的稀有类别。
比较 schema-valid rate、abstention rate、各类别的 confusion、延迟分布,以及每个已接受结果按用量计算的成本。成本应当进入决策,但它不能抵消标签污染或隐私不匹配的问题。除非业务已经明确记录每种错误类型对应的损失,否则我不确定使用单一加权分数是否站得住脚;一组指标向量虽然不够方便,却更加诚实。
Batch 执行应拥有一套独立的契约。Batch API 指南介绍了如何通过上传请求文件和设置完成时间窗口来进行异步处理,因此它对评估运行和 backfill 的适用方式,与交互式分类请求并不相同。应保留一个自定义标识符,将每条 batch 记录映射回不可变命令,而且不能假设返回记录可以按照请求顺序提交。相比之下,交互式流量需要受限的截止时间、取消行为,以及在不存在已接受分类结果时应向调用方展示什么的明确决策。
测试 adapter 时,应关注行为,而不只是请求兼容性。conformance suite 应覆盖 malformed JSON、额外字段、标识符缺失、分类体系之外的标签、旧 schema 版本、拒绝、截止时间到期、重复投递,以及两个有效候选结果争夺晋升资格的情况。还要验证常规 telemetry 中不存在敏感文本。然后检查模型和 adapter 文档,确认 schema enforcement、token accounting、regional processing、retention 和 batch semantics;相似的封装形式并不能证明它们提供了相似的保证。
这比检查 message.content 是否包含某个标签要耗时得多。
但它也能发现真正重要的故障。
有三种部署形态可以向 Node.js 暴露稳定接口。没有任何一种能够在所有场景中胜出。
应根据由谁承担故障处理责任来选择。如果没有人负责 internal gateway 的 on-call 路径,那么它就是一笔糟糕的投入。如果十个服务都要分别实现 secret 轮换和 fallback,那么 in-process adapter 也是一笔糟糕的投入。如果策略要求特定的部署区域、retention 行为或审计细节,而 managed gateway 的契约无法提供保证,那么它同样不适用;此时应继续使用直接受控的路由。
不要声称切换模型不需要修改任何代码。稳定的请求封装可以避免重写应用,但分类体系变更仍然需要进行数据迁移;新 prompt 可能会改变类别边界;provider 特有的能力也可能要求扩展 adapter。更诚实的承诺应该更克制:对于已经符合并通过测试的分类契约的路由,业务代码无需更改。
首先为当前分类体系和 prompt 添加版本,然后从合法且受访问控制的样本中建立一套经过审核的评估集。加入候选 adapter 并运行 conformance test。只有在数据策略允许的情况下,才对具有生产环境特征的流量执行 shadow;并将候选输出写入 append-only 的评估存储,而不是 canonical 标签列。按照 cohort 比较分歧,对代价高昂的分歧进行审核,然后在一个经过刻意限制的流量切片中执行 canary promotion。
在逐步扩大范围期间,监控 route、model、schema、prompt、attempt、normalized outcome、latency 和 usage,但不要把原始敏感文本写入通用日志。在新路由通过约定的观察窗口之前,保留此前的晋升策略。回滚改变的是允许晋升哪个候选结果;它不会删除证据,也不会通过重新执行未版本化的 prompt 来恢复标签。
最终验收测试简单得令人愉快:Node.js 调用方仍然使用同一个 application key 提交同一条命令;每个已存储标签都标明创建它的契约和路由;重复尝试无法通过竞争写入 canonical 字段;运维人员无需查看原始文本,就能根据错误类别解释拒绝原因。只有当模型变更变得可控、可观察时,模型路由才算成功,而不是几个 adapter 恰好使用了相同的 JSON 标点符号。
OpenAI,《Batch API 指南》
OpenAI,《Structured Outputs 指南》
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。