1. 从“ax”这个标题说起:一个被低估的运行时编排命题
第一次看到“ax”这个标题,很多人会一头雾水——两个字母,既不像产品名,也不像技术栈缩写。但把热搜词摊开来看,线索就清楚了:agentic、orchestration、runtime、Kubernetes这几个词反复出现,再加上“karmada正式毕业”“agentic cloud坚实底座”这类行业动态,基本可以判断,这里讨论的“ax”不是某个具体软件的名字,而是指向一类面向智能体(Agent)的运行时编排层——你可以把它理解成“Agent 时代的调度内核”。
我之所以敢这么判断,是因为过去一年多我在几个内部项目里反复折腾过类似的东西:把一堆各自为战的 Agent 服务、工具调用、模型推理进程,塞进一个统一的运行时里,让它们能像 Kubernetes 调度容器一样被调度、被观测、被扩缩容。这个过程踩的坑,比当年从物理机迁移到容器还多。原因很简单——容器是“无状态、可重启、生命周期短”的,而 Agent 是有上下文、有会话、有工具依赖、有推理成本的,它的“状态”比传统微服务复杂一个数量级。
所以这篇博文,我想把“ax”当作一个运行时编排命题来拆解:它要解决什么问题,核心抽象是什么,怎么落地到 Kubernetes 这类基础设施上,以及在实际操作中哪些地方最容易翻车。适合正在做 Agent 平台、AI 基础设施、或者单纯想把多个模型服务编排起来的同学参考。哪怕你只是刚接触 Kubernetes,我也会尽量用生活化的类比把关键概念讲清楚。
先给一个最直白的定义:ax 在这里代表的是一层“Agent 运行时编排”,它向下对接 Kubernetes 等资源调度底座,向上暴露 Agent 生命周期管理、工具注册、会话路由、推理进程托管等能力。它不是模型本身,也不是某个框架,而是把“Agent 怎么跑起来、怎么被调度、怎么被观测”这件事工程化的那一层。理解了这一点,后面所有的技术选型和踩坑才有落脚点。
2. 为什么 Agent 需要独立的运行时编排层
2.1 传统容器编排为什么不够用
Kubernetes 解决的是“进程怎么被调度到机器上、怎么保持期望状态”的问题。它的核心抽象是 Pod、Deployment、Service,假设的是无状态、可水平复制、随时可杀的工作负载。这套模型对 Web 服务、批处理任务非常合适,但放到 Agent 场景就会露出短板。
Agent 的典型特征是:一次会话可能持续几分钟甚至几小时,中间要调用多个工具、访问外部 API、维护对话历史;推理进程(比如 llama-server、vLLM 这类)启动慢、显存占用大、不能随便重启;不同 Agent 之间还有依赖关系,A 的输出是 B 的输入。这些特性决定了它不能简单地当成一个 Deployment 来对待。
我举个实际例子。早期我们直接把一个 Agent 服务打包成 Deployment,副本数设成 3。结果发现:用户会话被随机打到不同副本,上下文对不上;某个副本因为调用外部工具超时被 OOM kill,整个会话直接断掉;推理进程冷启动要 40 多秒,用户等得直接关页面。这就是典型的“用容器编排的思维套 Agent,必然水土不服”。
2.2 Agent 运行时的四个核心抽象
踩了这些坑之后,我逐渐总结出 Agent 运行时必须提供的四个核心抽象,这也是判断一个“ax”类系统是否合格的标准:
- 会话(Session):比 Pod 更粗的调度单位,绑定用户上下文,保证同一会话路由到同一运行时实例。它解决的是“状态一致性”问题。
- 工具(Tool):Agent 可调用的外部能力,需要注册、发现、鉴权、限流。它解决的是“能力复用”问题。
- 推理进程(Inference Runtime):模型加载、显存管理、请求排队。它解决的是“昂贵资源复用”问题。
- 编排策略(Orchestration Policy):决定哪个 Agent 在什么时候、用什么资源、按什么顺序执行。它解决的是“多 Agent 协作”问题。
把这四个抽象映射到 Kubernetes 上,就形成了“ax”这类系统的典型架构:Kubernetes 负责底层资源池化和故障自愈,运行时层负责会话粘性、工具治理和推理托管。两者是互补关系,不是替代关系。
2.3 从 Karmada 毕业看编排层的演进方向
热搜里提到“karmada正式毕业”,这个信号值得单独说一句。Karmada 解决的是多集群编排问题——把工作负载分发到多个 Kubernetes 集群,并保持策略一致。它毕业意味着多集群编排从“实验特性”走向“生产可用”。
这对 Agent 运行时意味着什么?意味着未来的 Agent 编排很可能不是单集群的,而是跨集群、跨地域的。一个推理进程可能跑在 GPU 集群,工具服务跑在通用集群,会话状态存在边缘节点。运行时层必须能感知这种分布,并做出合理的调度决策。这也是为什么“ax”这类系统不能只盯着单机或单集群,必须从一开始就把多集群编排纳入设计。
3. 核心细节拆解:会话、工具与推理进程怎么落地
3.1 会话粘性:让同一个用户始终落到同一个实例
会话粘性是 Agent 运行时最基础也最容易做错的一环。Kubernetes 的 Service 默认是轮询负载均衡,同一个用户的请求会被打到不同 Pod,上下文自然就丢了。解决办法有几种,我按推荐程度排序:
第一种是基于会话 ID 的一致性哈希。在 Service 前面加一层网关(比如 Envoy、Nginx),根据请求头里的 session-id 做一致性哈希,保证同一会话落到同一后端。优点是改动小,缺点是后端扩缩容时会话会重新分布,需要配合优雅下线。
第二种是会话状态外置。把对话历史、工具调用记录存到 Redis 或数据库,运行时实例变成无状态的,任何实例都能处理任何会话。这是最干净的方案,但引入了外部依赖,且状态读写有延迟。
第三种是会话亲和 + 状态外置混合。热数据放本地内存加速,冷数据落外部存储,实例重启时从外部恢复。这是我在生产环境最终采用的方案,兼顾了性能和可靠性。
注意:会话粘性不是“绑定 IP”那么简单。用户可能切换网络,IP 会变;移动端可能频繁重连。一定要用业务层的 session-id,而不是网络层的标识。
3.2 工具注册与发现:别让 Agent 硬编码工具地址
Agent 要调用工具,最原始的做法是在代码里写死工具地址。这在工具有限时能跑,但一旦工具数量上到几十个、还要动态上下线,就会变成灾难。正确的做法是引入工具注册中心。
工具注册中心的核心职责有三个:注册(工具启动时上报自己的能力和地址)、发现(Agent 按能力查询可用工具)、健康检查(自动摘除不可用工具)。实现上可以基于 Kubernetes 的 Service + ConfigMap,也可以引入独立的注册中心如 Consul、Nacos。
我踩过的一个坑是:工具注册后没有做版本管理,结果某个工具升级接口不兼容,所有依赖它的 Agent 全部报错。后来我们在注册信息里强制加了apiVersion字段,Agent 调用前先校验版本,不匹配就降级或报错,问题才收敛。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 硬编码地址 | 简单直接 | 无法动态变更 | 工具少于 5 个的原型 |
| K8s Service + ConfigMap | 复用现有设施 | 更新有延迟 | 中小规模、同集群 |
| 独立注册中心 | 功能完整、跨集群 | 运维成本高 | 大规模、多集群 |
3.3 推理进程托管:显存是最贵的资源
推理进程(llama-server、vLLM、TGI 等)是 Agent 运行时里最“重”的部分。一个 7B 模型加载要占十几 GB 显存,启动要几十秒,而且不能像普通进程那样随便重启。托管推理进程的核心目标是最大化显存利用率,同时保证请求不丢。
我的做法是把推理进程单独抽成一层,用 Kubernetes 的 StatefulSet 管理,每个 Pod 绑定一块 GPU。请求进来先排队,由调度器决定发给哪个推理实例。关键参数有三个:maxBatchSize(批处理大小,影响吞吐和延迟)、gpuMemoryUtilization(显存利用率,一般设 0.9 留余量)、maxModelLen(最大上下文长度,直接影响显存占用)。
这里有个容易忽略的点:显存碎片。如果频繁加载卸载不同模型,显存会产生碎片,最终导致明明有空间却加载不了新模型。解决办法是尽量固定模型组合,或者用支持 PagedAttention 的推理引擎(如 vLLM),它能显著减少碎片。
3.4 编排策略:从串行到 DAG
最简单的 Agent 编排是串行:A 执行完执行 B,B 执行完执行 C。但真实场景往往是 DAG(有向无环图):A 的输出同时给 B 和 C,B 和 C 都完成后触发 D。运行时层必须支持这种依赖表达。
实现 DAG 编排有两种思路:一种是把依赖关系写进 Agent 定义里,由运行时解析执行;另一种是引入工作流引擎(如 Argo Workflows、Temporal),把 Agent 当成工作流中的一个节点。前者轻量但功能有限,后者强大但引入额外复杂度。我的建议是:Agent 数量少于 20 个、依赖关系简单时用前者;超过这个规模就上工作流引擎,否则自己实现的调度逻辑迟早会变成一团乱麻。
4. 实操过程:在 Kubernetes 上搭一套最小可用的 Agent 运行时
4.1 环境准备与前置检查
动手之前,先把环境确认清楚。我用的是一套三节点的 Kubernetes 集群(v1.26.0),一个 master、两个 worker,worker 上各挂一块 GPU。如果你没有 GPU,用 CPU 跑小模型也能验证流程,只是推理会慢很多。
前置检查清单如下:
- Kubernetes 版本不低于 1.24,确保
PodScheduler和RuntimeClass特性可用。 - 容器运行时正常。如果看到
container runtime is not running这类报错,先检查 containerd 或 CRI-O 服务状态,别急着往下走。 - GPU 节点装好驱动和 device plugin,
kubectl describe node能看到nvidia.com/gpu资源。 - 准备好镜像仓库,推理镜像动辄几个 GB,本地构建推送很慢。
提示:
[preflight] running pre-flight checks这类输出是 kubeadm 初始化时的正常日志,看到它说明流程在走,不用慌。真正要关注的是后面有没有[ERROR]级别的输出。
4.2 部署会话网关
会话网关是整个运行时的入口,负责解析 session-id 并做一致性哈希。我用 Envoy 实现,核心配置片段如下:
static_resources: listeners: - name: agent_gateway address: socket_address: { address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: agent domains: ["*"] routes: - match: { prefix: "/" } route: cluster: agent_runtime hash_policy: - header: header_name: "x-session-id"这段配置的关键是hash_policy,它让 Envoy 根据x-session-id请求头做一致性哈希。客户端每次请求都带上同一个 session-id,就能保证落到同一个后端实例。
4.3 部署推理进程 StatefulSet
推理进程用 StatefulSet 部署,每个 Pod 绑定一块 GPU。核心配置如下:
apiVersion: apps/v1 kind: StatefulSet metadata: name: inference-runtime spec: serviceName: inference-runtime replicas: 2 selector: matchLabels: { app: inference-runtime } template: metadata: labels: { app: inference-runtime } spec: containers: - name: llama-server image: ghcr.io/ggerganov/llama.cpp:server-cuda args: - "--model" - "/models/qwen2.5-7b-instruct-q4_k_m.gguf" - "--host" - "0.0.0.0" - "--port" - "8080" - "--n-gpu-layers" - "99" - "--ctx-size" - "8192" - "--parallel" - "4" resources: limits: nvidia.com/gpu: 1 volumeMounts: - name: models mountPath: /models volumeClaimTemplates: - metadata: { name: models } spec: accessModes: ["ReadOnlyMany"] resources: requests: storage: 50Gi参数说明:--n-gpu-layers 99表示把所有层都放到 GPU 上;--ctx-size 8192是上下文长度,越大越吃显存;--parallel 4表示同时处理 4 个请求,配合批处理能提升吞吐。实测下来,7B 的 Q4 量化模型在 16GB 显存上跑 8192 上下文,显存占用约 13GB,留了 3GB 余量,比较稳。
注意:如果你看到
no lm runtime found for model format 'gguf'这类报错,说明推理引擎版本和模型格式不匹配。gguf 格式需要较新版本的 llama.cpp,老版本不认。升级镜像即可。
4.4 工具服务注册与健康检查
工具服务用普通 Deployment 部署,启动时向注册中心上报。我用一个简单的 ConfigMap 加 sidecar 实现,sidecar 定期上报健康状态。核心逻辑是:工具启动后,sidecar 每 10 秒向注册中心发一次心跳,注册中心超过 30 秒没收到心跳就摘除该工具。
这里有个细节:摘除工具时要通知正在使用它的 Agent。否则 Agent 还在往一个已经下线的工具发请求,会一直超时。我的做法是注册中心维护一个“工具状态”接口,Agent 每次调用前先查状态,状态为draining时就不再发起新请求,等存量请求处理完再完全摘除。
4.5 编排 DAG 的落地
DAG 编排我用一个轻量的自研调度器实现,核心数据结构是邻接表。每个 Agent 节点记录自己的依赖节点列表,调度器从入度为 0 的节点开始执行,每完成一个节点就更新下游节点的入度,入度归零则触发执行。
from collections import deque def schedule(dag): indegree = {node: 0 for node in dag} for node, deps in dag.items(): for dep in deps: indegree[node] += 1 queue = deque([n for n, d in indegree.items() if d == 0]) order = [] while queue: node = queue.popleft() order.append(node) for n, deps in dag.items(): if node in deps: indegree[n] -= 1 if indegree[n] == 0: queue.append(n) return order这段代码返回的是拓扑排序后的执行顺序。实际运行时,每个节点执行完还要把输出传给下游节点,这部分我用一个共享的上下文存储(Redis)来传递,避免节点之间直接耦合。
5. 常见问题与排查技巧实录
5.1 推理进程启动慢、冷启动超时
这是最高频的问题。7B 模型冷启动 30 到 60 秒很正常,如果客户端超时设得短,用户直接看到报错。解决办法有三个:一是预热,在流量低峰期主动加载模型,保持常驻;二是就绪探针,把initialDelaySeconds设大一点(比如 60 秒),确保 Pod 真正就绪后才接流量;三是请求排队,网关层做队列,冷启动期间的请求先排队,等实例就绪再转发。
我实测下来,预热加就绪探针组合最有效。就绪探针配置如下:
readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 60 periodSeconds: 10 failureThreshold: 35.2 显存不足导致 OOM
显存 OOM 的排查思路是:先看nvidia-smi确认实际占用,再看推理引擎日志确认模型加载参数。常见原因是ctx-size设太大,或者parallel设太高导致并发请求把显存吃满。我的经验值是:16GB 显存跑 7B Q4 模型,ctx-size 不超过 8192,parallel 不超过 4。超过这个配置就要考虑换更大显存的卡,或者用更激进的量化。
5.3 会话丢失、上下文对不上
会话丢失通常有两个原因:一是网关没做一致性哈希,请求被轮询到不同实例;二是实例重启后本地状态没恢复。排查时先看网关日志,确认同一 session-id 是否落到同一后端;再看实例日志,确认重启后是否从外部存储恢复了状态。如果两个都没问题,那可能是客户端没带 session-id,检查请求头。
5.4 工具调用超时、级联失败
工具调用超时如果处理不当,会引发级联失败:A 等 B,B 等 C,C 挂了,A 和 B 都卡住。解决办法是给每个工具调用设独立超时,并配置熔断。超时后立即返回错误,让 Agent 决定是重试还是降级。熔断则是在工具连续失败 N 次后,直接拒绝后续请求一段时间,避免雪崩。
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 推理启动慢 | 模型大、冷启动 | 看启动日志耗时 | 预热 + 就绪探针 |
| 显存 OOM | ctx-size 或 parallel 过大 | nvidia-smi + 引擎日志 | 调小参数或换卡 |
| 会话丢失 | 无一致性哈希 | 网关日志 | 配置 hash_policy |
| 工具级联失败 | 无超时无熔断 | 调用链日志 | 独立超时 + 熔断 |
5.5 多集群场景下的调度延迟
如果你的 Agent 运行时跨多个集群,调度延迟会明显上升。原因是跨集群网络往返比同集群慢一个数量级。我的优化手段是:把会话状态和工具服务尽量部署在离用户近的集群,推理进程集中在 GPU 集群。这样会话读写是本地操作,只有推理请求跨集群,延迟可控。另外,跨集群调用要配置连接池和长连接,避免每次请求都重新建连。
6. 我在这套方案里踩过的几个真实坑
第一个坑是把推理进程和 Agent 逻辑塞进同一个 Pod。看起来省事,实际上两者生命周期完全不同:Agent 逻辑要频繁更新,推理进程要长期稳定。放一起的结果是每次更新 Agent 都要重启推理,冷启动把用户全赶跑了。后来拆成两个 Deployment,各自独立发布,问题才解决。
第二个坑是忽略工具调用的幂等性。Agent 重试工具调用时,如果工具不是幂等的,会产生重复副作用。比如“发邮件”工具被重试两次,用户收到两封邮件。解决办法是在工具层引入幂等键,Agent 每次调用带一个唯一 ID,工具层根据 ID 去重。
第三个坑是没有给推理进程设资源上限。有一次某个 Agent 疯狂发请求,把推理进程的显存吃满,导致同节点其他推理实例全部 OOM。后来给每个推理 Pod 设了nvidia.com/gpu: 1的硬限制,并在网关层做了限流,才杜绝了这类问题。
第四个坑是日志和指标没打通。Agent 出问题时,你需要在网关、运行时、推理进程、工具服务之间来回跳转查日志,效率极低。后来我们统一了 trace-id,每个请求从入口到出口带同一个 ID,所有组件日志都打这个 ID,排查时一个 ID 串起全链路,效率提升非常明显。
这套东西没有银弹,每个环节都要根据实际负载调。我现在的做法是:先用最小配置跑通全流程,再逐步加压,观察瓶颈在哪,针对性优化。别一上来就追求完美架构,那样只会陷入过度设计。