目录 · 第 3 / 16 章
ActorPart I · 为什么与是什么

03一个 50 岁模型的 AI 复兴

1973 Hewitt → Erlang → Akka → Orleans → AI 时代多智能体,以及 Go 生态的实现谱系。

Actor 绝非新生事物。摸清它半个世纪的来路,你才能看透「它为何偏偏在 AI 时代忽然翻红」。

1973:一个物理学家的数学起源

Actor 模型由 Carl Hewitt 于 1973 年在 MIT 提出,合作者是 Peter Bishop 与 Richard Steiger,论文标题就叫「A Universal Modular ACTOR Formalism for Artificial Intelligence」,发表于 IJCAI——这个细节值得留意:Actor 从一开始就不是为了解决系统工程问题而生,而是 Hewitt 想给 AI 推理系统找一种描述方式——把知识与计算表示成一群各自独立、靠消息协作的实体,而不是一整块耦合在一起的程序。这正是五十年后 Multi-Agent AI 重新撞上的同一个问题。

它的设计理念深受物理学(尤其是量子物理和相对论)影响,而非传统的数学逻辑或集合论——这解释了它「消息传递、无全局状态、无全局时钟」的世界观:就像物理世界里信息传递有速度上限、没有绝对同时性,一个 Actor 也只能通过发消息去影响另一个 Actor,不存在「瞬时共享」。往形式化里说,一个 Actor 不过是「一个地址 + 一段行为」:收到消息后,它能做且只能做三件事——给它已知的地址发消息、创建新的 Actor、为自己指定处理下一条消息的行为(即 become)。这份词汇表小到不能再小,却足以刻画一切可计算的过程。它还与λ 演算有深刻联系,并建立了基于域理论(Domain Theory)的指称语义,让这套模型的性质可以被严格证明,而不只是「感觉上对」。

真正值得记住的一点是:理论先行,硬件迟到了几十年。1973 年既没有商用多核芯片,更没有数据中心,单线程计算才是默认选项,「大规模并发」还只是研究课题里的奇谈。而 Actor 模型从第一天起就把「无限并行」当作默认假设。此后是电信交换机,再是多核 CPU,再是云端集群,直到今天成群结队的 LLM Agent——硬件和负载花了半个世纪,才追上这副数学骨架。

1986:Erlang 把它工业化

真正让 Actor 落地的是爱立信的 Erlang,由 Joe Armstrong、Robert Virding、Mike Williams 从 1986 年前后着手打造,目标只有一个:撑住绝不能宕机的电话交换机。Erlang 从语言层面就没有共享内存这个选项——数据天生不可变,进程(Erlang 对 Actor 的叫法)之间只能发消息。容错不是后来补上的功能,而是这门语言存在的理由本身。

let it crash 与监督树正诞生于此,后来在 Armstrong 2003 年的博士论文《Making Reliable Distributed Systems in the Presence of Software Errors》里被系统化。与其在每个函数里防御式地堵住所有可能的错误,不如干脆让 Erlang 进程直接崩溃,再由监督者(它本身也只是另一个进程,组织成一棵树)把它重启回一个已知的干净状态。最著名的案例是爱立信的 AXD301 ATM 交换机,常被引用为 Erlang 系统达到「九个 9」可用性的例证。另一个更晚近却更为人熟知的案例是 WhatsApp:用 Erlang 支撑全球数十亿用户的实时消息,零外部消息队列依赖——每个用户会话对应一个 Actor,彼此直接发消息。部件越少,能出错的地方就越少,支撑同等流量所需的运维团队规模也远小于同行。这不是理论优雅,是亿级流量下的真实工程验证。

2009:Akka 把它带进 JVM 主流

