拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Kubernetes的Agentic工作负载编排:ax运行时架构设计与实践

基于Kubernetes的Agentic工作负载编排:ax运行时架构设计与实践

1. 从“ax”这个标题说起:一个被低估的运行时编排命题

第一次看到“ax”这个标题,很多人会一头雾水。它不像“Kubernetes 入门”那样直白,也不像“agentic rag”那样自带热度。但把热搜词摊开来看,脉络就清楚了:ax、agentic、orchestration、runtime、Kubernetes这几个词放在一起,指向的是一个非常具体的工程命题——在 Kubernetes 之上,如何为 agentic 工作负载构建一套可编排、可观测、可复现的运行时层。

我接触这类需求是从一个内部工具链整合项目开始的。当时团队已经有了一套基于 Kubernetes 的服务编排,但新来的 agentic 任务(多步骤推理、工具调用、状态回传)跑在上面非常别扭:Pod 生命周期和 agent 的会话生命周期对不上,重试逻辑和 agent 的幂等设计打架,日志散落在各个容器里根本串不起来。于是我们开始琢磨,能不能抽一层薄薄的 runtime,专门管 agent 的编排,而把调度、网络、存储这些脏活继续交给 Kubernetes。

“ax”在我的理解里,就是这层 runtime 的一个代号。它不是一个具体的开源项目名(至少目前不是),而更像一个架构代号:a 代表 agentic,x 代表 orchestration 里的交叉调度与执行。你完全可以把它当成“agentic runtime on Kubernetes”的缩写来理解。这篇文章要聊的,就是这层 runtime 到底该怎么设计、怎么落地、踩过哪些坑,以及为什么它和传统的微服务编排不是一回事。

适合谁看?如果你正在把 LLM 驱动的 agent 往生产环境搬,或者你已经在 Kubernetes 上跑了一些实验性的 agent 任务但被状态管理搞得焦头烂额,那这篇内容会对你有直接帮助。如果你只是听说过 agentic 这个词,想搞清楚它和普通服务编排的区别,也能从里面拿到一个完整的工程视角。

2. 为什么 agentic 工作负载需要独立的 orchestration runtime

2.1 传统 Kubernetes 编排和 agent 编排的根本差异

Kubernetes 的编排模型是为无状态、短生命周期、可水平替换的服务设计的。一个 Deployment 管一组 Pod,Pod 挂了就重建,请求打到哪个副本都行。这套模型跑 Web 服务、跑批处理任务都非常成熟。

但 agentic 工作负载有几个特性直接和这套模型冲突:

  • 会话是有状态的。一个 agent 在执行多步任务时,中间会产生大量上下文:工具调用的返回值、推理链的中间结果、用户的历史交互。这些状态不能随便丢,也不能随便换一个副本继续。
  • 执行路径是动态的。传统服务的调用链在代码里写死了,agent 的下一步动作往往是根据上一步的输出动态决定的。你没法在部署时就把 DAG 画好。
  • 单步耗时差异极大。一次 LLM 调用可能 200ms,也可能 30s;一次外部工具调用可能瞬间返回,也可能超时重试好几轮。用统一的 liveness probe 和 timeout 去管,要么误杀,要么卡死。
  • 失败语义不同。微服务里一个请求失败,重试通常是无害的。但 agent 的某一步失败后重试,可能会重复执行一个有副作用的工具调用(比如已经发了一封邮件)。

我实测下来,最直接的感受是:把 agent 当成普通 Pod 来管,你会花 80% 的时间在处理状态和重试的边界情况上,而不是在 agent 本身的逻辑上。这就是为什么需要一层专门的 runtime。

2.2 “ax” runtime 的定位:薄编排层而非重平台

这里有个关键的设计取舍。市面上有两种做法:

一种是做一个大而全的 agent 平台,把调度、状态、工具注册、模型网关全部包进来。这种方案上手快,但和现有 Kubernetes 基础设施的整合会很痛,而且容易被平台锁定。

另一种是做一个薄编排层,只解决 agent 特有的问题:会话状态、动态步骤调度、幂等重试、执行追踪。底下的调度、网络、存储、密钥管理继续用 Kubernetes 原生能力。

