
情感 AI 的对话状态机设计从条件分支到可预测的状态流转一、对话流程中隐藏的混乱状态一个情感陪伴 AI 的对话流程大致是问候 → 倾听 → 共情回应 → 建议引导。但在实际运行中共情回应阶段用户突然说算了我不想聊了如果状态机直接跳转到结束之前建立的对话上下文会全部丢失——下次用户重新打开AI 又会从问候开始表现得像从未认识过用户。更复杂的是并行状态。用户在建议引导阶段既可以追问具体建议进入细化建议子状态也可以完全切换话题退出当前状态机。如果用简单的 if-else 管理这些分支3 个主状态 × 4 个子状态 × 2 种退出条件 24 种状态组合if-else 代码将无法维护。状态机是管理对话流程的标准解法。它不做聪明的决策只做可预测的流转——给定当前状态和用户输入输出下一状态和对应的系统动作。二、对话状态机的结构设计stateDiagram-v2 [*] -- Greeting : 用户打开对话 Greeting -- Listening : 用户开始倾诉 Greeting -- SmallTalk : 用户寒暄 Listening -- EmpathyResponse : 用户情绪表达完整 Listening -- Listening : 用户继续倾诉 Listening -- CrisisDetected : 检测到危机关键词 EmpathyResponse -- SuggestionGuide : 用户寻求建议 EmpathyResponse -- Listening : 用户继续情绪表达 EmpathyResponse -- Closure : 用户表示不想聊了 SuggestionGuide -- SuggestionDetail : 用户追问具体建议 SuggestionGuide -- Listening : 用户切换回情绪话题 SuggestionDetail -- SuggestionGuide : 用户返回建议列表 SuggestionDetail -- Closure : 用户表示足够了 CrisisDetected -- CrisisProtocol : 启动危机干预 CrisisProtocol -- HumanHandoff : 转接人工 SmallTalk -- Listening : 用户开始情绪表达 SmallTalk -- Closure : 用户结束对话 Closure -- [*] : 对话结束 Closure -- Listening : 用户再次发起倾诉 note right of CrisisDetected : 独立于主流程的并行状态 note right of EmpathyResponse : 核心状态——大部分时间停留于此状态设计的原则每个状态职责单一——共情回应状态只做共情不混入建议逻辑转换条件显式——每条箭头标注触发条件不存在隐式的默认跳转危机检测是并行状态——在任何状态下都可以触发不阻塞主流程。三、XState 实现对话状态机// 情感陪伴 AI 的对话状态机 // 设计意图使用 XState v5 实现可预测的状态流转 // 每个状态对应一组可执行的动作发送消息、调用 API import { createMachine, assign } from xstate; // 对话上下文——状态机管理的运行时数据 interface ChatContext { // 当前对话轮次 turnCount: number; // 累积的情绪标签——用于长期记忆 emotionTags: string[]; // 最近一次用户消息 lastUserMessage: string; // 是否检测到危机信号 crisisDetected: boolean; } // 状态机事件——用户输入和系统信号 type ChatEvent | { type: USER_MESSAGE; text: string } | { type: EMOTION_DETECTED; emotion: string; intensity: number } | { type: CRISIS_SIGNAL; text: string } | { type: USER_WANTS_SUGGESTION } | { type: USER_WANTS_DETAIL; topic: string } | { type: USER_GO_BACK } | { type: USER_CLOSE } | { type: USER_RESTART } | { type: TIMEOUT }; const chatMachine createMachine({ id: emotionalChat, initial: greeting, context: { turnCount: 0, emotionTags: [], lastUserMessage: , crisisDetected: false, } satisfies ChatContext, states: { greeting: { entry: sendGreeting, on: { USER_MESSAGE: [ { // 守卫条件——用户消息较短且无情绪词 → 寒暄 guard: ({ event }) event.text.length 10, target: smallTalk, }, { // 默认→倾听 target: listening, }, ], // 超时——用户 5 分钟未响应回到初始状态 TIMEOUT: { target: greeting }, }, }, listening: { entry: [incrementTurn, recordMessage], on: { USER_MESSAGE: { target: listening, // 内部转换——不退出再进入保留上下文 internal: true, actions: [incrementTurn, recordMessage], }, EMOTION_DETECTED: { target: empathyResponse, actions: assign({ emotionTags: ({ context, event }) [...context.emotionTags, event.emotion], }), }, CRISIS_SIGNAL: { target: crisisDetected, actions: assign({ crisisDetected: true, lastUserMessage: ({ event }) event.text, }), }, TIMEOUT: { target: greeting, actions: assign({ emotionTags: [], }), }, }, }, empathyResponse: { entry: generateEmpathyResponse, on: { USER_WANTS_SUGGESTION: { target: suggestionGuide, actions: recordMessage, }, USER_MESSAGE: [ { // 情绪强度 0.7——继续倾听 guard: ({ event, context }) context.emotionTags.length 0 context.emotionTags.slice(-1)[0] ! neutral, target: listening, }, { target: listening, }, ], USER_CLOSE: { target: closure }, TIMEOUT: { target: greeting }, }, }, suggestionGuide: { entry: generateSuggestions, on: { USER_WANTS_DETAIL: { target: suggestionDetail, actions: assign({ lastUserMessage: ({ event }) event.topic, }), }, USER_MESSAGE: { // 用户在建议区发起情绪话题→回到倾听 target: listening, }, USER_CLOSE: { target: closure }, }, }, suggestionDetail: { entry: generateDetailSuggestion, on: { USER_GO_BACK: { target: suggestionGuide }, USER_CLOSE: { target: closure }, }, }, // 危机检测——并行状态可在任何阶段触发 crisisDetected: { entry: initiateCrisisProtocol, on: { USER_MESSAGE: { target: crisisDetected, internal: true, actions: recordMessage, }, }, // 危机状态 30 分钟后自动退出 after: { 1800000: { target: greeting }, }, }, smallTalk: { entry: generateSmallTalkResponse, on: { USER_MESSAGE: { target: listening, }, USER_CLOSE: { target: closure }, }, }, closure: { entry: sendClosureMessage, on: { USER_RESTART: { target: greeting }, // 5 分钟无响应 → 结束 TIMEOUT: { type: final as const }, }, }, }, });三个状态设计原则在代码中的体现守卫条件guard决定状态转换——用户消息长度判断是寒暄还是情感倾诉internal: true的内部转换——在倾听状态内追加消息不触发退出/进入钩子危机检测可在任何状态触发——用户在任何对话阶段说出危机关键词都会跳转到危机处理。四、状态机的边界与常见误区状态爆炸问题。当情感类别从 3 种增加到 10 种时如果为每种情绪创建独立状态倾听_焦虑、倾听_愤怒……状态数量会从 8 个膨胀到 30。状态应该表达对话阶段而非用户属性。情绪类别应存储在 context 的emotionTags中而非状态名中。与 AI 决策的职责边界。状态机决定在哪个对话阶段做什么类型的系统动作AI 模型决定系统动作的具体内容。状态机不应该参与具体说哪句话的决策——那是 AI 模型的职责。不适合完全开放域的对话。状态机适合有明确意图分类和对话流程的场景。如果是完全开放域的闲聊状态机只能管开始和结束中间的状态划分没有意义。五、总结情感 AI 对话状态机的设计原则状态表达对话阶段而非用户属性——避免状态爆炸守卫条件和显式转换——消除隐式跳转带来的不可预测性状态机管做什么阶段AI 模型管说什么内容_并行状态处理跨阶段事件——危机检测在任何对话阶段都可触发。落地建议先用简单的 5-8 个状态覆盖核心对话流程使用 XState 的 visualizer 可视化状态流转与产品经理对齐预期在 context 中而非状态名中存储用户属性定期审查状态转换日志删除从未被触发的转换路径。