消灭并发重复调用:基于 Agent 调用 LLM 的分布式 Single-Flight 实战

🌟 背景:被浪费的昂贵 Token

在构建 AI Agent 应用时,LLM 的 API 接口无疑是系统中最宝贵、最昂贵的资源。不仅因为按 Token 计费的成本高昂,更因为其响应延迟大、并发吞吐量受限(RPM/TPM 限制)。

在实际生产环境中,我们经常会遇到一种让人头疼的**"并发重复请求"**现象:

  1. 评分请求:同一个考生的作答,由于前端重试或网关重试,瞬间打出 5 个相同的评分请求。
  2. 追问生成:同一个上下文,多个 Agent 线程同时决定生成追问,导致 3 个线程同时调用 LLM。
  3. 简历抽题:同一份简历 ID,短时间内被多个下游服务并发请求抽题。

如果这些请求打到了同一个 Agent 实例,甚至更糟------打到了不同的 Agent 实例(分布式环境),传统的做法是每个线程各自为战,发起独立的 LLM 调用。

后果是什么?

  • 💸 成本成倍增加:原本只需 1 次调用的开销,变成了 N 次。
  • 🐢 系统被拖垮:并发骤增导致 LLM API 限流,正常请求也被阻塞。
  • 🎲 结果不一致:相同输入可能得到不同的 LLM 输出,给业务逻辑带来隐患。

为了解决这个问题,业内有一个成熟的模式------Single-Flight(合并请求/单飞模式)

💡 核心概念:从单机到分布式

1. 什么是 Single-Flight?

Single-Flight 的核心思想非常简单:当多个相同的请求并发到达时,只让第一个请求真正去执行,其他请求"搭便车"等待第一个请求的结果,然后共享这个结果。

就像多个人同时按电梯,只有第一个人的按电梯动作有效,其他人不需要再按,但大家都能坐上同一部电梯。

2. 单体 Single-Flight vs 分布式 Single-Flight

特性 单体 Single-Flight 分布式 Single-Flight
作用范围 单个 JVM / 单个进程内 整个分布式集群 / 多个 Pod 间
底层存储 本地内存 (如 ConcurrentHashMap + CompletableFuture) 分布式缓存 (如 Redis)
适用场景 单机部署,无状态服务的前置过滤 微服务架构,K8s 多实例部署
解决痛点 单机内的线程并发重复 跨节点的网络请求并发重复

⚠️ 为什么单体版不够用?

现代 AI Agent 服务几乎都是无状态、多实例部署的。如果只做单机 Single-Flight,请求 A 打到 Pod 1,请求 B 打到 Pod 2,它们在本地内存中互不可见,依然会发起两次重复的 LLM 调用。因此,对于 Agent 调用 LLM 的场景,必须实现分布式 Single-Flight。

🏗️ 架构设计:分布式 Single-Flight 如何工作?

我们借助 Redis 作为分布式协调中心,实现跨实例的请求合并。核心流程分为三个阶段:请求拦截与标记执行与等待结果广播与销毁

关键设计点:

  1. 请求指纹:如何判断两个请求是否"相同"?必须对业务参数(如:Prompt、模型名、Temperature等)进行一致性 Hash,生成唯一的 Request Key。
  2. Leader 选举 :利用 Redis 的 SETNX (Set If Not Exists) 指令,谁先设置成功,谁就是真正去调 LLM 的 Leader。
  3. 结果传递 :Follower 如何拿到结果?有两种主流方案:
    • 方案一:Pub/Sub(发布订阅)。Leader 完成后通过 Redis Pub/Sub 广播结果。优点是实时性高;缺点是 Pub/Sub 不持久化,如果 Follower 在广播瞬间断线会丢失结果。
    • 方案二:短暂缓存+轮询 。Leader 完成后将结果存入 Redis(TTL 设短,如 10s),Follower 发现 SETNX 失败后,循环短暂休眠去 Redis GET 结果。这种方式更稳定,推荐在工程中使用。

💻 代码实战:基于 Redis 的分布式 Single-Flight

这里采用 Java + Spring Boot + Redisson 演示(基于短暂缓存+等待机制,逻辑更健壮),其他语言原理相通。

1. 定义请求指纹工具类

java 复制代码
public class LLMRequestKeyUtil {
    
    /**
     * 生成 LLM 请求的唯一指纹
     * 必须包含所有影响 LLM 输出的核心参数
     */
    public static String generateKey(LLMRequest request) {
        String rawKey = request.getModel() + ":" + 
                        request.getPrompt() + ":" + 
                        request.getTemperature() + ":" + 
                        request.getTopP();
        // 使用 MD5 或 SHA256 缩短 Key 长度
        return DigestUtils.md5Hex(rawKey);
    }
}

2. 核心拦截器:DistributedSingleFlight

java 复制代码
@Component
public class DistributedSingleFlight {

    @Autowired
    private RedissonClient redissonClient;

    // 等待 LLM 响应的轮询间隔
    private static final long POLLING_INTERVAL_MS = 100;
    // 等待超时时间(防止 Leader 异常宕机导致 Follower 死等)
    private static final long WAIT_TIMEOUT_MS = 30000; 