“ax”走的是第二条路。它的核心抽象只有三个:

抽象对应 Kubernetes 概念职责
Session无直接对应,用 CRD 表达管理一次 agent 会话的完整生命周期和状态
StepJob 的增强版表示 agent 的一个执行步骤,支持动态生成
Runtime SidecarSidecar 容器负责状态同步、追踪上报、幂等控制

这个设计的逻辑是:Kubernetes 已经解决了 90% 的分布式系统问题,runtime 只需要解决剩下 10% 的 agent 特有问题。不重复造轮子,也不强行把 agent 塞进不合适的模型里。

2.3 选型背后的三个关键考量

为什么不是直接用 Argo Workflows 或者 Tekton 这类工作流引擎?我试过,有几个绕不过去的点:

第一,工作流引擎的 DAG 是静态定义的。你可以在运行时传参,但没法在步骤执行过程中动态追加新步骤。agent 的典型模式是“执行一步,看结果,决定下一步”,这需要 runtime 支持动态步骤生成。

第二,工作流引擎的状态管理是面向完成度的,不是面向会话的。它关心的是“这个 workflow 跑完了没有”,而 agent runtime 关心的是“这个会话的上下文现在是什么状态,能不能从中断点恢复”。

第三,追踪粒度不一样。工作流引擎追踪的是步骤级别的成功失败,agent runtime 需要追踪的是推理链级别的因果关系:为什么走了这一步,上一步的哪个输出触发了这个决策。这对调试和审计至关重要。

所以“ax”的选型结论是:用 CRD 扩展 Kubernetes API 来表达 Session 和 Step,用 controller 模式做编排,用 sidecar 做运行时支撑。这套组合既保留了 Kubernetes 的运维生态,又给了 agent 足够的表达空间。

3. 核心细节拆解:Session、Step 与 Runtime Sidecar 的实现要点

3.1 Session CRD 的状态机设计

Session 是整个 runtime 的核心。它需要表达的状态比普通 Kubernetes 资源复杂得多。我们最终设计的状态机是这样的:

Pending -> Initializing -> Running -> WaitingForInput -> Running -> ... -> Completed | | v v Failed Cancelled

几个关键设计点:

WaitingForInput 是一等状态。很多 agent 任务需要人工介入或者等待外部事件。如果把这个状态藏在 Running 里面,外部系统就没法感知 agent 在等什么。单独拎出来之后,可以配合 Kubernetes 的 Event 机制做通知。

状态转换必须幂等。controller 会不断 reconcile,同一个状态转换可能被触发多次。我们在 Session 的 status 里加了一个transitionVersion字段,每次转换递增,controller 只在版本匹配时才执行副作用。

状态和上下文分离存储。Session 的 status 只存轻量级的状态信息,真正的会话上下文(消息历史、工具调用记录)存在单独的 ConfigMap 或者外部存储里。这样做的好处是 Session 对象不会因为上下文膨胀而变得巨大,影响 etcd 的性能。

注意:Session CRD 的 status 子资源一定要开启,否则并发更新时会出现冲突。我们早期没开,结果在高并发场景下丢了不少状态更新。

3.2 Step 的动态生成与依赖解析

Step 的设计是整个 runtime 里最烧脑的部分。传统 Job 是你提交一个 spec,它跑完就结束。但 agent 的 Step 是在执行过程中动态产生的。

我们的做法是:Step 本身也是一个 CRD,但它有一个parentSession字段和一个sequence字段。controller 监听 Session 的状态变化,当 Session 进入 Running 且没有活跃 Step 时,调用 agent 的决策逻辑(可以是一个 webhook,也可以是一个内置的 planner),生成下一个 Step。

这里有个关键问题:怎么保证动态生成的 Step 不会无限循环?我们加了三道防线:

  • 每个 Session 有一个maxSteps字段,默认 50,超过就强制进入 Failed 状态。
  • 每个 Step 有一个deadline,超过就标记为 Timeout,由 controller 决定是重试还是终止会话。
  • 决策逻辑本身要有终止条件,不能依赖 runtime 兜底。