Akka 由 Jonas Bonér 于 2009 年为 Scala/Java 生态打造,把 Actor 模型带给了一大批从未碰过 Erlang、也没打算碰的后端工程师。它一出场就带着整套工具箱:Akka Persistence(事件溯源式的状态持久化)、Akka Cluster(把 Actor 分片到一群 JVM 上)、Akka Streams(背压式的响应流,底层同样跑在 Actor 上)。后来加入的 Akka Typed 更进一步——把最初「不带类型」的 ActorRef 换成了编译期可检查的类型化引用,发错消息类型这种错误,编译器直接拦下,不必等到运行时才炸。早期 Twitter 也大量使用 Scala 的 Actor 实现,并开源了仅约两千行代码却能扛住推文洪峰的 Kestrel。Akka 真正的意义不在发明了什么新东西,而是把 Erlang 在爱立信已经验证过的思想,原样搬进了大多数后端团队本就站在上面的日常 JVM 技术栈。

2014:Orleans 的「虚拟 Actor」

微软研究院走了另一条路:与其让开发者去操心一个 Actor 活在哪台机器上、什么时候该启动或停掉,不如干脆让这些问题消失。Orleans 的核心单元叫 Grain,是一个「虚拟 Actor」——从调用者的视角看,每个 Grain 永远存在,不管它此刻是否真的有实例驻留在内存里。调用一个 Grain,运行时会自动决定该在哪台机器上激活它;如果还没有实例,就按需现场创建一个;闲置一段时间后自动回收以释放资源;如果承载它的机器挂了,Orleans 会在别处自动重新激活它。开发者不再需要为常见场景手写「放置逻辑、存活检测、手动监督」——这些都被运行时兜底了。Orleans 最早在微软内部支撑 Halo 的云端服务得到验证,随后扩展到 Xbox 与 Skype 的基础设施。如果说 Erlang 与 Akka 是把 Actor 递到你手上让你亲自管理,Orleans 则让它看起来像一个普普通通的远程对象——位置透明成了运行时的问题,不再是你的问题。

Go 生态的实现谱系

