暗无天日

=============>DarkSun的个人博客

分层控制:从 AI 技能到 Unix 管道的共同设计原则

最近读到 Skill Graphs 2.0 这篇文章,讨论 AI 智能体的技能系统如何从"图结构"转向"分层结构"。核心主张是:当技能规模扩大时,依赖关系会失控,AI 智能体开始"迷路"。解决思路是把技能分成三层——原子(确定性执行)、分子(受控组合)、复合体(自治但受监控)。

读完之后我发现,这三层架构并不是什么新发明。它是"分层控制"这一经典设计原则在 AI agent 领域的再发现。而且这个原则在 Unix 管道、Clojure 的 transducer、Emacs 的扩展体系中反复出现。

为什么图结构会失控

原文描述的场景很具体:技能图在项目初期看起来优雅——每个技能是一个节点,依赖关系是边,像 Obsidian 的双链笔记。但技能数量增长到上百个后,依赖链变得又深又密,AI 智能体在执行时路径选择开始不稳定。你让它用技能 A,它可能跳到技能 C,或者在某个依赖路径上彻底迷路。

更致命的是循环依赖:技能 A 依赖 B,B 依赖 C,C 又依赖 A。系统退化成"随机行为发生器",每次运行结果都不一样。

这个问题的本质是:图结构只描述了"连接关系",没有约束"决策权"。所有节点在理论上平等,没有任何机制告诉系统"你应该先做什么、后做什么、什么时候停下来"。

分层控制的两条规则

Skill Graphs 2.0 的三层架构可以抽象出两条通用规则:

  1. 越底层越追求确定性,越高层越允许自主性 。原子层不允许调用其他技能,输出必须稳定可预测。复合体层允许 AI 智能体自主决策,但需要人类定期检查。分子层处于中间——允许组合,但调用顺序和条件分支必须事先写清楚。
  2. 层与层之间是"控制边界"而非"连接关系" 。不再强调"谁依赖谁",而是强调"谁拥有决策权,谁只负责执行"。上层决定"做什么",下层只管"怎么做"。

这两条规则听起来平淡,但它们在多个领域反复出现。

Unix 管道:最经典的分层系统

Unix 哲学是分层控制的教科书级案例。

grep ERROR access.log | sort | uniq -c | sort -rn | head -5

这条管道中,每个命令就是一个"原子技能":

  • grep 只做一件事:过滤匹配行
  • sort 只做一件事:排序
  • uniq -c 只做一件事:去重并计数
  • sort -rn 只做一件事:按数字倒序排列
  • head -5 只做一件事:取前 5 行

每个命令的输入输出格式完全确定,行为高度可预测。没有人会担心 sort 突然决定跳过某些行或者自作主张把结果发给 wc 而不是下一个管道。

管道和简单 shell 脚本都是"分子技能"。管道是调用顺序固定的流水线;脚本可以包含条件分支和循环,但所有逻辑都是事先写死的——这正好对应 Skill Graphs 2.0 中分子层"有限编排器"模式(最多几次判断,每次不超过两个分支)。

那"复合体"在哪?在人身上。运维工程师根据当前状况决定跑哪个脚本、用什么策略、出问题了切什么备选方案。这个决策过程无法事先完全写死,需要根据实际情况动态判断。这和 Skill Graphs 2.0 中"人类在复合体层设定目标、定期检查、偏离时介入纠正"的描述完全吻合。

Doug McIlroy 在 1964 年提出管道概念时,出发点就是"不要让程序之间产生复杂的依赖关系,用统一的接口(文本流)连接它们"。这和 Skill Graphs 2.0 把"连接关系"换成"控制边界"是同一个思路。

Clojure Transducer:函数式编程中的分层

Clojure 的 transducer(转换器)是分层控制在函数式编程中的体现。

;; 原子:单个 transducer
(map inc)           ;; 输入一个数,输出加一后的数
(filter even?)      ;; 输入一个数,只保留偶数
(take 10)           ;; 只取前 10 个

;; 分子:组合多个 transducer + 应用到数据源
(def xf (comp (map inc) (filter even?) (take 10)))
(into [] xf (range 1000))
;; → [2 4 6 8 10 12 14 16 18 20]

每个单独的 transducer 是"原子技能"——它只做一次变换,输入输出完全确定。 comp 把多个 transducer 组合起来, into 把组合应用到数据源上,整体构成"分子技能"——组合顺序固定,执行结果完全可预测(同样的输入永远出同样的输出)。

"复合体"在哪?和 Unix 一样,在人身上。开发者根据业务需求决定写哪些 transducer、用什么顺序组合、数据从哪来到哪去。这个设计过程涉及动态判断,无法事先完全写死。

这里的关键设计决策和 Skill Graphs 2.0 完全一致:单个 transducer 不允许"决定"数据流向哪里,它只负责变换经过自己的那一个元素。数据流的控制权在写 compinto 的人手中。

Rich Hickey 设计 transducer 时特意把"做什么变换"和"如何应用变换"解耦。这和 Skill Graphs 2.0 把"决策权"和"执行权"分层是同一件事。

Emacs:从命令到宏到包

Emacs 的扩展体系也遵循类似的分层:

  • 原子层 :单条命令,如 M-x sort-linesM-x query-replace 。每条命令做一件事,行为确定。
  • 分子层 :keyboard macro(键盘宏)把多条命令录制成一个序列,按固定顺序回放。或者写一个简单的 Elisp 函数,把几条命令串起来。
  • 复合体层 :用户根据当前编辑任务动态决定用哪些命令、宏和函数。和 Unix 运维工程师一样,Emacs 用户面对的不是写死流程能覆盖的问题——你得看当前文件是什么、代码结构怎样、想达到什么效果,然后决定是跑个宏还是调个函数还是装个插件。

三层之间的复杂度跃迁很直观:写一个 defun 很容易,因为流程是固定的;写一个可靠的 minor mode 就难得多,因为涉及 keymap 继承、hook 管理、buffer-local 变量等动态行为;而把各种工具灵活组合起来解决眼前的问题,这个判断力需要长期积累。这和原文说的"分子技能内部包含超过 8-10 个原子技能后复杂度再次爆炸"是同一个现象。

分层的代价

原文坦诚地指出了两个代价,它们同样适用于所有分层系统:

第一, 规模天花板 。一个复合体技能内部包含超过 8-10 个分子技能时,复杂度会再次爆炸。这意味着分层只是推迟了复杂度问题,并没有消除它。Unix 的 shell 脚本也有类似的天花板——超过一定复杂度后,人们会转向 Python 或 Go。Clojure 的 transducer 链太长时也会变得难以理解。

第二, 测试成本 。每一层都需要独立验证,组合起来还要做集成测试。原文说"没有任何捷径可走,你只能老老实实写测试用例"。这和软件工程中"测试金字塔"的经验完全一致——底层单元测试最多,中间集成测试适中,顶层端到端测试最少但也最贵。

当你面对失控的系统

分层控制不是银弹,但它提供了一个可操作的诊断框架:当你面对一个行为不可预测的复杂系统时,不要试图用更复杂的规则来管理它。

先问两个问题:

  1. 这个系统中,哪些部分的行为是确定的?把它们压到底层。
  2. 哪些部分必须动态决策?把它们提到高层,并且加人监控。

从 Unix 管道到 Clojure transducer 到 AI 技能系统,这个思路一次又一次地被重新发现。也许因为这就是对付复杂性的基本方法。

系统设计 : AI : Clojure : Emacs : Unix