如何养活百万级 Agent?
假设老板丢给你一个任务:给一百万个用户,每人配一个专属 AI agent——一个能记住他、陪着他、随时被叫醒干活的智能体。
你心想:这不就是 new Agent() 一百万次吗?
然后你坐下来,准备认真设计。还没写第一行代码,三个问题就把你问住了。
三个把你问住的问题
第一个问题:一个用户,可以有几个 agent?
别急着说「一个」。更要紧的是反过来问:你的架构允许一个用户挂多个 agent 吗?
因为很多时候,一个用户最好不是一个全能 agent,而是一个小团队——一个主 agent 做总协调,底下几个子 agent 各管一摊(查资料的、出题的、批改的),主 agent 把活拆下去、再把结果收回来。这就是 agent 编排。
要支持这个,你得先能精确地按名字找到每一个 agent(「张三的主 agent」「张三的批改 agent」),它们之间还要能互相调用。如果 agent 在你的设计里是个含糊的东西——一会儿是进程、一会儿是会话——那「一个还是多个」「怎么协同」都无从谈起,后面的状态、记忆、计费也全跟着歪。
第二个问题:1 万个 agent,10 个 pod,能摊匀吗?
假设此刻有 1 万个 agent 同时活跃,你有 10 个 pod 扛着。它们能均匀地一个 pod 一千个、不偏不倚地分布吗?
更棘手的是:万一一个 pod 挂了,里面那 1000 个 agent 也跟着没了——它们恢复得快吗?还有一种情况更难发现:同一个用户的 agent,会不会因为调度抖动,同时被两个 pod 持有,各自改各自的状态,最后打架?(分布式里这叫「脑裂」,是最难写对的一类 bug。)
第三个问题:那一整天,它在干嘛?
这是最反直觉的一个。一个 agent 第一次被叫醒,干了 3 分钟活;然后用户走了,隔了整整 24 小时,第二天再回来,它又干了 6 分钟。
真正烧算力的,只有那加起来 9 分钟。那中间的一整天呢?它在干嘛?
如果它是一个常驻进程,一直挂在那儿不退出——那就是一百万个进程同时占着内存、连接和调度槽。这一天的服务器账单,你付得起吗?
三个问题,三个方向:身份与编排、并发、成本。看起来要分头解决。
但当你把答案都想透,会发现它们指向同一句话。
三个问题,其实是同一个答案
先从最反直觉的第三个问题说起。
那一整天,agent 到底在干嘛?答案是:它根本不在。
我们脑子里默认有一个「agent loop」——一个读输入、想、调工具、回答的循环。我们想当然地以为,那一天它就是这个循环睡着了,挂起在那儿等下一句话。
不是的。真相是:
1 | 第一次激活: 一轮 loop 在跑(读状态 → 推理 → 调工具 → 回答) |
那一天的零成本,不是「省」出来的,是「无」。压根没有东西在跑。loop 不是一根睡了一天的线程,而是「跑完一轮就蒸发、下次重新拉起」的东西。每一轮回答结束,都是一个天然干净的存档点——没有在途工作,状态落盘,agent 从内存里消失。
反面教材是
while(true){ 等输入… }这种写法:它会真的阻塞一整天,占着内存和连接。一百万个这样的循环同时挂着,机器根本扛不住。
想通了这个,第二个问题(pod 挂了怎么办)也松动了。
既然 agent 的「真相」是存储里那行状态,而不是某个 pod 里的内存对象——那么一个 pod 被杀,丢掉的只是 1000 份「临时工作副本」,不是 1000 个 agent。恢复,无非是换一个 pod,把那 1000 行状态重新加载进来。快不快,取决于加载一行状态有多快,而不是抢救一个进程有多难。
至于「会不会被两个 pod 同时持有」——这就是那个最难写对的保证,叫单活(同一时刻,一个 agent 只能归一个 pod 管)。好消息是:有一类专门的运行时,立身之本就是免费帮你保证这一条,外加用一致性哈希把 agent 均匀摊到所有 pod 上。这个我们下一节就会见到。
最后回到第一个问题。一个用户几个 agent?答案是「随你」——但前提是你把 agent 当成一个可按名寻址的逻辑身份,而不是一台机器、一个进程。「张三的主 agent」是一个逻辑名字(按 ID 寻址),今天它在 3 号 pod,明天在 7 号 pod,后天不在任何 pod,但始终是同一个。
一旦每个 agent 都是这样一个可寻址的实体,「一个用户挂一个 agent」还是「挂一个 agent 团队」就只是多开几个实体的区别;而 agent 之间的编排——主 agent 调子 agent、再把结果收回来——本质上就是一个实体按名字调用另一个实体,也就是经典的「actor 调 actor」。换句话说,能不能做多 agent 编排,不取决于某个花哨的框架,而取决于你有没有把 agent 设计成可寻址的状态实体。
三个问题,绕了一圈,收敛成一句话:
Agent 是状态,不是进程。
「一个用户一个 agent」是一个逻辑身份,不是一百万个常驻进程。内存里那个对象只是「临时把状态捞出来跑一下」的壳,用完即弃;壳可以生灭、可以换 pod、可以崩溃重建,而 agent 始终是同一个——因为 agent 的本体,是存储里那行状态。
想清楚这一点,「养活一百万个 agent」就从「养一百万个进程」(不可能)变成了「存一百万行状态 + 维持一个小小的、按当前在线人数伸缩的 pod 池」(很现实)。
这个池子有多大?取决于并发,不是总量。一百万是总人口,真正同时在线的可能只有几万。几万个活跃 agent 摊到十几个 pod 上,剩下九十几万只是存储里安静躺着的状态行。
那这个「身体」跑在什么上面?
道理讲通了,工程上总得有个东西来真正提供「身份寻址 + 单活 + 激活休眠 + 状态加载」这套能力。这套东西,我叫它 agent 的「身体」。
身体怎么来?两条路:直接买一个现成的 agent 运行时,还是拿一个通用的有状态运行时,自己搭一个。
路线一:买现成的 agent 运行时
代表是云厂商的托管 agent 运行时(比如 AWS Bedrock AgentCore 这类产品)。
它的模型是:每个活跃会话,平台帮你起一个轻量的隔离沙箱(microVM),会话结束就回收;跨会话的长期记忆交给平台托管的 memory 组件。你只交「agent 代码 + 配置」,pod、控制面、单活、伸缩,统统不用你管。
flowchart TB
U["User app / web"]
CF["Edge: Cloudflare or CloudFront<br/>(CDN / WAF / DNS)"]
subgraph AWS["AWS · Bedrock AgentCore (fully managed)"]
subgraph RT["AgentCore Runtime"]
direction LR
A1["session microVM 1 (user-123)<br/>agent = Docker image (OCI, push to ECR)<br/>any language; Python easiest via SDK<br/>agent loop / prompt / call LLM + tools<br/>isolated Firecracker microVM per session, up to 8h"]
A2["session microVM 2"]
A3["session microVM N"]
end
MEM["AgentCore Memory<br/>cross-session long-term profile + short-term"]
GW["AgentCore Gateway / Identity<br/>APIs as tools + secure access"]
DS["Existing services (profile / content)<br/>same region, no cross-cloud"]
LLMGW["LLM gateway<br/>key / rate-limit / tiering / cost / guardrails / trace"]
LLM["Bedrock: LLM brain"]
end
U --> CF --> RT
RT <-->|persist / recall| MEM
RT --> GW --> DS
RT --> LLMGW --> LLM
好处:零运维,开箱即用,伸缩和隔离都是平台兜底。代价:你被这家平台锁住,控制力弱,单元(一个 microVM)比「百万轻量对象」要重,而且这类产品大多很新,百万级的生产验证还不多。
一句话:省心,但把方向盘交了出去。
路线二:拿通用有状态运行时,自己搭
如果你想攥住方向盘——掌控数据、不被锁定、用自己现有的技术栈——那就自己搭。这里有三个性格迥异的选项。
选项 A:Virtual Actor(以 Dapr 为代表)
这是最贴近常规 k8s 栈的选项。它的核心是一个 placement(放置)服务:用一致性哈希把 agent ID 均匀摊到所有 pod,并保证全局单活。前面那个「1 万 agent 摊 10 个 pod + 防脑裂」的问题,它白送。
flowchart TB
U["User app / web"]
CF["Cloudflare edge<br/>CDN / WAF / DNS"]
subgraph AWS["AWS · EKS (self-hosted)"]
R["Router / Gateway"]
subgraph DCP["Control plane (ns: dapr-system)"]
PL["dapr-placement<br/>consistent hashing + single-activation"]
OIS["operator + injector + sentry<br/>cluster-wide, separate pods"]
end
subgraph POOL["Worker pool — one Deployment, many pods"]
direction LR
subgraph P1["app Pod 1 (2 containers)"]
APP1["app container = YOU write (business)<br/>actor objects live here (in-mem)<br/>agent loop / prompt / call LLM + tools"]
DAPR1["daprd sidecar = Dapr (plumbing)<br/>routing / single-activation / state R-W / hibernate"]
APP1 <-->|localhost| DAPR1
end
P2["app Pod 2<br/>app + daprd"]
P3["app Pod N<br/>app + daprd"]
end
DS["Existing services (profile / content)<br/>reuse; agent is consumer"]
LLMGW["LLM gateway<br/>key / rate-limit / tiering / cost / guardrails / trace"]
TP["Temporal (optional)<br/>durable long-flows: wait-days / wait-human / saga"]
end
HOT["ElastiCache: hot state"]
COLD["DynamoDB: durable truth"]
LLM["Bedrock: LLM brain"]
U --> CF --> R --> POOL
OIS -.->|inject daprd| POOL
PL -->|"hash(actorId) to pod, even"| P1
PL --> P2
PL --> P3
POOL --> HOT
POOL --> COLD
POOL --> DS
POOL --> LLMGW --> LLM
POOL -.->|only if long tasks| TP
图里有两块要分开看:一组集群级共享的控制面 pod(dapr-placement 负责一致性哈希 + 单活,operator / injector / sentry 管注入和证书),和一个装着很多 app pod 的工作池。注意每个 app pod 里其实是两个容器——你写的业务容器(actor 对象就活在它的内存里),加一个叫 daprd 的 sidecar,替你打理寻址、单活、状态读写、休眠。状态热的放 ElastiCache、冷的落 DynamoDB;pod 池按在线人数自动伸缩,深夜可以缩得很小。用你现有的语言写,概念负担最小——这通常是默认选择。
选项 B:一个对象就是一个 agent(Cloudflare Durable Objects)
这套东西最初不是为 AI 造的——它本来是给无状态的边缘函数补一个「有状态的协调点」,用来做聊天室、协同文档、游戏房间。但你会发现,「聊天室 / 游戏会话 / 一个 agent」结构上是同一种东西:长生命周期、有状态、单一协调点、多个事件汇入。所以它天生适合当 agent 的身体。
flowchart TB
U["User app / web"]
subgraph CFP["Cloudflare platform (fully managed)"]
EDGE["Edge: CDN / WAF / DNS + routing"]
WK["Worker (stateless)<br/>auth + route to the right DO"]
subgraph DOS["Durable Objects (CF-managed)"]
direction LR
DO1["DO 1 = UserAgent (one per user-id)<br/>agent logic — TS (Rust via WASM / Python beta)<br/>agent loop / prompt / call LLM + tools<br/>state in co-located SQLite<br/>single-activation + hibernate = CF-managed"]
D2["DO 2"]
D3["DO N"]
end
WF["Cloudflare Workflows (optional)<br/>durable long-flows"]
end
subgraph AWS["AWS — your systems of record"]
DS["Existing services (profile / content)"]
COLD["DynamoDB: central user data"]
end
LLMGW["LLM gateway (CF AI Gateway or self-built)<br/>key / rate-limit / tiering / cost / guardrails / trace"]
LLM["LLM brain: Workers AI (limited) or Bedrock"]
U --> EDGE --> WK --> DOS
DOS -.->|only if long tasks| WF
DOS -->|cross-cloud boundary| DS
DOS -->|cross-cloud boundary| COLD
DOS --> LLMGW --> LLM
一个对象 = 一个 agent,按用户 ID 寻址,状态就贴在对象自带的 SQLite 里(不用外接 Redis/数据库),单活、休眠全是平台内部搞定、对你隐形。
注意:它虽然也免运维(你不跑 pod),但它给你的是一个通用的有状态对象——agent 的逻辑(怎么想、怎么调工具、教学策略)还是你自己写。这也是它和「买现成 agent 运行时」的区别:那个连大脑都帮你框好了,这个只给你一副好身体。代价是:开发语言基本锁定在 TypeScript,调你自己的后端数据要跨网络,数据驻留也得单独评估。
选项 C:WASM(以 Golem 为代表)
最激进的一个。WASM 实例可以做到微秒级唤醒、单机塞几万到几十万个、闲置时几乎不占内存——是「密度之王」,专为「一百万个偶尔活跃的实体」这种场景设计。Golem 在 WASM 之上又加了 durable execution:agent 的状态就是它的执行日志(oplog),闲置时驱逐,下次按日志重放恢复。
flowchart TB
U["User app / web"]
CF["Cloudflare edge<br/>CDN / WAF / DNS"]
subgraph AWS["AWS · EKS (self-hosted)"]
R["Router / Gateway"]
subgraph GCP["Golem control plane (you install)"]
SM["golem-shard-manager<br/>shard worker-id to executor (= placement)"]
CS["component-service + worker-service<br/>WASM component store + invoke API"]
end
subgraph POOL["worker-executor pool (many pods)"]
direction LR
E1["executor Pod 1<br/>WASM workers = agents (Rust / TinyGo, compiled to WASM)<br/>durable: state in oplog<br/>idle evict / replay to resume<br/>agent loop / prompt / call LLM + tools"]
E2["executor Pod 2"]
E3["executor Pod N"]
end
DS["Existing services (profile / content)<br/>reuse"]
LLMGW["LLM gateway<br/>key / rate-limit / tiering / cost / guardrails / trace"]
end
OPLOG["Golem oplog store (Redis + Postgres + blob)<br/>state + durable long-flows BUILT-IN"]
COLD["DynamoDB: central user data (optional)"]
LLM["Bedrock: LLM brain"]
U --> CF --> R --> POOL
SM -->|"shard worker-id, even"| E1
SM --> E2
SM --> E3
POOL --> OPLOG
POOL --> DS
POOL --> LLMGW --> LLM
POOL -.optional.-> COLD
听起来很美,但代价也实在:你得用 Rust 或受限的 TinyGo 写(编译进 WASM 沙箱),还要适应它一整套独立于 k8s 的编排,AI 相关的库塞不进 WASM。而且——它最大的杀手锏「密度」,你大概率用不上:你的并发只有几万,agent 闲置那天本来就已经不占资源了,几万级别的密度,前面两个选项早就够了。
横着看一眼
| 身体在哪 | 谁运维 | 状态存哪 | 语言 | |
|---|---|---|---|---|
| 买现成(AgentCore) | 云厂商托管 fleet | 平台(零运维) | 托管 memory | 容器,任意语言 |
| Dapr | 你的 k8s 集群 | 你 | Redis + 数据库 | 你现有的栈 |
| Cloudflare DO | 边缘平台 fleet | 平台(零运维) | 同驻 SQLite | TypeScript |
| WASM / Golem | 你的集群(叠一层) | 你(+ 平行平台) | oplog 内建 | Rust / WASM |
没有绝对的最佳,只有「你的约束下的最佳」。要掌控、用现有栈、最成熟 → Dapr;想彻底免运维、能接受锁定 → 买现成;想要极致密度、又啃得动 Rust → WASM;想要一个对象一个 agent 的优雅、又能接受 TS → DO。
痛点是相通的
兜了一大圈,你可能会说:这都是「一百万个学生 agent」这种极端场景才会遇到的麻烦吧?
恰恰相反。把场景换成一百万个客服 agent、一百万个游戏 NPC、一百万个个人助理——你撞上的,是同样的五堵墙:身份怎么定、并发怎么不打架、闲置怎么不烧钱、崩了怎么恢复、账单怎么扛得住。
更有意思的是:这五堵墙,没有一堵是 AI 带来的。
聊天室、游戏会话、电信连接、微服务实体——这些「长生命周期、有状态、偶尔活跃」的东西,几十年前就存在了,解决它们的运行时也早就成熟了。AI agent 不过是往这些老身体里,加了一句「调一下大模型」。所以你会看到,做边缘平台的顺手在有状态对象上盖了个 agent SDK,做微服务运行时的顺手做了个 agent 框架——因为它们手里那副「身体」,本来就是 agent 要的那副。
所以,当你纠结「怎么设计一个百万级 agent」时,真正在设计的,并不是 AI。
你在设计的,是一个会休眠的、有状态的实体——而这个问题,比 AI 老了几十年。