Google Cloud 在 2026 年 5 月 20 日发布了 Agent Executor(仓库 google/ax),两位作者 Jaana Dogan 与 Ethan Bao 给它的定位是「Google 的智能体执行、恢复与分布式部署开源运行时标准」,同期由 GKE 团队开源了它依赖的底座 Agent Substrate。仓库自己的说明只有一句:
Declare an agentic task with workspaces and model specifications. AX sandboxes it, wires up its workspace, and helps running it at scale.
我抓取时仓库显示 641 次提交、约 1.19 万 star,Apache-2.0,Go 实现。README 顶部挂着一条警告,值得先放在最前面:核心概念、协议与规范仍在活跃打磨,稳定版发布前预计会引入重大破坏性变更;当前 API 版本是 ax.io/v1alpha1。
但真正决定这套设计长什么样的,是 README「Why?」那一节的第一段:
Agents are a new kind of workload. They are neither stateless microservices nor run-to-completion batch jobs. They accumulate state, need strict isolation, call out to model APIs and tool servers, and can burn money in a loop if nobody is watching.
这句话里有四个特征,每一个都恰好踩在 K8s 原生抽象的一块缺失上:
- accumulate state:状态不只是数据,还包括会话上下文、内存里的中间结果、文件系统——而 Pod 是按无状态假设设计的;
- strict isolation:跑的是模型生成的、可能不可信的代码;
- call out to model APIs and tool servers:出站流量、凭据、工具服务器都要有人管;
- burn money in a loop:这是最不像传统工作负载的一条——失败的任务通常不烧钱,失控的 Agent 会。
AX 的应对方式是把「跑一个 Agent」拆成三个正交的原语。这一步决定了后面所有的架构选择,所以我们从它开始。
一、三个原语:隔离、环境、模型
README 给了一张很短的对照表(你想要什么 → AX 给你什么),docs/concepts.md 把它展开成了三份语义。
Task:最小的隔离执行单元。 文档对它的定义里有一句话定了调子:
一个 agent 并非「一个跑到底的进程」——在其生命周期中会规划、委派、重试、分叉工作。AX 不去建模这种复杂形态,而是提供一个创建、隔离、挂起、丢弃都很廉价的原语。
这解释了 Task 看起来「字段少得可疑」的字段表:容器镜像与命令、算力请求与限制、环境变量、spec.workspaces,就这些。也解释了文档为什么特意强调「无论它是整个作业,还是 Agent 拆解问题时派生出的任务树中的根节点,每个节点都获得相同的沙箱、相同的生命周期、相同的工具链」。
AX 不打算表达「这是一个多 Agent 工作流」。它只保证你随时能廉价地复制出下一个同样的执行单元,编排的智慧留给 Agent 自己——这是一个刻意的减法。
挂载语义也在这里:每个 workspace 挂到各自独立的路径,第一个 workspace 作为命令的工作目录。这个「位置即约定」的细节后面还会出现一次,因为它是 runner 契约的一部分。
另外,Task 一经创建就是不可变的(CreateTask 的注释写着 immutable once created)。这不是随手写下的限制,第三节会展开。
Workspace:把「进入可工作状态」变成声明式的。 这一段的动机写得很实在:在 Agent 做出第一个有效动作之前,需要仓库按正确 revision 克隆、bucket 挂好、工具可用、skills 就位;如果每个需要相同环境的 Task 都重复这套准备,每个 Agent 框架都会各自重新发明一遍。所以 Workspace 要做的事被概括为「填充文件系统与工具版图」:
- Git 仓库:克隆到 workspace 路径的子目录中;
- MCP 服务器与注册表:给 Agent 调用的工具入口;
- skill 注册表:以及 skills 被物化到的目标路径。
它声明一次,可以被任意多个 Task 绑定;runner 在命令启动之前,在每个沙箱里把它物化一遍。
Model:模型配置是集群资源。 字段包括 provider、模型标识、provider 特定的生成参数(例如 max_tokens),以及一条指向 Kubernetes Secret 的凭据引用。做成集群资源的好处是运维动作收敛成一次 ax apply:轮换密钥、固定或升级模型版本、调参,都不必去几十个 Task 定义里翻找。还有一个容易被跳过的联动——AX 自己的组件也会读 Model 资源,比如在按 goal 规划 workspace 的时候。
三者之外还有一个作用域概念:atespace。每个资源都活在某个 atespace 里,默认叫 default,CLI 上用 -a 切换。它对应的需求是「同一套控制面、多份互相隔离的资源视图」。
把三个原语放进一个文件,就是 README 里那段示例:
# task.yaml
apiVersion: ax.io/v1alpha1
kind: Workspace
metadata:
name: golang
spec:
git:
- repo: https://github.com/golang/go.git
branch: "my-fix"
---
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: test
spec:
workspaces:
- name: golang
goal: "Ensure that Go tool chain is available and is built from source"
debug: true # lets you `ax ssh` into the sandbox
这段 YAML 里最不寻常的是 goal:它不是一条命令,也不是 Dockerfile 里的某个指令,而是一句自然语言的期望状态。文件系统可以声明,但「工具链可用、并且是从源码构建的」这件事,AX 选择让另一个 Agent 去完成。这是整个设计里最有野心的一笔,我们放到沙箱那一节细看。
二、一条 apply 之后发生了什么
DESIGN.md 第一段回答了一个很关键的问题:为什么不把这些任务做成 Kubernetes CRD?答案是规模:
把数百万个短生命周期任务以 CRD 形式存进 etcd,会超出 etcd 的承受范围(个位数 GB 存储上限、写入速率瓶颈、控制面性能退化)。
于是 AX 把状态放进 Redis,并用 Redis Streams 作为 API server 与水平扩展的 controller 池之间的工作队列。整条链路是这样的:
axCLI 通过 gRPC 调控制面。README 说得很直白:它「刻意做成 kubectl 的形状」——apply、get、describe、watch、delete,再加几个 agent 专属动词。CLI 会跟随当前 kube context,在后台解析并隧道到对应集群的控制平面,也可以--context直接指定而不切 context,隧道状态落在~/.ax/tunnels。ax-server是无状态 gRPC API,监听 8080(同端口上有GET /healthz),负责校验 manifest、持久化到 Redis、发布事件。- Redis 承担三件事:Task Hashes(任务状态)、Event Streams(被控制器用
XREADGROUP消费,也就是工作队列)、PubSub(事件分发)。 ax-controller是调和 Worker 池,消费 Stream,在 Agent Substrate 上供给 atespaces 与 actors,把任务驱向期望状态;横向扩展的方式就是加副本数。- Agent Substrate 接过最后一段:atespace 供给、actor 创建与激活、worker 分配。
控制面暴露的 gRPC 服务是 ax.v1alpha1.AX,RPC 数量很少:Tasks 8 个、Workspaces 4 个、Models 4 个(Get / List / Update / Delete,其中 Update 是 upsert 语义)。
这 16 个 RPC 里藏着一个我认为最值得注意的不对称:Workspace 和 Model 有 Update,Task 没有。 CreateTask 一经创建不可变,你只能 Suspend、Resume、Delete、Watch。
再结合它的动词集合看——没有「修改」,只有「暂停 / 恢复 / 观察 / 删除」——设计意图就很清楚了:Task 不是一份可编辑的配置,而是一条不可变的执行记录。 你要调整的东西在 Workspace 与 Model 里;Task 本身只负责「这一次执行」的身份与结果。WatchTask 是服务端流式 RPC,在状态与条件发生转换时实时推送,这套组合更像事件日志加订阅,而不是资源管理器。
顺带说一句:文档里并没有一个独立的 scheduler 组件。调度职责实际分散在 ax-controller 的调和逻辑与 Substrate 的 worker assignment 之间——这也是它「看起来像 K8s,但不是 K8s」的一个具体表现。
三、沙箱内部:runner 是唯一的契约
如果只读 README,很容易以为 AX 做的事情就是「把 YAML 变成 Pod」。docs/runner.md 会纠正这个印象:控制器从不把 spec.command 当作容器 entrypoint,容器命令永远是 /usr/local/bin/ax-task-runner;你的命令只通过环境变量 AX_TASK_YAML 到达 runner,由 runner 决定怎么解析、怎么启动。
这条约定推出两个硬性结果:镜像里那个路径必须存在可执行文件(符号链接或包装脚本都行);以及 spec.command 的语义完全由 runner 解释。换句话说,runner 是控制平面与 Agent 之间唯一的契约面。
启动四步
docs/sandbox.md 把 runner 的启动流程写得很清楚,四步:
- 加载
Task以及所有已绑定的Workspacespec; - 在 80 端口启动一个元数据兼 guest 管理守护进程;
- 仅在第一次运行时,按绑定顺序为每个 workspace 做准备:克隆 Git 仓库、建 skills 路径、写 MCP 配置;如果该绑定带
goal,就把 goal 交给一个 Agent 去完成环境搭建(默认 runner 里是 Antigravity,需要容器内存在GEMINI_API_KEY,默认超时 10 分钟,可用AX_BOOTSTRAP_TIMEOUT覆盖); - 以第一个 workspace 为工作目录,把
spec.command作为子进程启动并监管,同时注入AX_METADATA_URL与spec.env。
在所有 workspace(包括那次 agent 引导)完成之前,任务一律上报为 not-ready。也就是说,「就绪」这件事的语义从「文件都到位了」变成了「另一个 Agent 说它可以了」。
元数据面:让被跑的程序自省
端口 80 上是一个刻意做得很小的 HTTP 面,设计目标是「agent 无需任何 SDK 即可自省自身配置」:
| 端点 | 行为 |
|---|---|
/healthz |
runner 一存活即返回 200 |
/readyz |
workspace 未就绪返回 503,就绪后 200;控制器轮询它来设置任务的 WorkspaceReady |
/metadata/v1alpha1/ax/task |
以 YAML 返回 Task 启动配置(不含 status) |
/metadata/v1alpha1/ax/workspaces |
以多文档 YAML 流返回每个绑定的 Workspace,按绑定顺序 |
这几个端点看起来平淡,但它们是「沙箱里的程序如何知道自己是来干什么的」这个问题的答案。宿主编排系统通常把这类信息塞进环境变量或挂载文件;AX 选择开一个只读的小 HTTP 服务,让沙箱内的任何进程(不限语言、不限框架)自己来取。
三处最见功力的细节
读 runner.md 时,真正体现设计者经验的是三处:
① 命令退出后,runner 必须继续存活。 Runner 是 PID 1,容器存活时长等于 runner 存活时长。如果命令退出时 runner 也退出,元数据服务器就没了,ax ssh 也会失效。文档顺带写了一句很诚实的话:控制平面目前不会从容器读回命令的退出状态——退出码只记进日志。
② 每个 workspace 只准备一次。 Runner 必须在持久卷上记录「已设置」标记,后续启动跳过。原因写得很直接:
resume 会重启容器,重新 clone 进已恢复的工作区,会摧毁 agent 的状态。
默认 runner 的做法是为每个 workspace 路径在 /ax 下写一个 marker 文件。
③ /workspace 是 suspend/resume 中唯一幸存的东西。 Agent Substrate 在任务挂起时对 /workspace 卷做快照,恢复时还原进一个全新容器——runner 看到相同的文件,但面对的是全新的进程树。这一条是理解 AX 的「持久化执行」到底持久化了什么的钥匙:它保存的是文件系统,不是进程。
关闭时序同样是显式的:Stop 和 Suspend 都会向 PID 1 发 SIGTERM,runner 转发给命令的进程组,等待一个有界宽限期(默认 runner 是 10 秒),然后对剩余进程 SIGKILL。
debug 开关是一道安全边界
spec.debug: true 时,runner 要在同一端口用 h2c 额外提供 Agent Substrate 的 guest gRPC 服务——进程服务(启动 / 检查 / 流式输出 / 杀掉进程,ax ssh 的底层能力)与文件系统服务(workspace 内的流式读写)。文档把它为什么默认关闭说得很白:这两个服务允许沙箱内的任意进程执行与文件访问,所以当 spec.debug 为 false 时必须关闭,而 ax ssh 会拒绝连接未 opt-in 的任务。
把「调试便利」和「攻击面」做成一个显式开关、并让客户端也承担拒绝责任,这种处理方式在 Agent 基础设施里并不多见。
runner 可替换,是这套设计里最像"基础设施"的部分
默认 runner 不强制使用。任何遵守契约的二进制,都可以打包进镜像并在 spec.image 里指定,控制平面一视同仁。定制分三级,按需要选最浅的一级:
- 扩展默认镜像:pin 住 digest、保持 entrypoint,在上面
RUN apt-get install你需要的沙箱工具; - 嵌入 runner 包:import
github.com/google/ax/runner,自己调runner.Run,通过OnCommandExit钩子上传产物或通知 webhook; - 任意语言从零实现:读
AX_TASK_YAML与AX_WORKSPACES_YAML,满足契约,产物装到/usr/local/bin/ax-task-runner。
还有一个细节值得记一笔:控制器会为每个不同的「镜像 + 环境」组合提供专用的 Agent Substrate actor template,所以同一个 atespace 里可以并行跑不同 runner 的任务。默认 runner 镜像本身是 Python 3.12 + git/curl/openssh-client + Antigravity——这是 Google 风味的默认路径,但只是默认,不是唯一。
四、生命周期:一个词的 phase,和可以等待的 conditions
AX 的状态模型是两层:status.phase 是一句话的粗粒度摘要(Running、Suspended、Failed、Terminating 等),细节全部由 Conditions 承载。文档定义了两种 condition:
WorkspaceReady:所有 workspace 都完成设置后为 True,此后持续保持 True;Ready:任务正在运行且WorkspaceReady为 True。文档在这里加了一句很实用的提示——这是应当等待的条件。
三个动词对应的状态流转也都写清楚了:ax suspend 把 Ready 置为 False(reason 是 TaskSuspended);ax resume 把它置回 True;ax delete 让任务进入 Terminating,控制器拆除沙箱,记录被完全移除,而且 ax delete 会阻塞直到这个过程结束。
把这套接口放在一起看,它的服务对象其实不是人:Ready 是一个可以 wait 的条件,pause/resume 是一等动词,delete 有明确的终结语义且同步返回结果,WatchTask 是流式订阅。这是一套给 CI、给上层调度器、甚至给另一个 Agent 用的控制面——毕竟标题里那个词是 orchestrator,编排者本身也会是程序。
观测手段也按受众分好了:ax watch 流式看变化,ax describe 给人读详情,ax get task 拿完整 YAML(spec + 实时 status),ax ssh 进沙箱看现场。
五、底座:密度不在 AX 里,在 Agent Substrate 里
有一个容易被忽略的事实:AX 只是半个系统。README 明确写着,AX 把每个任务都作为沙箱化 actor 调度在 Agent Substrate 上,所以部署 AX 之前 Substrate 必须在集群里跑起来——它装在 ate-system 命名空间,控制 API 暴露在 api.ate-system.svc.cluster.local:443,AX 默认就在那里找它。而 AX 自己装进 ax-system,make deploy 会先部署 Redis,再用 ko 构建并部署控制面镜像。
Substrate 的自我定位是「secure-by-default 的 Agent 执行运行时」,目标是在比标准容器运行时高 10 倍的密度上运行数百万个沙箱,resume 小于 500ms、suspend/resume 激活速率大于 500 次/秒,并提供零信任的内核与网络隔离,支持 microVM 与 gVisor 两种沙箱技术且生命周期操作一致。
它的关键机制是一个赌注:Agent 类应用大部分时间处于空闲。于是 Substrate 把大量 actor 映射到少量 ready worker 上做重度复用——官方 demo 是在一个集群里把约 250 个有状态 actor 复用(multiplex)到仅 8 个物理 pod 上,30 倍以上的超额订阅。支撑这件事的是全量状态快照:挂起时把易失性 RAM 与文件系统状态一起保留,恢复时可以落回池中任意一个 worker(他们管这叫 Actor Teleport)。
它和 Kubernetes 的分工也很清楚:用 K8s 做基础设施供给与 worker 生命周期管理(worker 是 Pod,可复用 Pod autoscaling),Substrate 自己实现 Agent 专属的调度与控制,来换取更低延迟。生态上它是框架无关的——ADK、LangChain、Claude Code / CodeX / Antigravity 都能跑,MCP server 也可以作为 actor 部署;CNCF sandbox 项目 kagent 已经在用它。
回到 AX:它的 suspend/resume/checkpoint/fork 语义,大半是由底座兑现的。AX 定义接口,Substrate 提供能力。这也给了一个判断平台上限的方法——不要看 API 长什么样,要看底座是不是真的能把「挂起时的内存」留住。
六、路线图:从 v1alpha1 到生产还差三道关
docs/roadmap.md 列了五个方向:稳定核心规范、Actor 架构、Agentic 环境、网络身份治理与可观测性、文档。把工程上的实质收拢一下,其实是三道必须跨过的关。
第一关:规范稳定化。 目标是冻结 ax.io/v1alpha1 的 schema,以及 API、CLI、控制器之间的生命周期契约。范围包括:Task 的生命周期阶段、状态条件、资源限制、token 与超时预算、审批策略;Workspace 的 Git 检出、静态/注册表 MCP 服务器、skill 注册表、挂载路径、goal 覆盖;Model 的 provider 绑定、模型标识、采样参数、凭据引用;以及 Sandbox / SandboxConfig——运行时隔离后端、内核与系统调用约束、文件系统挂载、安全配置。
注意最后这一项:Sandbox 目前还只是路线图条目,不是 v1alpha1 里的一种 kind。隔离相关的配置将来会以什么形状暴露,现在还没有答案。
第二关:Actor 架构重构。 这一关最实在,四条:
- 把环境准备拆成独立的 setup actor:Git 克隆、MCP 与 skill 物化、goal 驱动 bootstrap 从任务运行时里解耦出来,准备完再把状态移交给任务 actor;
- 最小权限:setup actor 与任务 actor 各配一套严格策略——仓库与注册表凭据、setup 阶段的出站流量只限初始化期间;任务 actor 只保留最小运行时权限、出站与能力;
- 空闲检测与自动挂起:持续监控 actor 活动(进程执行、I/O、网络流量、活跃的 gRPC/SSH 会话),检测到空闲就自动触发检查点挂起,回收 CPU 与内存来提高集群密度;
- 有状态任务分叉:把运行中或已挂起的 Task——包括检查点内存与工作区文件系统状态——fork 成多个并行任务,用来并发探索投机执行路径。
第三关:身份、治理与可观测。 三条:为每个 Task actor 签发任务级 SPIFFE 身份(X.509-SVID),实现任务、MCP 服务器与内部服务之间的零信任 mTLS;满足 Google 平台治理基线(MCP 与技能注册表的访问控制、策略执行、预算护栏、审计合规);在 runner 层自动收集并导出 OpenTelemetry 指标、分布式追踪与结构化 agent 轨迹——提示词、模型响应、工具调用、进程执行、生命周期转换,无需用户容器做任何埋点。
第三条如果真做到,价值会超出 AX 本身:Agent 的「可观测性」目前普遍靠框架自己埋点,谁能在运行层无侵入地拿到完整轨迹,谁就有资格做审计和成本归因。
另外还有两个必须先接受的现实:README 的破坏性变更警告仍然挂着;底座 Substrate 也是 pre-1.0、不保证向后兼容,而且它的 worker 容量是按版本标签控制的——新加入的节点必须手动打上 ate.dev/substrate-version 标签才会承载 worker。
七、我的几点判断
第一,它抢的不是 Agent 框架的位置,是集群控制面的位置。 三个原语里没有 graph、没有 chain、没有 node、没有 state machine——没有任何一个概念在描述「Agent 内部怎么编排」。AX 不管你用什么框架写 Agent(runner 可替换、harness 无关),只负责把不可信的 Agent 隔离开、把环境喂饱、按期望状态驱动到结束。这也让我重新读懂了 README 那句 “If you have used Kubernetes, ax will feel similar”——它不是营销类比,是设计目标本身。
第二,Workspace 上的 goal 是最有野心、也最脆弱的一笔。 让一个 Agent 去把环境搭好,等于承认「环境该长什么样」这件事有时只能意会。它把不确定性引入了启动路径:依赖模型、依赖凭据(GEMINI_API_KEY)、有一个默认 10 分钟的超时。而路线图里「setup actor 拆分 + 最小权限 + 动态策展」三条,恰好就是在给它补课——先让它可审计(独立 actor、凭据只在初始化阶段可见),再让它可预测(自动检查仓库、解析工具链、从注册表发现 MCP 与 skills)。我的判断是:这个机制会成为「Agent 平台」与「CI 系统」之间的分水岭。CI 用确定性构建换可复现,Agent 平台愿意用可控的不确定性换覆盖能力。
第三,不可变的 Task 是最容易被忽略、但最像"运行时"的信号。 CreateTask 不可变、没有 UpdateTask,而 Workspace 与 Model 都是 upsert 语义。这说明设计者把 Task 当「执行记录」而不是「配置文件」。要接进自己的系统,就得先接受这个约束:你调整的对象是 Workspace 与 Model,任务一旦开始,它只负责被观察、被暂停、被恢复、被删除。
第四,用 Redis 换 CRD 是一次清醒的取舍。 DESIGN.md 的论证没有绕弯(etcd 存不下数百万短生命周期任务)。代价是放弃了 K8s 原生的观测与工具链,换来的是能承载的规模,再用 kubectl 手感把体验补回来。我认为这种「借 K8s 的形、不借 K8s 的里」的做法会在 Agent 基础设施里越来越常见——因为 Agent 这种负载的写入频率与生命周期特征,本来就不适合 CRD 的模型。
第五,现在该怎么用它:不要放进生产关键路径。 v1alpha1 + 底座 pre-1.0 + 破坏性变更预告,已经把它定位成设计参考与实验平台。真正值得现在抄走的是三件事:把隔离、环境、模型拆成三个正交原语;让环境声明化,甚至允许 Agent 参与搭建;在沙箱内提供一个无需 SDK 的元数据面,让被跑的程序自己知道「我是谁、我要做什么」。这三条与具体实现无关,换掉 AX 也成立。
Kubernetes 当年把「怎么跑一个服务」标准化了;AX 想标准化的是「怎么跑一个会思考、会烧钱、还会忘记自己在干什么的东西」。这个标准现在只是 v1alpha1——但方向比实现更值得看。
延伸阅读
- 仓库:google/ax(Apache-2.0,
ax.io/v1alpha1) - 发布博客:Agent Executor, Google’s distributed Agent Runtime(2026-05-20,Jaana Dogan & Ethan Bao)
- 执行底座:agent-substrate/substrate
- 关键文档:DESIGN.md、docs/concepts.md、docs/sandbox.md、docs/runner.md、docs/roadmap.md
- 生态参照:CNCF sandbox 项目 kagent 同样构建在 Agent Substrate 之上
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付