☰
Agent 扛并发:把 AI Agent 当集群负载的 AX 与 Substrate 架构实践
2026/10/1 14:08:25 网站建设 项目流程

这几个月一直在折腾 AI agent 怎么扛并发的问题。做 agent 框架选型时,圈子里反复出现两个词:Google AX 和 Substrate,加上“把 Agent 当集群负载”这个说法,基本就是一套完整的基建思路。这篇文章把这套思路从头到尾拆一遍——先说为什么 agent 要按集群负载来做,再分别拆解 AX 和 Substrate 各自的职责,最后给出一套我自己落地的最小方案和踩坑记录。适合正在做 agent 生产环境部署、被并发和稳定性问题折磨的团队,也适合想系统理解 agent 基建的新手。

1. 为什么要用集群负载的思路做 Agent

1.1 Agent 其实就是一个需要被调度的“工人”

很多团队做 agent 应用,第一反应是写好 prompt、调好工具、把流程跑通,然后就上线了。等到用户量一上来,问题全来了:任务排队、超时、上下文串了、LLM 接口限流、进程崩溃……这时候才意识到,开发环境里跑一个 agent demo 和生产环境里扛住几百个并发任务,根本是两码事。

如果你做过后端服务,你会发现 agent 的处境和传统 Web 服务很像:你有 N 个请求进来,如果不做队列、不做限流、不做负载均衡,服务迟早被压垮。区别在于,传统 Web 请求通常几十毫秒就返回了,而一个 agent 任务可能要跑几十秒甚至几分钟。它不是一个“请求”,它更像一个“任务”,有自己的生命周期,需要被单独调度、跟踪、恢复。

这就是“把 Agent 当集群负载”的核心含义:把 agent 实例当作集群里可以被调度的工作单元,而不是一个特殊的、只能单跑的进程。我见过太多项目把 agent 写成一个常驻进程,用户请求进来直接调 agent.run(),并发全靠线程池硬扛,这本质上就是拿单体应用的方式做分布式系统,迟早出问题。

1.2 Agent 负载的三个特殊性决定了它不能按 Web 服务来做

做集群负载,先要看负载长什么样。Agent 任务和普通 Web 请求有本质区别,我总结了三方面:

第一,有状态。一个 agent 任务在运行过程中,会不断累积上下文、工具调用记录、中间结果,甚至要跨多轮和用户交互。这个状态不能被随便丢弃,否则任务就得从头开始。

第二,长耗时。LLM 推理本身就慢,加上工具调用、检索、多步推理,一个任务跑几分钟很正常。这意味着“超时重试”不能盲目做,否则任务永远做不完。

第三,强外部依赖。agent 几乎每一步都在调外部系统:LLM API、数据库、内部工具、第三方服务。任何一个环节抖动,都会导致整个任务失败。你不仅要处理自己的系统故障,还要处理上游的限流和超时。

这三个特性叠加起来,比单纯的高并发 Web 请求难处理得多。它要求你不仅要考虑怎么分发流量,还要考虑怎么保存状态、怎么恢复任务、怎么控制外部依赖的调用节奏。传统 Web 的负载均衡方案只能解决一部分问题。

对比维度普通 Web 请求Agent 任务
生命周期毫秒级秒级到分钟级
状态无状态或 Session 级强状态、多轮上下文
外部依赖通常只有数据库LLM、工具、检索、多系统
失败处理重试即可需要 checkpoint 恢复
资源消耗相对稳定突发性强,Token 消耗大

2. Google AX:把编排控制面独立出来

2.1 AX 到底在管什么

“Google AX”这个称呼,圈内通常指 Google 在 agent 运行时方向的一套设计理念——AX 是 Agent eXecution 的缩写,强调把 agent 的执行过程当作一组可编排、可调度、可观测的任务来管理。在公开的设计思路里,它核心回答一个问题:当一个 agent 任务到达集群时,谁来决定它怎么拆解、谁先跑、谁后跑、跑不动了怎么办?

也就是说,AX 是整个系统的“大脑”,它负责任务的编排和控制,但它自己不干具体活儿。具体干活的是下面的执行单元,可能是 worker 进程、容器或者服务器。这种分工在分布式系统里是极其常见的——Kubernetes 的控制面和数据面分离,就是一个典型例子。AX 扮演的正是控制面的角色,它告诉系统“现在有多少个任务、每个任务处于什么阶段、下一步该让谁执行”。

