1. 从“ax”这个标题说起:一个被低估的运行时编排切口
第一次看到“ax”这个标题,很多人会以为是某个命令行工具的缩写,或者某个前端框架的代号。但把热搜词摊开来看——agentic、orchestration、runtime、Kubernetes——这四个词拼在一起,指向的其实是一个非常具体的工程命题:在 Kubernetes 之上,如何为 agentic 工作负载提供一个可编排、可观测、可复现的运行时层。ax 在这里不是一个孤立的产品名,而更像是一个代号,代表“agent execution”这一类运行时抽象。
我之所以对这个方向感兴趣,是因为过去一年里,围绕 agentic 系统的讨论大多停留在 prompt 编排和工具调用层面,真正落到 runtime 这一层的工程实践少得可怜。大家热衷于讨论 agent 能做什么,却很少讨论 agent 跑在哪里、怎么被调度、失败了怎么恢复、多个 agent 之间怎么共享状态。而 Kubernetes 生态里,Karmada 这类多集群编排项目已经在往 agentic cloud 的方向靠,说明基础设施层已经开始认真对待这类负载了。
这篇文章适合三类人看:一是正在把 agent 应用往生产环境推的后端工程师,二是负责平台建设、需要评估 agent 运行时方案的架构师,三是对 Kubernetes 编排机制感兴趣、想了解 agentic 场景特殊性的运维同学。我会从设计思路、核心机制、实操落地、问题排查四个层面,把 ax 这类 agentic runtime 在 Kubernetes 上的完整链路拆开讲。不堆概念,尽量用我在实际搭建和调试过程中踩过的坑来说明问题。
2. 整体设计思路:为什么 agentic runtime 不能直接套用普通微服务模型
2.1 agentic 负载和普通服务的本质差异
普通微服务是无状态的、请求驱动的、生命周期相对确定的。一个 HTTP 请求进来,处理完返回,Pod 该重启重启,该扩缩容扩缩容,Kubernetes 的原生抽象基本够用。但 agentic 负载不一样,它有几个很别扭的特性。
第一,执行过程是有状态的且长时运行的。一个 agent 任务可能跑几分钟甚至几小时,中间要维护对话历史、工具调用结果、中间推理状态。你不能像处理 HTTP 请求那样随时把它杀掉重启,否则上下文就丢了。
第二,执行路径是动态的、非确定的。同一个任务,agent 可能走不同的工具调用链,可能中途需要人工介入,可能因为外部 API 限流而暂停。这意味着你不能用固定的 Deployment 副本数来描述它,调度器需要理解“这个 agent 现在处于什么阶段”。
第三,资源消耗是脉冲式的。agent 在推理时吃 GPU 或大量 CPU,在等待工具返回时几乎不占资源,在等待人工确认时完全挂起。如果按峰值分配资源,浪费极大;如果按均值分配,又会在推理时被限流。
这三个特性决定了,直接把 agent 塞进普通 Deployment 里跑,短期能work,长期一定出问题。ax 这类 runtime 的核心价值,就是在这三个特性上做针对性抽象。
2.2 为什么选择 Kubernetes 作为底座而不是自建调度
有人会问,既然 Kubernetes 原生模型不匹配,为什么不自己写一个调度器?我的判断是:Kubernetes 提供的不是调度能力本身,而是一整套围绕调度的生态契约。CRD、Operator 模式、准入控制、RBAC、网络策略、存储编排、可观测性接入,这些东西自建的成本极高,而且很难和现有基础设施打通。
ax 这类 runtime 的正确姿势,不是绕过 Kubernetes,而是在它之上加一层 agent-aware 的抽象。具体来说,就是用 CRD 定义 agent 任务的生命周期,用 Operator 实现状态机,用 Kubernetes 原生的调度和资源管理能力做底层支撑。这样既复用了生态,又解决了 agentic 负载的特殊性。
我实测下来,这个思路的收益很明显:原本需要自己维护的任务队列、状态存储、失败重试逻辑,大部分可以下沉到 Kubernetes 的控制器模式里,业务侧只需要关注 agent 本身的逻辑。
2.3 编排层的关键设计取舍
在编排层,有几个绕不开的取舍,我逐个说。
取舍一:agent 任务用 Job 还是自定义 CRD。Job 的好处是原生支持、语义清晰,坏处是它假设任务是批处理式的、有明确完成态。agent 任务虽然也有完成态,但中间状态太多,Job 的 status 字段不够表达。我的做法是用自定义 CRD,但在控制器里复用 Job 的部分语义,比如 backoffLimit、activeDeadlineSeconds 这些概念可以借鉴。
取舍二:状态存哪里。选项有三个:存在 Pod 本地、存在外部数据库、存在 Kubernetes 的 etcd 里。本地存储的问题是 Pod 重启就丢;etcd 的问题是写入频繁、不适合存大块对话历史;外部数据库最灵活,但引入了额外依赖。我最终选的是混合方案:关键状态机信息存在 CRD status 里,大块上下文存在外部对象存储或数据库里,通过引用关联。这样既保证了状态可恢复,又不会把 etcd 撑爆。
取舍三:多 agent 之间怎么通信。直接走 Kubernetes Service 是最自然的,但 agent 之间的调用往往是动态的、临时的,为每个 agent 建一个 Service 太重。我采用的是在 runtime 层维护一个轻量的服务发现表,agent 通过 runtime 提供的 sidecar 或 SDK 来寻址,底层还是走集群网络,但省去了大量 Service 对象的创建和销毁。
这三个取舍没有标准答案,取决于你的规模、团队技术栈和运维能力。但思路是一致的:尽量复用 Kubernetes 的原生能力,只在真正不匹配的地方做自定义。
3. 核心机制拆解:ax runtime 的四个关键组件
3.1 Agent 任务的生命周期抽象
ax runtime 里最核心的抽象是 AgentTask 这个 CRD。它的 spec 定义了任务要做什么、用什么镜像、需要什么资源、超时多久、失败怎么重试。status 则记录了当前处于哪个阶段、已经执行了哪些步骤、产出了什么中间结果。
生命周期大致分这几个阶段:Pending(等待调度)、Initializing(拉镜像、挂载存储、注入配置)、Running(agent 主逻辑执行中)、Waiting(等待外部事件,比如工具返回或人工确认)、Succeeded、Failed、Suspended(被主动挂起)。
这里有个容易踩的坑:Waiting 状态和 Running 状态的区分。如果 agent 在等一个外部 API 返回,它到底算 Running 还是 Waiting?我的判断标准是看它是否占用计算资源。如果只是阻塞在 IO 上、不消耗 CPU/GPU,就标为 Waiting,这样调度器可以把它当作低优先级负载处理,甚至在资源紧张时临时驱逐。这个区分在集群资源紧张时非常关键,我后面会展开讲。
3.2 编排器如何做 agent-aware 调度
Kubernetes 默认调度器看的是资源请求和节点亲和性,它不理解“这个 agent 需要和另一个 agent 共享缓存”或者“这个 agent 必须在有特定工具链的节点上跑”。ax runtime 的编排器在默认调度器之上加了一层过滤和打分逻辑。
具体做法是:在 AgentTask 的 spec 里声明约束,比如requiredTools、affinityTo、cacheLocality。编排器在创建 Pod 之前,先根据这些约束筛选出候选节点,然后通过 nodeSelector 或 affinity 把 Pod 绑上去。对于更复杂的约束,比如“这两个 agent 必须能低延迟通信”,可以用拓扑感知的调度策略,把它们放到同一可用区甚至同一节点。
实测下来,这套机制在几十个 agent 并发的规模下工作良好。但要注意,约束越多,可调度性越差。我见过有人给每个 agent 都加了七八条约束,结果集群里明明有资源,任务却一直 Pending。经验是:只加真正必要的硬约束,其余用软亲和性表达。
3.3 状态持久化与恢复机制
agent 任务最怕的就是跑到一半挂了,上下文全丢。ax runtime 的恢复机制分三层。
第一层是检查点。agent 在执行过程中,定期把关键状态写入外部存储。写入频率是个权衡:太频繁影响性能,太稀疏恢复时丢太多。我的经验值是每完成一个工具调用或每 30 秒写一次,取两者中较频繁的。
第二层是状态机重建。Pod 重启后,runtime 从 CRD status 和外部存储里读取最后的状态,重新构造 agent 的上下文。这里的关键是,agent 的逻辑要设计成幂等的,或者至少能从任意检查点继续。
第三层是任务级重试。如果整个 Pod 无法恢复,runtime 会创建一个新的 Pod,从最后一个检查点继续。这要求检查点里包含足够的信息,让新 Pod 能接着跑。
注意:检查点里不要存敏感信息,比如 API key、用户凭证。这些应该通过 Secret 挂载,恢复时重新注入。
3.4 可观测性接入:日志、指标、追踪一个都不能少
agentic 系统的可观测性比普通服务难做,因为执行路径不固定,你很难预先定义好要监控什么。ax runtime 的做法是提供统一的 sidecar,自动采集三类数据。
日志方面,sidecar 把 agent 的标准输出和结构化日志都收集起来,打上 task ID、step ID、agent ID 等标签,方便按任务维度检索。指标方面,除了常规的 CPU/内存,还采集 agent 特有的指标,比如工具调用次数、推理耗时、等待时长、重试次数。追踪方面,每个 agent 任务生成一个 trace,内部的每次工具调用、每次推理都是一个 span,这样能完整还原执行路径。
我踩过的一个坑是:指标基数爆炸。如果给每个 task ID 都打标签,Prometheus 很快就会被高基数指标拖垮。解决办法是,task ID 只放在日志和 trace 里,指标只保留 agent 类型、阶段、结果这些低基数维度。
4. 实操落地:从零搭一个最小可用的 ax runtime
4.1 环境准备与依赖清单
先列一下我用的环境,方便你对照。
| 组件 | 版本 | 说明 |
|---|---|---|
| Kubernetes | v1.26.0 | 实测这个版本稳定,v1.28+ 也可以 |
| 容器运行时 | containerd 1.6+ | 注意别用已经废弃的 dockershim |
| Operator SDK | v1.30+ | 用来生成 CRD 和控制器骨架 |
| 对象存储 | MinIO 或 S3 兼容 | 存检查点和上下文 |
| 消息队列 | NATS 或 Redis Stream | agent 间通信和事件通知 |
| 可观测性 | Prometheus + Loki + Tempo | 指标、日志、追踪 |
安装 Kubernetes 时,preflight 检查经常会报容器运行时的问题,比如container runtime is not running。这个多半是 containerd 配置不对,检查/etc/containerd/config.toml里的SystemdCgroup是否设为 true,然后重启 containerd。
4.2 定义 AgentTask CRD
CRD 是整个 runtime 的契约,定义清楚了后面就顺了。下面是我用的简化版。
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: agenttasks.ax.example.com spec: group: ax.example.com versions: - name: v1alpha1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: image: type: string command: type: array items: type: string resources: type: object properties: cpu: type: string memory: type: string gpu: type: integer timeoutSeconds: type: integer checkpointIntervalSeconds: type: integer requiredTools: type: array items: type: string maxRetries: type: integer status: type: object properties: phase: type: string lastCheckpoint: type: string currentStep: type: string retryCount: type: integer scope: Namespaced names: plural: agenttasks singular: agenttask kind: AgentTask shortNames: - at这个 CRD 里,checkpointIntervalSeconds和maxRetries是两个关键字段。前者控制检查点频率,后者控制重试上限。我一般把 checkpointInterval 设成 30,maxRetries 设成 3。设太大容易无限重试,设太小又浪费。
4.3 控制器逻辑:状态机怎么跑
控制器是 runtime 的大脑,它 watch AgentTask 对象,根据当前状态决定下一步动作。核心逻辑是一个状态机。
func (r *AgentTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var task axv1alpha1.AgentTask if err := r.Get(ctx, req.NamespacedName, &task); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } switch task.Status.Phase { case "": return r.initializeTask(ctx, &task) case "Pending": return r.scheduleTask(ctx, &task) case "Running": return r.monitorTask(ctx, &task) case "Waiting": return r.checkWaitCondition(ctx, &task) case "Failed": return r.handleFailure(ctx, &task) case "Succeeded": return ctrl.Result{}, nil } return ctrl.Result{}, nil }这段代码看起来简单,但每个分支里都有讲究。比如scheduleTask里要做约束过滤和节点打分,monitorTask里要检查 Pod 状态和检查点时间,handleFailure里要根据 retryCount 决定是重试还是标记为最终失败。
我踩过的一个坑是:Reconcile 函数被频繁调用导致重复创建 Pod。原因是每次 status 更新都会触发一次 Reconcile,如果幂等性没做好,就会创建多个 Pod。解决办法是在创建 Pod 前先检查是否已存在同名 Pod,用client.Get查一下,存在就跳过。
4.4 检查点写入与恢复的实操细节
检查点写入我用的是 sidecar 模式。agent 主容器通过本地 HTTP 接口把状态发给 sidecar,sidecar 负责序列化并上传到对象存储。
import requests import json import time class CheckpointClient: def __init__(self, sidecar_url="http://localhost:8080"): self.sidecar_url = sidecar_url self.last_checkpoint = 0 self.interval = 30 def maybe_checkpoint(self, state, force=False): now = time.time() if not force and now - self.last_checkpoint < self.interval: return payload = { "task_id": state["task_id"], "step": state["current_step"], "context": state["context"], "timestamp": now } resp = requests.post( f"{self.sidecar_url}/checkpoint", json=payload, timeout=5 ) if resp.status_code == 200: self.last_checkpoint = now else: print(f"checkpoint failed: {resp.status_code}")恢复时,agent 启动后先调 sidecar 的/restore接口,拿到最后一个检查点,然后从那里继续。这里有个细节:检查点里的 context 可能很大,不要每次都全量上传。我的做法是只上传增量,sidecar 负责合并。这样能把上传量降下来,尤其是在长对话场景下。
4.5 多 agent 协作的通信配置
多 agent 协作时,通信是瓶颈。我试过三种方案。
第一种是直接走 Kubernetes Service,每个 agent 一个 Service。优点是原生、简单,缺点是 Service 数量多了之后,kube-proxy 的规则表会膨胀,而且 Service 的创建销毁有延迟。
第二种是走消息队列,agent 之间通过 NATS 或 Redis Stream 通信。优点是解耦、支持异步,缺点是需要额外维护消息队列,而且消息的顺序性和可靠性要自己保证。
第三种是 runtime 层维护服务发现表,agent 通过 sidecar 寻址。这是我现在用的方案,兼顾了灵活性和性能。sidecar 里维护一个内存表,记录当前活跃 agent 的地址,agent 调用时先查表,再直连。表的更新通过 watch AgentTask 对象实现。
apiVersion: ax.example.com/v1alpha1 kind: AgentTask metadata: name: coordinator-agent spec: image: my-registry/coordinator:latest command: ["python", "coordinator.py"] resources: cpu: "500m" memory: "1Gi" timeoutSeconds: 3600 checkpointIntervalSeconds: 30 requiredTools: ["http-client", "json-parser"] maxRetries: 3这个 coordinator agent 启动后,会通过 sidecar 发现其他 worker agent,然后分发任务。worker agent 的配置类似,只是 command 不同。
5. 常见问题与排查技巧实录
5.1 任务一直 Pending 怎么办
这是最常见的问题。排查顺序是:先看 AgentTask 的 status,确认 phase 是不是 Pending;再看有没有对应的 Pod 被创建;如果没有 Pod,说明是调度器没找到合适节点;如果有 Pod 但一直 Pending,说明是 Kubernetes 调度失败。
调度失败的原因通常有几个:资源不足、节点亲和性不满足、污点没有容忍。我遇到最多的是资源不足,尤其是 GPU。解决办法是检查集群的 GPU 配额,或者把任务的 GPU 需求降下来。
还有一个隐蔽的原因是requiredTools 约束太严。如果集群里没有节点满足所有工具要求,任务就会一直 Pending。排查方法是把 requiredTools 逐个去掉,看哪个是瓶颈。
5.2 检查点恢复后 agent 行为异常
这个问题的根源通常是状态不一致。检查点里存的状态和 agent 实际的内存状态对不上,恢复后 agent 就会做出错误决策。
我的排查方法是:在恢复时打印检查点的完整内容,和 agent 期望的状态做对比。常见的不一致包括:检查点里的 step 编号和 agent 内部计数器对不上、context 里的消息顺序乱了、工具调用结果丢失。
解决办法是在检查点里存足够的信息,并且恢复时做校验。如果校验失败,宁可从头开始,也不要带着错误状态继续跑。
5.3 指标基数爆炸导致 Prometheus 崩溃
前面提过这个问题,这里展开说。症状是 Prometheus 内存飙升、查询变慢、甚至 OOM。原因是某个指标的标签组合太多。
排查方法是查 Prometheus 的/api/v1/status/tsdb接口,看哪个指标的 series 数量最多。找到之后,检查它的标签,把高基数的标签去掉。
我的经验是,agent 相关的指标,标签只保留 agent_type、phase、result 这三个。task_id、step_id 这些高基数标签只放在日志和 trace 里。
5.4 常见问题速查表
| 问题 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 任务一直 Pending | 资源不足、约束太严 | 看 Pod 事件、检查 requiredTools | 降资源需求、放宽约束 |
| 检查点恢复异常 | 状态不一致 | 对比检查点和内存状态 | 加校验、必要时从头开始 |
| 指标基数爆炸 | 高基数标签 | 查 TSDB status | 去掉高基数标签 |
| Pod 重复创建 | Reconcile 不幂等 | 看 Pod 列表 | 创建前先 Get 检查 |
| agent 间通信超时 | 服务发现表过期 | 看 sidecar 日志 | 缩短 watch 间隔 |
| 容器运行时错误 | containerd 配置问题 | 看 kubelet 日志 | 检查 SystemdCgroup 配置 |
5.5 几个我踩过的坑和独家技巧
坑一:检查点写入阻塞主流程。一开始我是同步写检查点,结果每次写的时候 agent 都卡住。后来改成异步,sidecar 收到请求后立即返回,后台再上传。这样主流程不受影响,但要注意处理上传失败的情况,失败时要重试。
坑二:agent 任务超时后资源没释放。Kubernetes 的 activeDeadlineSeconds 会杀掉 Pod,但如果 agent 在外部系统里注册了自己,杀 Pod 不会自动注销。解决办法是在 agent 的退出钩子里做清理,或者 runtime 定期扫描僵尸注册。
技巧一:用 initContainer 做环境预热。agent 启动前往往需要拉取模型、加载工具链,这些操作耗时较长。用 initContainer 提前做好,主容器启动就能直接跑,能省不少时间。
技巧二:给 agent 任务打上 owner reference。这样删除 AgentTask 时,关联的 Pod、ConfigMap、Secret 会自动清理,不会留下垃圾。
技巧三:用 PodDisruptionBudget 保护长时运行的 agent。节点维护时,Kubernetes 会驱逐 Pod,如果没有 PDB,agent 可能被直接杀掉。加上 PDB 后,驱逐会等 agent 到达检查点再执行。
6. 这套 runtime 还能往哪些方向扩展
ax 这类 agentic runtime 目前还在早期,很多能力可以继续加。我自己在用的过程中,觉得有几个方向值得投入。
一是更智能的调度。现在的调度还是基于静态约束,未来可以引入基于历史执行数据的预测,比如预测某个 agent 任务大概要跑多久、吃多少资源,然后做更精细的装箱。
二是跨集群编排。Karmada 这类项目已经在做多集群调度,agentic runtime 可以复用它的能力,把 agent 任务分发到多个集群,提高资源利用率和容灾能力。
三是和 CI/CD 打通。agent 任务的镜像构建、版本管理、灰度发布,这些都可以复用现有的 CI/CD 流程。我现在是把 AgentTask 的 spec 放在 Git 里,用 ArgoCD 做同步,效果不错。
四是成本可观测性。agent 任务消耗的资源要能换算成成本,按任务、按团队分摊。这个在规模化之后会变得很重要,不然很容易失控。
最后分享一个我在实际使用中的体会:agentic runtime 的复杂度,八成来自状态管理,两成来自调度。把状态管理做扎实了,剩下的问题都好解决。我见过太多项目在调度上花大力气,结果状态一丢,整个系统就不可靠了。所以如果你正在搭类似的系统,建议先把检查点和恢复机制做透,再考虑调度优化。