前言

做 Agent 开发时,最让工程师头疼的问题往往不是底层大模型(LLM)不够聪明,而是 Agent 的行为难以可控。在真实的业务场景中,用户的一句话可能隐含多种意图,或者意图非常模糊。这就要求 Agent 必须学会在不同的"对话阶段"之间灵活切换——从最初的寒暄问候,到多轮的信息收集,再到决定调用外部工具,最后与用户确认并结束对话。

如果仅仅依靠 Prompt 让大模型自主决定下一步该做什么(例如纯粹的 ReAct 模式),系统会变得极其脆弱。模型可能会陷入死循环、产生幻觉,或者跳过关键的业务校验步骤。为了解决这种复杂性,我们需要引入软件工程中久经考验的标准工具:状态机(State Machine)。它能为概率性的 LLM 提供一个确定性的行为骨架。

什么是状态机

理解状态机最直观的方法是想象一扇普通的门。这扇门有两个状态:。你可以对它施加两个事件(动作):推 与 拉。

如果你对一扇处于状态的门执行事件,门就会进入状态。但如果门已经是的状态,再执行就没有任何意义(甚至是一个非法操作)。这就是状态机的核心逻辑:当前状态 + 触发事件 $\to$ 新状态

在计算机科学中,它的形式化定义通常是一个五元组:

  • 状态集合($S$):系统所有可能状态的有限集合。
  • 初始状态($s_0$):系统启动时的默认状态($s_0 \in S$)。
  • 事件集合($E$):触发状态改变的输入或条件。
  • 转移函数($\delta: S \times E \to S$):定义了在特定状态下接收到特定事件时,应该转移到哪个新状态。
  • 终止状态集合($F$):表示流程结束的状态集合($F \subseteq S$)。

在构建 Agent 时,我们通常会遇到两种常见的状态机类型:

  • 有限状态机(Finite State Machine, FSM):状态和事件的数量是有限的,所有的状态流转(转移函数)都是在代码中严格预先定义好的。它非常适合逻辑固定、边界清晰的任务。
  • 图状态机(Graph-based State Machine):这是目前复杂 Agent 框架的主流(如 LangGraph)。它的状态流转可以是动态计算的,原生支持条件分支、循环回路以及并行处理,更适合处理多轮对话和复杂的逻辑编排。

为什么 Agent 需要状态机

一个可靠的 Agent 绝不能是一个"黑盒"。它的运行生命周期应该经历明确的阶段,并且每个阶段(状态)都有单一、清晰的职责。我们来看一个典型的 Agent 运转流程:

flowchart TD A[空闲 IDLE] -->|用户提问| B[分析意图 ANALYZING] B -->|需要工具| C[调用工具 TOOL_CALL] B -->|无需工具| D[生成回复 RESPOND] C -->|工具完成| D D -->|发送回复| A

在这个流程中,核心关键点在于意图分析后的条件分支。有些用户请求(如"你是谁?")可以直接生成回复;而有些请求(如"今天香港的天气如何?")则必须先去调用天气 API。

如果没有状态机,你可能需要在一个庞大的 Prompt 中向 LLM 解释所有这些条件,并祈祷它每次都能严格遵守。而引入状态机后,这两种截然不同的处理路径被显式地定义在系统架构中。即使 LLM 输出了一些奇怪的内容,状态机也能将其限制在当前的节点中,或者通过异常处理回滚,从根本上杜绝了"Agent 卡死不知道下一步该干嘛"的窘境。

基础实现:有限状态机

为了看透本质,我们先不借助任何外部框架,用纯 Python 写一个最基础的有限状态机。只需几十行代码,就能讲清它的核心运转原理。

python
from enum import Enum, auto

# 1. 定义有限的状态集合 (S)
class AgentState(Enum):
    IDLE      = auto()   # 空闲等待输入
    ANALYZING = auto()   # 分析用户意图
    TOOL_CALL = auto()   # 执行外部工具
    RESPOND   = auto()   # 生成最终回复

