上一篇讲了怎么把单次调用跑通,这篇往上抬一层:如果一家企业要用三个模型,这层封装该怎么设计。这不是某个产品的说明书,是一套通用的架构模式。
一、调用链路
先看一次调用在系统里经过哪些环节。
企业应用发起一次请求,请求进入聚合层。聚合层完成鉴权与限流判定,把请求转换到目标模型的原生协议,附加上游密钥后转发给模型厂商。模型厂商返回结果,聚合层拿到响应,把厂商特有的结构解析成内部统一表示,再序列化为企业熟悉的格式返回。整条链路上,请求内容原样透传,不落盘、不参与模型训练。
用文字串起来是这样的:
企业应用,携带自己的身份凭证发起调用。
聚合层接入层:识别租户、校验凭证、判定配额。
聚合层路由层:按策略选定目标模型。
聚合层适配层:把统一协议翻译为该模型的原生协议。
模型厂商:执行推理并返回原生响应。
聚合层归一层:把原生响应还原为统一结构。
聚合层计量层:记录本次用量与归属。
企业应用:拿到统一结构的响应。
八个环节里,适配、路由、归一、计量是这一层真正的价值所在。接入和鉴权是任何服务都要做的基础工作,不构成技术含量。
二、五项核心职责
协议转换。各家模型的请求体字段命名、流式分块格式、工具调用结构、系统提示词的位置约定都不一样。聚合层对外暴露一套统一协议,内部维护一张路由表,把统一字段映射到具体厂商的字段名。这张表是整个系统里最容易腐化的地方,因为厂商会迭代接口。
模型路由。按什么规则选模型,常见策略有五种:按业务场景静态绑定,例如摘要走 A 模型、代码走 B 模型;按成本档位路由,高价值任务走高配模型,批量任务走轻量模型;按延迟敏感度路由,实时交互走低延迟路径;按权重灰度,用于新模型上线后的流量切换;按可用性路由,在线模型不可用时自动切换。这几种策略通常叠加使用。
故障转移。见下文第三节,单独展开。
限流。分三层:租户级限流,防止单个客户打爆共享资源;用户级或业务级限流,防止单条业务链路异常放大;模型级限流,保证不打穿上游配额。限流算法上令牌桶适合允许短时突发的场景,漏桶适合要求匀速出队的场景。
计量。每次调用记录四元组:输入 token、输出 token、命中缓存的 token、归属的业务标识。没有这层数据,成本分析无从谈起,且这类数据必须一开始就采集,等需要时再补是补不回来的。
三、难点一:故障转移的触发条件与冷却策略
这是整个聚合层最难写对的部分。写简单了会误切换,写复杂了会漏切换。
先说触发条件。触发切换的不是所有错误,而是特定的几类。可切换的错误:网络层连接失败、读超时、上游返回 5xx、上游返回 429 限流、连接被重置。不可切换的错误:参数校验失败 400、鉴权失败 401、余额或配额耗尽 403、模型名不存在 404、上下文超长 400。后面这几类是请求本身的问题,切换到另一个模型只会把同一个错误重演一遍,白白消耗一次上游配额。
这里有个容易忽略的细节:上下文超长在不同模型上的表现不一样,A 模型超长了,切换到 B 模型可能刚好能装下。所以这类是否纳入可切换,需要按业务决策,默认纳入比默认排除更合理。
核心是计数器与熔断器的组合:
class RouteGuard:
def __init__(self, threshold=5, window_s=60):
self.threshold = threshold # 滑动窗口内失败次数上限
self.window_s = window_s # 统计窗口
self.fails = deque() # 失败时间戳队列
self.opened_at = None # 熔断打开时刻
self.cooldown_s = 120 # 冷却时长
def allow(self) -> bool:
now = time.monotonic()
while self.fails and now - self.fails[0] > self.window_s:
self.fails.popleft()
if self.opened_at is None:
return True
if now - self.opened_at < self.cooldown_s:
return False
# 冷却结束,半开:放一个探针请求进去
self.opened_at = None
return True
def record(self, ok: bool):
now = time.monotonic()
if ok:
self.fails.clear()
return
self.fails.append(now)
if len(self.fails) >= self.threshold and self.opened_at is None:
self.opened_at = now
def on_failure(self):
self.opened_at = time.monotonic()
冷却策略的要点有四个。第一,冷却时长要区分错误类型。429 限流的冷却应该取上游给的 Retry-After 或一个较长的值,几秒钟后猛冲回去只会继续被限;5xx 故障可以用固定 60 到 120 秒;网络超时需要更长的冷却,因为这类问题往往不会立刻自愈。第二,冷却必须递增。同一个模型连续熔断三次,冷却时间应该逐次翻倍并设上限,否则会被反复打脸。第三,冷却结束是半开状态,只放一个探针请求,成功则完全关闭,失败则重新计时并延长冷却。第四,冷却期间不能把流量全部压到备选模型上,备选模型本身也有容量上限,需要在路由层做分散。
第四点引出一个常被忽略的问题:故障转移不能只看错误率,还要看目标模型是否还在配额内。如果备选模型的配额已经耗尽,切过去只是把失败换个地方发生。因此路由决策的输入应该是三元组:健康状态、剩余配额、平均延迟。三者都不可用时,正确做法是快速失败并返回明确错误,而不是无限重试。
四、难点二:多模型返回格式的归一
不同厂商的响应结构差异比请求差异更大。归一的关键是:内部定义一份最小公共表示,厂商特有的信息放进扩展字段,不要污染主体。
统一响应应该收敛到这些字段:id、model、role、content、finish_reason、usage、扩展字段。finish_reason 的映射是最典型的例子,各家取值都不一样,需要一张映射表收敛到有限几个语义:正常结束、达到长度上限、达到最大步数、命中内容过滤、模型侧异常。业务方只应看到这套统一枚举,看到厂商原值就说明归一层漏了。
流式场景的归一更麻烦。需要处理三件事:一是分块边界不可靠,一个字符可能被拆到两个数据块里,必须在聚合层做缓冲再下发,否则企业侧拼出来的文本会出现乱码或重复;二是有的厂商中途会插入心跳或注释行,需要过滤;三是流式中途失败时,已经下发的部分无法撤回,聚合层只能通过错误事件告知客户端,并在 usage 字段标记不完整。这一点要写进接入文档,否则调用方会把不完整统计当成有效数据。
有一个务实的建议:归一层宁可少收敛,也不要错收敛。字段拿不准时放进扩展字段原样透传,比强行映射到一个语义不准的枚举里要好得多。错映射会让业务逻辑静默出错,而这类问题通常在生产环境才会暴露。
五、容量与一致性
聚合层的容量瓶颈不在 CPU,在上游配额。单租户并发上限应与模型配额绑定,而不是给一个固定数字。计量写入需要考虑异步化,同步写库会让计量成为延迟瓶颈,但异步队列必须保证不丢,计量数据的丢失直接影响账单准确性。多实例部署时,健康状态、冷却时间、限流计数都要放共享存储,进程内状态在多副本下会立刻失效。
六、聚合层不做什么
明确边界比描述能力更有价值。聚合层不改变模型能力,不自研模型,不对特定任务的输出质量作保证。它解决的是接入工程问题:多协议收敛、多模型调度、供应连续性、用量可见性。哪类任务该用哪个模型,仍然要企业用自己的业务数据测试后决定。涉及金融、医疗、法律等高风险场景,必须保留人工复核环节。
关于 Token GO
河南词元集策科技有限公司,品牌 Token GO,主营大模型 API 聚合服务,主营方向为聚合封装主流成熟大模型能力,为企业提供可直接调用的服务层,不自研基础大模型。产品体系正在持续完善中,具体能力以官网公布信息为准。
ICP 备案:豫ICP备2026035797号