我自己在落地时对这个分层感受特别深。早期把编排逻辑和执行逻辑写在同一个进程里,代码越写越复杂,一个任务卡住整个进程跟着卡。后来把编排抽出来,任务状态单独管理,worker 只负责“拿到一个 step,跑完,上报结果”,清爽太多了。

2.2 控制面与执行面分离的价值

有人会问,我一个小团队,任务量也不大,有必要搞这么复杂吗?我的回答是,哪怕只有 5 个并发任务,控制面和执行面分离也能降低你的心智负担。

分离之后,你能获得几个实打实的好处:

扩展性强。执行单元不够了,加 worker 就行,控制面不用改代码。这跟 Web 服务加节点是一个道理,但因为 agent 任务有状态,加 worker 之前得确保调度器能正确地把任务分给空闲节点。

故障隔离。worker 崩了,控制面还活着,调度器可以把任务重新分发到其他 worker。如果编排和执行放在同一个进程,进程一挂,所有任务都没了。

升级无感。你想更新 agent 的 prompt 或者工具逻辑,只需要替换执行单元的代码,控制面不用动。运行中的任务也能自然完成,不需要造成大面积中断。

可控的调度策略。控制面可以决定“用户请求优先”“低延迟任务优先”“批量任务后台跑”等等,这些策略不需要改动执行单元。

2.3 AX 中的关键概念

结合公开设计思路和个人实践,我把 AX 这类控制面里的核心抽象整理成一张表。这些概念不是某种官方清单,而是任何想把 agent 当作集群负载来调度的系统都需要的基本单元:

概念作用类比
AgentRun一次完整任务的实例,有唯一 ID一个订单
Step任务拆解后的最小执行单元订单里的一个流程节点
Leaseworker 对某个 Step 的租约,防止多人抢活数据库行锁
Heartbeatworker 定期上报心跳,证明还活着TCP keepalive
Checkpoint保存任务执行到某一步的状态快照游戏存档
Event Log每个事件的可追溯记录操作审计日志

这些概念在代码里并不复杂。比如一个“查天气然后生成文案”的任务,AgentRun 是整次任务,Step 1 是调天气工具,Step 2 是调用 LLM 生成文案。worker 抢到 Step 1 的 Lease,执行完上报结果,控制面把状态推进到 Step 2,再等待空闲 worker 抢下一个 Lease。

我特别想说一下 Checkpoint,这个很多人会忽略。一个 agent 任务跑 3 分钟,如果在第 149 秒挂了,没有 Checkpoint 就得从头开始,体验非常差。有了 Checkpoint,第 149 秒挂掉,新 worker 可以从第 140 秒的状态续跑,最多只损失几秒。这在长时间任务里几乎是必需品。

3. Substrate:给 Agent 提供底座资源

3.1 没有基座,编排层就是空中楼阁

前面讲了 AX 这种控制面,但控制面本身不执行代码,真正跑 agent 逻辑的是下面的执行单元。这些执行单元跑在哪、资源从哪来、容器怎么调度、日志怎么收集、网络怎么打通?这些统称为基座层,也就是标题里提到的 Substrate。

我理解的 Substrate,并不是某一个具体产品,而是一类“承托 agent 执行的底层基础设施平台”。它至少要做三件事:把一台台物理机或虚拟机抽象成统一的资源池;为每个 agent 执行单元提供隔离环境;提供通用的状态存储、消息队列和可观测性能力。

可能有人觉得,这不就是容器平台吗?差不多,但又不完全一样。普通的容器平台是为长期运行的 Web 服务设计的,而 agent 任务是短生命周期、突发性强、资源波动大的负载,底层平台需要针对这种负载做很多适配。

3.2 Agent 跑起来到底需要哪些底座能力

以我一个实际 agent 服务为例,拆开看它依赖的底层能力:

容器运行时,这个最基础。agent 代码需要被打包成镜像,在隔离环境里跑。不同任务可能需要不同的依赖,容器隔离是最省心的方案。我见过直接用裸进程跑的,依赖冲突能把人搞疯。

状态存储。agent 的会话上下文、Checkpoint、任务元数据都要存下来。这部分通常落到 Redis、PostgreSQL 或者对象存储里。选型时有讲究:上下文访问要快,Redis 合适;Checkpoint 要大文件,对象存储更合适;任务状态要带事务,数据库更靠谱。

