线性代数是几何与代数的桥梁,AI 时代我们同样需要一种抽象表达来联通人 与 AI 的沟通桥梁

很多人说有了 AI,就不需要学习了,我实际体验下来,恰恰相反。agent 时代更需要学习,原因,我之前的推文里说过:

而普通 agent 代码里经常藏着一堆隐式流程:

shell
用户输入 -> 调 LLM -> LLM 要不要用工具? -> 执行工具 -> 把工具结果给 LLM -> 再判断 -> 结束

这个流程,可以称为状态流转,或者可执行图;当然,普通的 if-else 理论上也可以实现状态流转。不过,if-else 的代码,本质是用“条件组合”来代替“明确的状态”。

我举个具体的例子:

电商订单场景,一个订单有:

  • is_paid = true (已支付)
  • is_shipped = false (未发货)
  • is_refunded = false (未退款)

当业务增加,比如引入了“部分退款”、“拼团中”、“预售等待”…… 你的代码里就会充斥着这种组合:

shell
if (order.is_paid && !order.is_shipped && !order.is_refunded) { ... }

这就是痛点所在: 你在用一堆变量的组合去隐式表达一个状态。一旦变量多了,组合数爆炸(3个布尔值就有 ‭$2^3 = 8$‬‭‬ 种组合),代码必流脓。

状态机思维

状态机的思维是:别整那些虚的,这个订单现在有且仅有一个明确的、高内聚的状态(比如 PAID)。从 PAID 变成 SHIPPED,必须通过一个明确的动作(比如 SHIP)。

状态机四要素:

任何状态机,不管用什么语言写,都逃不出这四个核心概念。你只要在写代码时能对号入座,就说明你已经开始抽象了:

  1. State(状态):事物在某一时刻所处的稳定状况(如:草稿、待审批、已发布)。同一时刻只能有一个状态。
  2. Event(事件/动作):触发状态改变的“因”(如:提交、拒绝、发布)。
  3. Transition(流转/迁移):状态改变的“果”和过程。即:从状态 A 接收到事件 X,变成状态 B 的这个动作。
  4. Action(动作/副作用):在流转过程中顺便执行的业务逻辑(如:发邮件通知、改数据库、扣库存)。

传统 if-else vs 状态机抽象

我们拿最常见的**“工作流审批”(Workflow)**来做对比。假设状态有:Draft(草稿) → ‭Pending(待审批) → ‭Approved(已通过)

糟糕的传统写法(依赖条件判断)

这种写法下,逻辑散落在各个方法里,极难维护:

javascript
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 (已通过)不允许不允许不允许

有了这张表,我们在代码里怎么抽象?直接用配置(数据驱动)代替逻辑!

javascript
// 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

代码实现:它依然极其优雅

你看,不管连线多乱,对状态机配置表来说,无非就是多写一行配置而已。核心引擎代码一行都不需要改。

javascript
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(判定战败)这个动作,你的代码会写成这样:

javascript
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)。

而状态机怎么做?直接在矩阵上加路由或者拆分事件:

javascript
'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 中图结构的状态机实现。