    public LLMResponse execute(LLMRequest request, Supplier<LLMResponse> llmInvoker) {
        String key = "llm:flight:" + LLMRequestKeyUtil.generateKey(request);
        String resultKey = "llm:result:" + LLMRequestKeyUtil.generateKey(request);
        
        RLock lock = redissonClient.getLock(key);
        
        try {
            // 尝试获取分布式锁,非阻塞,立即返回
            boolean isLeader = lock.tryLock(0, 60, TimeUnit.SECONDS); // 60s为锁自动过期时间(防死锁)
            
            if (isLeader) {
                // ===== 我是 Leader,真正去调 LLM =====
                try {
                    LLMResponse response = llmInvoker.get();
                    // 将结果短暂缓存到 Redis,供 Follower 读取
                    redissonClient.getBucket(resultKey).set(response, 10, TimeUnit.SECONDS);
                    return response;
                } catch (Exception e) {
                    // 调用异常,也要存入异常信息,别让 Follower 死等
                    redissonClient.getBucket(resultKey).set(e, 10, TimeUnit.SECONDS);
                    throw e;
                } finally {
                    // 释放锁,允许后续相同请求进来
                    lock.unlock();
                }
            } else {
                // ===== 我是 Follower,等待 Leader 的结果 =====
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - startTime < WAIT_TIMEOUT_MS) {
                    // 轮询检查结果是否已放入 Redis
                    Object result = redissonClient.getBucket(resultKey).get();
                    if (result != null) {
                        if (result instanceof Exception) {
                            throw (Exception) result; // 业务异常抛出
                        }
                        return (LLMResponse) result; // 拿到结果,搭便车成功!
                    }
                    // 稍作休眠,避免 CPU 空转
                    Thread.sleep(POLLING_INTERVAL_MS);
                }
                throw new RuntimeException("等待 LLM 结果超时");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("线程被中断", e);
        }
    }
}

3.在 Agent 业务逻辑中使用

java 复制代码
@Service
public class AgentScoringService {

    @Autowired
    private DistributedSingleFlight singleFlight;
    @Autowired
    private LLMClient llmClient;

    public ScoreResult scoreAnswer(String userId, String question, String answer) {
        LLMRequest request = new LLMRequest("gpt-4", buildPrompt(question, answer), 0.1, 1.0);
        
        // 💡 魔法在这里:用 SingleFlight 包装 LLM 调用逻辑
        LLMResponse response = singleFlight.execute(request, () -> {
            // 只有 Leader 线程会执行到这里
            return llmClient.call(request); 
        });

        return parseScore(response);
    }
}

🚨 生产环境避坑指南

在将分布式 Single-Flight 推向生产环境时,有几个关键点需要特别注意:

  1. 指纹必须绝对准确
    • 不同的业务意图,哪怕只差一个标点符号,也不能合并。一定要仔细盘点 LLM Request 中的所有参数,千万不要把流式输出的流对象放入指纹计算
  2. 死锁与超时控制
    • 如果 Leader 节点在调用 LLM 时突然 OOM 或网络断开,必须保证锁能自动释放(Redisson 的看门狗机制或设置合理的 TTL)。
    • Follower 不能无限期等待,必须设置 WAIT_TIMEOUT_MS,超时后降级为直接调用 LLM(容错底线)。
  3. 流式响应(SSE/WebSocket)的复杂性
    • 如果你的 Agent 对外提供流式输出(Token 一个个蹦出来),Single-Flight 的实现难度会剧增。Leader 需要将 LLM 的流式结果拆包、存入 Redis 队列(如 Stream),Follower 从队列中消费。如果流式场景并发不高,建议绕开 Single-Flight,仅在非流式场景使用。
  4. 熔断与降级
    • SingleFlight 本质上是让多个请求共享一个出口,如果这个出口(LLM调用)挂了,所有等待的请求都会失败,产生局部雪崩效应。因此,LLM Client 必须配置合理的熔断降级策略。

🎉 总结

在 AI 接口越来越昂贵的今天,分布式 Single-Flight 是一种投入产出比极高的架构模式。

通过 Redis 的分布式锁和短暂缓存机制,我们成功地将并发重复的 LLM 调用合并为一次,不仅为公司省下了大笔 Token 费用,更有效缓解了 LLM API 的并发压力,提升了系统的整体稳定性。

如果你的 AI 业务也面临类似痛点,赶紧把这套方案安排上吧!🚀

相关推荐
专注VB编程开发20年6 小时前
Python 的 C 扩展,本质上就是“去中心化的 COM”
java·服务器·开发语言·ide·python
JAVA社区6 小时前
Java进阶全套教程(七)—— Redis超详细实战详解
java·linux·开发语言·redis·面试·职场和发展
porschev6 小时前
Hermes Edu Skills 从 170 到 188:一次中文教育 Agent Skill Pack 的工程化升级
agent·ai agent·ai教育·openclaw·hermes agent skills
皮卡祺q6 小时前
【算法-0】背包问题(三维+二维)
java·javascript·算法
qq_401700416 小时前
Qt 多线程编程
开发语言·qt
whuhewei6 小时前
手写Promise
开发语言·javascript·ecmascript
心中有国也有家6 小时前
ascend-boost-comm:一次写完,到处复用——算子公共平台的 M×N 哲学
人工智能·经验分享·笔记·分布式·算法
武雄(小星Ai)6 小时前
AI CLI 三巨头横评:Claude Code vs Codex CLI vs Gemini CLI(2026实测)
人工智能·aigc·agent
AI科技星6 小时前
空间圆柱螺旋运动第一性原理终极推导·证明·核验·全量纲闭环
开发语言·人工智能·算法·计算机视觉·量子计算