多层状态机:从单变量到4层架构的工程实践

大家好,我是程序员小策。

状态机这东西,大部分人都觉得自己懂了。毕竟不就是几个状态加几个箭头嘛------谁不会画?

但真要深挖,你确定你理解的是对的吗?先来几个问题热热身:

  • 你的系统里,"当前在做什么"是用一个 string 变量存的,还是用枚举+校验函数守的?
  • 状态之间的跳转,有没有写转换规则?还是哪里需要就哪里 setState()
  • 如果用户在"评审中"突然点了"开始写作",你的系统是直接放行,还是拦住报错?
  • 你的工作流状态机和业务状态机是同一个东西吗?如果不是,它们怎么协作?
  • 一个 AI Agent 系统从"加载上下文"到"生成草稿"到"提交评审",中间有十几个节点和条件分支------你用 if-else 能写清楚吗?

大部分人能回答前两个,到第三个开始犹豫,到第五个就卡住了。

今天这篇文章就是要把这五个问题一个一个拆开。而且不是空谈理论------我会用一个真实的 AI 小说创作系统里的 4 层状态机架构,带你看看生产级的状态机到底怎么设计。


问题定义:为什么朴素的状态管理不够用?

最朴素的做法是什么?一个变量存状态,哪里需要哪里改:

python 复制代码
self.status = "writing"
# ... 某个地方
self.status = "reviewing"

看起来够用了。但问题马上就来了:

谁能保证 reviewing 之后不会直接跳到 init 谁能保证 writing 不会跳过 reviewing 直接进入 rewriting?谁能保证"正在生成草稿"的时候不会有人触发"提交章节"?

没有约束的状态变量,就是一个没有红绿灯的十字路口------谁都能走,谁都能撞。

更严重的是,当你的系统有多个维度的状态(宏观阶段、微观流程、节点工作流、生命周期),它们之间还有依赖关系------朴素方案直接崩盘。


核心概念:4 层状态机架构

状态机的本质不是"存状态",而是"约束状态转换"------定义什么跳转可以发生,什么跳转永远不能发生。

想象你在玩一个 RPG 游戏:

  • 游戏的主线进度 (序章→第一章→第二章→通关)------对应 Phase 阶段状态机,只能向前推进,不能回档
  • 当前战斗的流程 (普通攻击→技能施放→受击→反击)------对应 FlowState 流程状态机,有循环、有分支
  • 一个技能的完整释放过程 (前摇→施法→后摇)------对应 LangGraph 工作流状态机,严格的节点流转
  • 游戏本身的运行状态 (标题画面→游戏中→暂停→通关)------对应 Host 生命周期状态机,控制整个系统的启停

四层各管各的,但上层依赖下层的状态,下层受上层的约束。这就是 4 层状态机架构的核心思想。


实现:4 层状态机的代码设计

第一层:Phase------宏观阶段,只进不退

python 复制代码
class Phase:
    INIT = "init"
    PREMISE = "premise"
    OUTLINE = "outline"
    WRITING = "writing"
    COMPLETE = "complete"

_PHASE_ORDER = {
    Phase.INIT: 1,
    Phase.PREMISE: 2,
    Phase.OUTLINE: 3,
    Phase.WRITING: 4,
    Phase.COMPLETE: 5,
}

def can_transition_phase(from_phase: str, to_phase: str) -> bool:
    if not to_phase:
        return False
    if not from_phase or from_phase == to_phase:
        return True
    if from_phase not in _PHASE_ORDER or to_phase not in _PHASE_ORDER:
        return False
    return _PHASE_ORDER[to_phase] >= _PHASE_ORDER[from_phase]

def validate_phase_transition(from_phase: str, to_phase: str) -> None:
    if not can_transition_phase(from_phase, to_phase):
        raise ValueError(f'invalid phase transition: "{from_phase}" -> "{to_phase}"')

