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

资讯详情

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

在Kubernetes上构建Agentic工作负载的运行时编排层

在Kubernetes上构建Agentic工作负载的运行时编排层

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

“ax”这个标题乍看像是一个缩写、一个代号,甚至像某个命令行工具的别名。但把热搜词摊开来看——agentic、orchestration、runtime、Kubernetes——这四个词凑在一起,指向的其实是一个非常具体的工程命题:在 Kubernetes 之上,为 agentic 工作负载构建一套可编排的运行时层。我第一眼看到这个组合时的判断是,这不是在讲某个单点工具,而是在讲一类系统设计思路,而“ax”很可能就是这套思路在某个项目里的代号或者入口命令。

先把话说直白一点。所谓 agentic 工作负载,指的是那些具备自主决策、多步推理、工具调用能力的智能体任务。它跟传统的无状态 HTTP 服务有本质区别:一次请求可能触发几十次内部循环,每次循环可能调用外部工具、读写记忆、再决定下一步走向。这种负载放到 Kubernetes 上,会立刻暴露几个问题——Pod 的生命周期跟任务的生命周期对不上,资源请求跟实际峰值对不上,失败重试的语义跟 K8s 默认的重启策略也对不上。orchestration 这个词在这里不是指 K8s 本身的调度,而是指在 K8s 之上再叠一层面向 agent 的编排逻辑,而 runtime 就是承载这层逻辑的执行环境。

我之所以对这个方向感兴趣,是因为过去一年多里,我陆续在几个内部项目里尝试过把 agent 任务直接塞进 K8s 的 Job 和 Deployment 里跑,踩的坑相当密集。最典型的一次是某个多步推理任务,Pod 因为 OOM 被 kill,K8s 按默认策略重启,结果任务从头再来一遍,前面已经完成的工具调用全部作废,还产生了重复的副作用。那次之后我才认真去想:agent 的运行时到底应该长什么样,它跟通用容器运行时之间的边界在哪里。

这篇文章适合几类人看。第一类是在做 agent 平台、想把任务调度做扎实的工程师;第二类是对 Kubernetes 有一定了解、但没想清楚 agent 负载该怎么落地的人;第三类是对 orchestration 和 runtime 这两个词的实际含义感到模糊、想找个具体场景把它讲透的读者。我会尽量把每个设计选择背后的“为什么”讲清楚,而不是只给结论。需要提前说明的是,文中涉及的具体参数和配置,一部分来自我自己的实测,一部分是基于常见工程实践做的合理推演,我会在相应位置标注清楚。

2. 核心概念拆解:agentic、orchestration、runtime 到底各指什么

2.1 agentic 负载的三个硬特征

要理解为什么需要专门的运行时,得先承认 agentic 负载跟普通服务不是一回事。我把它归纳成三个硬特征,这三个特征直接决定了后续所有的架构选择。

第一个特征是执行时长不可预测。一个普通的 REST 接口,P99 延迟可能就几百毫秒,你很容易给它定一个合理的 timeout。但 agent 任务不一样,它可能三步就结束,也可能循环四十步还在跑,中间还要等外部工具的响应。我实测过一个带检索增强的推理任务,同样的输入,因为检索结果不同,执行步数在 5 到 38 之间浮动,耗时从 8 秒到 4 分半不等。这种方差用固定的资源配额去套,要么浪费,要么频繁被 kill。

第二个特征是状态需要跨步骤保持。agent 的每一步都依赖前面的上下文,这个上下文可能是一段对话历史,可能是一组已经获取的事实,也可能是一个中间产物。传统无状态服务可以把状态外置到 Redis 或数据库,但 agent 的中间状态往往结构复杂、读写频繁,外置的代价很高。这就引出了运行时需要提供的能力:要么在进程内保持状态并保证进程不被随意中断,要么提供一套高效的检查点机制。

第三个特征是副作用需要精确控制。agent 会调用工具,工具可能发邮件、写数据库、下单、改配置。这些操作一旦重复执行,后果可能很严重。K8s 默认的重启策略是“挂了就重来”,这对无状态服务没问题,对 agent 就是灾难。所以运行时必须能区分“可安全重试”和“不可重复”的操作,并且在重启时做出正确的决策。