依赖解析方面,我们支持两种模式:串行模式(上一步完成才生成下一步)和并行模式(一次生成多个无依赖的 Step,用dependsOn字段表达依赖关系)。并行模式在工具调用场景下特别有用,比如同时查三个数据源。

3.3 Runtime Sidecar 的职责边界

Sidecar 是 runtime 和 agent 容器之间的桥梁。它的职责必须严格限定,否则会变成一个什么都管的“上帝容器”。我们最终给它划了四条职责:

  1. 状态同步:定期把 agent 容器的执行状态(通过本地 HTTP 接口暴露)同步到 Session CRD 的 status 里。
  2. 追踪上报:收集 agent 产生的 trace span,批量上报到追踪后端。
  3. 幂等控制:为每个有副作用的工具调用生成幂等键,在重试时复用。
  4. 优雅终止:收到 SIGTERM 后,通知 agent 容器保存检查点,等待其完成后再退出。

Sidecar 不负责的事情也很明确:不做模型调用、不做工具注册、不做权限校验。这些要么在 agent 容器里做,要么在更上层的网关做。

实操心得:Sidecar 和 agent 容器之间的通信一定要用 localhost,不要走 Service。走 Service 会引入额外的网络跳数和 DNS 解析开销,在频繁通信的场景下延迟很明显。我们用 localhost + Unix domain socket,延迟从平均 3ms 降到了 0.2ms。

3.4 与 Kubernetes 原生能力的对接方式

“ax” runtime 不重新实现调度、网络、存储,而是通过标准接口对接:

  • 调度:Session 和 Step 都通过标准的 Pod spec 表达资源需求,交给 kube-scheduler。我们额外加了一个nodeAffinity的约定,让有 GPU 的 Step 调度到 GPU 节点。
  • 网络:Step 之间的通信走 Kubernetes Service,但 Session 内部的 Step 之间用 headless service 做直连,避免不必要的负载均衡。
  • 存储:会话上下文用 PVC 挂载,保证 Pod 重建后上下文不丢。临时数据用 emptyDir。
  • 密钥:工具调用的凭证用 Secret 挂载,通过 volume 注入,不走环境变量(环境变量容易在日志里泄露)。

这套对接方式的好处是,运维团队不需要学新东西。他们继续用 kubectl、Prometheus、Grafana 这些工具,只是多了一组 CRD 要关注。

4. 实操过程:从零搭建一个最小可用的 ax runtime

4.1 环境准备与前置检查

在开始之前,确认你的集群满足以下条件:

# 检查 Kubernetes 版本,建议 1.26 及以上 kubectl version --short # 检查 CRD 支持 kubectl api-resources | grep customresourcedefinition # 检查是否有默认 StorageClass kubectl get storageclass # 检查节点资源 kubectl top nodes

我们踩过的一个坑是:集群版本太低(1.22 以下)时,CRD 的status子资源在某些场景下行为不一致,导致状态更新丢失。建议至少 1.26。

另外,如果你打算用 sidecar 模式,确认集群开启了SidecarContainers特性门控(1.29 之后默认开启)。早期版本需要用 init container 模拟,体验差很多。

4.2 定义 Session 和 Step 的 CRD

先写 Session 的 CRD 定义。这里只展示核心字段:

apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: sessions.ax.example.com spec: group: ax.example.com versions: - name: v1alpha1 served: true storage: true subresources: status: {} schema: openAPIV3Schema: type: object properties: spec: type: object properties: maxSteps: type: integer default: 50 agentImage: type: string contextPVC: type: string status: type: object properties: phase: type: string enum: [Pending, Initializing, Running, WaitingForInput, Completed, Failed, Cancelled] transitionVersion: type: integer currentStep: type: string scope: Namespaced names: plural: sessions singular: session kind: Session

Step 的 CRD 类似,关键字段是parentSession、sequence、dependsOn、deadline。

写完 CRD 之后,用kubectl apply应用,然后检查:

kubectl get crd | grep ax.example.com kubectl explain session.spec

kubectl explain能正常输出,说明 CRD 注册成功。