设计要点 :用序号比较实现"只进不退"。INIT(1) 可以跳到 WRITING(4),但 WRITING(4) 永远不能回到 INIT(1)validate_phase_transition() 在每次状态变更时强制校验,非法转换直接抛异常。

第二层:FlowState------微观流程,有循环有分支

python 复制代码
class FlowState:
    WRITING = "writing"
    REVIEWING = "reviewing"
    REWRITING = "rewriting"
    POLISHING = "polishing"
    STEERING = "steering"

def can_transition_flow(from_flow: str, to_flow: str) -> bool:
    if not to_flow:
        return False
    if not from_flow or from_flow == to_flow:
        return True
    if from_flow == FlowState.WRITING:
        return to_flow in {FlowState.REVIEWING, FlowState.REWRITING,
                           FlowState.POLISHING, FlowState.STEERING}
    if from_flow == FlowState.REVIEWING:
        return to_flow in {FlowState.WRITING, FlowState.REWRITING,
                           FlowState.POLISHING, FlowState.STEERING}
    if from_flow == FlowState.REWRITING:
        return to_flow in {FlowState.WRITING, FlowState.STEERING}
    if from_flow == FlowState.POLISHING:
        return to_flow in {FlowState.WRITING, FlowState.STEERING}
    if from_flow == FlowState.STEERING:
        return to_flow in {FlowState.WRITING, FlowState.REVIEWING,
                           FlowState.REWRITING, FlowState.POLISHING}
    return False

设计要点REWRITINGPOLISHING 完成后只能回到 WRITING 或进入 STEERING,不能直接跳到 REVIEWING------因为重写完了得先写新内容,才能再评审。STEERING 是万能中转站,用户随时可以干预方向。这就像游戏里的"暂停菜单"------不管你在干什么,都能按暂停,暂停后可以选继续、重来或换装备。

第三层:LangGraph 工作流------节点级编排

python 复制代码
def _build_graph(self):
    graph = StateGraph(GraphState)
    graph.add_node("load_runtime_context", load_runtime_context(self))
    graph.add_node("novel_context", novel_context_node(self))
    graph.add_node("plan_chapter", plan_chapter_node(self))
    graph.add_node("generate_draft", generate_draft_node(self))
    graph.add_node("commit_chapter", commit_chapter_node(self))
    graph.add_node("review", review_node(self))
    graph.add_node("rewrite", rewrite_node(self))
    graph.add_node("arc_summary", arc_summary_node(self))
    graph.add_node("volume_summary", volume_summary_node(self))
    graph.add_node("expand_arc", expand_arc_node(self))
    graph.add_node("checkpoint", checkpoint_node(self))
    graph.add_node("finish", finish_node(self))

    graph.add_edge(START, "load_runtime_context")
    graph.add_conditional_edges(
        "load_runtime_context", route_after_load,
        {"novel_context": "novel_context", "generate_draft": "generate_draft",
         "commit_chapter": "commit_chapter", "rewrite": "rewrite",
         "polish": "rewrite", "finish": "finish"},
    )
    graph.add_edge("novel_context", "plan_chapter")
    graph.add_conditional_edges(
        "plan_chapter", route_after_plan,
        {"generate_draft": "generate_draft", "finish": "finish"},
    )
    graph.add_edge("generate_draft", "commit_chapter")
    graph.add_conditional_edges(
        "commit_chapter", route_after_commit,
        {"review": "review", "rewrite": "rewrite", "polish": "rewrite",
         "arc_summary": "arc_summary", "volume_summary": "volume_summary",
         "expand_arc": "expand_arc", "checkpoint": "checkpoint", "finish": "finish"},
    )
    graph.add_edge("rewrite", "checkpoint")
    graph.add_edge("expand_arc", "checkpoint")
    graph.add_conditional_edges(
        "checkpoint", route_after_checkpoint,
        {"novel_context": "novel_context", "finish": "finish"},
    )
    graph.add_edge("finish", END)
    return graph.compile()