提示:如果你现在的 agent 任务还没有出现重复副作用的问题,很可能只是因为任务还简单、失败率还低。一旦任务变复杂、并发上来,这个问题一定会暴露。

2.2 orchestration 在 K8s 语境下的真实含义

很多人第一次听到 orchestration,会直接联想到 Kubernetes 的调度能力。但在 agentic 场景里,orchestration 指的是更高一层的协调逻辑,我把它拆成四个职责。

任务分解与依赖管理。一个复杂的 agent 请求,往往可以拆成若干子任务,子任务之间有先后依赖。比如先检索、再总结、再校验、最后输出。这层依赖关系 K8s 是不管的,K8s 只管 Pod 能不能起来,不管业务逻辑上的先后。

资源与配额分配。不同的子任务对资源的需求差异很大。检索类任务吃网络和内存,推理类任务吃 GPU,校验类任务可能很轻。orchestration 层需要根据任务类型,把请求路由到合适的节点池,而不是让所有 Pod 用同一套 resource request。

失败处理与重试策略。这是 orchestration 最核心的价值。哪些失败可以重试、重试几次、重试时是否复用之前的中间状态,这些决策必须在编排层做,不能交给 K8s 的默认行为。

可观测性与追踪。一个 agent 任务跨多个 Pod、多个步骤,出了问题要能快速定位是哪一步、哪个工具调用出的错。这需要编排层在任务维度上做统一的 trace 聚合。

我个人的经验是,orchestration 层做得越薄越好,但该有的决策点一个都不能少。见过一些项目把编排逻辑写得极其复杂,最后维护成本高到没人敢改。比较务实的做法是:把“任务生命周期管理”和“资源调度”分开,前者用一套轻量的状态机,后者尽量复用 K8s 原生能力。

2.3 runtime 的边界:它不该做什么

runtime 这个词被用得太泛了。在 agentic 语境下,我倾向于给它一个明确的边界:runtime 负责单个 agent 实例的执行环境,orchestration 负责多个实例之间的协调。这个边界一旦模糊,系统就会变得难以调试。

runtime 该做的事包括:加载 agent 的配置和依赖、管理进程内的状态、提供工具调用的统一接口、处理检查点的读写、暴露健康检查和指标。runtime 不该做的事包括:决定任务该不该重试、决定任务该调度到哪个节点、决定多个任务之间的依赖顺序。这些都属于 orchestration 的范畴。

为什么要把边界划得这么清楚?因为这两层的变更频率完全不同。runtime 相对稳定,一旦定型,改动很少;orchestration 则经常需要根据业务调整策略。如果两者耦合在一起,每次调整编排策略都要动 runtime,风险很大。我在一个项目里就吃过这个亏,早期把重试逻辑写进了 runtime,后来业务要求改重试策略,结果发现要重新构建整个 runtime 镜像,灰度发布折腾了整整两天。

3. 为什么要在 Kubernetes 上做这件事:选型背后的取舍

3.1 直接裸跑进程 vs 容器编排

最朴素的方案是不用 K8s,直接在一台机器上跑 agent 进程,用 supervisor 之类的工具管理。这个方案在小规模下完全可行,我早期就是这么干的。它的优点是简单、调试方便、没有额外的抽象层。但一旦规模上来,问题就来了:机器故障时任务怎么迁移、资源怎么隔离、多租户怎么保证互不干扰、扩容怎么自动化。这些问题每一个单独解决都不难,但凑在一起就是一座山。

K8s 的价值在于它把这些通用问题都解决过了,而且解决得相当成熟。你不需要自己造轮子去处理节点故障、资源配额、服务发现、滚动更新。代价是你要接受它的抽象模型,并且想办法让 agent 负载适配这个模型。这个适配过程就是本文要讲的核心。

3.2 为什么不用现成的 Serverless 方案

