从链到循环再到图:Agent 构建的每一次“创新”其实都在重新发现有限状态机 最近几个月Agent 构建社区又换了一次抽象层先是链chains后来是循环loops现在是图graphs。每一次新框架都宣称解决了上一个的痛点但剥开包装后会发现它们指向的其实是同一个东西——1950 年代就写进教科书的有限状态机Finite State Machine。这个形式化工具早已定义了 TCP 协议、CPU 控制单元也早已以“意外形式”存在于几乎所有代码库里一堆布尔标志、散落的条件判断以及靠运气才能维护的控制流。既然故意实现它几乎不增加成本却能带来强保证为什么不把这个模式真正理解透“不可能状态”的根源布尔标志 vs 枚举状态最常见的例子是数据获取状态建模// 典型的布尔地狱const[isLoading,setIsLoading]useState(false);const[isError,setIsError]useState(false);const[data,setData]useState(null);三个布尔值能产生 8 种组合但真正有意义的可能只有 3 种。isLoading isError这种自相矛盾的状态在类型系统里完全合法最终总会因为竞态条件被构造出来——加载中却显示错误同时还渲染着陈旧数据。状态越多问题指数级增长。五个布尔标志就有 32 种可能组合却只有少数合法。把同一个功能改成状态机只需要把标志换成枚举typeFetchState|{status:idle}|{status:loading}|{status:success;data:Data}|{status:error;error:Error};此时loading error这种状态根本无法被表达。非法状态在类型层面就消失了而不是靠测试或防御性 if 判断来事后堵漏。这正是函数式编程里常说的“让非法状态无法表示”make illegal states unrepresentable但状态机把它应用到了系统随时间演化的过程上。显式转移函数带来的三大保证有限状态机的核心只回答一个问题给定当前状态当某个事件发生时下一个状态是什么用函数形式写出来就是(state,event)nextState当你把这个转移逻辑集中到一个显式函数里而不是散落在各个事件处理器和条件分支里时系统立刻获得三个属性系统永远只处于一个明确状态。只有你定义过的转移才可能发生。无效事件会被静默忽略而不是产生未定义行为。第二条的实际威力很容易被低估。用户双击提交按钮时第二个 SUBMIT 事件到达时机器正处于 loading 状态而 loading 状态根本没有定义 SUBMIT 转移机器直接忽略它——重复提交的 bug 从来没有机会出现也不需要 debounce、锁、或 disabled 按钮标志。实现方式查表、Switch 与 Typestate最干净的实现是把转移函数当作数据用普通对象查表表达// 查表实现行为一目了然可序列化、可 diffconstmachine{idle:{SUBMIT:loading},loading:{SUCCESS:success,ERROR:error},// ...};完整行为能放在一个屏幕里代码审查 diff 直接展示行为变化对象还能序列化存数据库或自动渲染成图。用 switch 写也完全可行在 TypeScript 或 Rust 中编译器能强制穷举性检查——新增状态却忘记处理会直接编译失败。Rust 的 typestate 模式更进一步每个状态都是独立类型非法转移在编译期就报错。任何人用过 Redux reducer 或 useReducer其实都已经写过(state, action) nextState的形式。只是大多数 reducer 允许任意 action 在任意状态下修改任意东西悄悄把布尔标志问题用更好看的语法重新引入了。只有当 reducer 先检查当前状态再决定是否处理事件时它才真正成为状态机。扩展状态机把“模式”与“上下文”分开真实应用里总有表单值、数组、重试次数等无法枚举的量化数据。解决办法是把状态拆成两部分有限状态Finite State系统当前所处的“模式”idle、loading、editing、awaitingHuman小而可枚举。上下文Context / Extended State所有量化数据表单内容、重试次数、返回数据。转移函数现在返回更多信息(state,event,ctx)({nextState,actions?,ctxUpdate?})一个带重试上限的例子// 转移函数示例伪代码风格实际可用 XState / Stately 等实现functiontransition(state,event,ctx){if(stateevaluatingeventRETRY){if(ctx.retries3){return{nextState:planning,actions:[fetch],ctxUpdate:{retries:ctx.retries1}};}return{nextState:gaveUp};}// ...}这里出现了三个重要概念守卫Guard附加在转移上的条件ctx.retries 3。动作Action转移触发的副作用机器只决定“该执行哪些效果”由外部解释器真正执行保持转移函数纯净。进入/退出动作Entry/Exit Actions进入 loading 时启动 spinner 和超时退出时取消——无论通过哪条边离开都会执行清理。退出动作特别强大它能一劳永逸解决“泄漏定时器、悬挂订阅、忘记清理”的全家桶问题。Mealy 机与 Moore 机输出挂在哪里经典理论区分两种变体Moore 机输出只取决于当前状态交通灯常绿时一直亮。Mealy 机输出取决于状态 事件投币事件在 idle 状态时才出货。两者表达能力等价真实系统通常混合使用。入口/退出动作偏 Moore转移动作偏 Mealy。区分它们能帮你回答一个设计问题这个效果是因为“系统处于某个状态”还是因为“刚以某种方式到达这里”混淆二者会产生微妙的边缘 case bug。Agent 图的本质映射Agent 图描述 Agent 在规划、调用工具、评估结果、重试、询问人类、最终完成之间如何移动。从线性管道 → DAG承认失败和分支→ 带循环的图承认重试和人类反馈最终得到一个带标签边的有向图包含起始节点和终止节点。映射回有限状态机的 5 元组节点 状态集合 S边标签 事件集合 Σ边 转移函数 δ起始节点 s₀终止节点 F把 Agent 显式写成状态机后生产级收益立刻显现重试逻辑变成带守卫的边跑飞循环被结构本身禁止。控制流不再由模型直接驱动模型输出先被分类成事件再由机器检查是否合法转移。长运行 Agent 可持久化只需把(当前状态, 上下文)这对小值存进数据库进程终止后由 webhook 重新水合。调试变成单列查询“当前状态” 事件历史。状态图Statecharts处理独立并行关注点扁平状态机在面对独立关注点时会爆炸bold italic underline 需要 8 种组合状态。David Harel 在 1987 年提出的 Statecharts 通过三招解决层级Hierarchy状态可包含子状态父状态上的转移自动覆盖所有子状态。并行区域Parallel Regionsbold、italic、underline 可以是三个并行的双状态区域总状态数从 8 降到 6。历史状态History父状态记住上次活跃的子状态重入时恢复原位暂停菜单的原理。Statecharts 仍是状态机保留所有保证只是把大型机器的描述压缩了。UML 状态图本质就是 Statecharts。什么时候不要用状态机真正顺序执行、没有外部事件、没有值得建模的失败分支的逻辑就该写成普通函数。真正连续的状态物理仿真、数值求解器也没有离散模式可言。可靠的触发信号是第二个布尔标志出现并与第一个交互或者代码里出现“但只有当我们当前处于……时”这样的注释——此时代码已经长出了状态机的形状把它显式化会显著提升可维护性。结语链、循环、图最终都被证明是有限状态机只不过换了名字。无论接下来会出现什么新抽象都可以用同一个标准检验它能否清晰回答“当前状态是什么发生了什么事件下一个状态是什么”如果能它就是可被推理的状态机如果不能它只是包装得更好的散落控制流。把这个 70 年前的形式化工具重新命名并认真实现几乎不增加成本却能让 Agent以及几乎所有状态ful 系统获得类型级安全、结构化调试、可持久化、以及真正的生产可靠性。下一次你看到某个“革命性”的 Agent 框架时不妨先问自己它的核心转移表长什么样非法状态是否已经被类型系统禁止了我是紫微AI在做一个「人格操作系统ZPF」。后面会持续分享AI Agent和系统实验。感兴趣可以关注我们下期见。