设计要点 :这是整个系统的核心编排引擎。12 个节点、4 个条件路由函数,通过 pending_action 字段驱动跳转。关键设计是 checkpoint 节点------它是所有循环的汇聚点,决定"继续写下一章"还是"暂停等确认"还是"全部完成"。就像游戏里的存档点------打完一关,自动存档,然后决定是继续下一关还是休息。

第四层:Host 生命周期------系统级启停

python 复制代码
class Host:
    def __init__(self, cfg: Config) -> None:
        self.lifecycle = "idle"
        # ...

    def start(self, prompt: str) -> None:
        if self.lifecycle == "running":
            raise ValueError("already running")
        self.lifecycle = "running"
        self.loop.start(text)
        self.loop.wait_idle()
        self._mark_idle_or_complete()

    def abort(self) -> bool:
        if self.lifecycle != "running":
            return False
        self.lifecycle = "paused"
        self.loop.abort()
        return True

    def _mark_idle_or_complete(self) -> None:
        progress = self.store.progress.load()
        if progress and progress.phase == Phase.COMPLETE:
            self.lifecycle = "completed"
        elif self.store.signals.load_pending_checkpoint() is not None:
            self.lifecycle = "paused"
        else:
            self.lifecycle = "idle"

设计要点idle → running → paused/completed,简洁但严格。running 状态下不能重复 start(),非 running 状态下 abort() 直接返回 False_mark_idle_or_complete() 根据 Phase 状态机的终态来决定自己的终态------上层状态机依赖下层状态机的状态,这就是 4 层架构的协作方式。


边界情况与陷阱

看起来很完美了对吧?但实际跑起来,这几个坑你一定会踩:

陷阱一:状态机之间的状态不一致。 Phase 已经到了 COMPLETE,但 FlowState 还停在 REVIEWING。后果:前端显示"已完成",但后台还在跑评审循环。解法:在 _mark_idle_or_complete() 中,以 Phase 状态为权威源,Phase 完成了就强制结束循环。

陷阱二:条件路由函数返回了不在映射表里的 key。 route_after_commit() 返回了一个拼写错误的字符串,LangGraph 找不到对应节点直接报错。后果:整个创作流程中断。解法:路由函数的返回值必须是 add_conditional_edges() 映射表的 key 子集,写单元测试覆盖所有分支

陷阱三:Host 的 lifecycle 和 LangGraph 的 pending_action 脱节。 用户调了 abort()lifecycle 变成 paused,但 LangGraph 还在跑。后果:状态看起来停了,实际 LLM 还在烧钱。解法:abort() 同时调用 loop.abort() 设置 _aborted 标志,LangGraph 在 checkpoint 节点检测到标志后主动结束。


高级考量:多状态机协作的"权威源"问题

当系统有 4 层状态机时,最核心的设计问题是:谁说了算?

答案:最底层的数据是权威源,上层是视图。

  • PhaseFlowState 存在 Progress 对象里,持久化到磁盘------它们是权威状态
  • Host.lifecyclePhase + FlowState派生视图 ,每次 _mark_idle_or_complete() 都从 Progress 重新计算
  • GraphState.pending_action瞬时指令,只在当前执行周期内有效,不持久化

这就像 MVC 架构------Model(Progress)是数据源,View(Host.lifecycle)是展示层,Controller(LangGraph)是执行层。永远不要让视图去修改数据源,只让数据源驱动视图更新。

另一个考量:断点恢复时状态怎么重建? 系统重启后,Progress 从磁盘加载,load_runtime_context 节点根据 Phase/FlowState/pending_commit 的值决定从哪个节点恢复------这就是为什么状态机必须有显式的转换规则,而不是隐式的 if-else 堆砌。


对比表格:4 层状态机的职责划分