消息队列。调度器要和 worker 解耦,必须通过队列传递任务。队列本身也要承担责任——延迟、重试、死信,它决定了任务分发的可靠程度。

可观测性。日志、指标、追踪,每一项都是排查问题的底板。没有这三样,agent 并发一高,你根本不知道任务卡在哪一步。

资源调度。worker 的扩缩容、镜像的拉取、节点的故障替换,这些操作最好由平台自动完成,而不是人肉运维。

这些能力,就是 Substrate 这类基座平台要提供的。它在 AX 控制面的下方,也被称为“数据面”。AX 只做决策,Substrate 负责真正把决策落到底层资源上。

3.3 为什么不是直接上 K8s

很多人第一反应是,底层就用 Kubernetes 不就行了?我当时也是这么想的,实际落地后发现 K8s 和 agent 负载有摩擦,集中在三处:

第一,单位大小。K8s 最小调度单位是 Pod,但 agent 一次任务可能只跑几秒钟,跑完就该销毁。频繁创建销毁 Pod,调 API 的开销比任务本身还大。

第二,状态处理。K8s 的 Pod 是无状态的,重启从镜像重新来。agent 任务恰恰有状态,这就需要额外的 PVC、Sidecar 或者远程存储来兜底,复杂度上去了。

第三,调度策略。K8s 的调度策略偏向“让长期服务保持稳定”,对短任务、突发任务的支持不够细。你要等 Pod 调度、拉镜像、初始化,冷启动时间往往比任务执行时间还长。

所以现在很多 agent 底座的思路是“轻量运行时 + 任务队列 + 对象存储”,不用完整 K8s,而是把 K8s 的核心能力做减法,只留下 agent 真正需要的部分。Substrate 这类基座的定位,就是把这层“减过的 K8s 能力”封装成 agent 专用的底座。

4. 可落地的“Agent 即负载”架构与最小实现

4.1 整体链路与职责划分

把 AX 和 Substrate 的概念落到实际架构里,一条完整链路是这样的:

用户请求进来,先到 API 网关,网关做鉴权和初步参数校验,然后创建 AgentRun,投递到任务队列。AX 调度器监控队列,按照调度策略把可执行的 Step 分发给空闲 worker。worker 从 Substrate 平台拿到运行环境,执行 Step——调用 LLM、调工具、读写状态,再上报结果。AX 收到结果后推进任务状态,如果后续还有 Step,继续投递;如果没有,任务完成。

这条链路里,网关、AX、worker 是三个层次的逻辑角色,Substrate 则横跨执行环境,提供队列、存储、日志这些底层能力。每一层职责单一,出了问题定位也快。

4.2 最小复现方案

纸上谈兵没有意思,我给一套最小可跑的方案,用了最朴素的组件:Redis 做队列和状态存储,一个 Python worker 执行任务。你把它跑起来,就能直观感受“任务被调度、状态被持久化、worker 挂了任务还能续跑”这套机制。

这个方案的执行流程:网关侧把任务塞进 Redis 列表,用 BRPOPLPUSH 或者 Stream 的方式投递。worker 从队列里取出任务,先检查任务的 Checkpoint 是否存在,存在就恢复上下文继续执行,不存在就从头开始。每执行到关键节点,把进度和中间结果写回 Redis。执行完成后,标记任务完成。

我用一段简化代码来表示这个 worker 的核心逻辑:

import redis, json, uuid REDIS = redis.Redis(host='localhost', port=6379, decode_responses=True) def run_agent_task(task_id, checkpoint=None): # 模拟一个带状态的 agent 任务 if checkpoint is None: result = {"steps_done": 0, "partial": ""} else: result = json.loads(checkpoint) # 从存档恢复 # step 1: 调工具 if result["steps_done"] < 1: tool_output = call_tool("get_weather", city=result["city"]) result["partial"] += tool_output result["steps_done"] = 1 REDIS.setex(f"checkpoint:{task_id}", 3600, json.dumps(result)) # step 2: 调 LLM if result["steps_done"] < 2: llm_output = call_llm(result["partial"]) result["final"] = llm_output result["steps_done"] = 2 REDIS.setex(f"checkpoint:{task_id}", 3600, json.dumps(result)) REDIS.set(f"task:done:{task_id}", "1") return result while True: task = REDIS.blpop("task_queue", timeout=30) if not task: continue payload = json.loads(task[1]) ckpt = REDIS.get(f"checkpoint:{payload['task_id']}") try: run_agent_task(payload["task_id"], ckpt) except Exception as exc: # 失败不丢任务,放回队列,最多重试 3 次 retry_key = f"retry:{payload['task_id']}" retries = int(REDIS.get(retry_key) or 0) if retries < 3: REDIS.incr(retry_key) REDIS.lpush("task_queue", task[1]) else: REDIS.lpush("task_dlq", task[1])

