前言
做 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 运转流程:
在这个流程中,核心关键点在于意图分析后的条件分支。有些用户请求(如"你是谁?")可以直接生成回复;而有些请求(如"今天香港的天气如何?")则必须先去调用天气 API。
如果没有状态机,你可能需要在一个庞大的 Prompt 中向 LLM 解释所有这些条件,并祈祷它每次都能严格遵守。而引入状态机后,这两种截然不同的处理路径被显式地定义在系统架构中。即使 LLM 输出了一些奇怪的内容,状态机也能将其限制在当前的节点中,或者通过异常处理回滚,从根本上杜绝了"Agent 卡死不知道下一步该干嘛"的窘境。
基础实现:有限状态机
为了看透本质,我们先不借助任何外部框架,用纯 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 理念的极简示例:
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 或本地文件中。一个简单直观的示例:
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 中的
HumanMessage或AIMessage对象不能直接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)更加可靠有效。