# AX：用 Kubernetes 的方式编排 AI Agent

最近 Google 在 GitHub 上低调开源了一个新项目 [**AX**](https://github.com/google/ax)（Agent Executor / Agentic Orchestrator），口号：

> 一个高吞吐、声明式的编排器，用来在集群里运行数十亿个自治 Agent 工作负载。

## 为什么需要 AX：Agent 是一种新型工作负载

AX 的 README 开篇就点明了主旨：**Agent 既不是无状态微服务，也不是跑完即退的批处理任务**。它们有几个让传统基础设施头疼的特点：

*   **有状态**：Agent 在生命周期内不断积累上下文、文件、中间结果，不能随便重启；
    
*   **要隔离**：Agent 会执行模型生成的不可信代码，必须有沙箱和资源限额；
    
*   **常发呆**：等模型返回、等工具调用、等人工确认，90% 以上的时间是空闲的——但状态必须留着；
    
*   **会花钱**：一个陷入循环的 Agent 可以不停调用模型 API 直到账单爆炸。
    

Kubernetes 擅长管理无状态服务，也能管理 Job 之类的有状态任务，但是 Agent 工作负载稍有区别。AX 的思路是：**把 Agent 任务变成一条可以** `apply`**、可以观察、可以挂起、可以恢复的集群工作负载**，就像 `kubectl` 运行容器化工作负载一样。

为此 AX 站在另一个 Google 开源项目 [**Agent Substrate**](https://github.com/agent-substrate/substrate) 的肩膀上：Substrate 负责底层的沙箱执行与状态快照，AX 负责上层的声明式编排。两者都是 Apache-2.0 协议，都处于 pre-1.0 阶段（API 随时可能大变，官方 README 专门加了粗体警告）。

## 组件与功能

先看一张整体组件关系图：

![](https://cdn.hashnode.com/uploads/covers/680de39180c68f1c7a6ae2f1/3be3190e-7a2a-4c8e-b648-730e4db676ea.png align="center")

### AX 层：三个声明式原语 + 一个 kubectl 风格的 CLI

所有资源都用 `ax.io/v1alpha1` 的 YAML 清单描述，通过 `ax apply -f` 提交，体验和 Kubernetes 几乎一致。

#### Task —— 最小隔离执行单元

声明容器镜像、启动命令、CPU/内存请求与上限、环境变量，以及要挂载的 Workspace。每个 Task 都在独立沙箱里运行，是"便宜到可以随时创建、隔离、挂起、丢弃"的基本单元。一个 Agent 可以只用一个 Task，也可以在拆解问题时派生出一棵 Task 树——每个节点获得相同的沙箱和生命周期语义。

Task 的生命周期用 `status.phase`（Running / Suspended / Failed / Terminating…）和 Conditions（`WorkspaceReady`、`Ready`）表达，可以用 `ax watch` 实时观察。

\*\* Workspace —— 让 Agent "热启动"的环境声明\*\*

Agent 开始干活前的准备工作很繁琐：克隆指定分支的代码仓库、配置允许调用的 MCP 服务器、安装技能包（Skills）。Workspace 把这一切声明化：声明一次，被任意多个 Task 引用，运行器（Runner）会在命令启动前把环境物化到沙箱里。

有趣的是 `goal` 字段：你可以用自然语言描述想要的环境（例如"确保 Go 工具链从源码构建完成"），首次启动时会由一个内置 Agent 自动完成环境搭建。

#### Model —— 模型供应商配置

声明用哪家供应商、哪个模型、生成参数，以及存放凭证的 Kubernetes Secret。把模型配置变成集群资源后，换密钥、锁版本、调参数都只需要一次 `ax apply`，而不是翻遍每个 Agent 的环境变量。AX 自己的组件（比如执行 Workspace goal 的引导 Agent）也读 Model 资源。

#### ax CLI —— kubectl 风格的操作面

`apply / get / describe / watch / delete` 之外，还有几个 Agent 特色的动词：

*   `ax suspend task <name>` —— 对 Actor 做检查点（checkpoint），挂起并释放算力；
    
*   `ax resume task <name>` —— 从快照精确恢复到挂起前的状态；
    
*   `ax ssh <name> -- <cmd>` —— 直接 shell 进运行中的沙箱"围观"Agent 在干什么（需要 Task 声明 `debug: true`）；
    
*   `ax ctx` / `ax tunnel` —— CLI 通过 gRPC 连接控制面，跟随当前 kube context 自动建隧道。
    

#### 控制平面组件

部署在 `ax-system` 命名空间里：

*   **ax-server**：gRPC API 服务，CLI 的所有请求都打到这里；
    
*   **ax-controller**：核心协调器，把 Task 翻译成 Substrate 的 ActorTemplate 和 Actor，驱动挂起/恢复，轮询沙箱的就绪探针；
    
*   **Redis**：任务事件流与状态存储；
    
*   **ax-task-runner**：每个 Task 容器里的 PID 1。它是控制面和"Agent 实际进程"之间的桥梁——负责物化 Workspace、启动并监管用户命令、暴露 `/healthz` `/readyz` 和元数据服务，`debug: true` 时还提供 guest services（`ax ssh` 走的就是它）。你可以换成自己的 runner 镜像，只要遵守同样的契约。
    

### Agent Substrate 层：沙箱执行与高密度复用

Substrate 的核心洞察是：**Agent 大部分时间在 idle，因此可以把大量"Actor"（有状态的 Agent 进程）多路复用到少量"Worker"（物理 Pod）上**。官方演示里，250 个有状态 Actor 只跑在 8 个物理 Pod 上，挂起/恢复做到亚秒级。

主要组件（部署在 `ate-system` 命名空间，`ate` 是 Agent Task Execution 的缩写）：

*   **ate-api-server**：控制面 API（gRPC），管理 Actor/Worker 生命周期，状态存在 PostgreSQL；
    
*   **ate-controller**：Kubernetes 控制器，调和 `WorkerPool` 自定义资源（Worker 池的副本数、镜像、资源）；
    
*   **atelet**：节点级 DaemonSet，监督本节点的 Worker Pod、协调快照与状态迁移；
    
*   **atenet**：网络层（基于 Envoy 的 router + egress 代理），负责把流量路由到"此刻正好醒着"的 Actor，必要时先唤醒再转发（request parking）；
    
*   **ateom-gvisor / ateom-microvm**：跑在 Worker Pod 内部的沙箱助手，分别用 gVisor 和 microVM（Cloud Hypervisor）执行 Actor，负责 `runsc` 的检查点/恢复；
    
*   **podcert-controller**：为 Pod 证书签发提供 polyfill（该能力未来会进入上游 Kubernetes）；
    
*   **rustfs**：S3 兼容对象存储，保存 Actor 的快照（挂起时内存页和文件系统的镜像）。
    

沙箱隔离默认用 gVisor（有 KVM 的机器上可用 microVM），Actor 出网流量受 egress 策略管控——用来防止烧钱。

### 一个 Task 的旅程：从 `ax apply` 到挂起恢复

把两层串起来，一个 Task 从下发到执行、再到挂起恢复的完整流程如下（对应本文实测的每一步）：

![](https://cdn.hashnode.com/uploads/covers/680de39180c68f1c7a6ae2f1/3d47a6b3-ce19-48c3-b587-d234bc436b47.png align="center")

几个值得注意的细节：新 Task 创建后默认是 **Suspended**，要显式 `ax resume` 才会占用 Worker——这正是“为 idle 而生“的设计；挂起后快照落在对象存储里，恢复时**可以调度到任何一个有空闲容量的 Worker** 上（Actor Teleport），不必回到原节点。

## 本机实测：在 macOS 上跑通 Hello World

### 测试环境

*   MacBook（Apple Silicon / arm64），Docker（OrbStack）运行中
    
*   Go 1.27.1、kubectl v1.37、`ko` v0.19.1（`brew install ko`）
    
*   本地 K8s 用 kind（v1.37.0 节点），无需 GCP
    

> [**ko**](https://github.com/ko-build/ko) 是 Google 开源的 Go 容器镜像构建工具：不需要写 Dockerfile，`ko build ./cmd/myapp` 就能把 Go 二进制打进一个极简基础镜像并推送到 registry；它还认得 YAML 里的 `ko://github.com/xxx/cmd/app` 镜像引用，`ko apply` / `ko resolve` 会自动构建、推送并改写成真实镜像 digest 后再提交给集群。可以理解为"Go 项目的 docker build + push + kubectl apply 合一体"，Knative 等 Google 系项目都在用。AX 和 Substrate 的所有组件镜像都是靠它构建部署的。

### 拉起 kind 集群并安装 Agent Substrate

Substrate 自带了本机开发脚本，一条命令创建 kind 集群 + 本地镜像 registry（端口 5001）：

```bash
git clone https://github.com/agent-substrate/substrate.git
cd substrate
hack/create-kind-cluster.sh
hack/install-ate-kind.sh --deploy-ate-system
```

脚本会用 `ko` 从源码构建 ateapi、atelet、atenet 等组件镜像推入本地 registry，再部署 PostgreSQL、rustfs 和全套控制面。装完验证：

```bash
$ kubectl get svc api -n ate-system
NAME   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
api    ClusterIP   None         <none>        443/TCP   45m
```

### 部署 AX 控制平面

```bash
git clone https://github.com/google/ax.git
go install github.com/google/ax/cmd/ax@latest   # 安装 CLI
cd ax
make deploy AX_IMAGE_REPO=localhost:5001/ax     # ko 构建并部署 ax-server / ax-controller / Redis
```

### 创建 WorkerPool

这是一个 README 没写清楚的步骤：AX 的 Task 最终要调度到 Substrate 的 Worker 上，而新集群里没有任何 WorkerPool，直接跑任务会报 `no free workers available`。参照 Substrate 的 counter 示例，为 `default` 命名空间建一个 gVisor WorkerPool：

```yaml
# workerpool.yaml
apiVersion: ate.dev/v1alpha1
kind: WorkerPool
metadata:
  name: ax-workers
  namespace: default
spec:
  replicas: 1
  workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor
  template:
    nodeSelector:
      ate.dev/substrate-version: "1d7ca8c"   # 与安装时节点打的版本标签一致
    resources:
      limits:   { cpu: "2",  memory: 2Gi }
      requests: { cpu: 250m, memory: 2Gi }
```

用 `ko resolve` 构建 ateom-gvisor 镜像并一并提交：

```bash
cd substrate && KO_DOCKER_REPO=localhost:5001 ko resolve -f ../workerpool.yaml | kubectl apply -f -
```

### 自建 task-runner 镜像

官方示例里的任务镜像 `gcr.io/ax-substrate/ate-images/ax-task-runner` **匿名拉取会被拒绝**（403，该仓库未公开），所以本机自建一份。注意两点：kind 节点是 arm64，Makefile 里硬编码了 amd64，需要改；Substrate 要求 Actor 镜像**必须用 digest 固定**（镜像变了快照就失效）：

```bash
cd ax
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \
  -o bin/linux_arm64/ax-task-runner ./cmd/ax-task-runner
sed 's|bin/linux_amd64|bin/linux_arm64|' Dockerfile.task-runner > /tmp/Dockerfile.arm64
docker build --platform linux/arm64 -t localhost:5001/ax-task-runner:latest -f /tmp/Dockerfile.arm64 .
docker push localhost:5001/ax-task-runner:latest
```

（`localhost:5001` 会被 Substrate 的镜像缓存自动改写成集群内的 `kind-registry:5000`，无需手动处理。）

### 声明并运行 Hello World

最小的 Task——不带 Workspace 和 Model，开 `debug` 以便 ssh：

```yaml
# hello-task.yaml
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
  name: hello-ax
  atespace: default
spec:
  image: "localhost:5001/ax-task-runner@sha256:8cf61d06…"   # digest 固定
  env:
    - name: GREETING
      value: "Hello from AX!"
  debug: true
```

```bash
ax apply -f hello-task.yaml
ax resume task hello-ax        # 新 Task 默认是 Suspended，需要显式 resume
ax get tasks
```

```plaintext
NAME       ATESPACE   PHASE     ACTOR      WORKER-IP     AGE
hello-ax   default    Running   hello-ax   10.244.0.25   1m
```

`ax describe` 可以看到 `Ready=True (TaskRunning)`、`WorkspaceReady=True (SetupComplete)` 两个条件。

### SSH 进沙箱看看

```bash
$ ax ssh hello-ax -- sh -c 'echo "$GREETING"; uname -a; ls -la /workspace'
Hello from AX!
Linux actor 4.19.0-gvisor #1 SMP Sun Jan 10 15:06:54 PST 2016 aarch64 GNU/Linux
total 0
drwx------ 1 root root   0 Sep 26 17:04 .
drwx------ 1 root root 100 Sep 26 17:04 ..
```

注意 `uname` 的输出：**内核是** `4.19.0-gvisor`——这个 shell 确实跑在 gVisor 沙箱里，而不是普通容器。

### 挂起 / 恢复

先往工作目录写个文件，然后挂起：

```bash
ax ssh hello-ax -- sh -c 'echo "state survives suspend" > /workspace/memo.txt; date >> /workspace/memo.txt'
ax suspend task hello-ax
# PHASE 变为 Suspended，Worker 资源被释放
```

再恢复，读回文件：

```bash
$ ax resume task hello-ax && ax ssh hello-ax -- cat /workspace/memo.txt
state survives suspend
Sat Sep 26 17:11:44 UTC 2026
```

挂起时 Substrate 对 Actor 做了完整检查点（内存页 + `/workspace` 文件系统打成快照存入 rustfs），恢复后从断点继续——这正是"Agent 大部分时间 idle"场景的省钱关键：空闲时不占算力，醒来时上下文分毫不差。

### M1 踩坑记录

1.  **代理传播问题**：宿主机 shell 里的 `HTTP_PROXY=127.0.0.1:7890` 会被 kind 传进节点、被 buildkit 传进构建容器，而容器里的 `127.0.0.1` 是它自己——所有拉取全部失败。解决：创建集群和构建时把代理指向宿主机真实地址（如 `192.168.x.x:7890`），并把 `kind-registry` 加进 `NO_PROXY`。
    
2.  **官方 runner 镜像拉不动**：示例中的 `gcr.io/ax-substrate/ate-images/ax-task-runner` 未公开，必须自建（且注意 arm64）。
    
3.  **镜像必须 digest 固定**：用 `:latest` 会被 Substrate 拒绝（`must be pinned by digest`）。
    
4.  **快照 bucket 缺失**：AX 控制器默认把快照写到 `gs://snapshot-substrate-test-ax-substrate/`，而本地 rustfs 只初始化了 `ate-snapshots` 桶，导致首次挂起报 `NoSuchBucket`。在 rustfs 里手动建同名桶即可： `aws --endpoint-url http://rustfs.ate-system.svc:9000 s3api create-bucket --bucket snapshot-substrate-test-ax-substrate`
    
5.  **resume 竞态**：任务刚创建就 resume，控制器可能先处理创建事件把任务又挂回去；再执行一次 `ax resume` 即可。
    

## 总结

AX 的思路很清晰：把沙箱、状态、挂起恢复、网络策略这些工程地基收敛成三个声明式原语和一套 kubectl 风格的工具链，底层由 Agent Substrate 提供高密度复用和亚秒级快照。

它适合的场景是：需要在集群里大规模、可审计、可管控地运行自治 Agent 任务的团队。需要提醒的是，项目目前处于非常早期（pre-1.0，README 明说随时可能有破坏性变更），文档与代码存在脱节（示例镜像私有、WorkerPool 需要自建、默认快照桶名对不上），离生产可用还有距离。但作为观察"Agent 基础设施"演进方向的样本，非常值得动手一试。

## 参考链接

*   AX：https://github.com/google/ax
    
*   Agent Substrate：https://github.com/agent-substrate/substrate
    
*   概念文档：AX 仓库 `docs/concepts.md`，Substrate 仓库 `docs/api-guide.md`