这套东西看着简单,但“检查 Checkpoint 再续跑”这个动作,恰恰是 agent 任务区别于普通 Web 请求的关键。普通请求重放一遍就行,agent 任务必须从断点继续,这个是集群负载方案的核心设计。

4.3 关键设计决策

第一,幂等。agent 在执行过程中可能会调用外部工具,如果重复执行,副作用可能是发了重复邮件、重复扣钱。所以每个 Step 在设计时就要保证幂等,至少要在 Checkpoint 里记录“这个 Step 已经执行过了”,恢复时跳过。

第二,限流。LLM API 是最容易被限流的环节。我见过一个并发任务一上来,直接把所有任务同时打给 LLM,结果触发 429,然后重试风暴,API 网关直接被打爆。正确做法是在调度层做并发控制,比如用令牌桶限制同一时刻只允许 5 个 LLM 请求。给 LLM 的请求都要做退避重试,指数退避才是合理的。

第三,Checkpoint 时机。不是每调一次工具都要写 Checkpoint,IO 也是有成本的。我的经验是:在真正不可重放的操作之前写一次,比如调付款接口之前;或在跨系统的状态切换点写一次,比如工具结果返回后、准备调 LLM 之前。

5. 并发与稳定性问题排查实录

5.1 并发一高 session 上下文就串了

这个问题几乎每个 agent 项目都会遇到,表现是:任务 A 的对话内容跑到任务 B 里去了。根因很简单,把上下文变量写成了全局变量或类变量,Python 里常见的坑是模块级 dict 存上下文,多线程同时读写就串了。

解决思路倒也不复杂:上下文必须跟着任务 ID 走,每个任务一个独立上下文。我的习惯是给上下文设置一个生命周期,任务开始创建,任务结束销毁。不要做全局缓存,除非你有专门的缓存淘汰策略,不然迟早出问题。

排查方式也简单,给每个 AgentRun 分配一个 trace_id,让它贯穿所有日志。并发一高,只要日志里搜 trace_id,就能看到这个任务从入场到结束的全部轨迹。上下文是不是串了,一眼就能看出来。

5.2 LLM 限流触发重试风暴

这个我踩过好几次坑。现象是:某个时段大量任务堆积,所有 worker 都在疯狂调用 LLM,结果 LLM 返回 429,worker 收到错误后立刻重试,导致请求量比正常情况翻了好几倍。

根因在于重试策略写得太激进。无脑重试不仅放大了上游压力,还可能把自己的队列打穿。我的做法是:

第一,对 LLM 请求做并发限制,全局信号量或者令牌桶,限制最大同时请求数。第二,重试必须带退避,而且带随机抖动。第三,区分错误类型,网络错误可以快速重试,429 和 5xx 必须等更长时间甚至进入死信队列人工处理。

这里再说一个细节:多个 worker 抢同一个任务时,如果上游限流是共享的,分布式限流就很有必要。Redis 的 INCR 加过期时间可以实现一个简单的分布式令牌桶,成本低效果却很明显。

5.3 冷启动拖慢了整个链路

agent 任务的冷启动问题,比普通微服务更严重。原因在依赖:notebook 镜像、模型文件、工具链,动不动几百 MB。任务到了,worker 还在拉镜像,等它就绪,用户早走了。

我试过几个办法,最有效的:worker 常驻化,不让它经常销毁重建;模型做预加载,放在共享内存里,多个 worker 共用;镜像分层,基础依赖打底层,agent 业务代码放上层,业务更新时不用重新拉底层。

如果做不到常驻,至少要做 worker 预热——提前创建一批 worker 在池子里等着,任务到来直接分配。这和连接池、线程池的思路一模一样,本质都是抵挡冷启动冲击。

5.4 Checkpoint 与数据库状态不一致

任务在执行中写 Checkpoint,同时又往业务数据库写状态,两边都可能失败,最后就会不一致:业务库里显示已处理,Checkpoint 里显示还没完成,恢复时又执行了一次。