有人会问,既然 agent 任务时长不定、按需触发,为什么不直接用 Serverless?我的实测结论是:短任务可以,长任务不行。主流 Serverless 平台的单次执行时长上限通常在几分钟到十几分钟,而复杂 agent 任务很容易超过这个限制。另外 Serverless 的冷启动对 agent 这种需要加载模型或大量依赖的场景很不友好,冷启动一次可能要几十秒,用户体验直接崩掉。

还有一个更隐蔽的问题:Serverless 的计费模型是按执行时长算的,agent 任务在等待外部工具响应时是“空转”的,这段时间照样计费。如果 agent 任务里有大量等待,成本会高得离谱。相比之下,K8s 上你可以让 Pod 在等待时释放部分资源,或者用更细粒度的调度策略来优化。

3.3 自建 runtime 还是复用现有框架

这是我在项目里纠结最久的一个问题。市面上已经有一些面向 agent 的编排框架,它们提供了任务定义、工具注册、状态管理等功能。直接复用能省很多事,但也会带来约束:框架的抽象不一定贴合你的业务,深度定制时可能比自建还麻烦。

我的判断标准是看核心逻辑的独特性。如果你的 agent 逻辑跟框架的默认模型高度一致,复用是明智的;如果你的 agent 有大量特殊的工具调用、特殊的状态管理需求、特殊的失败处理逻辑,那自建 runtime 反而更省心。我最后选择的是混合方案:runtime 自建,但工具调用的协议、检查点的格式尽量对齐社区常见做法,这样将来要迁移或集成会容易很多。

4. 运行时层的核心设计:从任务模型到检查点

4.1 任务模型怎么定义才够用

任务模型是整个 runtime 的地基,定义得好,后面一切都顺;定义得不好,处处要打补丁。我踩过几次坑之后,总结出一个最小可用的任务模型,包含五个字段。

任务 ID:全局唯一,用于追踪和幂等。这个 ID 必须在任务创建时就确定,不能等到 Pod 起来才生成,否则重试时无法关联。

任务类型:决定用哪套执行逻辑、哪套资源配额。类型不宜过多,我一般控制在十种以内,太多了维护成本高。

输入参数:任务的输入,需要可序列化,方便在检查点里存储和恢复。

状态:至少要有 pending、running、succeeded、failed、retrying 这几个状态。状态转换必须由 orchestration 层统一管理,runtime 只上报,不自己改。

检查点引用:指向最近一次成功保存的检查点,用于失败恢复。

这个模型看起来简单,但每个字段的设计都有讲究。比如任务 ID 的生成,我一开始用的是 UUID,后来发现排查问题时很难从 ID 看出任务是什么时候创建的,就改成了“时间戳前缀 + 随机后缀”的格式,排查效率提升明显。

4.2 检查点机制:什么时候存、存什么、存哪里

检查点是 agent runtime 区别于普通容器运行时的关键能力。没有检查点,任务失败就只能从头再来;有了检查点,可以从最近的成功点恢复,省时省资源。

什么时候存是个策略问题。存得太频繁,开销大;存得太稀疏,恢复时浪费的工作多。我的经验是:在“不可重复的副作用操作”之前必须存,在“耗时较长的步骤”之后建议存。前者是为了避免重复副作用,后者是为了减少恢复时的重算量。具体间隔可以根据任务的平均步骤耗时来定,我一般设置在每步耗时超过 5 秒时触发一次检查点。

存什么也需要斟酌。全量存当然最安全,但体积可能很大。我通常只存三类数据:对话历史或上下文、已完成步骤的标识、外部工具调用的结果摘要。中间的计算过程不存,因为可以重算。这样检查点的体积通常能控制在几百 KB 到几 MB 之间。

存哪里取决于你的基础设施。对象存储适合大检查点,读写延迟稍高但成本低;Redis 适合小检查点,读写快但容量有限;数据库适合需要复杂查询的场景。我在生产环境用的是对象存储加本地缓存的组合:检查点先写本地,再异步上传到对象存储,恢复时优先读本地,本地没有再去对象存储拉。

4.3 工具调用的统一接口设计

agent 要调用工具,工具的种类五花八门,有 HTTP 接口、有本地函数、有数据库操作。如果每个工具都单独适配,runtime 会变得很臃肿。我的做法是定义一套统一的工具调用接口,所有工具都通过这个接口暴露。

