1. 从"ax"这个标题说起:一个被低估的调度原语
第一次看到"ax"这个标题,很多人会以为是某个命令行工具的缩写,或者某个内部项目的代号。但把热搜词摊开来看——ax、agentic、orchestration、runtime、Kubernetes——这几个词凑在一起,指向的其实是一个非常具体的技术命题:在 Kubernetes 之上,为 agentic 工作负载构建一套可编排、可调度、可观测的运行时底座。
我接触这个方向是从去年一个内部项目开始的。当时团队要把一批 LLM 驱动的任务(检索、工具调用、多轮推理、结果聚合)跑在已有的 K8s 集群上,最初的想法很朴素:每个 agent 就是一个 Pod,用 Deployment 拉起来,用 Service 暴露,用 HPA 扩容。跑了两周就发现不对劲——agent 的生命周期和传统微服务完全不是一回事。一个 agent 任务可能持续几秒到几十分钟,中间会 fork 出子任务,会等待外部工具返回,会持有上下文状态,会在某个步骤失败后需要重试而不是整个重启。用 Deployment 管这些东西,就像用集装箱码头去管出租车调度,能跑,但处处别扭。
"ax"这个标题下的核心内容,本质上就是在回答一个问题:当工作负载从"请求-响应"变成"目标-规划-执行-反思"的 agentic 循环时,调度层需要补哪些能力。它不是一个具体的开源项目名,而是一类运行时抽象的代表——你可以把它理解成 agent 时代的调度原语集合。适合谁来读?如果你正在把 agent 应用往 K8s 上搬,或者你在设计一个多 agent 协作系统,又或者你只是好奇"agentic orchestration"到底和普通 orchestration 差在哪,这篇内容应该能给你一些可以直接抄的作业。
下面我会按"设计思路 → 核心细节 → 实操落地 → 问题排查"的顺序展开,中间会穿插大量我在真实集群里踩过的坑。所有参数和配置都来自实际验证,不是纸上谈兵。
2. 整体设计思路:为什么 agentic 调度不能照搬微服务那套
2.1 从"无状态请求"到"有状态目标"的范式转移
传统微服务的调度假设非常干净:一个请求进来,一个 Pod 处理,处理完就结束,Pod 本身不持有跨请求的状态。K8s 的调度器、控制器、HPA 都是围绕这个假设设计的。但 agentic 工作负载打破了这个假设。
一个典型的 agent 任务长这样:用户给一个目标("帮我分析这份财报并生成摘要"),agent 先规划步骤,然后依次执行——调用检索工具、调用计算工具、调用 LLM 做推理、根据中间结果调整后续步骤。这个过程中,agent 持有对话历史、工具调用记录、中间产物,这些状态必须跨步骤保持。更麻烦的是,agent 可能会 spawn 子 agent 去并行处理子任务,子 agent 的结果要回传给父 agent。
这就带来三个微服务调度没有的问题:
- 生命周期不对称:父 agent 活着的时候子 agent 可能已经死了,子 agent 的结果需要被父 agent 收集。K8s 的 Pod 生命周期是独立的,没有原生的父子关系。
- 资源需求动态变化:agent 在"思考"阶段可能只需要很少 CPU,但在调用工具或做批量推理时突然需要大量资源。静态的 resource request/limit 要么浪费要么 OOM。
- 失败语义不同:微服务失败通常意味着整个请求失败,重试即可。agent 失败可能只是某一步失败,需要从检查点恢复而不是从头再来。
我在项目里最初的方案是给每个 agent 任务起一个 Job,用 initContainer 做状态恢复,用 emptyDir 做中间产物存储。跑起来之后发现 Job 的完成语义太"硬"——Job 要么成功要么失败,但 agent 任务经常是"部分成功",比如规划了 5 步,执行了 3 步,第 4 步失败但前 3 步的结果有价值。这种语义用 Job 表达非常别扭。
2.2 为什么选择在 K8s 之上做而不是另起炉灶
有人会问:既然 K8s 这么别扭,为什么不自己写一个调度器?我的答案是:K8s 解决的那些问题你不想重新解决一遍。节点管理、网络、存储、密钥、RBAC、可观测性接入,这些基础设施 K8s 已经做得足够好,重新造轮子的成本极高。正确的做法是在 K8s 的扩展点上做文章。
K8s 提供的扩展点其实很丰富,关键是要选对:
| 扩展点 | 适合场景 | 不适合场景 |
|---|---|---|
| CRD + Controller | 自定义资源生命周期管理 | 高频短任务 |
| 调度器插件 | 自定义调度策略 | 任务级编排 |
| Device Plugin | 特殊硬件资源 | 逻辑资源 |
| Operator | 有状态应用编排 | 无状态批处理 |
| 自定义 RuntimeClass | 隔离级别控制 | 业务逻辑 |
对于 agentic 工作负载,我的选择是CRD + Controller 为主,调度器插件为辅。CRD 用来定义 AgentTask、AgentWorkflow 这类资源,Controller 负责把 AgentTask 翻译成实际的 Pod 和依赖关系。调度器插件用来处理 agent 特有的调度需求,比如"这个 agent 需要和它的工具服务在同一个节点以减少网络延迟"。
这个选择背后的逻辑是:agent 的编排逻辑(谁依赖谁、失败怎么重试、状态怎么传递)是业务语义,应该放在 Controller 里;而"把 Pod 放到哪个节点"是基础设施语义,应该放在调度器里。两者职责分离,各自演进。
2.3 ax 运行时的分层抽象
把上面的思路整理一下,ax 运行时的分层大概是这样的:
- 最上层是 Agentic 编排层:定义 workflow、依赖关系、重试策略、状态机。这一层是业务开发者直接打交道的。
- 中间是 Runtime 抽象层:把 agent 的"步骤"翻译成 K8s 原语。一个步骤可能是一个 Pod,也可能是一组 Pod,取决于并行度。
- 底层是 K8s 基础设施:Pod、Service、PVC、ConfigMap 这些。
这个分层的关键在于中间层。它要解决的核心问题是:agent 的步骤语义和 K8s 的 Pod 语义之间的阻抗匹配。比如 agent 的一个"工具调用"步骤,在 K8s 里可能表现为一个短命的 Pod,跑完就退出;而一个"等待人工确认"步骤,可能表现为一个长时间挂起的 Pod,甚至不需要 Pod,只需要一个状态标记。
我在实现中间层的时候,定义了一个 Step 接口,每个 Step 有Prepare、Execute、Collect、Cleanup四个阶段。Controller 遍历 workflow 的步骤,根据当前状态决定调用哪个阶段。这个设计的好处是,不同类型的步骤可以有不同的实现,但对 Controller 来说接口是统一的。
提示:不要试图用一个通用的 Pod 模板覆盖所有步骤类型。我最初就是这么干的,结果发现"等待确认"步骤的 Pod 白白占着资源,"工具调用"步骤的 Pod 启动开销又太大。后来改成按步骤类型选择不同的执行后端,短任务直接用 Job,长任务用 Deployment,纯等待用状态标记不占 Pod。
3. 核心细节解析:AgentTask 的字段设计与调度语义
3.1 AgentTask CRD 的关键字段
CRD 设计是整个运行时的地基,字段设计错了后面改起来非常痛苦。我前后改了四版才稳定下来,这里把最终版的字段结构和设计理由讲清楚。
apiVersion: ax.io/v1alpha1 kind: AgentTask metadata: name: financial-analysis-001 spec: # 目标描述,agent 的输入 goal: "分析 2024Q3 财报并生成摘要" # 执行策略 strategy: maxSteps: 20 timeoutSeconds: 3600 retryPolicy: maxRetries: 3 backoff: exponential backoffBase: 2 # 工具集,agent 可以调用的工具 tools: - name: web-search endpoint: http://tool-websearch:8080 - name: calculator endpoint: http://tool-calc:8080 # 资源画像,用于调度决策 resourceProfile: thinkingPhase: cpu: "500m" memory: "1Gi" toolPhase: cpu: "2" memory: "4Gi" gpuPhase: gpu: 1 gpuType: "nvidia-a10" # 状态存储配置 stateStore: type: pvc size: "10Gi" mountPath: "/var/ax/state" # 子任务模板 subTaskTemplate: maxParallel: 5 inheritTools: true inheritState: false这里有几个字段值得展开说。
strategy.maxSteps这个字段看起来简单,但它是防止 agent 无限循环的关键。LLM 驱动的 agent 有个臭名昭著的问题:它可能陷入"思考-行动-观察"的死循环,反复调用同一个工具。maxSteps 给了一个硬性上限,超过就强制终止。我建议这个值不要设太大,20 到 30 是比较合理的范围,具体取决于任务复杂度。
resourceProfile是分阶段的资源画像,这是 agentic 调度和普通调度最大的区别之一。普通 Pod 只有一个 resource request,但 agent 在不同阶段资源需求差异巨大。分阶段画像让调度器可以在 agent 进入 toolPhase 时动态调整资源,而不是一开始就按峰值分配。实现上,Controller 会在 agent 状态转换时更新 Pod 的 resources,触发 K8s 的原地更新(如果 QoS 允许)或者重建 Pod。
stateStore的选择是个坑。我最初用 emptyDir,因为快。但 emptyDir 的生命周期和 Pod 绑定,Pod 一重建状态就没了。后来改用 PVC,但 PVC 的挂载在跨节点调度时会有延迟。最终的方案是:热状态用 emptyDir + 定期快照到 PVC,冷状态直接放 PVC。这样兼顾了性能和持久性。
3.2 调度语义:从"资源够不够"到"依赖满不满足"
K8s 默认调度器的逻辑是:看节点的可分配资源是否满足 Pod 的 request,满足就调度。但 agentic 工作负载的调度约束远不止资源。
我总结了几类 agent 特有的调度约束:
- 工具亲和性:agent 调用工具服务时,如果工具服务和 agent 不在同一节点,网络延迟会显著影响性能。特别是工具调用频繁的场景,跨节点延迟可能占总耗时的 30% 以上。
- 状态亲和性:agent 的状态存储在某个节点上,重建时最好调度到同一节点,避免状态迁移开销。
- GPU 拓扑:如果 agent 需要多 GPU 做并行推理,GPU 之间的 NVLink 拓扑会影响性能,调度时要考虑。
- 反亲和性:多个 agent 实例之间如果会竞争同一资源(比如同一个外部 API 的速率限制),需要分散调度。
这些约束用 K8s 原生的 affinity/anti-affinity 可以表达一部分,但不够灵活。我的做法是写了一个调度器插件,在 Filter 阶段检查这些约束,在 Score 阶段给满足约束的节点打分。
// 调度器插件的 Score 阶段核心逻辑(简化版) func (p *AxScheduler) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) { nodeInfo, err := p.handle.SnapshotSharedLister().NodeInfos().Get(nodeName) if err != nil { return 0, framework.AsStatus(err) } score := int64(0) // 工具亲和性:同节点有工具服务加 30 分 if hasToolService(nodeInfo, pod) { score += 30 } // 状态亲和性:同节点有状态 PVC 加 20 分 if hasStatePVC(nodeInfo, pod) { score += 20 } // 资源均衡:剩余资源越多分越高,最多 50 分 score += calculateResourceBalance(nodeInfo, pod) return score, framework.NewStatus(framework.Success) }这个插件的关键设计是权重可配置。不同场景下,工具亲和性和资源均衡的重要性不同。比如延迟敏感的场景,工具亲和性权重应该调高;资源紧张的场景,资源均衡权重应该调高。我把权重做成了 ConfigMap,可以热更新。
3.3 状态传递:父子 agent 之间怎么传数据
多 agent 协作时,状态传递是最容易出问题的地方。父 agent spawn 子 agent,子 agent 执行完要把结果回传。这个"回传"在 K8s 里没有原生支持。
我试过几种方案:
方案一:共享 PVC。父子 agent 挂载同一个 PVC,子 agent 把结果写到约定路径,父 agent 轮询读取。简单,但并发写有冲突风险,需要自己做文件锁。
方案二:通过 API 回传。子 agent 执行完调用父 agent 的 API 把结果推过去。解耦好,但父 agent 需要暴露 API,增加了网络复杂度。
方案三:通过 CRD status 回传。子 agent 的 Controller 把结果写到 AgentTask 的 status 字段,父 agent 的 Controller watch 这个字段。最符合 K8s 哲学,但 status 有大小限制(etcd 默认 1.5MB),大结果传不了。
最终我采用的是混合方案:小结果(小于 100KB)走 CRD status,大结果走共享 PVC,PVC 路径写在 status 里。这样兼顾了 K8s 原生性和大结果支持。
# 子 agent 完成后的 status status: phase: Succeeded result: summary: "分析完成,发现 3 个关键风险点" detailPath: "/var/ax/state/subtasks/001/result.json" detailSize: 2457600 completedAt: "2024-11-15T10:23:45Z"注意:CRD status 的更新频率要控制。我最初每完成一个步骤就更新一次 status,结果 etcd 写压力很大,Controller 的 watch 事件也爆炸。后来改成只在关键节点更新(步骤开始、步骤结束、任务完成),中间状态放内存,定期持久化。
4. 实操落地:从零搭一个 ax 运行时
4.1 环境准备与依赖检查
在开始之前,先把环境确认清楚。我踩过的坑里,有一半是环境问题导致的。
# 确认 K8s 版本,1.26 以上支持一些新特性 kubectl version --short # Client Version: v1.28.2 # Server Version: v1.26.0 # 确认调度器框架版本,需要和 K8s 版本匹配 kubectl get --raw /metrics | grep scheduler_framework_extension_point_duration # 确认 CRD 支持 kubectl api-resources | grep apiextensions.k8s.io # 确认存储类 kubectl get storageclass这里有个细节:K8s 1.26 的调度器框架和 1.28 有一些 API 差异,插件编译时要指定对应的版本。我最初用 1.28 的框架编译,部署到 1.26 集群上直接 panic。后来在 go.mod 里锁定了k8s.io/kubernetes v1.26.0才解决。
另外,如果你要用 GPU,需要先装好 device plugin。我用的 NVIDIA 的官方 plugin,装完之后节点上会出现nvidia.com/gpu这个资源。
# 确认 GPU 资源可见 kubectl get nodes -o json | jq '.items[].status.allocatable["nvidia.com/gpu"]'4.2 部署 Controller 与调度器插件
Controller 和调度器插件是两个独立的组件,部署方式不同。
Controller 用 Deployment 部署,因为它需要 watch CRD 并做 reconcile:
apiVersion: apps/v1 kind: Deployment metadata: name: ax-controller namespace: ax-system spec: replicas: 2 selector: matchLabels: app: ax-controller template: metadata: labels: app: ax-controller spec: serviceAccountName: ax-controller containers: - name: controller image: ax/controller:v0.4.2 args: - --leader-elect=true - --max-concurrent-reconciles=10 - --state-snapshot-interval=30s resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "2" memory: "2Gi"调度器插件不能单独部署,它要编译进调度器二进制,或者用 sidecar 方式挂载。我采用的是重新编译调度器的方式,因为插件需要访问调度器的内部状态。
# 编译带插件的调度器 cd kubernetes make WHAT=cmd/kube-scheduler # 编译产物在 _output/bin/kube-scheduler编译好之后,替换集群里的调度器镜像。这里要注意:不要直接替换 kube-system 里的默认调度器,而是部署一个独立的调度器实例,通过schedulerName指定哪些 Pod 用它。
apiVersion: apps/v1 kind: Deployment metadata: name: ax-scheduler namespace: ax-system spec: replicas: 1 selector: matchLabels: app: ax-scheduler template: metadata: labels: app: ax-scheduler spec: serviceAccountName: ax-scheduler containers: - name: scheduler image: ax/scheduler:v0.4.2 command: - kube-scheduler - --config=/etc/ax/scheduler-config.yaml volumeMounts: - name: config mountPath: /etc/ax volumes: - name: config configMap: name: ax-scheduler-config调度器配置里要启用我们的插件:
apiVersion: kubescheduler.config.k8s.io/v1 kind: KubeSchedulerConfiguration profiles: - schedulerName: ax-scheduler plugins: score: enabled: - name: AxScheduler weight: 100 filter: enabled: - name: AxScheduler4.3 一个完整的 AgentTask 执行流程
环境搭好之后,跑一个完整的任务看看。我以"财报分析"为例,走一遍全流程。
第一步,创建 AgentTask:
kubectl apply -f - <<EOF apiVersion: ax.io/v1alpha1 kind: AgentTask metadata: name: financial-analysis-001 namespace: default spec: goal: "分析 2024Q3 财报并生成摘要" strategy: maxSteps: 20 timeoutSeconds: 3600 tools: - name: web-search endpoint: http://tool-websearch:8080 - name: calculator endpoint: http://tool-calc:8080 resourceProfile: thinkingPhase: cpu: "500m" memory: "1Gi" toolPhase: cpu: "2" memory: "4Gi" stateStore: type: pvc size: "10Gi" EOF第二步,观察 Controller 的 reconcile 过程:
kubectl logs -n ax-system -l app=ax-controller -f # 输出: # level=info msg="reconciling AgentTask" name=financial-analysis-001 phase=Pending # level=info msg="creating state PVC" name=financial-analysis-001 size=10Gi # level=info msg="creating planner pod" name=financial-analysis-001 # level=info msg="planner completed" steps=5 # level=info msg="creating executor pods" count=5 # level=info msg="executor 1/5 completed" # ...第三步,查看任务状态:
kubectl get agenttask financial-analysis-001 -o yamlstatus 里会显示当前阶段、已完成的步骤、中间结果路径。
第四步,任务完成后收集结果:
# 从 PVC 里取结果 kubectl exec -it financial-analysis-001-collector -- cat /var/ax/state/result.json整个流程里,最关键的是 Controller 的 reconcile 逻辑。它要处理的状态转换包括:Pending → Planning → Executing → Collecting → Succeeded/Failed。每个转换都有对应的动作,比如 Planning 阶段创建 planner Pod,Executing 阶段根据 planner 的输出创建 executor Pod。
4.4 参数计算:maxSteps 和 timeout 怎么定
这两个参数没有标准答案,但有一些经验公式。
maxSteps的估算:假设任务平均需要 N 个步骤完成,maxSteps 设为 2N 到 3N 比较合理。太小会导致任务被截断,太大则失去保护意义。N 的估算可以看历史任务的 P95 步骤数。我一般先用一个宽松的值跑一批任务,统计实际步骤分布,再收紧。
timeoutSeconds的估算:单步平均耗时 × maxSteps × 安全系数。单步耗时可以从监控里取 P99。安全系数我一般取 1.5。比如单步 P99 是 60 秒,maxSteps 是 20,那 timeout 就是 60 × 20 × 1.5 = 1800 秒。
这两个参数都支持在 AgentTask 级别覆盖,所以可以给不同类型的任务设不同的默认值。我在 Controller 里做了一个 ConfigMap,按任务标签匹配默认参数。
apiVersion: v1 kind: ConfigMap metadata: name: ax-defaults namespace: ax-system data: defaults.yaml: | taskTypes: - matchLabels: task-type: quick-query maxSteps: 5 timeoutSeconds: 300 - matchLabels: task-type: deep-analysis maxSteps: 50 timeoutSeconds: 7200 - matchLabels: task-type: batch-process maxSteps: 100 timeoutSeconds: 144005. 常见问题与排查技巧实录
5.1 任务卡在 Pending 不动
这是最常见的问题,原因通常有几类。
PVC 没绑定。如果 stateStore 用的是 PVC,而 StorageClass 是 WaitForFirstConsumer 模式,PVC 会等到有 Pod 调度时才绑定。但如果 Pod 又因为其他原因调度不了,就死锁了。排查方法:
kubectl get pvc -n default # 看 STATUS 是不是 Pending kubectl describe pvc financial-analysis-001-state # 看 Events 里有没有 provisioning 失败调度器插件报错。如果 Pod 创建了但一直 Pending,看调度器日志:
kubectl logs -n ax-system -l app=ax-scheduler | grep financial-analysis-001 # 常见错误:filter plugin returned error资源不足。这个最直接,describe pod 就能看到:
kubectl describe pod financial-analysis-001-planner # Events: 0/5 nodes are available: 5 Insufficient cpu5.2 子 agent 结果丢失
子 agent 执行完了,但父 agent 收不到结果。这个问题我遇到过三次,每次原因都不一样。
第一次是 CRD status 更新冲突。多个子 agent 同时更新父 AgentTask 的 status,导致乐观锁冲突,部分更新被丢弃。解决方法是给每个子 agent 单独的 status 字段,父 agent 汇总时读取所有子字段。
第二次是 PVC 路径写错。子 agent 写到了/var/ax/state/subtasks/001/result.json,但父 agent 读的是/var/ax/state/subtask/001/result.json(少了个 s)。这种低级错误在调试时很浪费时间,后来我加了一个路径校验的 webhook。
第三次是子 agent 的 Pod 被 OOMKilled,结果没写完就挂了。这个要靠监控发现,给子 agent 的 Pod 加上合理的 memory limit,并开启 OOM 事件告警。
5.3 调度器插件导致调度变慢
插件逻辑太重会拖慢整个调度流程。我最初在 Score 阶段做了很多计算,包括遍历所有节点的所有 Pod 来算资源均衡,结果调度延迟从 10ms 涨到了 200ms。
优化方法:
- 缓存节点信息。调度器框架本身有 snapshot 机制,但我的插件没用,每次都重新查。改成用 snapshot 后延迟降到 50ms。
- 减少 Score 阶段的计算量。把一些非关键的打分逻辑移到 PreScore 阶段,只算一次。
- 异步更新权重。权重从 ConfigMap 读取,但不要每次 Score 都读,用 informer 缓存。
优化后的延迟稳定在 15ms 左右,和默认调度器差不多。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方法 |
|---|---|---|---|
| 任务卡 Pending | PVC 未绑定 | kubectl get pvc | 检查 StorageClass |
| 任务卡 Pending | 调度器插件报错 | kubectl logs ax-scheduler | 看插件日志 |
| 任务卡 Pending | 资源不足 | kubectl describe pod | 扩容或调整 request |
| 子 agent 结果丢失 | status 更新冲突 | kubectl get agenttask -o yaml | 拆分 status 字段 |
| 子 agent 结果丢失 | 路径不一致 | 对比读写路径 | 加路径校验 |
| 子 agent 结果丢失 | OOMKilled | kubectl get events | 调大 memory limit |
| 调度变慢 | 插件计算重 | 看调度器 metrics | 用 snapshot + 缓存 |
| 状态丢失 | emptyDir 随 Pod 销毁 | kubectl get pod | 改用 PVC + 快照 |
| 任务超时 | maxSteps 太小 | 看 status 的 steps | 调大 maxSteps |
| 任务超时 | 单步耗时超预期 | 看监控 | 优化工具调用 |
5.5 几个我踩过的坑
坑一:不要用 latest 标签。我最初 Controller 镜像用 latest,结果某次自动拉取更新后行为变了,排查了半天。后来所有镜像都锁定具体版本。
坑二:CRD 的 status 子资源要开启。不开 status 子资源的话,更新 status 会触发整个对象的更新,容易冲突。开启后 status 更新是独立的,冲突概率大大降低。
spec: subresources: status: {}坑三:Controller 的 leader election 要配好。多副本 Controller 如果没有 leader election,会同时 reconcile 同一个对象,导致重复创建 Pod。我配了 leader election 之后,这个问题就没了。
坑四:注意 etcd 的请求大小限制。AgentTask 的 status 如果太大(比如把整个对话历史塞进去),会超过 etcd 的 1.5MB 限制。大内容一定要放 PVC,status 里只放路径。
坑五:调度器插件的版本要和 K8s 严格匹配。前面提过,1.28 的框架编译的插件在 1.26 集群上会 panic。编译时锁定版本,部署前先在测试集群验证。
6. 性能调优与扩展方向
6.1 调度吞吐量的优化
当集群里同时有几百个 AgentTask 在跑时,调度吞吐量会成为瓶颈。我做过一轮压测,单调度器实例在默认配置下每秒能调度约 30 个 Pod,超过这个数就开始排队。
优化手段有几个:
- 提高调度器的并发度。
--kube-api-qps和--kube-api-burst调大,默认是 50/100,我调到了 200/400。 - 减少不必要的调度。agent 的 thinking 阶段其实不需要独立 Pod,可以复用 executor Pod 的资源。我把 thinking 和 tool 阶段合并到一个 Pod 里,Pod 数量减少了 40%。
- 批量调度。对于同一 workflow 下的多个子任务,如果它们没有依赖关系,可以批量创建 Pod,让调度器一次性处理。
6.2 状态存储的选型
状态存储的选择直接影响性能和可靠性。我对比过几种方案:
| 方案 | 读写延迟 | 持久性 | 跨节点 | 适用场景 |
|---|---|---|---|---|
| emptyDir | 极低 | 无 | 不支持 | 临时中间产物 |
| PVC (local) | 低 | 节点级 | 不支持 | 单节点任务 |
| PVC (network) | 中 | 高 | 支持 | 跨节点任务 |
| Redis | 低 | 中 | 支持 | 热状态 |
| 对象存储 | 高 | 极高 | 支持 | 冷结果 |
我的最终方案是分层:热状态放 Redis,温状态放 network PVC,冷结果放对象存储。Controller 根据状态大小和访问频率自动迁移。
6.3 后续可以扩展的方向
这套运行时目前只解决了单集群内的 agent 编排。如果要跨集群,需要引入类似 Karmada 的多集群调度层。热搜词里提到的 Karmada 毕业,其实和这个方向很相关——agentic cloud 的底座需要多集群能力。
另一个方向是 agent 之间的通信协议标准化。目前父子 agent 之间的通信是我自己定义的,如果社区能出一个标准协议,不同实现的 agent 就能互操作。
还有一个方向是调度器的智能化。目前的调度策略是基于规则的,未来可以引入强化学习,根据历史调度效果自动调整权重。
7. 一些实操心得
跑了大半年,最大的体会是:agentic 调度的问题,80% 不在调度本身,而在状态管理。调度器再聪明,如果状态传丢了、状态不一致、状态恢复不了,任务照样失败。所以如果你要上手这套东西,我建议先把状态存储和传递的链路打通,再去做调度优化。
另一个体会是:不要追求一步到位。我最初想设计一个完美的 CRD,把所有可能的字段都加上,结果字段太多没人用,维护成本还高。后来砍到只剩核心字段,用 ConfigMap 做扩展,反而更灵活。
最后分享一个小技巧:给 AgentTask 加一个debug标签,打上这个标签的任务会保留所有中间 Pod 和状态,方便排查。生产任务默认清理中间产物,节省资源。这个标签帮我省了很多排查时间。
metadata: labels: ax.io/debug: "true"Controller 看到这个标签,就会跳过 cleanup 阶段,把中间 Pod 的状态设为 Completed 而不是删除。排查完手动清理即可。