这个问题的解法,从简单到复杂都有:

最简单的是把所有状态写到 Redis 一个 key 里,Checkpoint 包含了业务状态,保证单点。一旦任务完成,再把最终结果同步到业务数据库,此时是最终写入,不是中间态。

复杂一点就是引入流程编排的事务机制,比如用 Saga 模式让每个 Step 有对应的补偿操作。但对大多数 agent 场景来说,单点 Checkpoint 已经够用了,关键是想清楚“记录的时机”。

我建议的写法是:每个 Step 执行前记录即将做什么,执行成功后记录已经做了什么。恢复时,如果发现“即将做”和“已做”不一致,一律重新执行“即将做”的 Step,因为查询类操作重放没有副作用,写操作要有补偿或者幂等设计。

问题现象根因解决方案
上下文串任务全局变量存了 Session 状态上下文随任务 ID 走,独立生命周期
LLM 限流重试风暴重试无退避、无并发限制分布式令牌桶 + 指数退避 + 随机抖动
任务排队但 worker 空转冷启动慢,worker 没就绪worker 常驻、预加载、预热池
状态库和 Checkpoint 不一致多处写入没有事务单点 Checkpoint,完成后再同步最终结果

6. 选型建议与常见问题速查

6.1 这套架构适合什么规模的团队

把 agent 当集群负载,本质上是拿分布式系统的成熟经验来治理 agent 任务。这套思路的强度,是给准备上生产环境的团队准备的,而不是给本地 demo 用的。

如果你正处在“agent 只有一个用户、自己调试阶段”,确实不需要考虑这么多,直接跑单进程就行。但当你的 agent 要服务多个外部用户、任务要跨分钟级、失败不能随便重头再来时,就该认真考虑这套架构了。

从我接触的团队看,十几个并发任务其实就可以开始用队列 + Checkpoint 这套模式,因为这套东西并没有想象中复杂,Redis 一个中间件就能撑起最小方案。团队越小,越应该尽早把状态管理做好,否则后面任务量上来后再重构,成本高得多。

6.2 什么时候不建议上

这里我也说点反话。如果你的 agent 只是做一个交互式聊天机器人,单轮对话、无状态、不调工具,那确实没有必要把编排层、基座层拆得那么细。杀鸡用牛刀,增加部署成本和维护成本,反而拖慢迭代。

另外,如果你们的 agent 依赖的 LLM 平台对并发请求本来就宽松,请求量大也能稳定承接,那限流和重试的优先级可以降低。但我不建议赌上游永远稳定,毕竟工具链一多,总有一个环节会出问题。保守一点,至少把队列和超时做好。

6.3 常见问题速查表

很多团队在落地这套架构时会问我同一个问题:到底要不要自研?我的看法是,如果只是内部工具,团队也没有专职 SRE,用已有的消息队列和脚本脚本完全可以;如果有外部客户、需要服务等级协议,那投入资源自研或者引入开源编排框架,是值得的。

问题建议
任务重复执行会不会有副作用?每个 Step 做幂等,工具调用记录到 Checkpoint
任务跑几小时,Checkpoint 会不会太大?只保存必要的中间状态,大文件放对象存储
Worker 崩溃后任务怎么恢复?心跳超时后 Lease 过期,控制面重新派发
多个 worker 抢同一个任务怎么办?分布式锁或 Lease 机制,确保只有一个执行者
队列堆积太多怎么办?加 worker 是最直接的,但也要看 LLM 限流
如何观测任务链路?trace_id 贯穿日志,任务状态存 Redis 可视化

我个人在实际操作中最大的体会是,agent 框架的迭代速度非常快,但底层调度、状态、队列这些分布式系统的老问题一点都没变。把 agent 当集群负载,意味着你不再依赖某个框架的“魔法”,而是用自己能掌控的基础设施把任务托起来。Google AX 和 Substrate 这类概念的最大价值,是帮我建立了一个清晰的认知框架:编排归编排,执行归执行,资源归资源,三层各自负责,出了问题也知道往哪层查。

最后再分享一个小技巧:给每个 AgentRun 生成 trace_id 时,不要把 trace_id 只放到日志里,还要在 Checkpoint 里带一份。这样排查时,无论你是看日志还是看状态存储,都能用同一个 ID 串起整个任务的一生。这个习惯我用了很久,每次排查并发问题都能省下大量时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询