接口的核心是一个结构化的请求和响应。请求里包含工具名、参数、调用 ID、超时设置;响应里包含状态、结果、错误信息、耗时。调用 ID 很关键,它用于幂等控制:同一个调用 ID 的重复请求,runtime 应该返回缓存的结果,而不是重新执行。

这里有个容易忽略的细节:工具调用的超时设置要分层。单次调用的超时、整个步骤的超时、整个任务的超时,三层都要有,而且要有合理的递进关系。我见过只设了任务级超时的项目,结果某个工具卡住,整个任务被拖到超时才失败,中间的资源全浪费了。

注意:工具调用的幂等性不能只靠调用 ID 来保证,还要看工具本身是否支持幂等。对于不支持幂等的工具,runtime 必须在调用前先查检查点,确认这个调用是否已经执行过。

5. 在 Kubernetes 上落地的实操细节

5.1 用 Job 还是用 Deployment

这是落地时第一个要回答的问题。我的结论是:短任务用 Job,长任务用 Deployment 加自定义控制器。

Job 的优点是语义清晰,跑完就结束,K8s 原生支持重试和并行。但 Job 的重试是“整个 Pod 重来”,不支持从检查点恢复。如果你的任务能在几分钟内跑完,且失败重试的代价可以接受,Job 是很好的选择。

长任务的问题在于,Job 的 activeDeadlineSeconds 一旦设置,超时就会强制终止,而 agent 任务的时长方差很大,很难设一个合适的值。这时候用 Deployment 管理一组常驻的 worker Pod,由 worker 从队列里拉任务执行,会更灵活。worker 可以自己控制任务的生命周期,失败时从检查点恢复,不受 K8s 重启策略的干扰。

我现在的项目用的是混合模式:轻量任务走 Job,重量任务走常驻 worker。两套并存确实增加了复杂度,但换来的是每类任务都能用最合适的模型。

5.2 资源配额的设置思路

agent 任务的资源需求波动大,用固定的 request 和 limit 很容易出问题。我的做法是分三层设置。

基础层:给所有 agent Pod 一个保底的 request,保证调度能成功。这个值可以设得比较小,比如 0.5 核 CPU、512MB 内存。

峰值层:limit 设得比 request 高一些,允许 Pod 在峰值时 burst。但 limit 不能设得太高,否则节点超卖严重,一个 Pod 峰值时可能把整个节点拖垮。我一般把 limit 设为 request 的 2 到 3 倍。

动态层:对于确实需要大资源的任务,用单独的节点池,配合 nodeSelector 或 affinity 把 Pod 调度过去。这样既保证了普通任务的密度,又保证了大任务有足够的资源。

这里有个实测数据可以参考:一个带检索的推理任务,稳态内存占用约 800MB,峰值能到 2.5GB,主要峰值来自检索结果的加载和上下文拼接。如果 limit 只设 1GB,任务会在峰值时被 OOM kill。设到 3GB 之后,连续跑了一周没有再出现 OOM。

5.3 健康检查与优雅退出

agent Pod 的健康检查不能照搬普通服务的做法。普通服务的 readiness 探针检查的是“能不能接收请求”,agent Pod 的 readiness 应该检查的是“能不能接收新任务”。一个正在执行任务的 agent Pod,即使还在正常运行,也不应该接收新任务,否则会过载。

我的做法是给 agent Pod 加一个“忙碌标记”,readiness 探针检查这个标记。Pod 开始执行任务时标记为忙碌,readiness 返回失败,K8s 就不会把新任务路由过来。任务结束后标记清除,readiness 恢复。

优雅退出同样重要。agent 任务被中断时,runtime 应该有机会保存检查点。这需要 Pod 在收到 SIGTERM 后,先停止接收新任务,再等待当前任务到达一个可保存的点,保存检查点,然后退出。K8s 的 terminationGracePeriodSeconds 要设得足够长,我一般设 120 秒,给检查点保存留足时间。

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

6.1 任务重复执行的排查路径

任务重复执行是 agent runtime 最常见也最头疼的问题。表现是同一个任务被执行了两次甚至多次,产生了重复的副作用。排查时按这个顺序走。

