三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从难度分类到状态管理:构建健壮AI模型路由系统的工程实践

从难度分类到状态管理:构建健壮AI模型路由系统的工程实践

1. 从“难度分类”到“状态管理”:重新理解生产模型路由

最近在跟一个做推荐系统的朋友聊天,他提到一个让我印象深刻的观点:他们团队在线上做模型路由(Model Routing)时,发现最大的挑战根本不是大家常说的“难度分类”——比如根据请求的复杂度,把简单请求分给轻量模型,复杂请求分给大模型。这个认知上的转变,恰恰是很多团队从“能用”走向“稳定可靠”的关键一步。

所谓“难度分类”,听起来很合理,但它本质上是一个静态的、基于单次请求的决策。它假设我们能在请求到达的瞬间,就完美判断出哪个模型最合适,并且这个决策不会带来任何后续的连锁反应。但在真实的生产环境里,模型路由更像是一个动态的、有状态的“交通管制系统”。你不仅要考虑当前这辆“车”(请求)要去哪、有多重,还得考虑整个路网的实时拥堵情况(模型负载)、之前有没有发生过交通事故(模型异常)、以及这辆车如果中途抛锚了该怎么处理(请求失败与重试)。

这背后,其实是两个核心概念的升级:从“硬约束可行域”到“状态检查点”。前者是设定规则,告诉你“什么能做,什么不能做”;后者是建立监控和恢复机制,确保“在规则内,即使出了问题也能兜底”。而要实现后者,就离不开“幂等键”和“副作用账本”这两个听起来有点学术,但实操中至关重要的工具。今天,我就结合自己的踩坑经验,来拆解一下这套思路的落地过程。

2. 硬约束可行域:不只是规则,更是安全边界

当我们谈论“硬约束可行域”时,很多人第一反应是配置文件里的一堆参数:max_latency: 100ms,qps_limit: 1000,model_version: v2。这些当然是约束,但如果只把它们当成静态规则,就浪费了其真正的价值。硬约束的本质,是为系统的运行时行为划出一个明确的“安全操作空间”。任何决策,都必须在这个空间内进行。

2.1 约束的三层维度:资源、性能与业务

在我的实践中,我会把硬约束分为三个层次,层层递进:

  1. 资源层约束:这是最基础的物理限制。比如:

    • GPU内存:单个推理实例能加载的最大模型参数规模。
    • CPU/内存:预处理、后处理逻辑的资源消耗上限。
    • 网络带宽:模型服务节点与特征服务、数据库之间的数据传输限制。
    • 这部分约束通常是固定的,由基础设施决定。路由系统需要知晓每个模型服务后端的资源画像。
  2. 性能层约束:这是面向SLA(服务等级协议)的约束,直接关系到用户体验和成本。

    • 延迟(Latency):这是最常见的约束,例如P99延迟必须低于200毫秒。但这里有个关键点:延迟约束不是单个模型的,而是整个路由链路的。你需要考虑网络开销、序列化/反序列化时间、甚至多个模型串行调用(如召回+精排)的总时间。
    • 吞吐量(Throughput/QPS):单个模型实例或整个集群能承受的每秒查询率。这里容易踩的坑是,测试环境的压测QPS往往高于生产环境,因为生产环境有更多干扰因素(如其他混部服务、网络抖动)。
    • 可用性(Availability):要求服务成功率不低于99.9%。这要求路由系统能快速感知下游模型服务的健康状态。
  3. 业务层约束:这是最灵活也最容易忽略的一层,它把技术指标和业务目标绑定。

    • 成本约束:例如,“对于流量峰值期的低价值用户请求,优先使用成本低于X元/千次的模型”。这需要路由策略能理解每次请求的预估业务价值。
    • 效果约束:例如,“对于VIP用户,必须使用A/B测试中胜出的最新模型,即使它的延迟更高”。这要求路由策略能获取用户分层和实验配置信息。
    • 合规约束:例如,“某些地域的请求,数据不得流出境,必须路由到本地部署的特定模型”。

