1. 为什么一个 YAML 文件能让技术圈吵翻天
Google 把 AX 开源出来的那天,技术社区几乎被同一个话题刷屏。核心争议点其实特别朴素:用声明式 YAML 去编排几十亿个 agent,这件事到底靠不靠谱。有人觉得这是把 Kubernetes 那套"期望状态收敛"的思路搬到了 agent 领域,是顺理成章的进化;也有人觉得 agent 天生是动态、有状态、行为不可预测的,硬套声明式模型纯属削足适履。
我自己做 agent 相关项目有一段时间了,从最早的硬编码 pipeline,到后来的事件驱动框架,再到现在的编排层抽象,几乎每一代方案都踩过。所以看到 AX 的第一反应不是"哇好先进",而是"它到底解决了哪一类真实痛点,又在哪些场景下会翻车"。这篇就把我理解的 AX 拆开讲清楚:它的核心抽象是什么、声明式编排 agent 的底层逻辑、YAML 到底怎么写、和 Kubernetes 的关系在哪、以及实际落地时会遇到哪些坑。
先说清楚适合谁看。如果你已经在写 agent、被多 agent 协作的状态管理折磨过、或者想搞清楚"声明式"这套东西为什么能从容器编排一路蔓延到 AI 编排,那这篇对你有用。如果你完全没接触过 agent,建议先补一下 agent 和普通 LLM 调用的区别——简单说,普通调用是"问一句答一句",agent 是"给个目标,自己规划步骤、调用工具、根据结果调整",多了循环和状态。
AX 想做的事,用一句话概括:把"我要一群 agent 干这件事"写成一份配置文件,剩下的调度、重试、状态流转、依赖管理交给运行时。这个思路的诱惑力在于,它把 agent 从"代码逻辑"变成了"声明资源",而声明资源是可以被版本控制、被 diff、被审计、被大规模复制的。
2. AX 到底在编排什么:把 agent 当资源看
2.1 从命令式到声明式的思维切换
要理解 AX,得先理解命令式和声明式的根本差别。命令式是"你先做 A,再做 B,如果 C 就做 D",每一步都要你显式写出来。声明式是"我要最终状态是 X",中间怎么走到 X 由系统负责。
容器领域已经把这个切换演示得很彻底了。早期部署服务要写一堆 shell 脚本,启动顺序、健康检查、重启逻辑全靠脚本维护,服务一多就变成意大利面条。Kubernetes 出现后,你只写"我要 3 个副本、镜像是什么、暴露哪个端口",剩下的调度、自愈、滚动更新它全包了。
AX 把这套逻辑搬到了 agent 上。传统写多 agent 协作,你大概会这么干:手动创建 agent 实例、手动串联调用、手动处理某个 agent 失败后的重试、手动管理共享的上下文。agent 数量一上去,这套代码的复杂度是爆炸式增长的。AX 的思路是,你声明"我需要哪些 agent、它们之间什么依赖关系、每个 agent 的输入输出契约是什么",运行时负责实例化、调度、失败重试和状态传递。
注意:声明式不是银弹。它擅长的是"结构相对稳定、规模大、需要统一治理"的场景。如果你的 agent 行为高度依赖运行时动态决策、每次执行路径都不一样,硬套声明式反而会增加一层无谓的抽象负担。
2.2 agent 作为一等资源意味着什么
在 AX 的模型里,agent 是一个可以被声明、被引用、被组合的资源对象。这跟 Kubernetes 里 Pod 是资源、Service 是资源是一个道理。一旦 agent 变成资源,就自动获得了几件事:
- 可版本控制:agent 的定义是文本,能进 Git,能 review,能回滚。
- 可组合:一个 agent 的输出可以声明为另一个 agent 的输入,依赖关系显式化。
- 可复制:同一份定义,改个参数就能起一批,适合大规模并行。
- 可观测:运行时知道每个 agent 的状态,能统一收集日志和指标。
这几点里,我觉得可组合是最关键的。多 agent 系统最难的地方从来不是单个 agent 写得好不好,而是 agent 之间的接口和状态怎么对齐。声明式把这种对齐关系从"藏在代码里的隐式约定"变成了"写在配置里的显式契约",这是质的区别。
2.3 和 Kubernetes 的类比到底成不成立
HN 上吵得最凶的就是这个类比。支持方说,Kubernetes 证明了声明式编排能管住海量无状态和有状态工作负载,agent 无非是另一种工作负载。反对方说,容器是"给定输入必然产生确定行为"的,agent 不是,它有随机性、有外部依赖、有不可预测的失败模式。
我的看法是,类比在编排层成立,在执行层不成立。编排层关心的东西——依赖解析、调度、重试、状态机——agent 和容器确实共享同一套问题结构。但执行层,agent 的行为不确定性远高于容器,所以 AX 这类系统必须额外处理"语义级失败":不是进程挂了,而是 agent 输出了错误结果、陷入了循环、或者调用了不该调用的工具。这部分是 Kubernetes 模型没有覆盖的,也是 AX 真正需要证明自己的地方。
3. 声明式 agent 编排的核心机制拆解
3.1 期望状态与收敛循环
声明式系统的灵魂是收敛循环:系统持续对比"期望状态"和"实际状态",发现偏差就采取行动消除偏差。Kubernetes 的 controller 就是这么工作的,你声明 3 个副本,实际只有 2 个,controller 就补一个。
AX 把这个循环用在 agent 上,期望状态就是"这些 agent 应该都成功完成、输出符合契约"。实际状态是"某些 agent 还在跑、某些失败了、某些输出不合法"。收敛动作包括重新调度失败的 agent、重试、或者触发补偿逻辑。
这里有个关键设计点:收敛的粒度。如果粒度太粗,一个 agent 失败就重跑整个流程,浪费巨大;如果粒度太细,每个 agent 独立收敛,又可能破坏 agent 之间的依赖一致性。AX 需要在两者之间找平衡,通常的做法是按依赖图分层收敛,同一层内并行、层与层之间串行。
3.2 依赖图与数据流
多 agent 协作的本质是一张有向图。节点是 agent,边是数据依赖。声明式编排要求你把这张图显式写出来,而不是藏在代码的调用顺序里。
显式化依赖图的好处是运行时可以做全局优化:哪些 agent 能并行、哪些必须等待、哪个节点是瓶颈、失败时影响范围有多大,全都一目了然。坏处是灵活性下降,动态生成的依赖关系不好表达。
我实测下来的经验是,静态依赖图能覆盖 80% 的常见场景,剩下 20% 需要动态决策的,可以用"子图"的方式处理:主图静态声明,某个节点内部再动态展开。AX 这类系统一般会提供这种嵌套能力。
3.3 状态传递与上下文管理
agent 之间传递的不只是数据,还有上下文。一个 agent 的中间推理过程、工具调用记录、临时结论,可能都是下游 agent 需要的。声明式编排必须定义清楚:状态存在哪、怎么传、生命周期多长。
常见的做法是把状态分成两类:显式输出(agent 声明自己要产出的结构化结果)和隐式上下文(运行时自动维护的共享状态)。显式输出走依赖图的边传递,隐式上下文走共享存储。分开处理的好处是,依赖关系清晰,同时又不至于让每个 agent 都显式声明一堆琐碎的上下文依赖。
提示:上下文管理是 agent 编排里最容易失控的地方。我踩过的坑是,早期把所有中间状态都塞进共享上下文,结果 agent 数量一多,上下文膨胀到没法调试。后来改成"显式输出为主、共享上下文只放全局配置",可维护性立刻上来了。
4. YAML 怎么写:一份可落地的 AX 配置拆解
4.1 最小可用配置的结构
声明式系统的入口就是配置文件。AX 用 YAML,这跟 Kubernetes 一脉相承。一份最小的 agent 编排配置,通常包含几个部分:元信息、agent 定义、依赖关系、运行时参数。
apiVersion: ax/v1 kind: AgentGraph metadata: name: research-pipeline namespace: default spec: agents: - name: collector image: agent/collector:latest inputs: - name: query type: string outputs: - name: raw_docs type: array - name: summarizer image: agent/summarizer:latest inputs: - name: docs from: collector.raw_docs outputs: - name: summary type: string runtime: maxRetries: 3 timeout: 300s这份配置声明了两个 agent,collector 产出 raw_docs,summarizer 消费它。运行时负责按依赖顺序调度,collector 失败就重试,超时就终止。
4.2 依赖声明与数据绑定
依赖声明是配置里最需要想清楚的部分。上面用的是from: collector.raw_docs这种引用式绑定,意思是 summarizer 的 docs 输入来自 collector 的 raw_docs 输出。这种写法的好处是,依赖关系和数据流是同一件事,不用额外维护一张依赖表。
更复杂的场景可能需要条件依赖:某个 agent 只在特定条件下才执行。这时候通常会引入when字段或者条件表达式。我的建议是,条件逻辑尽量简单,复杂的判断放到 agent 内部去做,配置层只做粗粒度的开关。
4.3 参数计算与资源声明
agent 不像容器那样有明确的 CPU/内存需求,但它有别的资源约束:并发数、token 预算、外部 API 调用配额。这些都需要在配置里声明,运行时才能做全局调度。
举个例子,如果你声明了 100 个 agent 并行,但外部 API 每秒只能承受 10 次调用,那运行时必须做限流。限流参数怎么定?假设每个 agent 平均调用外部 API 3 次,100 个 agent 就是 300 次调用,按每秒 10 次算,至少需要 30 秒才能跑完一轮。这个计算过程决定了你的并发上限,配置里就得体现出来。
runtime: concurrency: 10 rateLimit: external: 10/s budget: tokens: 500000注意:token 预算这类参数,宁可一开始设保守一点,跑几轮看实际消耗再调。我见过太多项目一上来就把预算拉满,结果某个 agent 陷入循环疯狂消耗,账单直接爆掉。
5. 大规模 agent 编排的真实难点
5.1 语义级失败的检测与处理
前面提过,agent 的失败不只是"进程挂了"。更麻烦的是语义级失败:agent 跑完了,输出了结果,但结果是错的、是幻觉、是循环重复。声明式编排系统如果只处理进程级失败,那对语义级失败就完全无能为力。
处理语义级失败通常需要引入校验 agent或者断言机制。在依赖图里插入校验节点,检查上游输出是否符合预期,不符合就触发重试或降级。这会增加图的复杂度,但对付大规模 agent 系统是必要的。
5.2 状态一致性与幂等性
agent 重试的时候,副作用怎么处理?如果 agent 已经调用了外部 API 发了消息,重试会不会重复发送?这就是幂等性问题。声明式系统必须假设任何 agent 都可能被重试,所以 agent 本身要设计成幂等的,或者运行时提供去重机制。
我的经验是,把副作用和计算分离。agent 内部只做计算,产出"待执行的动作",由运行时统一执行动作并保证幂等。这样重试计算是安全的,动作执行有统一的去重层。
5.3 可观测性与调试
几十亿个 agent 跑起来,出问题怎么定位?这是声明式系统绕不开的挑战。Kubernetes 有成熟的日志、指标、追踪体系,AX 这类系统需要建立类似的体系,而且要针对 agent 的特点做增强:记录每个 agent 的输入输出、推理链路、工具调用序列。
调试 agent 编排比调试容器编排更难,因为 agent 的行为是概率性的,同样的输入可能产生不同的输出。所以可观测性不只是"看日志",还要能重放:给定相同的输入和随机种子,能不能复现问题。
6. 常见问题速查与避坑清单
6.1 典型问题排查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| agent 一直不结束 | 陷入循环或等待外部依赖 | 检查 agent 内部循环条件、外部调用超时设置 |
| 依赖顺序错乱 | 依赖声明缺失或循环依赖 | 检查依赖图是否有环、绑定引用是否正确 |
| 重试导致重复副作用 | agent 非幂等 | 检查副作用是否与计算分离、是否有去重层 |
| 上下文膨胀 | 共享状态无清理机制 | 检查上下文生命周期、是否只放必要数据 |
| 并发上不去 | 限流参数过保守 | 核算外部依赖的实际承载能力,调整限流 |
6.2 独家避坑经验
第一个坑是过度声明。刚上手声明式编排的人,容易把每个细节都写进配置,结果配置文件比代码还长。我的原则是:配置只声明"结构"和"契约",具体逻辑放 agent 内部。配置越薄,越容易维护。
第二个坑是忽视收敛成本。收敛循环不是免费的,每次对比状态、调度、重试都有开销。agent 数量少的时候无所谓,规模上去之后,收敛本身可能成为瓶颈。所以收敛频率要可调,不是越快越好。
第三个坑是把声明式当万能。有些场景命令式反而更清晰,比如流程高度动态、每次执行路径都不同的任务。硬套声明式只会让配置变成一堆条件分支,还不如直接写代码。
提示:判断该不该用声明式编排,问自己一个问题——"这个流程的结构是不是相对稳定、只是规模大"。是,就用声明式;不是,老老实实写命令式代码。
7. 我对声明式 agent 编排的真实看法
折腾了这么多,我个人的体会是,AX 这类系统真正的价值不在于"用 YAML 写 agent"这个形式,而在于它强迫你把 agent 系统的结构显式化。以前写多 agent,依赖关系藏在代码里,改一处不知道影响哪里;现在写在配置里,依赖图一目了然。这个思维转变本身,比工具本身更有价值。
至于它会不会成为 agent 编排的标准答案,我觉得要看两件事:一是语义级失败的处理能不能标准化,二是可观测性体系能不能跟上。这两件事解决了,声明式编排在大规模 agent 场景下就是顺理成章的选择;解决不了,它就只是个好看的抽象层。
最后分享一个我自己的小习惯:每次设计 agent 编排,先在纸上画依赖图,画到能一眼看清数据流向,再动手写配置。图都画不清楚的流程,写出来的配置一定是乱的。这个习惯帮我省了无数次返工。