先看 orchestration 层的任务状态机,确认是不是状态转换出了问题,比如任务已经 succeeded 但状态没更新,导致被重新调度。再看 runtime 的检查点,确认失败恢复时是不是从错误的检查点恢复,导致已经完成的步骤被重做。最后看工具调用层,确认幂等控制是否生效,同一个调用 ID 是不是被重复执行了。

我遇到过一次很隐蔽的重复执行:任务在保存检查点后、上报成功前崩溃,orchestration 层看到任务还是 running,就重新调度了。修复方法是在保存检查点后立即上报一个“即将完成”的状态,orchestration 层看到这个状态就等待一段时间再决定是否重试。

6.2 检查点损坏或丢失怎么办

检查点损坏通常是因为写入过程中进程被 kill,导致文件不完整。防范方法是先写临时文件,写完再原子重命名。这样即使写入中断,也不会破坏已有的检查点。

检查点丢失则可能是存储层的问题。我的做法是检查点至少存两份,本地一份、远端一份,恢复时优先本地,本地没有再去远端拉。如果两份都丢了,那就只能从头执行,但要确保从头执行不会产生重复副作用——这就是为什么幂等控制必须在工具调用层做,而不能只依赖检查点。

6.3 资源不足导致的连锁失败

资源不足的表现是 Pod 频繁被 OOM kill 或调度失败。排查时先看 Pod 的 events,确认是调度失败还是运行中被 kill。调度失败通常是 request 设得太大,节点放不下;运行中被 kill 通常是 limit 设得太小,峰值时超了。

我整理了一个速查表,覆盖几种典型情况。

现象可能原因排查方法处理建议
Pod 一直 Pendingrequest 过大或节点资源不足看 describe pod 的 events降低 request 或扩容节点池
Pod 频繁重启limit 过小或内存泄漏看 pod 的 restart count 和 OOM 记录提高 limit 或排查泄漏
任务超时失败超时设置不合理或工具卡住看任务各步骤耗时分布调整超时或加工具级超时
检查点写入失败存储不可用或权限问题看 runtime 日志检查存储连接和权限配置

6.4 几个我踩过的坑

第一个坑是把 agent 的上下文存在了 Pod 的本地磁盘。Pod 重启后本地磁盘清空,上下文全丢。后来改成检查点存对象存储,本地只做缓存,问题解决。

第二个坑是用 K8s 的 liveness 探针检查 agent 的存活。agent 在执行长任务时,主线程可能被占用,导致探针超时,Pod 被误杀。后来把 liveness 探针改成检查一个独立的健康端点,不依赖主线程,问题解决。

第三个坑是任务队列没有做优先级。所有任务一视同仁,结果一个长任务堵在前面,后面的短任务全被拖慢。后来加了优先级队列,短任务和高优先级任务可以插队,整体吞吐提升明显。

7. 我对这套方案的一些个人体会

做 agentic runtime 这件事,最大的感受是边界比功能重要。一开始总想着把功能做全,结果 runtime 越来越臃肿,调试越来越难。后来想清楚了 runtime 和 orchestration 的边界,把该分出去的逻辑分出去,系统反而稳定了。

另一个体会是检查点的设计要趁早。我第一个版本没做检查点,任务失败就从头来,调试阶段还能忍,上了生产就完全不行。后来补检查点,发现很多地方要改,成本比一开始就设计高得多。所以如果你现在正在做类似的东西,哪怕任务还简单,也建议把检查点的接口先留出来。

还有一个反直觉的经验:不要过度追求资源利用率。我早期总想把节点资源压榨到极致,结果 Pod 频繁因为资源竞争被 kill。后来把资源留出 30% 的余量,虽然成本高了一点,但稳定性提升了一大截,综合算下来反而更划算。

这套东西还在持续演进,Kubernetes 社区对 agentic 负载的支持也在变化。我目前关注的一个方向是,能不能用更原生的方式表达 agent 任务的生命周期,而不是靠自定义控制器去补。如果这块有进展,现在的很多 workaround 可能就不需要了。

返回列表