把这些约束整合起来,就形成了一个多维度的“可行域”。一个路由决策是否有效,就看它对应的资源消耗、性能指标和业务结果,是否同时落在这个多维空间的内部。

2.2 动态可行域与实时决策

静态配置的约束是死的,但生产环境是活的。因此,“可行域”也必须是动态的。举个例子,你的延迟约束是200ms。正常情况下,模型A平均耗时50ms,模型B平均耗时150ms,两者都在可行域内。但当模型B所在的物理机遭遇网络波动,其P99延迟突然飙升到500ms时,对于一个新的请求,模型B就从可行域内“掉出去”了。

这就要求路由系统有一个实时更新的、基于遥测数据(Telemetry)的约束视图。我们当时的做法是:

  • 每个模型服务实例都暴露关键指标(延迟、错误率、负载)。
  • 路由层(如Envoy Sidecar或自研的路由代理)以高频(如每秒)拉取这些指标。
  • 路由决策算法在计算时,使用的不是配置的标称值,而是经过平滑处理(如EWMA)后的实时观测值。
  • 对于像成本这样的业务约束,则需要从业务逻辑层实时获取或计算。

这样,每次路由决策都是在最新的“动态可行域”中做出的,极大地提升了系统的自适应能力。

3. 状态检查点:为不确定性装上“安全带”

硬约束划定了安全区,但无法防止所有意外。模型推理本身可能因数值不稳定而崩溃;网络可能瞬间闪断;依赖的特征服务可能超时。状态检查点的核心思想,就是在关键操作步骤前后埋点,记录下足够的信息,使得当故障发生时,系统能够明确知道“故障点在哪里”,以及“如何安全地重试或回退”

3.1 检查点应该记录什么?

不是所有信息都值得记录。检查点数据需要精炼,足以定位问题和恢复现场。通常包括:

  • 请求唯一标识(Request ID):贯穿整个调用链。
  • 路由决策结果:最终选择了哪个模型、哪个实例、决策依据(如打分)是什么。
  • 关键输入快照:不是完整的特征数据(可能很大),而是能唯一确定本次计算“状态”的哈希值或关键特征子集。例如,用户ID、物品ID、场景ID的拼接哈希。
  • 系统状态:决策时刻,各候选模型的实时指标(延迟、错误率)。
  • 时间戳:进入路由逻辑、做出决策、开始调用下游模型等关键时刻。

3.2 检查点的触发与消费

检查点不应是简单的日志打印,而应是一个轻量级的事件流。我们的实现方式是:

  1. 异步写入:在路由决策和发起模型调用的关键步骤,将检查点数据异步写入一个高性能的中间件(如Redis Stream或Kafka)。绝对不要同步写入数据库或文件,这会极大增加请求延迟。
  2. 统一消费:由一个独立的消费者服务来消费这些检查点事件。它的职责包括:
    • 聚合分析:实时计算各模型、各实例的健康度,反馈给路由决策模块,用于更新“动态可行域”。
    • 故障诊断:当某个请求失败时,能通过Request ID快速关联到所有相关的检查点,重现故障现场。
    • 审计与复盘:定期分析路由决策的合理性,比如有多少次决策因为“模型B延迟突增”而切换到了效果稍差的模型A,这种切换是否合理。

通过状态检查点,路由系统就从“开环控制”变成了“闭环控制”,具备了自我观察和反馈调整的能力。

4. 幂等键:应对重试乱局的“定海神针”

有了状态检查点,我们知道问题出在哪。接下来就要解决:出错了怎么办?最直接的想法是重试。但重试是生产环境的一大“毒药”,如果处理不当,会导致重复扣费、重复推送、数据不一致等严重问题。幂等键(Idempotency Key)就是确保“同一操作执行多次,效果与执行一次相同”的关键

4.1 为什么模型路由需要幂等性?

