线性代数是几何与代数的桥梁,AI 时代我们同样需要一种抽象表达来联通人 与 AI 的沟通桥梁
很多人说有了 AI,就不需要学习了,我实际体验下来,恰恰相反。agent 时代更需要学习,原因,我之前的推文里说过:
而普通 agent 代码里经常藏着一堆隐式流程:
用户输入 -> 调 LLM -> LLM 要不要用工具? -> 执行工具 -> 把工具结果给 LLM -> 再判断 -> 结束这个流程,可以称为状态流转,或者可执行图;当然,普通的 if-else 理论上也可以实现状态流转。不过,if-else 的代码,本质是用“条件组合”来代替“明确的状态”。
我举个具体的例子:
电商订单场景,一个订单有:
- is_paid = true (已支付)
- is_shipped = false (未发货)
- is_refunded = false (未退款)
当业务增加,比如引入了“部分退款”、“拼团中”、“预售等待”…… 你的代码里就会充斥着这种组合:
if (order.is_paid && !order.is_shipped && !order.is_refunded) { ... }这就是痛点所在: 你在用一堆变量的组合去隐式表达一个状态。一旦变量多了,组合数爆炸(3个布尔值就有 $2^3 = 8$ 种组合),代码必流脓。
状态机思维
状态机的思维是:别整那些虚的,这个订单现在有且仅有一个明确的、高内聚的状态(比如 PAID)。从 PAID 变成 SHIPPED,必须通过一个明确的动作(比如 SHIP)。
状态机四要素:
任何状态机,不管用什么语言写,都逃不出这四个核心概念。你只要在写代码时能对号入座,就说明你已经开始抽象了:
- State(状态):事物在某一时刻所处的稳定状况(如:草稿、待审批、已发布)。同一时刻只能有一个状态。
- Event(事件/动作):触发状态改变的“因”(如:提交、拒绝、发布)。
- Transition(流转/迁移):状态改变的“果”和过程。即:从状态 A 接收到事件 X,变成状态 B 的这个动作。
- Action(动作/副作用):在流转过程中顺便执行的业务逻辑(如:发邮件通知、改数据库、扣库存)。
传统 if-else vs 状态机抽象
我们拿最常见的**“工作流审批”(Workflow)**来做对比。假设状态有:Draft(草稿) → Pending(待审批) → Approved(已通过)
糟糕的传统写法(依赖条件判断)
这种写法下,逻辑散落在各个方法里,极难维护:
class ApprovalService {
// 审批通过操作
approve(ticket) {
// 1. 痛苦的防御性校验
if (ticket.status !== 'Pending') {
throw new Error("只有待审批的单子才能审批!");
}
// 2. 状态变更
ticket.status = 'Approved';
// 3. 业务副作用
sendNotification(ticket.owner);
}
cancel(ticket) {
// 如果状态是 Draft 或者 Pending 都能取消...
// 随着业务发展,这里的 if 会越来越长
if (ticket.status === 'Draft' || ticket.status === 'Pending') {
ticket.status = 'Canceled';
} else {
throw new Error("当前状态不能取消");
}
}
}完美的抽象写法(状态机矩阵)
状态机最高明的地方在于:它把零散的 if-else 变成了一张“二维网格(矩阵)”。
我们把状态和事件列出来:
| 当前状态 \ 触发事件 | Submit (提交) | Approve (通过) | Reject (拒绝) |
|---|---|---|---|
| Draft (草稿) | → Pending | 不允许 | 不允许 |
| Pending (待审批) | 不允许 | → Approved | → Draft |
| Approved (已通过) | 不允许 | 不允许 | 不允许 |
有了这张表,我们在代码里怎么抽象?直接用配置(数据驱动)代替逻辑!
// 1. 定义状态机配置(把那张表搬进代码)
const FSM_CONFIG = {
'Draft': {
'SUBMIT': { nextState: 'Pending', action: () => console.log('记录提交日志') }
},
'Pending': {
'APPROVE': { nextState: 'Approved', action: () => sendEmail('通过') },
'REJECT': { nextState: 'Draft', action: () => sendEmail('驳回') }
},
'Approved': {} // 终态,不接受任何事件
};
// 2. 状态机引擎(通用、不带业务逻辑)
class StateMachine {
constructor(entity, config) {
this.entity = entity; // 你的业务对象,比如 ticket
this.config = config;
}
// 核心:处理事件的方法
handleEvent(event) {
const currentState = this.entity.status;
const transition = this.config[currentState]?.[event];
// 统一的防御性校验!再也不用在各个方法里写 if 了
if (!transition) {
throw new Error(`无法从状态 [${currentState}] 执行事件 [${event}]`);
}
// 状态流转
this.entity.status = transition.nextState;
// 执行副作用
if (transition.action) {
transition.action(this.entity);
}
}
}
// 3. 实际业务调用
const ticket = { id: 1, status: 'Draft' };
const fsm = new StateMachine(ticket, FSM_CONFIG);
// 正常流转
fsm.handleEvent('SUBMIT'); // ticket.status 变成 'Pending'
fsm.handleEvent('APPROVE'); // ticket.status 变成 'Approved'
// 异常测试:如果已经 Approved 了,有人恶意调 SUBMIT
fsm.handleEvent('SUBMIT'); // 直接抛错: 无法从状态 [Approved] 执行事件 [SUBMIT]状态机认识思维
当你用这种视角去看代码时,你会发现世界清晰了很多:
- 世界是确定性的: 状态机提供了一种“白名单”机制。传统的 if-else 是在写“黑名单”(如果不是 A,如果不是 B...),极易漏掉边界。而状态机是“只有定义了的转换才合法,没定义的统统报错”,天生具备极高的安全性(防御式编程)。
- 业务与框架分离: 上面的 StateMachine 类是一个通用的引擎,它可以去跑订单流转、跑游戏怪物的 AI、跑前端的 UI 组件状态。你只需要换一个 FSM_CONFIG 配置表就行。
- 可视化: 因为状态机是一张表,你可以很容易地用工具把配置表自动生成流程图(比如用 Mermaid 或 Graphviz)。甚至可以直接拿着代码里的 JSON 去跟产品经理对需求:“你看,产品文档上说 Draft 状态下能点审批,我这矩阵上没配,说明你逻辑冲突了。”
“非线性”的状态跳转
如果所有的状态都是老老实实从 A → B → C 这样一条路走到黑(即纯线性流程),那用一个简单的 stage 字段或者 if-else 其实也能勉强应付。
正是因为现实业务中充满了 “插队”、“跳过”、“直接取消”、“一键倒回” 这种错综复杂的连线,如果不用状态机,代码里的 if 条件就会彻底失控。 复杂的非线性案例
一个销售线索,它的生命周期非常任性:
- 正常流程(线性):新线索 (New) → 跟进中 (In_Progress) → 已签约 (Won)
- 直接战败(A → C 跨越):刚进来的新线索,销售一打电话发现是同行来探底的,或者电话是空号,会直接标记为已战败 (Lost)。
- 死灰复燃(逆向流转):已经已战败的线索,过了半年客户主动找上门了,需要重新激活,变成跟进中。
我们把这个复杂的跳转关系直接画成状态机矩阵表:
| 当前状态 \ 触发事件 | FOLLOW (跟进) | SIGN (签约) | FAIL (判定战败) | REACTIVATE (重新激活) |
|---|---|---|---|---|
| New (新线索) | → In_Progress | 不允许 | → Lost (A → C) | 不允许 |
| In_Progress (跟进中) | 不允许 | → Won | → Lost | 不允许 |
| Won (已签约) | 不允许 | 不允许 | 不允许 | 不允许 |
| Lost (已战败) | 不允许 | 不允许 | 不允许 | → In_Progress |
代码实现:它依然极其优雅
你看,不管连线多乱,对状态机配置表来说,无非就是多写一行配置而已。核心引擎代码一行都不需要改。
const LEAD_FSM_CONFIG = {
'New': {
'FOLLOW': { nextState: 'In_Progress' },
// A -> C:新线索直接战败,不需要经过跟进中
'FAIL': { nextState: 'Lost', action: () => console.log('记录坏账/无效线索原因') }
},
'In_Progress': {
'SIGN': { nextState: 'Won', action: () => console.log('触发财务开票、开通权限') },
'FAIL': { nextState: 'Lost' }
},
'Won': {
// 终态
},
'Lost': {
// 逆向流转:战败线索被激活
'REACTIVATE': { nextState: 'In_Progress', action: () => console.log('重新分配给销售') }
}
};如果你不用状态机,而是用传统的逻辑写这个 CRM 系统,为了实现 FAIL(判定战败)这个动作,你的代码会写成这样:
function markAsLost(lead, reason) {
// 销售点“战败”时,系统要拼命检查当前到底在哪一步,能不能战败
if (lead.status !== 'New' && lead.status !== 'In_Progress') {
throw new Error("已经签约的单子不能战败!");
}
lead.status = 'Lost';
lead.failReason = reason;
// 后续业务...
}这还只是一个 markAsLost 方法。如果业务又加了新规则:
“为了防止作弊,‘新线索’直接到‘战败’,必须主管审批;而‘跟进中’到‘战败’,销售自己决定就行。”
这时候传统写法就痛苦了,你要在 markAsLost 里写更加复杂的 if (lead.status === 'New' && !isAdmin)。
而状态机怎么做?直接在矩阵上加路由或者拆分事件:
'New': {
'FAIL_BY_ADMIN': { nextState: 'Lost' } // 明确定义只有主管事件能触发
},
'In_Progress': {
'FAIL': { nextState: 'Lost' }
}状态机的本质
状态机(FSM)里的 F(Finite,有限)指的是状态的数量是有限的,但它绝对没有限制状态之间的连线(Transition)必须是线性的。
你可以把它想象成地铁线路图:
- 线性就像 1 号线,一站一站往前开。
- 非线性就像整个地铁网,你可以从 A 站坐一站到 B 站,也可以在某个换乘站直接坐特快线跳过好几站直接到 X 站,甚至可以坐回踩线。
只要“你在任意车站(状态)时,拿着某张车票(事件),接下来的去向是唯一确定的”,这就完美契合状态机。
状态机的等价代换
状态机是个抽象概念,对于普通的状态机,我们可以用一张二维矩阵表来表示,例如前文的示例:
| 当前状态 \ 触发事件 | Submit (提交) | Approve (通过) | Reject (拒绝) |
|---|---|---|---|
| Draft (草稿) | → Pending | 不允许 | 不允许 |
| Pending (待审批) | 不允许 | → Approved | → Draft |
| Approved (已通过) | 不允许 | 不允许 | 不允许 |
水平方向是事件,垂直方向是状态。
虽然现在是 AI Coding 时代,AI 通常能自己抽象出状态机;但是相信我,请记住 “二维矩阵表” 这个提示词。既可以用来让 AI 给你展现当前代码状态机的实现,帮助你 review AI 的 Coding 结果。也可以你自己先画一张 “二维矩阵表”,然后让 AI 作为参考,实现状态机代码。
回到文章开头那句话:
线性代数是几何与代数的桥梁,AI 时代我们同样需要一种抽象表达来联通人 与 AI 的沟通桥梁
二维矩阵表应该进入 AI Coding 的词汇表。
“状态机”与“图(Graph)结构
除了“二维矩阵表”,有些复杂的状态机需要用到图结构来表达。在数据结构中:
- 图的节点(Vertex / Node) → 就是状态机的 State(状态)。
- 图的有向边(Edge) → 就是状态机的 Transition(流转),边上的标签就是 Event(事件)。
LangGraph(包括前端的 XState、甚至工作流引擎 Activiti/Flowable),它们在底层的核心数据结构就是一张有向图(Directed Graph)。
无论是 LangGraph 还是我们上面写的 JSON 配置表,本质上都是在描述这张图。
- 矩阵表视角:适合人类阅读和离散配置(像 Excel 表格)。
- 图结构视角:适合程序进行复杂的拓扑遍历、条件分支跳转和动态编排。
关注我,下一篇,我们继续拆解 LangGraph 中图结构的状态机实现。