4.3 Controller 的核心 reconcile 逻辑

Controller 用 client-go 或者 controller-runtime 写都可以。我们用 controller-runtime,核心 reconcile 逻辑大概是这样:

func (r *SessionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var session axv1alpha1.Session if err := r.Get(ctx, req.NamespacedName, &session); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } switch session.Status.Phase { case "Pending": return r.initializeSession(ctx, &session) case "Running": return r.advanceSession(ctx, &session) case "WaitingForInput": return r.checkInput(ctx, &session) default: return ctrl.Result{}, nil } }

advanceSession是核心中的核心。它的逻辑是:

  1. 查询当前 Session 下所有 Step 的状态。
  2. 如果有 Step 在运行,等待。
  3. 如果所有 Step 都完成,调用 planner 生成下一个 Step。
  4. 如果 planner 返回终止信号,把 Session 置为 Completed。
  5. 如果生成的 Step 数量超过maxSteps,置为 Failed。

这里有个细节:planner 调用必须是幂等的。因为 reconcile 可能被重复触发,同一个状态下可能多次调用 planner。我们的做法是在 Session 的 status 里记录lastPlannedStepSequence,只有当前 sequence 大于记录值时才调用 planner。

4.4 Sidecar 注入与状态同步配置

Sidecar 注入用 MutatingWebhook 实现。当创建带有ax.example.com/inject: "true"标签的 Pod 时,webhook 自动注入 sidecar 容器。

Sidecar 的核心配置:

- name: ax-runtime-sidecar image: ax/runtime-sidecar:v0.1.0 ports: - containerPort: 9090 name: state-sync env: - name: SESSION_NAME valueFrom: fieldRef: fieldPath: metadata.labels['ax\.example\.com/session'] - name: AGENT_ENDPOINT value: "http://localhost:8080" volumeMounts: - name: session-context mountPath: /var/ax/context resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi

状态同步的间隔我们设的是 2 秒。太频繁会给 API Server 压力,太慢会导致状态滞后。2 秒是实测下来比较平衡的值。

注意:Sidecar 的资源 limits 一定要设。我们早期没设,结果某个 agent 疯狂产生 trace,sidecar 内存涨到 2G 被 OOM kill,连带 agent 容器也挂了。

4.5 一次完整的 agent 会话执行记录

下面是一次真实的执行记录(脱敏后):

[00:00:00] Session created, phase=Pending [00:00:01] Controller initialized session, phase=Initializing [00:00:03] PVC mounted, agent container started, phase=Running [00:00:05] Step-1 generated: "search_knowledge_base" [00:00:08] Step-1 completed, result: 3 documents found [00:00:09] Step-2 generated: "summarize_documents" [00:00:15] Step-2 completed, result: summary generated [00:00:16] Step-3 generated: "call_external_api" [00:00:45] Step-3 timeout, retry with idempotency key [00:01:10] Step-3 completed after retry [00:01:12] Planner returned terminal signal [00:01:13] Session phase=Completed

整个会话耗时 1 分 13 秒,其中 Step-3 的重试占了 25 秒。如果没有幂等控制,这次重试会导致外部 API 被调用两次,产生重复数据。

5. 常见问题与排查技巧实录

5.1 状态不同步:Session 卡在 Running 不动

这是最常见的问题。表现是 Session 的 phase 一直是 Running,但没有任何 Step 在跑。

排查顺序:

  1. 先看 controller 的日志,确认 reconcile 有没有被触发。如果没触发,检查 controller 的 watch 配置。
  2. 如果 reconcile 触发了但没进展,看 planner 的调用日志。大概率是 planner 返回了错误但被吞掉了。
  3. 检查 Session 的transitionVersion有没有在递增。如果不递增,说明状态更新被 API Server 拒绝了,通常是 resourceVersion 冲突。

我们遇到过一次,原因是 planner 的 webhook 超时了,但 controller 没有设置超时,导致 reconcile 一直挂着。后来给所有外部调用都加了 5 秒超时。

5.2 Step 重复执行:幂等键没生效

表现是同一个 Step 被执行了多次,产生了重复的副作用。

