ARTICLE DETAIL

资讯详情

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

Kubernetes 上构建 Agentic 编排运行时:状态一致性与重试实践

Kubernetes 上构建 Agentic 编排运行时:状态一致性与重试实践 1. 从ax这个标题说起一个被低估的运行时缩写第一次看到ax这个标题大多数人会一头雾水。它太短了短到不像一个正经项目名。但结合热搜词里的agentic、orchestration、runtime、Kubernetes这几个关键词方向其实很清晰——这是一个围绕Agentic 编排运行时的工程实践主题。而ax本身在工程语境里通常是一个精简的代号可能是某个内部工具链的缩写也可能是agent execution的简写形态。不管它具体指代什么核心命题是确定的如何让多个智能体在 Kubernetes 之上被可靠地调度、编排和运行。我自己在过去一年多的时间里断断续续搭过三套不同规模的 agent 编排系统从最早的单机脚本调度到后来基于 K8s 的分布式运行时踩过的坑足够写一本小册子。所以这篇内容不打算讲空泛的概念而是把ax这类 agentic runtime 在 K8s 上落地时真正会遇到的问题、真正需要做的取舍一条一条摊开来讲。适合谁看如果你正在做 AI Agent 相关的后端工程或者你手上有一堆 agent 任务需要被统一管理又或者你只是好奇agentic orchestration到底在工程上意味着什么这篇都能给你一些能直接抄的参考。先说一个反直觉的结论agent 编排最难的部分从来不是编排本身而是运行时状态的一致性。大多数人一开始会把精力花在 DAG 设计、任务依赖、调度策略上但真正让系统在生产环境里翻车的往往是 agent 执行到一半时容器挂了、状态丢了、重试之后产生了重复副作用。这个问题在传统任务编排里也存在但 agent 场景把它放大了十倍因为 agent 的每一步执行都可能是有副作用的、不可幂等的、依赖外部 API 的。2. Agentic Runtime 到底在运行时做什么2.1 和传统任务编排的本质区别传统任务编排比如 CronJob、Argo Workflows、Airflow 这类工具核心模型是任务图 状态机。一个任务要么成功要么失败重试逻辑相对简单因为大多数任务是幂等的批处理。但 agentic runtime 不一样一个 agent 的执行过程本身是有状态的、多轮的、可能长时间挂起的。它可能在等一个外部工具的返回可能在等人类审批可能在等另一个 agent 的输出。这就意味着运行时必须能持久化 agent 的中间状态并且在任意时刻恢复。我早期犯过一个错误把 agent 的每一步都当成一个独立的 K8s Job 来跑。听起来很合理每个 step 一个 Pod跑完就销毁。但实际跑起来问题一大堆——step 之间的上下文传递要靠外部存储Pod 冷启动延迟让多轮对话变得极慢而且一旦某个 step 失败整个链路的重试成本极高。后来改成长驻 Pod 内部状态机的模式性能立刻上了一个台阶。2.2 运行时需要管理的四类状态在 agentic runtime 里状态可以粗分成四类每一类的处理策略都不一样状态类型典型内容存储位置生命周期会话状态对话历史、上下文窗口Redis / 外部 KV分钟到小时级执行状态当前 step、待执行队列数据库 内存秒到分钟级工具状态外部 API 调用凭证、限流计数Secret 内存与 Pod 同生命周期产物状态生成的文件、中间结果对象存储 / PVC持久这四类状态如果混在一起管理系统很快就会变得不可维护。我的做法是按生命周期分层会话状态放 Redis 并设置合理 TTL执行状态落库保证崩溃可恢复工具状态尽量无状态化凭证从 Secret 挂载产物状态统一走对象存储。这样每一层都可以独立扩展和排障。2.3 为什么 K8s 是天然的载体K8s 之所以适合做 agentic runtime 的底座核心原因是它提供了三样东西声明式调度、自愈能力、以及统一的资源抽象。agent 任务本质上是一种需要弹性伸缩、需要故障恢复、需要资源隔离的工作负载这正好是 K8s 最擅长的领域。但要注意K8s 原生的工作负载类型Deployment、Job、StatefulSet没有一个是完全为 agent 场景设计的。Deployment 适合长驻服务但不适合一次性任务Job 适合批处理但不适合长会话StatefulSet 适合有状态服务但调度不够灵活。所以实践中通常是组合使用用 Deployment 跑 agent runtime 的主进程用 Job 跑一次性的工具调用用 StatefulSet 跑需要稳定网络标识的协调组件。3. 在 K8s 上搭 Agent 编排层我的分层设计3.1 控制面与数据面的切分任何编排系统第一件事都是把控制面和数据面分开。控制面负责决策——哪个 agent 该跑、跑在哪、什么时候重试数据面负责执行——真正去调用模型、调用工具、处理数据。这个切分在 agent 场景里尤其重要因为数据面的负载波动极大模型调用可能几秒也可能几分钟而控制面需要保持轻量和快速响应。我的控制面通常是一个无状态的 Go 或 Python 服务部署成 Deployment副本数 2-3 个前面挂一个 Service。它对外暴露 gRPC 接口接收任务提交对内通过 watch 机制监听任务状态变化。数据面则是每个 agent 一个 Pod通过 sidecar 或者 init container 注入运行时依赖。提示控制面一定要做成无状态的所有状态落库。我见过太多把状态放在控制面内存里的实现一旦控制面重启整个编排链路就断了。3.2 任务队列的选择与取舍任务队列是编排层的心脏。可选方案很多Redis Stream、NATS、Kafka、RabbitMQ甚至直接用数据库轮询。选哪个取决于你的规模和一致性要求。小规模每秒几十个任务用 Redis Stream 就够了部署简单延迟低。中等规模每秒几百到几千建议上 NATS 或 RabbitMQ它们对 consumer group 和 ack 机制的支持更成熟。大规模每秒上万才需要考虑 Kafka但 Kafka 的运维成本很高除非你已经有现成的 Kafka 集群否则不建议为了 agent 编排单独引入。我自己的经验是先用 Redis Stream 跑通等真的遇到瓶颈再换。过早优化队列是典型的过度工程。数据库轮询虽然看起来土但在任务量不大每分钟几十个的场景下反而最省心因为不需要额外维护一个中间件。3.3 Agent Pod 的生命周期管理Agent Pod 和普通业务 Pod 最大的区别是它的生命周期是由任务驱动的而不是由副本数驱动的。一个 agent 任务来了需要拉起一个 Pod任务结束了Pod 应该被回收。这听起来像 Job 的语义但 agent 任务可能持续很久而且中途可能需要暂停和恢复。我的做法是自定义一个 CRDCustom Resource Definition比如叫AgentTask然后用一个 controller 去 reconcile。Controller 监听 AgentTask 的创建根据任务类型决定拉起什么样的 Pod监听 Pod 的状态把结果写回 AgentTask 的 status。这样整个生命周期就是声明式的符合 K8s 的设计哲学。apiVersion: ax.example.com/v1 kind: AgentTask metadata: name: research-agent-001 spec: agentType: researcher maxDuration: 3600 tools: - web-search - doc-reader context: sessionId: sess-abc123 status: phase: Running currentStep: 3 startedAt: 2026-01-15T10:00:00Z这个 CRD 的好处是所有 agent 任务都变成了 K8s 原生对象可以用kubectl直接查看和管理也可以复用 K8s 的 RBAC、namespace 隔离、资源配额等机制。4. 那些让我熬夜的坑状态一致性与重试4.1 重复执行的副作用问题Agent 任务最怕的就是重复执行。假设一个 agent 正在执行发送邮件这个工具调用Pod 在调用发出后、结果返回前挂了。Controller 检测到 Pod 异常触发重试。新 Pod 起来后从数据库读到发送邮件这个 step 还没完成于是又发了一次。用户收到两封邮件。这个问题的根源是工具调用不是原子的。解决办法有两个方向一是让工具调用幂等发送邮件时带一个唯一 ID服务端去重二是引入执行意图记录——在调用前先写一条准备执行的记录调用成功后更新为已完成重试时先检查这条记录。我倾向于两者结合关键工具必须幂等同时运行时层做意图记录。因为不是所有外部工具都能改造成幂等的运行时层的保护是最后一道防线。4.2 长任务的超时与心跳Agent 任务可能跑很久比如一个研究型 agent 可能要跑半小时。K8s 的 liveness probe 如果配置不当会误杀正在正常工作的 Pod。我的经验是liveness probe 只检查进程是否存活不要检查业务逻辑业务层面的健康度用 readiness probe 或者自定义的 heartbeat 机制。具体做法是 agent 主进程定期往一个本地文件或者 Redis key 写心跳controller 通过检查心跳来判断 agent 是否卡死。如果超过阈值没有心跳才触发重启。这样既避免了误杀又能及时发现真正的卡死。4.3 上下文窗口的截断策略多轮 agent 对话很容易撑爆模型的上下文窗口。什么时候截断、截断哪些内容直接影响 agent 的表现。我试过几种策略滑动窗口保留最近 N 轮简单但会丢失早期重要信息摘要压缩把早期对话用模型压缩成摘要效果好但增加延迟和成本关键信息提取只保留结构化的关键信息如已确认的事实、待办事项丢弃冗余对话实测下来摘要压缩 关键信息提取的组合效果最好。具体是当上下文达到窗口的 70% 时触发一次压缩把最早的 30% 对话压缩成摘要同时提取出关键信息单独存储。这样既控制了长度又保留了重要内容。5. 可观测性Agent 编排系统的眼睛5.1 三类必须采集的信号Agent 编排系统的可观测性和普通微服务不太一样需要重点关注三类信号第一类是任务级信号任务提交量、完成量、失败量、平均耗时、P95/P99 延迟。这些指标反映系统的整体健康度。第二类是 agent 级信号每个 agent 的 step 数、工具调用次数、模型 token 消耗、重试次数。这些指标帮你定位是哪个 agent 出了问题。第三类是资源级信号Pod 的 CPU/内存使用、队列积压、数据库连接数。这些是基础设施层面的指标。三类信号要能在同一个 dashboard 上关联查看否则排障时要在多个系统之间来回切换效率极低。我通常用 Prometheus 采集指标用统一的 label 体系task_id、agent_id、session_id把它们关联起来。5.2 分布式追踪在 agent 场景的特殊性普通的分布式追踪是请求-响应模型一个 trace 对应一次请求。但 agent 任务的 trace 是树状的、可能跨越很长时间的。一个 agent 任务可能触发多个子 agent每个子 agent 又调用多个工具整个 trace 可能持续几十分钟。用 OpenTelemetry 的话关键是正确传播 context。每个 agent step 都要继承父级的 trace context同时为工具调用创建 span。这样在追踪系统里就能看到完整的调用树。要注意的是长时间运行的 span 可能会被追踪系统丢弃很多系统有 span 时长限制所以对于特别长的任务建议分段上报。5.3 日志的结构化与关联Agent 的日志量很大而且很多是模型输出的自然语言直接看日志基本没法排障。我的做法是强制结构化日志每条日志必须包含 task_id、agent_id、step、level 这几个字段模型输出单独存到一个字段里不要和普通日志混在一起。import structlog logger structlog.get_logger() logger.info( tool_call_completed, task_idtask_id, agent_idagent_id, stepcurrent_step, tool_nameweb-search, duration_ms1234, result_size5678 )这样在日志系统里就可以按 task_id 聚合快速定位某个任务的所有日志。6. 资源隔离与多租户别让一个 agent 拖垮整个集群6.1 命名空间级别的隔离如果多个团队或者多个业务线共用一套 agent 编排平台命名空间隔离是必须的。每个租户一个 namespace配合 ResourceQuota 和 LimitRange防止某个租户的 agent 把集群资源吃光。apiVersion: v1 kind: ResourceQuota metadata: name: agent-quota namespace: tenant-a spec: hard: requests.cpu: 20 requests.memory: 40Gi limits.cpu: 40 limits.memory: 80Gi count/agenttasks.ax.example.com: 50注意最后一行通过自定义资源的 count 配额可以限制单个租户同时运行的 agent 任务数。这个在防止任务风暴时特别有用。6.2 Agent 之间的资源竞争同一租户内的多个 agent 也会竞争资源。特别是模型调用如果所有 agent 同时打向同一个模型服务很容易触发限流。我的做法是在 agent runtime 里内置一个令牌桶限流器按模型服务维度限流而不是按 agent 维度。这样即使某个 agent 疯狂重试也不会影响其他 agent。另外CPU 密集型的 agent比如做本地推理的和 IO 密集型的 agent比如做网络请求的最好调度到不同的节点池避免相互干扰。这可以通过 nodeSelector 或者 affinity 来实现。6.3 优雅退出与任务迁移K8s 节点维护或者缩容时Pod 会被驱逐。对于 agent 任务来说直接杀掉 Pod 意味着任务中断。所以必须实现优雅退出收到 SIGTERM 后agent 应该把当前状态持久化然后主动退出。Controller 检测到 Pod 退出后把任务重新调度到其他节点从持久化的状态恢复。这个机制的关键是状态持久化要足够频繁。如果只在任务结束时持久化那中断就意味着全部重来。我的做法是每个 step 完成后都持久化一次这样最多丢失一个 step 的进度。7. 从单机到集群我的演进路径与踩坑记录7.1 第一阶段单机脚本调度最早的时候我就是用 Python 写了个脚本用subprocess拉起 agent 进程用文件锁做简单的并发控制。这个阶段最大的问题是没有隔离——一个 agent 崩了可能把整个脚本带崩而且资源没法限制一个 agent 吃满 CPU 其他 agent 就饿死。但这个阶段也有价值它让我快速验证了 agent 的核心逻辑搞清楚了哪些状态需要持久化、哪些工具调用容易出问题。不要跳过这个阶段直接上 K8s否则你会把业务逻辑的问题和基础设施的问题混在一起排障时非常痛苦。7.2 第二阶段Docker Compose 编排验证完逻辑后我把每个 agent 打包成 Docker 镜像用 Docker Compose 编排。这个阶段解决了隔离问题每个 agent 在自己的容器里跑资源可以限制。但 Compose 的编排能力很弱没有自愈、没有弹性伸缩、没有滚动更新。而且单机部署意味着没有高可用宿主机一挂全挂。这个阶段我踩的最大的坑是容器内的时区问题。Agent 任务经常涉及时间计算容器默认是 UTC和业务时区不一致导致定时任务全部错位。后来统一在 Dockerfile 里设置TZ环境变量才解决。7.3 第三阶段K8s 集群化迁移到 K8s 后前面提到的所有能力都有了但新的问题也来了。最典型的是冷启动延迟。Agent 镜像通常很大包含各种依赖和模型文件Pod 启动要几十秒。对于需要快速响应的场景这个延迟不可接受。解决办法有几个一是用镜像预热把常用镜像提前拉到节点上二是用 init container 做依赖的懒加载三是对于特别敏感的场景用长驻 Pod 内部任务队列的模式避免频繁创建 Pod。我最后采用的是第三种因为 agent 任务的启动频率其实没那么高长驻 Pod 的资源利用率反而更好。7.4 踩坑记录一次因为 etcd 压力导致的全集群抖动有一次我们的 agent 编排系统突然大面积超时排查了半天发现是 etcd 压力过大。原因是我们的 controller 用了过于频繁的 reconcile 循环每秒对每个 AgentTask 都做一次全量查询导致 etcd 读请求暴涨。修复方法是引入workqueue 的限流和去重机制并且给 controller 加上 informer cache避免每次都直接查 etcd。这个坑让我深刻理解了一件事K8s 的 controller 模式虽然强大但用不好会成为集群的负担。任何自定义 controller 都必须认真对待限流和缓存。8. 关于ax这类运行时我的一些个人判断回到ax这个标题本身。它代表的是 agentic runtime 这个正在快速演进的领域。从热搜词里能看到Kubernetes、Karmada、agentic cloud 这些概念正在被越来越多的人讨论说明这个方向确实在从实验走向生产。我的判断是未来一两年agent 编排会逐渐标准化。现在每家都在自己造轮子但很快会出现类似agent 界的 Kubernetes这样的标准运行时。到那时候今天这些手写的 controller、自定义的 CRD、自己搭的队列可能都会被标准化的组件替代。但在标准出现之前自己动手搭一套仍然是最有价值的学习方式。因为只有真正踩过状态一致性的坑、处理过重复执行的副作用、调优过队列的吞吐你才能理解标准方案为什么那样设计。这些经验不会因为工具的迭代而失效。最后分享一个我一直在用的小技巧给每个 agent 任务打上足够的标签。不只是 task_id还包括提交者、业务线、优先级、预估时长。这些标签在排障、计费、容量规划时都会派上用场。我见过太多系统因为标签体系没设计好后期想加一个维度就要改一堆代码。前期多花半小时设计标签后期能省几十个小时。
返回列表