1. 从 Pod 调度到 Agent 调度:一个正在成形的抽象层
Kubernetes 把"容器"这个原本散落在各处的概念,抽象成了 Pod 这个可被统一调度的最小单元,于是才有了声明式、可编排、可观测的云原生生态。现在 Google 开源 AX 想做的事情,本质上是把"Agent"也抽象成类似 Pod 的东西——让一个智能体不再是一段跑在笔记本里的脚本,而是一个可以被集群统一调度、扩缩、观测、回收的工作负载。
这个方向不是凭空冒出来的。过去一年里,Agent 开发从"写个 prompt 调 API"迅速演化成了"多 Agent 协作 + 工具调用 + 长任务编排",随之而来的问题非常具体:一个 Agent 跑长任务时怎么保证不丢状态?多个 Agent 抢同一个工具资源怎么办?某个 Agent 卡死了谁来重启它?这些问题在单机脚本时代根本不存在,但一旦 Agent 数量上到几十上百个,就变成了彻头彻尾的调度问题。
AX 的核心价值就在这里:它试图定义一套 Agent 的运行时契约,让 Agent 拥有类似 Pod 的生命周期语义——创建、调度、运行、健康检查、重启、销毁。对做 Agent 开发的人来说,这意味着你不再需要自己写一套"任务队列 + 状态机 + 重试逻辑",而是把这些交给调度层。对做基础设施的人来说,这意味着 Agent 终于可以像普通工作负载一样被纳入现有的集群管理体系。
这篇文章会从 AX 的设计动机讲起,拆解它到底抽象了什么、和 Pod 的类比边界在哪里、实际落地时会遇到哪些坑,以及如果你现在就想在自己的项目里借鉴这套思路,应该从哪几个点切入。适合正在做 Agent 平台、多智能体编排、或者单纯被 Agent 状态管理折磨过的开发者。
2. AX 到底抽象了什么:Agent 作为一等调度单元
2.1 为什么"Agent 即工作负载"是个合理的抽象
先想清楚一件事:Agent 和普通微服务到底差在哪?如果只是"接收请求、调用模型、返回结果",那它就是个无状态服务,用 Deployment 就够了,根本不需要新概念。真正让 Agent 变得特殊的是三个特征。
第一是长时运行与状态依赖。一个 Agent 处理复杂任务时可能跑几分钟甚至几小时,中间要维护对话历史、工具调用结果、中间推理状态。这跟 Pod 里跑一个长时间 Job 很像,但 Agent 的状态更"软"——它不是文件系统里的数据,而是内存里的上下文。
第二是资源竞争与配额。Agent 调用的模型 API 有速率限制,工具(比如浏览器、代码执行沙箱)是稀缺资源,多个 Agent 同时抢一个工具就会互相拖垮。这跟 Pod 抢 CPU、内存、GPU 是同构的问题。
第三是生命周期的不确定性。Agent 可能因为模型返回异常、工具超时、上下文溢出而"卡住",需要外部介入重启或迁移。这正是调度器最擅长处理的事情。
AX 的抽象逻辑就是:既然 Agent 具备这三类特征,那就应该给它一套和 Pod 对等的调度语义,而不是让每个 Agent 框架各自造轮子。
2.2 AX 与 Pod 的类比边界:哪些能照搬,哪些不能
把 Agent 类比成 Pod 很好理解,但不能无脑照搬。下面这张表是我梳理的核心差异,理解这些差异比记住 AX 的 API 更重要。
| 维度 | Pod | Agent(AX 抽象) | 关键差异 |
|---|---|---|---|
| 生命周期 | 创建到终止,状态明确 | 可能"假活"(进程在但推理卡住) | 健康检查更难定义 |
| 状态 | 主要靠 Volume 持久化 | 上下文在内存,需快照机制 | 状态迁移成本高 |
| 资源 | CPU/内存/GPU 可量化 | 模型 token、工具并发数 | 资源度量不标准 |
| 调度依据 | 节点资源、亲和性 | 模型配额、工具可用性、上下文长度 | 调度维度更"软" |
| 失败恢复 | 重启容器即可 | 重启后上下文可能丢失 | 需要状态检查点 |
这张表里最关键的一行是"健康检查"。Pod 的 liveness probe 很简单——进程活着、端口能通就算健康。但 Agent 的"健康"是个模糊概念:它可能进程正常、端口正常,但推理已经陷入死循环,或者上下文已经溢出导致输出全是垃圾。AX 要解决的核心难题之一,就是定义一套 Agent 层面的健康语义。
2.3 调度层需要接管的三件事
如果让我总结 AX 这类系统真正要接管的能力,就是三件事:放置、编排、回收。
放置(Placement)决定一个 Agent 应该跑在哪个执行环境上。这个决策要考虑的因素比 Pod 多得多:模型配额够不够、需要的工具在不在这个节点、上下文长度是否超过该节点的处理能力、甚至 Agent 之间的通信延迟。
编排(Orchestration)处理 Agent 之间的依赖关系。多 Agent 系统里,Agent A 的输出是 Agent B 的输入,这种 DAG 依赖需要调度层来保证执行顺序和错误传播。这跟 Kubernetes 里 Job 的依赖管理思路一致,但 Agent 的依赖是动态的——运行时才知道下一个要调谁。
回收(Reclamation)是容易被忽视的一环。Agent 跑完任务后,它的上下文、占用的工具连接、缓存的模型会话都需要释放。如果回收不干净,跑几百个 Agent 之后系统就会被僵尸资源拖垮。这一点在单机脚本时代靠进程退出自动解决,但在调度层里必须显式处理。
3. 调度器视角下的 Agent:放置、编排与回收的完整链路
3.1 放置决策:Agent 该被调度到哪里
放置决策是调度器的第一道关卡。Pod 的放置相对简单,看节点剩余资源、亲和性规则、污点容忍就够了。Agent 的放置要复杂一个量级,因为它要同时满足"硬约束"和"软约束"。
硬约束是那些不满足就完全跑不起来的条件。比如某个 Agent 依赖一个特定的代码执行沙箱镜像,那它只能被放到预装了该镜像的节点上。再比如 Agent 需要访问一个内部工具服务,网络不通的节点直接排除。这些约束和 Pod 的 nodeSelector、taints 是一个思路。
软约束是"最好满足但不强制"的条件。比如某个 Agent 对延迟敏感,那优先放到离模型服务近的节点;某个 Agent 上下文很长,优先放到内存充裕的节点。软约束用打分机制处理,每个候选节点算一个分数,选最高的。
我实际做 Agent 平台时踩过的一个坑是:把模型配额当成了硬约束。结果某个节点配额临时用满,调度器就把 Agent 判定为"无法调度",任务直接失败。正确做法是把配额当软约束——配额紧张时降级到排队,而不是直接拒绝。这个区别在 Pod 世界里不明显(CPU 不够就是不够),但在 Agent 世界里,配额是可以"等一等"的。
3.2 编排链路:多 Agent 依赖怎么表达
多 Agent 系统的编排,本质上是把一个任务拆成有向无环图(DAG),每个节点是一个 Agent 执行单元,边是数据依赖。调度层要做的是:按拓扑顺序触发 Agent,把上游输出传给下游,处理某个节点失败时的重试或跳过。
这里有个和传统工作流引擎(比如各种 DAG 调度器)的关键区别:Agent 的 DAG 往往是动态的。传统工作流在提交时图就固定了,但 Agent 经常在运行时才决定下一步调用谁——比如一个"研究 Agent"根据搜索结果决定要不要再派一个"验证 Agent"。这就要求调度层支持动态加节点。
AX 这类系统的处理方式通常是引入一个"控制 Agent"的概念,它负责在运行时生成子任务并提交给调度器。控制 Agent 本身也是一个被调度的 Agent,只是它的输出是"新的调度请求"而不是"最终结果"。这种递归结构让系统能表达任意复杂的编排逻辑,代价是调度器要处理动态增长的负载。
3.3 回收与状态检查点:别让 Agent 变成僵尸
回收这件事,我在两个项目里都吃过亏。第一个项目是 Agent 跑完任务后没释放模型会话,跑了一天之后模型服务的连接池被打满,新任务全部超时。第二个项目是 Agent 的上下文快照没清理,磁盘被写满。
AX 的回收机制应该包含三层:执行环境回收(释放容器/沙箱)、资源回收(释放模型会话、工具连接)、状态回收(清理或归档上下文快照)。前两层和 Pod 的回收类似,第三层是 Agent 特有的。
状态检查点(Checkpoint)是回收的反面——不是删除状态,而是把状态存下来以便恢复。一个长任务 Agent 跑到一半被抢占,如果没有检查点,重启后就得从头再来。检查点的粒度是个权衡:太粗(比如只在任务边界存)恢复后丢失工作多,太细(每步都存)开销大。我的经验是按"不可逆操作"为边界存检查点——比如调用了一个有副作用的工具之后立刻存,因为这一步重放代价高。
4. 把 AX 思路落地到自己的 Agent 平台:可复现的实践路径
4.1 最小可行抽象:先定义 Agent 的 Spec
如果你现在就想在自己的项目里借鉴 AX 的思路,不要一上来就搞完整调度器。第一步是定义一个 Agent 的 Spec——也就是"描述一个 Agent 需要哪些字段"。这是整个抽象的地基,定义错了后面全要返工。
一个够用的 Agent Spec 至少包含这几块:镜像/运行时(Agent 跑在什么环境里)、入口(怎么启动它)、资源需求(模型配额、工具、内存)、依赖(上游 Agent 或数据源)、健康检查(怎么判断它还活着)、检查点策略(状态怎么存)。
# 一个借鉴 AX 思路的 Agent Spec 示例 apiVersion: agent.example.com/v1 kind: Agent metadata: name: research-agent spec: runtime: image: agent-runtime:latest entrypoint: ["python", "-m", "agent.main"] resources: modelQuota: provider: internal tokensPerMinute: 100000 tools: - name: web-search concurrency: 2 - name: code-sandbox concurrency: 1 memory: 2Gi dependencies: - agent: planner-agent outputKey: plan healthCheck: type: progress-based stallTimeout: 120s checkpoint: strategy: on-side-effect storage: s3://agent-checkpoints/这个 Spec 里最值得说的是healthCheck的progress-based类型。传统的进程存活检查对 Agent 没用,你需要的是"进度检查"——如果 Agent 在 120 秒内没有任何有意义的进展(没有新的工具调用、没有新的输出 token),就判定它卡住了。这个stallTimeout的值要根据任务类型调,我一般设成"正常单步耗时的 3 到 5 倍"。
4.2 调度循环:从队列到执行的完整代码骨架
有了 Spec 之后,调度循环的核心逻辑其实不复杂。下面是一个简化版的调度器骨架,用 Python 写,重点是展示"放置-执行-回收"的完整链路。
import time from dataclasses import dataclass from typing import List, Optional @dataclass class AgentSpec: name: str model_quota: int tools: List[str] memory_mb: int @dataclass class Node: name: str free_quota: int available_tools: List[str] free_memory_mb: int def score_node(node: Node, spec: AgentSpec) -> Optional[float]: """返回 None 表示硬约束不满足,直接排除""" # 硬约束:工具必须全部可用 if not set(spec.tools).issubset(set(node.available_tools)): return None # 硬约束:内存必须够 if node.free_memory_mb < spec.memory_mb: return None # 软约束:配额越充裕分数越高 quota_score = min(node.free_quota / max(spec.model_quota, 1), 1.0) memory_score = min(node.free_memory_mb / max(spec.memory_mb, 1), 1.0) return 0.6 * quota_score + 0.4 * memory_score def schedule(spec: AgentSpec, nodes: List[Node]) -> Optional[Node]: scored = [(score_node(n, spec), n) for n in nodes] valid = [(s, n) for s, n in scored if s is not None] if not valid: return None valid.sort(key=lambda x: x[0], reverse=True) return valid[0][1] def run_agent(spec: AgentSpec, node: Node): """执行 Agent,带进度健康检查和检查点""" node.free_quota -= spec.model_quota node.free_memory_mb -= spec.memory_mb last_progress = time.time() try: while True: # 这里是实际的 Agent 执行逻辑 progress = step_agent(spec) if progress: last_progress = time.time() if progress.is_side_effect: save_checkpoint(spec, progress) if progress and progress.done: break if time.time() - last_progress > 120: raise TimeoutError(f"Agent {spec.name} stalled") finally: # 回收:无论成功失败都要释放资源 node.free_quota += spec.model_quota node.free_memory_mb += spec.memory_mb这段代码里有两个设计点值得展开。第一是score_node返回None表示硬约束不满足,而不是返回 0 分——这个区别很重要,0 分可能被误选,None会被明确排除。第二是finally块里的资源回收,这是防止僵尸资源的关键,无论 Agent 是正常结束还是抛异常,资源都会被释放。
4.3 健康检查的三种实现方式与选择依据
Agent 的健康检查是整个系统里最难做对的部分。我试过三种方式,各有适用场景。
进程存活检查最简单,就是看 Agent 进程还在不在。这种方式对"崩溃型"失败有效,但对"卡死型"失败完全无效。适合那些执行时间短、逻辑简单的 Agent。
进度检查是我最推荐的默认方案。核心思路是让 Agent 在执行过程中定期上报"进度信号",调度器维护一个last_progress时间戳,超过阈值就判定卡死。进度信号可以是"完成了一次工具调用""产生了新的输出""更新了内部状态"等。这种方式能抓住绝大多数卡死场景,代价是 Agent 需要配合上报。
心跳检查介于两者之间,Agent 定期发心跳,调度器看心跳是否超时。它比进程检查强,但比进度检查弱——因为 Agent 可能心跳正常但实际没进展(比如在一个循环里空转)。适合那些无法方便上报进度的第三方 Agent。
选择依据很简单:能改 Agent 代码就用进度检查,不能改就用心跳检查,两者都不行才退回进程检查。我在一个项目里因为用的是第三方 Agent 框架改不了代码,只能用心跳检查,结果遇到过一个 Agent 心跳正常但推理陷入循环的情况,最后是靠加了一个"输出重复度检测"才兜住。
5. 实测中的坑:Agent 调度和 Pod 调度不一样的地方
5.1 上下文迁移:为什么 Agent 不能像 Pod 一样随便漂移
Pod 被驱逐后可以在另一个节点重建,因为它的状态主要在 Volume 里,重建后挂载同一个 Volume 就恢复了。Agent 不行——它的核心状态是内存里的上下文,迁移意味着要把整个上下文序列化、传输、反序列化,成本极高,而且很多上下文(比如模型内部的 KV cache)根本没法序列化。
这个差异导致一个直接后果:Agent 调度要尽量避免迁移。Pod 调度器可以激进地做负载均衡,把 Pod 到处挪;Agent 调度器应该保守,优先在原地重启,只有节点彻底不可用才迁移。
我在实际项目里的做法是给 Agent 加一个"粘性"标记,调度器优先把它放回上次运行的节点。如果那个节点不可用,才走迁移流程,并且迁移时强制从最近的检查点恢复,而不是试图搬运内存状态。
5.2 资源度量的模糊性:token 配额不是 CPU
CPU 是个精确的度量——你需要 2 核就是 2 核,调度器算得清清楚楚。但 Agent 的资源需求是模糊的:一个 Agent "需要多少 token"取决于任务复杂度,可能这次用 1 万 token,下次用 10 万。你没法在 Spec 里精确声明。
这个模糊性带来两个问题。第一是放置决策不准,你可能把一个实际需要大量 token 的 Agent 放到了一个配额紧张的节点。第二是配额超卖,多个 Agent 声明各需 5 万 token,加起来 15 万,但节点只有 10 万,实际运行时就会互相挤兑。
我的应对策略是用历史统计代替静态声明。Agent Spec 里的配额声明只作为初始值,调度器维护每个 Agent 类型的历史 token 消耗分布(P50、P95),放置决策时用 P95 而不是声明值。这样虽然保守一点,但能避免大部分超卖问题。同时给配额加一个"软上限",超过就降速而不是直接杀,给 Agent 一个优雅降级的机会。
5.3 失败传播:一个 Agent 挂了,整条链怎么办
多 Agent 链路里,一个 Agent 失败会沿着依赖边传播。Pod 世界里这个问题相对简单——Job 失败就重试,重试次数用完就标记失败。Agent 链路要复杂得多,因为失败的类型多样:可能是模型返回异常(重试有用)、可能是工具不可用(重试没用,要换工具)、可能是上下文溢出(重试没用,要压缩上下文)。
AX 这类系统通常提供几种失败策略:重试(原地重试 N 次)、降级(换一个更简单的模型或工具)、跳过(标记该节点失败但继续下游)、中止(整条链停掉)。选择哪种取决于业务语义。
我的经验是默认用"重试 + 降级"组合:先原地重试 2 次,如果还是失败就降级到更保守的执行方式(比如换小模型、减少工具调用),再失败才中止。跳过策略要慎用,因为下游 Agent 拿到一个"空输入"可能产生更隐蔽的错误,比直接失败更难排查。
5.4 观测性:Agent 的日志和 Pod 的日志不是一回事
Pod 的日志是线性的——进程启动、处理请求、输出结果、退出。Agent 的日志是树状的——它可能派生子 Agent、调用工具、产生中间推理,这些都需要被追踪。用传统的日志系统看 Agent 日志,你会看到一堆交错的输出,根本理不清哪个属于哪个子任务。
正确的做法是给每个 Agent 执行分配一个 trace ID,所有相关的日志、工具调用、子 Agent 都带上这个 ID。这样你就能在观测系统里看到一棵完整的执行树。我在项目里用的是 OpenTelemetry 的 span 机制,每个 Agent 是一个 span,工具调用是子 span,子 Agent 是嵌套 span。这样排查问题时能一眼看出是哪个环节慢、哪个环节错。
6. 从 AX 看 Agent 基础设施的下一步
AX 把 Agent 抽象成可调度单元这件事,方向是对的,但它现在还处在"定义概念"的阶段,离"生产可用"还有距离。我从自己的实践出发,觉得接下来这个领域会往三个方向走。
第一个方向是标准化的 Agent 运行时接口。现在每个 Agent 框架都有自己的启动方式、状态管理方式、工具调用协议,调度层要为每个框架写适配器。如果有一个类似 CRI(容器运行时接口)的 Agent 运行时接口,调度层就能统一对接。AX 有可能成为这个标准的雏形。
第二个方向是Agent 感知的自动扩缩。Pod 的 HPA 基于 CPU、内存这些指标扩缩,但 Agent 的负载指标是"待处理任务数""平均任务时长""模型配额利用率"。这些指标需要新的采集和扩缩策略。我试过用待处理任务数做扩缩,效果比 CPU 指标好得多,因为 Agent 的瓶颈通常不在 CPU。
第三个方向是跨集群的 Agent 调度。单个集群的 Agent 调度解决后,下一个问题就是多个集群之间怎么调度——比如把 Agent 放到离数据近的集群、或者放到配额充裕的集群。这需要一套跨集群的调度协议,复杂度比单集群高一个量级。
如果你现在正在做 Agent 平台,我的建议是不要等标准成熟,先用本文第 4 节的思路搭一个最小可用的调度层。核心就是三件事:定义 Agent Spec、实现放置打分、做好资源回收。这三件事做扎实了,后面无论 AX 怎么演进,你的系统都能平滑对接。真正难的不是调度算法本身,而是把 Agent 的生命周期语义想清楚——这一点,Pod 的设计给了我们足够好的参照。