状态机 状态维度 转换规则 持久化 核心约束
Phase 宏观阶段(5 个) 只进不退(序号比较) 磁盘 写完了不能回大纲阶段
FlowState 微观流程(5 个) 有向图(白名单校验) 磁盘 重写完必须先写再评审
LangGraph 工作流节点(12 个) 条件路由函数 内存 节点跳转必须经过映射表
Host.lifecycle 系统生命周期(4 个) 方法级守卫 内存 运行中不能重复启动

一句话总结:Phase 管"到哪了",FlowState 管"在干嘛",LangGraph 管"下一步做什么",Host 管"能不能做"。


面试追问

追问 1:如果 FlowState 的转换规则需要动态调整(比如某个场景下允许 REWRITING 直接跳到 REVIEWING),你的架构能支持吗?

→ 回答方向:把 can_transition_flow() 的规则从硬编码改为策略模式,注入不同的规则集。但要注意------放宽约束容易,收紧约束难,动态规则会增加调试难度。

追问 2:LangGraph 的条件路由和 FlowState 的转换校验会不会冲突?比如路由函数允许跳到 review,但 FlowState 不允许从 REWRITING 跳到 REVIEWING?

→ 回答方向:会冲突,而且这是实际会发生的 bug。解法是路由函数内部也要读 FlowState ,或者把 FlowState 校验作为路由的前置条件。当前代码中 route_after_commit() 只读 pending_action,没有校验 FlowState------这是一个潜在的改进点。

追问 3:为什么 Host.lifecycle 不用枚举而用字符串?

→ 回答方向:因为 lifecycle 的状态集合是封闭的(只有 4 个),而且只在 Host 内部使用,不需要跨模块校验。如果未来需要跨模块共享(比如前端也要校验),就应该升级为枚举+校验函数。

追问 4:4 层状态机之间的通信开销怎么控制?

→ 回答方向:上层读下层状态是 O(1) 的字典查找,没有消息传递开销。唯一有开销的是 LangGraph 的 invoke() 调用,但那是 LLM 调用本身的开销,不是状态机通信的开销。关键设计是上层不主动推状态给下层,而是下层自己拉


总结

状态机的价值不在于"存状态",而在于"约束转换"------让非法的状态跳转在代码层面就不可能发生。

读完这篇你应该能:设计一个多层状态机架构并说明每层的职责、用白名单校验函数替代隐式 if-else、解释为什么"权威状态源"必须是持久化的最底层、在面试时说出"4 层状态机各管各的,上层是下层的视图"而不只是"用状态机管理状态"。

下次看到 self.status = "xxx" 这种代码,先问自己一个问题:谁能保证这个赋值是合法的? 如果答案是"没人能保证"------恭喜,你需要一个状态机了。

相关推荐
Coder小相13 小时前
LangChain1.0第四篇 - 统一接口多厂商模型适配
人工智能·langchain·agent
JaydenAI13 小时前
[MAF预定义ChatClient中间件-05]动态修改对话配置的两种解决方案
ai·c#·agent·maf·chatclient管道
_未完待续13 小时前
从零打造 AI Agent (二)—— 让 AI 拥有记忆
agent·ai编程
PeterLi13 小时前
LangChain v1.x 最新官方完整教程(六大核心组件全解析+生产级代码示例)
langchain·agent
十正14 小时前
Hermes记忆预取机制深度解析
python·ai·agent·hermes
JaydenAI14 小时前
[MAF预定义ChatClient中间件-04]ReducingChatClient——通过精减对话实施又不丢失基本语义
ai·c#·agent·maf·chatclient管道·对话历史压缩
程序员柒叔14 小时前
Dify 一周动态-2026-W22
人工智能·大模型·github·agent·知识库·dify
Trouvaille ~15 小时前
【OpenClaw篇】OpenClaw 实战入门:在 VMware 虚拟机里部署第一个本地 AI Agent
人工智能·大模型·agent·vmware·虚拟机·tools·openclaw
谢白羽15 小时前
PlugMem 论文解读:把 Agent 经历抽象成可复用知识图的插件式记忆模块
llm·agent