假设一个用户请求生成一张图片,路由系统将其发给了模型服务A,但由于网络超时,路由层没有收到响应。此时,常见的重试策略可能会将同一个请求再次发给模型服务A,或者根据负载均衡策略发给模型服务B。

  • 场景一:模型服务A实际上已经处理完成,生成了图片并保存了结果。重试导致同一张图片被生成两次,浪费计算资源。
  • 场景二:重试发给了模型服务B,用户最终收到了图片,但无法确定是A还是B生成的,给效果归因和计费带来混乱。
  • 场景三:如果这个请求涉及数据库更新(如扣除积分),那么重试可能导致重复扣费。

因此,我们必须让整个“路由+模型调用”链具备幂等性。

4.2 幂等键的设计与传递

一个有效的幂等键方案需要上下游协同:

  1. 生成:最好由最上游的客户端或网关生成一个全局唯一的ID(如UUID),作为本次业务请求的幂等键。如果客户端无法生成,则由网关在第一次收到请求时生成。
  2. 传递:这个幂等键必须随着请求头(如X-Idempotency-Key)贯穿整个调用链:网关 -> 路由层 -> 模型服务 -> 数据库/缓存。
  3. 使用
    • 路由层:在发起对下游模型服务的调用前,先将(幂等键, 目标模型)的组合写入一个分布式缓存(如Redis),并设置一个合理的过期时间(略大于模型最大超时时间)。如果写入失败(键已存在),说明针对这个幂等键的相同模型调用已经发起过,此时应直接查询之前调用的结果,而不是发起新调用。
    • 模型服务:在执行业务逻辑(尤其是写操作)前,同样检查幂等键。如果该幂等键对应的业务结果已经存在(例如图片已生成并存储),则直接返回已有结果,跳过计算过程。
  4. 存储结果:无论是路由层还是模型服务,在处理成功后,都应将幂等键 -> 处理结果的映射关系存入缓存(可设置更长TTL),供后续可能的重复请求查询。

这套机制确保了无论网络如何波动、重试多少次,对于同一个业务请求,至多只有一次有效的模型计算和业务副作用发生。

5. 副作用账本:分布式场景下的“事务日志”

幂等键解决了“重复执行”的问题,但还有一个更棘手的问题:部分成功(Partial Success)。在分布式模型路由中,一个用户请求可能触发多个模型的调用(比如并行调用多个模型取最优结果,或先召回后精排的流水线)。如果其中部分调用成功,部分失败,系统状态就会不一致。

副作用账本(Side Effect Ledger)就是为了记录和协调这些分布式操作而设计的。你可以把它理解为一个微型的、针对业务操作的“事务日志”。

5.1 账本记录什么?

账本中的每一条记录,对应一个有业务副作用的操作单元。对于模型路由来说,副作用可能包括:

  • 调用计费接口,扣除一次模型调用费用。
  • 向消息队列发送一个事件,通知下游系统“图片已生成”。
  • 更新数据库中的用户状态。
  • 向缓存中写入模型的推理结果。

账本记录的核心字段:

  • 账本ID (Ledger ID):关联一个主请求或一个会话。
  • 操作ID (Op ID):本次副作用操作的唯一标识,通常与幂等键结合使用。
  • 操作类型:如CHARGE,NOTIFY,UPDATE
  • 操作状态PENDING(待执行),EXECUTING(执行中),SUCCEEDED(成功),FAILED(失败)。
  • 操作内容:序列化的请求参数。
  • 操作结果:序列化的响应结果或错误信息。
  • 创建时间与更新时间

5.2 基于账本的补偿与最终一致性