class AgentFSM:
    def __init__(self):
        # 2. 设定初始状态 (s0)
        self.state = AgentState.IDLE

        # 3. 定义转移函数表 (Delta)
        # 格式: (当前状态, 触发事件): 目标状态
        self.transitions = {
            (AgentState.IDLE,      "user_input"): AgentState.ANALYZING,
            (AgentState.ANALYZING, "need_tool"):  AgentState.TOOL_CALL,
            (AgentState.ANALYZING, "no_tool"):    AgentState.RESPOND,
            (AgentState.TOOL_CALL, "tool_done"):  AgentState.RESPOND,
            (AgentState.RESPOND,   "sent"):       AgentState.IDLE,
        }

    # 4. 状态转移引擎
    def send(self, event: str):
        key = (self.state, event)
        if key in self.transitions:
            old_state = self.state
            self.state = self.transitions[key]
            print(f"✅ 状态转移: [{old_state.name}] --({event})--> [{self.state.name}]")
        else:
            # 非法转移直接抛出异常,防止 Agent 做出越界行为
            raise ValueError(
                f"❌ 无效转移: 在状态 {self.state.name} 下无法处理事件 '{event}'"
            )

# 测试运行
fsm = AgentFSM()
fsm.send("user_input")  # IDLE -> ANALYZING
fsm.send("need_tool")   # ANALYZING -> TOOL_CALL
fsm.send("tool_done")   # TOOL_CALL -> RESPOND
fsm.send("sent")        # RESPOND -> IDLE

在这个例子中:

  • Enum 定义了 Agent 的生命周期状态。
  • transitions 字典充当了交通警察,严格规定了哪条路能走,哪条路不能走。
  • send() 方法负责推动流程前进。

这就是 FSM 的全部骨架。在实际应用中,你只需要将调用 LLM 的代码、请求 API 的逻辑分别塞进这些状态节点里即可。

图状态机:LangGraph 的核心理念

虽然简单的 FSM 足够清晰,但随着 Agent 变得越来越复杂,状态和事件的数量会呈现指数级增长(即所谓的状态爆炸)。现代 Agent 框架(如 LangChain 生态下的 LangGraph)通常采用图状态机(Graph-based State Machine)。

与传统 FSM 的"线性、预定义"不同,图状态机的核心理念在于:

  • 状态(State):不再仅仅是一个枚举标签(如 IDLE),而是一个贯穿全局的数据结构(通常是一个字典或对象)。它记录了对话历史、中间结果和上下文。
  • 节点(Node):图中的顶点。本质上就是一个纯函数(Python function),它接收当前的状态数据,执行特定的业务逻辑(如调用 LLM),然后返回需要更新的状态片段。
  • 边(Edge):连接节点的有向弧。分为普通边(执行完 A 必定去 B)和条件边(执行完 A 后,根据 A 的输出结果动态决定去 B 还是 C)。

这是一个使用 LangGraph 理念的极简示例:

python
from typing import TypedDict
from langgraph.graph import StateGraph, END

# 1. 定义全局共享的 State 数据结构
class AgentState(TypedDict):
    messages: list[str]     # 聊天记录
    next_action: str        # 路由标记:"tool" 或 "respond"
    tool_result: str | None # 工具返回的结果

# 2. 定义处理节点 (Nodes)
def analyze(state: AgentState) -> dict:
    print("正在分析意图...")
    # 模拟 LLM 判断:假设用户问了天气,需要工具
    return {"next_action": "tool"}

def call_tool(state: AgentState) -> dict:
    print("正在调用天气 API...")
    return {"tool_result": "香港今天晴朗,28度"}

def respond(state: AgentState) -> dict:
    print("生成最终回复...")
    return {"messages": state["messages"] + ["最终回复内容"]}

# 3. 构建图状态机
graph = StateGraph(AgentState)

# 注册节点
graph.add_node("analyze", analyze)
graph.add_node("call_tool", call_tool)
graph.add_node("respond", respond)

# 4. 定义边与路由逻辑
graph.set_entry_point("analyze")