Go 本身走的是 CSP 路线,并没有把 Actor 内建进语言(下一章会讲清楚这两者为何是近亲)——但 Go 的原语与 Actor 的形状足够贴近,还是自然长出了一片小生态。一个持有私有状态、从 channel 里取消息的 goroutine,本身就是一个微缩版 Actor:goroutine 便宜到可以开成千上万个,一个有缓冲的 channel 本身就是自带背压的 FIFO 收件箱。Go 没有白送你的是监督、位置透明和带类型的消息协议——而这恰恰是社区框架要填的坑。proto-actor 跨平台、跨语言(移植到了 C#、Kotlin),带着独特的虚拟 Actor 模型和内建的位置透明;ergo 老老实实地在 Go 里重建了 Erlang/OTP 的监督树语义,网络透明、零依赖;hollywood 押的是另一个方向——API 刻意做到极简,专注单机场景下的原始吞吐,更贴近游戏后端这类对延迟敏感的场景,而非跨节点的自动放置;goakt 居中,提供轮询、最少负载、一致性哈希等多种路由策略,存储层可插拔;Tideland Go Actor 最为克制,极简 API,不碰集群。本笔记不直接套用这些框架,而选择手写一个几百行的极简运行时——要学的是「思想」,不是背 API 表。先看懂手写版,再回头看这些框架,会觉得一切理所当然,而不是魔法。

📝 注意

换个马甲,骨架不变:Erlang 管它叫 process,Akka 管它叫 actor,Orleans 管它叫 grain,今天的 Multi-Agent 框架管它叫 agent。私有状态、一个收件箱、中间隔着消息——名词换了一代又一代,底下这副骨架从 1973 年起就没挪过窝。

AI 时代的复兴

Multi-Agent 系统的形态,天生就是 Actor 的形态——而且这不只是比喻,落到具体工具上同样成立。微软 AutoGen v0.4 已经基于 Actor 模型完全重构:每个 Agent 是一个彼此隔离的 Actor,靠发消息协作,而不是共享状态的函数调用。阿里 AgentScope 把 Actor 当作核心的分布式机制,每个 Agent 都是独立接收消息、自行计算的 Actor。Ray Actors(出自 UC 伯克利 RISELab)撑起了 OpenAI、Uber、Spotify 的分布式强化学习与大规模模型训练——每个 Actor 独占一份计算资源,持有那些共享起来并不安全的模型或训练状态。Xoscar 是推理平台 Xinference 的核心,更进一步把每个 Actor 直接绑定到一块 CPU 或 GPU 上,让「Actor」本身就是硬件分配的单位。

这一切都不是巧合。与其说是一个老模型被套用到了新场景,不如说是一次「回家」。LLM 应用恰好需要 Actor 从诞生第一天起就在提供的两样东西:每个 Agent 私有的状态(上下文、记忆、会话历史,绝不能在不同会话间串味),以及故障隔离(一次模型调用超时、被限流、或者中途失败,应该只拖垮一个 Agent,而不是整个系统)。一个 1973 年从 AI 研究里诞生的模型,当初就是为了给 AI 推理系统一套「独立个体 + 消息通信」的语言;它先后在电信交换机、JVM 后端、云端游戏服务里找到落脚点,几十年后,又重新走回了自己出发的地方。

🔑 设计钥匙

一个五十岁的模型为什么恰好契合 AI 与多智能体时代?这不是时间上的巧合——Hewitt 1973 年那篇论文,本就是为了给 AI 系统提供「独立、靠消息通信的推理单元」而写,那时硬件与负载都还远未催生出这个需求。LLM 应用几十年后自己重新发现了同一组要求:每个 Agent 的状态要私有隔离(上下文、记忆、进度互不串味),以及任意一环崩溃不能拖垮整体(模型调用超时、限流、中途失败是常态)。Actor 在 1973 年就解决了一个当时还不存在的问题;Multi-Agent AI,不过是这个负载终于姗姗来迟。

一条自然的建模路径由此浮现,这也是本笔记 demo 的设计哲学:用 Actor 构建最基础的「活动对象」;用一组 Actor 构建一个 Tool 或 Agent;多个 Agent 以消息协作聚合成处理单元;多个处理单元再集成为分布式 Agent 网络。用 Actor 打底,向上生长出 Multi-Agent 乃至 Agent Network,水到渠成,不是硬凑。

graph LR
  hewitt["1973 · Hewitt(MIT)<br/>为 AI 推理写的数学模型"] --> erlang["1986 · Erlang/OTP<br/>爱立信 · let-it-crash + 监督树"]
  erlang --> akka["2009 · Akka<br/>JVM 主流 · 类型化 Actor"]
  akka --> orleans["2014 · Orleans<br/>微软 · 按需激活的虚拟 Actor"]
  orleans --> go["2010s · Go 生态<br/>proto-actor / ergo / hollywood"]
  go --> aiera["2020s · AI 时代<br/>AutoGen / AgentScope / Ray Actors"]

Actor 模型五十年:从 AI 研究里的数学起源,到 AI 时代的复兴

小结

  • Actor 有一副写给 AI 研究用的严格数学骨架(1973 Hewitt、Bishop、Steiger),硬件迟了几十年才追上;Erlang(1986,let-it-crash + 监督树)在电信级系统里验证了工业可行性,Akka(2009,类型化消息)把它带进 JVM 主流,Orleans(2014,按需激活的 Grain)则做成了免运维的「虚拟 Actor」。
  • Go 没有内建 Actor,但 goroutine + channel 与 Actor 的形状足够接近,社区因此长出了真实的生态——proto-actor、ergo、hollywood、goakt 等在位置透明、监督语义、原始吞吐之间各有取舍;本笔记选择手写而非套框架,为的是先看穿骨架,再评判任何框架的抽象是否可信。
  • AI 时代的复兴与其说是新应用,不如说是一次回归:AutoGen、AgentScope、Ray Actors、Xoscar 不约而同选中 Actor,因为 Multi-Agent 系统恰好需要它从一开始就在提供的东西——私有状态与故障隔离;毕竟 Hewitt 当初设计它,面对的就是 AI 推理系统。
  • 下一章,把 Actor 放到你更熟悉的两门语言旁边:Go 的 CSP 与 Actor 如何同源双生,Rust 的所有权系统又如何殊途同归地消灭同一个敌人——数据竞争。
源码

正在读取完整文件…