
Agent 接口在高并发下确实比普通接口更考验后端架构。普通接口通常是“请求-响应”模式几十毫秒返回Agent 接口走的是多轮推理、工具调用、上下文拼接的链路一个请求可能占用几秒甚至几十秒期间还会扇出调用多个下游服务。同样 100 并发进来普通接口可能只是排队变慢Agent 接口可能直接把线程池打满接着把模型服务、数据库、外部 API 一起拖垮。这就是雪崩的起点。要稳住 Agent 服务不能只靠加机器。本次要聊的是一套四层防护设计缓存、限流、负载均衡、熔断按顺序挡住无效流量、突发流量、故障节点和下游异常。这四层不是各自独立而是串成一条链路每层解决不同阶段的问题。下面直接展开先把整体架构讲清楚再逐层给代码和配置。1. 为什么 Agent 接口在高并发下比普通接口更危险1.1 Agent 请求的工作特征先看一个普通商品查询接口到数据库按 ID 查一次构造 JSON 返回耗时 20ms 到 100ms线程占用很短。如果用 200 核的机器部署理论上能支撑很大的并发。Agent 接口完全不一样单次请求要调用大模型多次每次生成文本耗时 1 秒到 10 秒整体链路可能 30 秒以上。请求期间持有工作线程长耗时请求会快速占满 Tomcat 的线程池。一个 Agent 任务可能调用多个工具工具再回调模型一个用户请求等于在系统内放大成几十次内部调用。上下文字符串会越来越大每次重放给模型都会成倍增加计算时间和 token 消耗。输出常常是流式或者长连接网关和负载均衡不能按普通短连接处理。这意味着Agent 服务的“并发上限”不只是看 QPS更要看同时在途请求数和线程/内存占用。1.2 雪崩是怎么发生的一次典型雪崩路径是这样的某个 Agent 服务上线了新 Prompt 模板推理时间变长单请求耗时从 3 秒涨到 8 秒。线程池承接能力下降新请求开始排队。排队的请求超时用户不断重试。重试流量再次灌进服务线程池彻底打满。服务内部调用数据库和外部工具 API这些下游也开始超时。下游打满后其他依赖它们的服务也被拖垮。从单体服务角度问题表现为超时和拒绝从全链路角度就是局部故障被传播成系统级雪崩。防止雪崩的核心思路是在流量进入系统的每道关卡上提前丢弃或降级一部分请求。1.3 四层防护的整体思路四层防护与雪崩阶段的对应关系如下防护层解决什么问题生效位置典型手段缓存层减少重复计算和重复调用模型进入业务逻辑前结果缓存、语义缓存、上下文缓存限流层控制进入系统的流量拒绝超出容量部分网关和实例入口令牌桶、滑动窗口、并发数限流负载均衡层把请求分散到健康实例避免单点过载网关、注册中心最少连接数、会话保持、健康检查熔断层下游异常时快速失败防止故障传播调用下游时熔断器、降级回调、超时控制四层不是替换关系。缓存挡不住第一次请求的突发限流放行的请求也可能遇到下游抖动负载均衡无法处理坏节点一直返回错误的情况熔断是最后的兜底。下面逐层讲。2. Agent 高并发场景下缓存层设计2.1 缓存不止是 Redis KV普通接口的缓存一般直接key请求参数value响应结果命中就返回。Agent 场景缓存更复杂至少可以拆成下面五类缓存类型缓存内容命中效果完整答案缓存用户问题和最终回复完全跳过 Agent 链路中间推理缓存当前会话的思考过程和工具调用结果多轮对话时避免重复执行工具工具结果缓存外部工具返回的数据同一查询不重复请求外部 APIPrompt 模板缓存系统提示词和公共上下文减少拼接时间向量语义缓存相似历史问题和答案相似问题直接返回历史结果其中语义缓存是 Agent 特有的。普通接口参数精确匹配缓存命中率不高Agent 用户问的问题往往在语义上接近比如“查询上季度销售数据”和“上季度的销售情况怎么样”可能对应同一个执行流程。引入向量化检索引擎后可以先对新问题做 embedding去向量库找相似度超过阈值的历史问题命中后直接复用结果避免重新启动整个 Agent 链路。2.2 缓存击穿、穿透、雪崩在 Agent 场景的表现缓存穿透用户伪造不存在的用户 ID 或股票代码缓存和数据库都查不到请求每次都会穿透缓存。Agent 场景更严重穿透后不仅查数据库还会触发模型推理。缓存击穿某个热点问题比如“系统怎么使用”的缓存过期瞬间大量用户同时请求全部打到模型和数据库。缓存雪崩大量 key 在同一时间过期回源请求瞬间翻倍如果这时刚好有热点事件Agent 服务直接被打崩。解决穿透有两种常见做法参数校验 空值缓存或者布隆过滤器。击穿可以用互斥锁也可以把热点 key 的过期时间错开。雪崩要在设置过期时间时做随机化基础过期时间加一个随机偏移量避免大批 key 同时失效。2.3 Spring Cache Redis 缓存实现示例下面是针对一个“获取股票行情工具结果”的缓存实现采用双检锁防止热点 key 击穿时重复调用工具Service public class StockToolService { Autowired private StringRedisTemplate redisTemplate; private static final String CACHE_KEY_PREFIX agent:tool:stock:; public String getStockQuote(String stockCode) { String cacheKey CACHE_KEY_PREFIX stockCode; // 1. 先读缓存 String cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return cached; } // 2. 缓存未命中加锁防止并发回源 String lockKey cacheKey :lock; boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(5)); if (!locked) { // 短暂休眠后重试避免所有线程同时打向下游 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getStockQuote(stockCode); } try { // 3. 双检另一个线程可能已经回填 cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return cached; } // 4. 调用外部工具 String result callStockApi(stockCode); // 5. 写入缓存过期时间加随机偏移 int baseExpire 300; int randomExpire ThreadLocalRandom.current().nextInt(60); redisTemplate.opsForValue().set(cacheKey, result, Duration.ofSeconds(baseExpire randomExpire)); return result; } finally { redisTemplate.delete(lockKey); } } private String callStockApi(String stockCode) { // 实际对接外部行情接口注意依赖超时控制 return {\code\:\ stockCode \,\price\:12.34}; } }这里还应当注意缓存一致性问题。如果外部工具数据变化频繁需要在数据更新接口里主动删除对应缓存或者在 Result 对象里带上更新时间戳由业务层判断是否过期。Agent 场景建议把“缓存 key”与“数据源 参数 版本号”一起拼避免旧数据被当作新结果返回。3. 限流层设计3.1 普通 QPS 限流为什么不够很多项目对普通接口用 QPS 限流比如每秒最多 2000 次请求。直接把同一套配置用在 Agent 接口上会出问题一个 Agent 用户请求可能相当于 50 次模型调用每个模型调用还消耗不同数量的 token。只看 QPS无法区分“每秒 2000 个简单查询”和“每秒 2000 个长文档分析任务”之间的差异后者可以把 GPU 和模型 API 瞬间打爆。Agent 接口限流建议同时考虑四个维度维度说明示例QPS每秒最大请求数每秒 500 次并发数同时在执行的 Agent 任务数上限 100 个token 消耗速率每分钟调用模型的总 token 数每分钟 100 万 token人均配额单用户或单会话频率每分钟 10 次调用3.2 限流算法对比算法特点适合场景潜在问题固定窗口实现简单每个时间窗口计数一般接口窗口边界出现双倍流量峰值滑动窗口将窗口切分小块按时间滑动有突发但需要平滑的场景存储开销稍大令牌桶按固定速率补充令牌允许突发Agent 调用大模型 API需要控制桶容量避免过度突发漏桶恒定速率处理请求保护数据库写入无法利用瞬时空闲资源很多文章会把令牌桶和漏桶搞混这里说清楚令牌桶控制的是“能否发送”系统有能力处理突发漏桶控制的是“输出速率”把请求强制压平到固定速率适合限制对数据库或第三方的写入频率。滑动窗口存在什么问题如果窗口切成 1 秒一个格子一个 5 秒的滑动窗口在 1 秒内有大量请求突然进来仍然可能造成下游拥堵滑动窗口本质上是让流量更平滑但它无法限制“短时间内连续 N 个高消耗 Agent 任务”这种情况。所以高消耗任务还要额外做并发数和 token 维度限流。3.3 按 Agent 维度设计限流 Key普通限流按 IP 或用户 ID 做 key。Agent 场景建议用更细的维度限流 Key 用户ID 会话ID 模型名 Agent 类型同一个用户可以在不同会话里并行使用 Agent限制单个会话比限制整个用户更合理。错误示范是只按用户 ID 限流一个用户开多个会话所有请求被拉通统计会导致误伤正常使用如果完全不按会话维度限流单用户就能用多个会话绕过配额。3.4 Redis 令牌桶限流 Lua 实现分布式环境下最简单的限流实现是用 Redis 跑 Lua 脚本保证原子性。下面是令牌桶脚本-- KEYS[1] 令牌桶 key -- ARGV[1] 桶容量 -- ARGV[2] 每秒补充速率 -- ARGV[3] 本次请求需要的令牌数 -- ARGV[4] 当前时间戳毫秒 local key KEYS[1] local capacity tonumber(ARGV[1]) local refillRate tonumber(ARGV[2]) local requested tonumber(ARGV[3]) local now tonumber(ARGV[4]) local tokens redis.call(get, key) if not tokens then tokens capacity end tokens tonumber(tokens) local lastRefill tonumber(redis.call(get, key .. :ts) or 0) if lastRefill 0 then lastRefill now end tokens math.min(capacity, tokens (now - lastRefill) / 1000 * refillRate) redis.call(set, key .. :ts, now) if tokens requested then redis.call(set, key, tokens - requested) return 1 else redis.call(set, key, tokens) return 0 endJava 侧调用Component public class TokenBucketRateLimiter { private static final String SCRIPT ...; // 上面 Lua 脚本 Autowired private StringRedisTemplate redisTemplate; public boolean tryAcquire(String key, int capacity, int refillRate, int requested) { DefaultRedisScriptLong script new DefaultRedisScript(SCRIPT, Long.class); ListString keys List.of(key); Long result redisTemplate.execute(script, keys, String.valueOf(capacity), String.valueOf(refillRate), String.valueOf(requested), String.valueOf(System.currentTimeMillis())); return result ! null result 1; } }限流应该在网关层和实例层同时启用。网关层拦截外部突发流量实例层应对某个节点流量不均衡的情况。两层限流阈值不要设置成一样比如网关设置 1000 QPS单实例设置 300 QPS这样网关放进来之后实例还能再过滤一层。4. 负载均衡层设计4.1 Agent 服务负载均衡的特点Agent 服务和普通微服务在负载均衡上有显著区别单个请求耗时很长不能只按请求数分发。流式输出要求网关不能缓冲整个响应。同一会话后续请求依赖前序上下文虽然可以用 Redis 共享但减少跨实例调度仍然有利于性能。某些 Agent 实例因为模型加载差异处理能力不同简单轮询可能把慢实例打死。因此Agent 服务更推荐最少连接数策略而不是默认的轮询。负载均衡器实时查看每个后端节点活跃连接数把新请求交给活跃连接最少的节点长请求场景下效果更明显。4.2 无状态化改造如果 Agent 上下文只在实例内存里保存负载均衡一转发用户就会丢失上下文。无状态化改造是使用负载均衡的前提把会话上下文写入 Redis用 sessionId 做 key。模型调用记录和中间结果也存入 Redis 或外部存储。实例只负责执行推理不持有用户状态。改造后请求可以随便路由到任意实例负载均衡才真正有用。否则只能做会话粘滞一旦实例重启所有会话都会断线。4.3 Nginx 配置示例下面是针对 SSE 流式输出的 Nginx 配置upstream agent_cluster { least_conn; server 10.0.0.1:8080 max_fails3 fail_timeout30s; server 10.0.0.2:8080 max_fails3 fail_timeout30s; server 10.0.0.3:8080 max_fails3 fail_timeout30s; keepalive 64; } server { listen 80; server_name agent.example.com; location /agent/v1/chat { proxy_pass http://agent_cluster; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 流式接口不能缓冲 proxy_buffering off; proxy_cache off; # Agent 响应时间长不宜使用默认 60s 超时 proxy_read_timeout 600s; proxy_send_timeout 600s; } location /agent/v1/health { proxy_pass http://agent_cluster; proxy_http_version 1.1; proxy_set_header Connection ; } }注意proxy_buffering off是关键。如果开启缓冲SSE 流式输出会被 Nginx 攒批次用户端看到的内容会有明显卡顿体验很差。4.4 负载均衡算法选择算法适用场景Agent 场景建议轮询各节点无差异请求耗时接近不推荐长请求会导致负载不均加权轮询节点能力有差异可用于不同规格机器混部最少连接数请求耗时长且差异大推荐作为默认算法一致性哈希需要会话粘滞未做无状态化时可临时使用基于响应时间节点性能波动明显需要额外指标采集可作为进阶方案5. 熔断降级层设计5.1 熔断器三态熔断器有三个状态关闭状态正常调用下游统计错误率和慢调用占比。打开状态直接拒绝请求不调用下游快速失败。半开状态打开后等待一定时间放少量请求试探下游是否恢复。参数里最重要的三个是failureRateThreshold错误率阈值达到后熔断打开。slowCallDurationThreshold超过这个耗时的请求算慢调用。waitDurationInOpenState进入半开状态前等待的时间。Agent 场景还需要关注错误类型。调用大模型接口时超时错误和 429 限流错误占大多数如果下游连续返回 429说明已经限流熔断器应该尽快打开让服务降级而不是反复重试。5.2 Agent 服务的熔断粒度不建议对整个服务做一个大熔断器。一个 Agent 调用链可能同时调用好几个模型供应商和多个外部工具任何一个下游故障都不应该影响其他能力。建议按以下维度拆分熔断器熔断器名称保护对象示例llmApi大模型供应商 APIOpenAI、通义、文心等stockTool行情工具接口第三方股票数据 APImailSender邮件发送服务SMTP、企业邮件网关redisClient缓存服务数据库链接池例如调用股票工具时熔断只影响股票类 Agent 任务模型 A 熔断模型 B 仍然可以继续服务其他任务。这样故障影响面最小。5.3 Resilience4j 熔断配置与降级配置文件resilience4j: circuitbreaker: configs: default: slidingWindowSize: 100 failureRateThreshold: 50 slowCallRateThreshold: 50 slowCallDurationThreshold: 10000 waitDurationInOpenState: 30s permittedNumberOfCallsInHalfOpenState: 10 instances: llmApi: baseConfig: default stockTool: baseConfig: defaultJava 侧代码Slf4j Service public class AgentExecutor { Autowired private LlmClient llmClient; CircuitBreaker(name llmApi, fallbackMethod llmFallback) public String callLlm(String prompt) { return llmClient.chat(prompt); } public String llmFallback(String prompt, Throwable t) { log.error(LLM 调用失败prompt{}, prompt, t); // 降级返回本地规则引擎的处理结果 return localRuleEngine.reply(prompt); } }如果 Agent 服务本身或者它调用的第三方 API 已限流调用方反复重试只会加剧问题。比如有的模型供应商在超负荷时返回类似 agent execution provider did not respond in time 的报错普通调用方会误以为逻辑有问题持续重试。实际上更合理的处理方式是在熔断器打开期间把超过等待时间的请求直接降级到本地兜底逻辑同时记录一次重试待半开状态恢复后再放量。6. 四层防护完整链路与验证方法6.1 请求处理链路请求进入后的完整顺序如下请求到达网关网关先做缓存判断如果命中完整答案缓存直接返回不进入后端。缓存未命中执行限流检查QPS、并发数、token 配额任一超限返回 429 或排队。通过限流后负载均衡选择最空闲的健康实例。实例内部调用模型或工具前先检查对应熔断器状态熔断打开则立即降级。正常调用下游获得结果后写入 Redis 缓存再把结果逐字返回给客户端。这五步对应四层防护每层都有独立的丢弃口径。需要保留一个指标每层拦截了多少请求这样压测时能看出瓶颈在哪一层。6.2 压测验证流程推荐先用小规模压测验证再逐步放大压测步骤目标关注指标单实例基线压测确定单节点处理能力和耗时分位数QPS、P99 延迟、线程池活跃度缓存命中率测试设置不同缓存命中率观察回源压力缓存命中率、后端 QPS限流触发测试验证超过配额后是否返回 429 和排队限流拦截率、错误率熔断恢复测试模拟下游故障验证熔断打开和半开恢复开放状态时间、降级比例削峰填谷测试模拟突发流量场景观察是否出现雪崩被拒绝请求数、故障传播范围第一次压测建议只开一个 Agent 接口关掉其他任务这样结果更容易分析。6.3 关键监控指标监控项采集方式异常预兆线程池活跃度Spring Actuator持续 80%队列等待时间应用日志连续上升缓存命中率Redis 应用埋点突然下降超过 20%限流拦截数网关指标单秒拦截数超过放开数熔断器状态变化Resilience4j Metrics打开状态次数增长下游 API 错误率调用链埋点错误率 5%模型 token 消耗速率LLM API 本地计数接近供应商阈值7. 常见问题与排查方法问题现象可能原因排查方式解决方案缓存命中率很低缓存 key 设计不合理参数顺序或大小写不同查看缓存 key 统计统一参数规整引入语义缓存热点问题一过期服务瞬间高负载缓存击穿查看 Redis key 过期时间和回源请求量加互斥锁、热点 key 永不过期定期更新限流生效但接口还是超时限流只看 QPS没限制并发数和 token 消耗检查线程池和下游调用延迟增加并发数限流按 token 维度限流固定窗口限流在边界出现双倍流量窗口切换时计数清零观察每秒 QPS 曲线改为滑动窗口或令牌桶流式输出卡顿Nginx 开启缓冲查看 Nginx 配置增加proxy_buffering off熔断器打开后一直不恢复半开状态放行的请求仍然失败查看下游服务健康状态增大半开等待时间修复下游后再恢复一个 Agent 任务失败导致整个服务降级熔断粒度太粗查看熔断器维度按模型/工具拆分熔断器压测时下游第三方 API 开始报错拒绝本地限流参数超过第三方配额查看第三方配额日志降低令牌桶速率增加排队机制8. 最佳实践与使用建议第一先做一次容量估算。不管代码写得再好永远知道系统极限很重要。用单实例压测结果乘以节点数再乘以一个冗余系数得到你愿意放给用户的流量上限。限流阈值按这个值的 70% 设置留出缓冲。第二缓存优先做结果缓存再考虑语义缓存。结果缓存实现简单、收益直接语义缓存需要向量库和 embedding 服务属于进阶优化不要一开始就上。第三限流配置应该走配置中心。Agent 任务类型会不断变化模型收费策略也可能调整限流参数如果写死在代码里每次调整都要发版线上排障成本很高。使用 Nacos 或 Apollo 集中管理限流阈值和熔断配置。第四熔断降级必须真实可用。降级不是简单返回 “系统繁忙”而是要有一个能回答用户的基础逻辑。比如本地有一个专门的规则引擎至少能处理通用问题或者把失败问题放入队列稍后人工处理。第五流量到达前先做认证和参数校验。大量非法参数请求会穿透缓存直接打到模型白白消耗 token。网关层应该拒绝明显非法的请求再进入四层防护链路。第六做好合规边界。Agent 服务如果涉及用户数据、人脸、声音、版权素材或私有业务数据进入日志、缓存和模型调用前必须做脱敏与授权检查。调用第三方模型 API 时也要确认数据流向是否符合隐私政策不能把敏感信息直接拼进 Prompt 发给外部服务。第七发布新 Prompt 模板或模型版本时先小流量灰度。Agent 接口的行为非常不稳定一个 Prompt 的小改动可能把响应时间从 3 秒推到 10 秒直接影响整个并发容量。灰度期间观察 P99 延迟、错误率和 token 消耗再逐步放量。四层防护做下来核心不是组件多炫而是每一层都清楚自己挡什么流量。缓存挡重复请求限流挡超量请求负载均衡挡单点过热熔断挡下游故障。四层配合Agent 服务在高并发下才有机会保持稳定。先从最小可用方案开始结果缓存 令牌桶限流 最少连接数负载均衡 单粒度熔断跑通后再逐步细化。