# 核心:条件边(Conditional Edge)
# 根据 analyze 节点计算出的 next_action 动态决定走向
graph.add_conditional_edges(
    "analyze",
    lambda state: "call_tool" if state.get("next_action") == "tool" else "respond"
)

# 固定边
graph.add_edge("call_tool", "respond")
graph.add_edge("respond", END)

# 编译为可执行应用
app = graph.compile()

这种模式的强大之处在于:LLM 负责"思考"并在 State 中写入决策数据,而图的边(Edge)负责根据这些数据严格地执行"路由"控制。

状态持久化与断点恢复

在真实的生产环境中,Agent 的执行过程可能会持续很长时间(例如查阅大量文档、等待外部系统的回调等)。如果在执行到一半时服务器发生重启或崩溃,我们不能让 Agent 从头再来。此外,很多 Agent 需要实现 Human-in-the-loop(人工确认环节),这也要求状态机必须能够随时挂起并恢复。

因此,状态持久化(Checkpointing)是一项关键的工程实践。我们需要将图状态机的全局 State 序列化,存储到 Redis、PostgreSQL 或本地文件中。一个简单直观的示例:

python
import json
from typing import Any

def save_checkpoint(state: dict[str, Any], step_name: str, session_id: str):
    """将当前状态及停留的节点保存到磁盘"""
    filepath = f"./checkpoints/session_{session_id}.json"
    checkpoint_data = {
        "current_step": step_name,
        "state_payload": state
    }
    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(checkpoint_data, f, ensure_ascii=False, indent=2)
    print(f"💾 进度已保存至断点: {step_name}")

def load_checkpoint(session_id: str) -> dict[str, Any]:
    """从磁盘恢复状态,以便继续执行"""
    filepath = f"./checkpoints/session_{session_id}.json"
    try:
        with open(filepath, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return {}

注:在使用真正的框架处理状态持久化时,需特别注意复杂对象的序列化问题(例如 LangChain 中的 HumanMessageAIMessage 对象不能直接 json.dump,需要转换为基础的 dict)。

最佳实践

在用状态机编排 Agent 时,建议遵循以下工程原则:

  • 控制状态的颗粒度(5-10 个为宜):如果一个图或 FSM 的状态超过 15 个,它的复杂度和维护成本会呈指数上升。遇到复杂业务时,应该采用嵌套状态机(将某几个状态封装成一个子图 Sub-graph)。
  • 转移表/路由图集中定义:一定要让所有的边(Edges)和转移规则在代码的同一个地方清晰可见。这极大方便了团队的 Code Review,一眼就能看出系统的拓扑结构。
  • 明确的生命周期钩子(on_enter / on_exit):在进入某个状态时进行资源初始化(如重置 Token 计数器),在离开时进行清理。保持状态函数的纯粹性。
  • 让非法转移"痛感"明显:遇到非法的状态跳转时,直接抛出明显的 Exception(如 InvalidTransitionError),千万不要静默失败或在内部默默重试。及早崩溃(Fail Fast)能帮你迅速定位 LLM 的幻觉问题。
  • 确保 State 数据可完全序列化:这不仅是为了断点恢复,更是为了方便后续回放(Replay)日志、调试 bug 以及收集优质数据用于模型微调(Fine-tuning)。

进一步阅读

如果你想深入探索状态机与 Agent 架构设计的结合,以下资源不可错过:

  • Statecharts: A Visual Formalism for Complex Systems (1987) — David Harel:图状态机理论的开山鼻祖。读懂这篇论文,你会深刻理解为什么线性的 FSM 在应对复杂并发系统时会显得力不从心。
  • LangGraph 官方文档:目前将图状态机理念应用在 Agent 编排上最成功的开源框架之一。重点关注它的 Checkpoint 机制和 Subgraphs 的用法。
  • Building Effective Agents (Anthropic):Claude 背后的公司关于 Agent 设计模式的深度博文。文章中明确指出,对于大多数任务,确定性的编排(Routing/State Machines)往往比让 LLM 自由发挥(Autonomous Agents)更加可靠有效。