有了副作用账本,处理故障的流程就变得清晰和可靠:

  1. 预写账本:在执行任何一个有副作用的操作之前,先在账本中插入一条状态为PENDING的记录。这是一个“预提交”操作。如果连账本都写不进去,说明系统严重异常,应直接失败,不执行任何副作用。
  2. 执行与更新:然后执行实际操作(如调用计费API)。根据执行结果,将账本记录更新为SUCCEEDEDFAILED
  3. 定期核对与补偿:启动一个后台的“对账”服务,定期扫描账本。
    • 扫描长时间处于EXECUTING状态的操作(可能由于进程崩溃导致未更新状态)。
    • 扫描处于PENDING状态但早已超过超时时间的操作。
    • 对于这些“悬而未决”的操作,根据其操作类型和内容,执行补偿逻辑
      • 如果是CHARGE操作,去向计费系统查询该幂等键是否已扣费。如果已扣费,则将账本状态更新为SUCCEEDED;如果未扣费,则尝试重试或标记为FAILED,并触发业务告警。
      • 如果是NOTIFY操作,重新向消息队列发送消息(消息队列本身应具备幂等性)。

通过这套“预写日志 + 后台对账”的机制,即使面对进程崩溃、网络分区等极端情况,我们也能最大限度地保证各个分布式副作用的最终一致性,避免资损或数据混乱。

6. 实战串联:一个完整的请求生命周期

让我们把一个用户请求流经这套增强型路由系统的完整过程串起来看。

假设一个用户请求生成故事续写:

  1. 请求入口:网关收到请求,生成Request-ID: R123Idempotency-Key: IK456
  2. 路由决策
    • 路由服务收到请求,携带R123IK456
    • 查询实时“动态可行域”:模型A(小模型)延迟30ms,成本低;模型B(大模型)延迟120ms,成本高,但效果更好。
    • 根据业务规则(用户为VIP),决策引擎决定选用模型B。
    • 写入检查点:异步记录{R123, decision: model-B, reason: vip_user, timestamp: T1}
  3. 幂等检查与调用
    • 路由服务尝试在Redis中设置route:IK456:model-B。如果设置成功,继续;如果键已存在,则直接读取之前缓存的结果并返回。
    • 向模型B的实例发起调用,在请求头中传递X-Idempotency-Key: IK456
  4. 模型服务处理
    • 模型服务B收到请求,看到IK456
    • 它先检查自己的缓存或数据库,看IK456是否已处理过。如果是,则直接返回历史结果。
    • 如果没有,开始推理。推理过程中,需要调用计费服务扣费。
    • 预写副作用账本:在调用计费前,先插入记录{LedgerID: L789, OpID: IK456_CHARGE, Type: CHARGE, Status: PENDING, ...}
    • 调用计费服务成功,更新账本记录状态为SUCCEEDED
    • 推理完成,生成故事文本。
    • 将结果{IK456 -> story_text}缓存起来,并返回给路由层。
  5. 响应与清理
    • 路由层收到结果,返回给用户。
    • 将最终结果也关联IK456缓存,并清理路由层的临时幂等键(设置较短TTL,由Redis自动过期即可)。
  6. 故障场景处理
    • 场景A:路由层调用模型B超时
      • 路由层未收到响应,但幂等键route:IK456:model-B已存在。
      • 触发重试逻辑时,由于键存在,不会发起新调用,而是去查询模型服务B是否缓存了结果(这里需要一个查询结果的反查机制,或者等待模型B的异步通知)。
      • 同时,检查点记录了超时事件,可用于后续分析模型B的网络问题。
    • 场景B:模型B扣费后崩溃,未返回结果
      • 路由层超时,重试时因幂等键存在,不会触发新计算。
      • 模型B重启后,其本地缓存可能丢失,但账本中IK456_CHARGE的状态是SUCCEEDED
      • 后台对账服务扫描到IK456对应的故事文本结果缺失,但扣费已成功。这可能触发告警,由人工或自动脚本根据日志和输入哈希,决定是否重新计算或补偿用户。

整个流程下来,虽然系统复杂度增加了,但换来的是面对生产环境各种不确定性时的从容与稳定。从简单的“if-else”式难度分类,演进到由硬约束、状态检查、幂等键和副作用账本共同构筑的健壮体系,这正是工程化解决AI系统上线问题的核心所在。这套模式不仅适用于模型路由,对于任何需要做决策、有状态、涉及分布式调用的系统,都有很高的参考价值。

← 返回列表