根因通常是幂等键的生成逻辑有问题。我们的幂等键是sessionID + stepSequence + toolName的哈希。早期版本漏了stepSequence,导致同一个会话里不同步骤调用同一个工具时,幂等键冲突,第二次调用被误判为重复而跳过。

修复方法很简单,但发现过程很痛苦。建议在 sidecar 里加一个调试接口,能查询任意幂等键的状态。

5.3 Sidecar 资源竞争导致 agent 变慢

表现是 agent 的执行时间比预期长很多,但 CPU 和内存看起来都不高。

这种情况通常是 sidecar 和 agent 容器在争抢同一个 CPU 核。Kubernetes 默认的 CPU 分配是共享的,如果 sidecar 在做密集的状态同步,会挤占 agent 的 CPU 时间。

解决办法是给 sidecar 设置cpu: 100m的 request 和cpu: 200m的 limit,并且开启 CPU manager 的 static 策略,让 agent 容器独占核心。

5.4 常见问题速查表

现象可能原因排查方法解决方案
Session 卡在 Runningplanner 超时或返回错误查 controller 日志和 planner 调用记录给外部调用加超时,错误要显式处理
Step 重复执行幂等键生成逻辑有误查 sidecar 的幂等键调试接口修正幂等键生成规则
agent 执行变慢sidecar 资源竞争查容器的 CPU throttling 指标设置合理的 request/limit,开启 static 策略
状态更新丢失resourceVersion 冲突查 API Server 的审计日志开启 status 子资源,用乐观锁重试
Sidecar OOMtrace 数据积压查 sidecar 内存曲线设置内存 limit,加 trace 采样率
PVC 挂载失败StorageClass 不匹配查 PVC 的 event确认 StorageClass 存在且可动态供应

5.5 几个独家避坑技巧

技巧一:给 Session 加一个dryRun模式。在这个模式下,Step 会被生成但不实际执行,只记录执行计划。这在调试 planner 逻辑时非常有用,能快速验证决策路径是否正确,而不用真的跑一遍完整的 agent。

技巧二:trace 数据一定要采样。我们早期全量上报,结果追踪后端被打爆。后来改成按 Session 采样,10% 的会话全量上报,其余只上报关键 span。调试问题时可以临时提高采样率。

技巧三:Step 的 deadline 要留足余量。我们一开始把 deadline 设成预期耗时的 1.5 倍,结果 LLM 调用偶尔抖动就超时。后来改成 3 倍,超时率从 8% 降到了 0.5%。代价是失败检测变慢,但整体体验更好。

技巧四:Session 的上下文要定期压缩。长会话的上下文会越来越大,最终撑爆 PVC 或者拖慢 planner。我们的做法是每 10 个 Step 做一次上下文摘要,把历史消息压缩成一段总结。这个逻辑放在 sidecar 里做,对 agent 透明。

6. 这套 runtime 后续可以怎么扩展

跑通最小可用版本之后,我们陆续加了几个扩展,效果都不错。

第一个是多 agent 协作。在 Session 层面支持subSessions字段,一个主 Session 可以派生出多个子 Session,各自独立执行,最后汇总结果。这比在单个 agent 里做多角色模拟要清晰得多,因为每个子 Session 有独立的资源和状态。

第二个是执行回放。因为所有 Step 的输入输出都记录在案,可以按时间顺序回放整个会话。这在排查“为什么 agent 做了这个决策”时特别有用。回放功能我们做成了一个 CLI 工具,输入 Session ID 就能看到完整的执行链路。

第三个是成本追踪。每个 Step 记录消耗的 token 数和工具调用次数,汇总到 Session 级别。这样能清楚地看到一次会话花了多少钱,哪个步骤是大头。对于需要控制成本的场景,这个功能是刚需。

如果你也在做类似的事情,我的建议是先把最小闭环跑通:一个 Session、一个 Step、一个 sidecar,能完整跑一次 agent 任务就行。不要一上来就追求多 agent、回放、成本追踪这些高级功能。基础的状态机和幂等控制做扎实了,后面的扩展都是水到渠成的事。

返回列表