
AI 判题服务的容错设计模型推理超时后的自动重试与结果合并策略一、当模型迟迟不回判题服务该怎么办线上判题系统最让人头疼的场景是什么不是模型给错答案而是模型压根不给答案。在一个典型的算法训练平台架构里用户提交代码后系统会将代码和题目描述一并发送给大语言模型进行评测。正常流程下模型在 5 到 10 秒内返回分析结果。可总有些时候请求发出去了15 秒过去了30 秒过去了——响应方只返回一个冷冰冰的超时错误。原因可能很复杂模型服务端 GPU 资源瞬时耗尽网络链路在某处发生抖动请求被调度到一台正处于冷启动阶段的推理节点上。无论根因是什么对于判题系统来说结果只有一个用户盯着转圈的加载动画体验断崖式下跌。这就引出了本文要讨论的核心问题如何为 AI 判题服务设计一套健壮的容错机制使得单次模型推理的不可靠性不会传导为整个判题流程的不可靠。二、自动重试不是简单 while 循环提到重试很多人的第一反应是包一层for循环失败就重来。但生产环境的重试机制远比这复杂。首先是退避策略。如果模型服务因为瞬时过载而超时立刻重试大概率还是超时——重试请求只会加剧服务端的压力形成雪崩效应。因此必须引入指数退避首次重试延迟 1 秒之后翻倍为 2 秒、4 秒、8 秒并配合随机抖动避免多个重试请求在同一个时间窗口同时到达。其次是重试上限的设定。理论上可以无限制重试直到成功但实际上用户等待的耐心是有限的。一般将总超时控制在 30 秒以内重试次数不超过 3 次。如果 3 次重试全部失败就触发降级策略。最后是重试的语义安全。如果模型已经接收了请求并正在推理只是响应因网络问题迟迟未到达盲目重试会导致模型侧出现重复计算。判题场景中这不算致命问题——两次推理得到同样的结果也不影响正确性。但对于涉及状态变更的接口就必须考虑幂等性了。三、生产级重试调度器的实现下面给出一个带指数退避和超时控制的重试调度器实现。代码使用 Java 编写实际场景可按语言特性做适配。/** * AI 判题重试调度器 * * 设计要点 * 1. 指数退避 随机抖动避免重试风暴 * 2. 总超时控制防止用户无限等待 * 3. 异常分类处理可重试异常 vs 不可重试异常 */ public class RetryableJudgeScheduler { // 最大重试次数 private static final int MAX_RETRIES 3; // 基础退避时间毫秒 private static final long BASE_BACKOFF_MS 1000L; // 单次请求超时时间毫秒 private static final long REQUEST_TIMEOUT_MS 10000L; // 整体超时时间毫秒 private static final long TOTAL_TIMEOUT_MS 30000L; /** * 带重试的模型推理调用 * * param prompt 判题 prompt * return 模型推理结果重试耗尽后返回空 */ public OptionalJudgeResult callWithRetry(String prompt) { long startTime System.currentTimeMillis(); int retryCount 0; while (retryCount MAX_RETRIES) { // 检查总超时。总超时到了就不再重试直接降级 if (System.currentTimeMillis() - startTime TOTAL_TIMEOUT_MS) { log.warn(判题总超时触发降级。已耗时: {}ms, System.currentTimeMillis() - startTime); return Optional.empty(); } try { // 发起模型推理请求设置单次超时 JudgeResult result modelClient.judge(prompt, REQUEST_TIMEOUT_MS); // 成功返回记录重试次数用于监控 metrics.recordRetryCount(retryCount); return Optional.of(result); } catch (TimeoutException e) { // 超时异常可能是模型侧过载退避后重试 retryCount; if (retryCount MAX_RETRIES) { log.error(判题重试耗尽共重试 {} 次, MAX_RETRIES); break; } // 计算退避时间base * 2^retry 随机抖动 long backoffMs BASE_BACKOFF_MS * (1L retryCount); long jitterMs ThreadLocalRandom.current().nextLong(backoffMs / 2); long sleepMs backoffMs jitterMs; log.info(判题超时第 {} 次重试退避 {}ms, retryCount, sleepMs); Thread.sleep(sleepMs); } catch (NonRetryableException e) { // 不可重试异常如 prompt 格式错误直接返回空 log.error(判题遇到不可重试异常: {}, e.getMessage()); return Optional.empty(); } } return Optional.empty(); } } /** * 自定义异常区分可重试和不可重试的场景 */ class NonRetryableException extends RuntimeException { public NonRetryableException(String message) { super(message); } }这段代码的核心设计思路有两点。第一通过异常类型区分了值得重试和不值得重试的场景。超时可能是临时的重试有意义但 prompt 格式错误是永久性的重试 100 次也没用。第二总超时和单次超时是两个维度的控制——单次超时管的是单次请求的耐心总超时管的是整个判题流程的耐心。四、多模型 Fallback 与结果合并的边界权衡当重试耗尽后最后一个兜底手段是不再死磕一个模型而是并发请求多个模型取最先返回的结果。也就是所谓的竞速模式。这种模式的优势是显而易见的。不同模型对同一道题目的理解速度不同甚至同一模型的不同实例也可能因调度差异产生响应时间波动。并发请求多个目标本质是用冗余资源换取延迟的下限。但代价也不容忽视。首先是成本每多一个并发目标就多一份推理算力的消耗。如果是按 token 计费的商业模型账单会成倍增长。其次是结果一致性两个模型可能给出不同的评分或分析结论。如果一个说这段代码时间复杂度 O(n)另一个说O(n log n)以哪个为准一种务实的做法是竞速模式下先返回的那个结果直接作为最终结果后续结果不再使用。如果对一致性有更高要求可以在后台做异步对比当检测到两个模型结果显著不一致时将题目标记为需人工复核。这里还有一个容易被忽略的设计要点竞速请求的取消。首个结果返回后必须主动取消其余还在进行中的请求否则这些请求会白白消耗算力直到自然完成。取消的实现依赖于底层 HTTP 客户端的请求中断能力。五、总结判题服务的容错设计核心不是保证每次调用都成功——这在大模型服务中是不现实的。真正的目标是用合理的成本让不可靠的底层服务对外表现出可接受的可靠程度。重试机制是基础但需要配合退避、超时和异常分类才能发挥作用。多模型竞速是兜底但要权衡一致性和成本。这些设计思路不只适用于判题场景任何依赖外部不稳定服务的系统都可以借鉴其中的容错模式。