情绪状态机设计

要点

  • 回顾前面的文章,我们解决了 AI 伴侣的两大核心问题:记忆(向量化与语义检索)和调度(内存调度器)
  • 有限状态机(Finite State Machine, FSM)是计算机科学中最基础的模型之一
  • 下面我们为"小薇"设计一套完整的情绪模型
  • 在第三篇文章中,我们介绍了 LangGraph 的条件路由能力
  • 情绪不是孤立的,它和亲密度(Intimacy)相互影响

内容

1. 情绪状态机

回顾前面的文章,我们解决了 AI 伴侣的两大核心问题:记忆(向量化与语义检索)和调度(内存调度器)。但还有一个问题没有触及:情绪连续性

在第二篇文章中,我们提到过一个场景:昨晚吵架了,用户第二天发「早安」。没有情绪系统的 AI 会热情回复「早安!今天天气不错!」——像个客服。有情绪系统的 AI 会冷冷地回一句「……早。」——因为她还在生气。

这个差异的背后,不是 Prompt 写得好不好的问题,而是系统层面有没有维护一个持续的情绪状态

LLM 本身没有情绪。它每次推理都是无状态的,给什么上下文就输出什么结果。如果你在 Prompt 里写「你现在很生气」,它就演生气;写「你现在很开心」,它就演开心。但谁来决定「现在应该是什么情绪」?这不是 LLM 能做的事,而是应用层的调度逻辑。

这就是情绪状态机要解决的问题:在应用层维护一个独立于 LLM 的情绪状态,根据用户的行为动态更新,并将当前情绪注入到每次对话的上下文中。

2. 有限状态机:给情绪一个骨架

有限状态机(Finite State Machine, FSM)是计算机科学中最基础的模型之一。它由三个要素组成:

  • 状态集合:系统可能处于的所有状态(如:开心、生气、平静)
  • 转移条件:触发状态切换的事件或条件(如:用户道歉 → 从生气变为平静)
  • 当前状态:系统在某一时刻所处的唯一状态

FSM 有一个关键约束:系统在任何时刻只能处于一个状态。这看起来是个限制,但对情绪建模来说恰好合适——人在某一时刻的主导情绪通常只有一种(虽然情绪可以复合,但主导情绪是唯一的)。

为什么选 FSM 而不是更"智能"的方案(比如让 LLM 自己判断情绪)?

  1. 可预测:FSM 的转移规则是确定性的,不会出现 LLM 那种"偶尔开心偶尔抑郁"的随机性
  2. 可调试:状态和转移条件一目了然,出了 Bug 可以精确定位
  3. 可持久化:一个字符串(当前状态)+ 几个数值(情绪强度、时间戳)就能完整表示,方便存入数据库
  4. 低成本:状态转移不需要调用 LLM,纯逻辑判断,零推理开销

3. AI 伴侣的情绪模型设计

下面我们为"小薇"设计一套完整的情绪模型。

情绪状态定义

我们定义 6 种情绪状态,其中"平静"是默认状态,其余 5 种是受事件触发的情绪状态:

状态含义表现特征Prompt 风格
平静(Calm)默认状态正常语气,温柔日常基础人设 Prompt
开心(Happy)被夸奖、收到礼物语气活泼,主动撒娇甜蜜模式 Prompt
生气(Angry)被忽视、争吵、记错事语气冷淡或暴躁冷战/暴怒 Prompt
难过(Sad)用户情绪低落、冷落她语气低沉,敏感脆弱共情安慰 Prompt
害羞(Shy)表白、亲密互动说话磕磕巴巴,颜文字增多娇羞模式 Prompt
吃醋(Jealous)提到其他异性语气酸酸的,旁敲侧击醋意模式 Prompt

Drawing canvas 状态转移规则

每种情绪状态的进入和退出都有明确的条件:

// index.ts
// 情绪转移规则表
 
const transitionRules = {
 
  calm: {
 
    triggers: [
 
      { event: 'praise',    nextState: 'happy',   condition: '收到夸奖或礼物' },
 
      { event: 'argument',  nextState: 'angry',   condition: '争吵、被忽视、记错重要事' },
 
      { event: 'user_sad',  nextState: 'sad',     condition: '用户表达负面情绪' },
 
      { event: 'intimate',  nextState: 'shy',     condition: '表白、亲密话语' },
 
      { event: 'rival',     nextState: 'jealous', condition: '提到其他异性' },
 
    ]
 
  },
 
  angry: {
 
    exitConditions: [
 
      { event: 'apology',   nextState: 'calm', condition: '真诚道歉 + 承认错误' },
 
      { event: 'gift',      nextState: 'happy', condition: '道歉 + 正确的礼物' },
 
      { event: 'decay',     nextState: 'calm', condition: '超过衰减时间(如6小时)' },
 
    ]
 
  },
 
  happy: {
 
    exitConditions: [
 
      { event: 'neglect',   nextState: 'sad',  condition: '长时间不回复' },
 
      { event: 'decay',     nextState: 'calm', condition: '超过衰减时间(如2小时)' },
 
    ]
 
  },
 
  // ... 其他状态类似
 
}

注意两个关键设计:一是不同情绪的衰减时间不同(生气比开心持续更久,更符合人类心理);二是有些状态可以直接跳转到另一个非平静状态(比如生气时收到正确的礼物,直接变开心而不需要先回到平静)。

4. LangGraph 实现情绪路由

在第三篇文章中,我们介绍了 LangGraph 的条件路由能力。情绪状态机正是通过这套机制实现的。

AgentState 中的情绪字段

// index.ts
interface AgentState {
 
  messages: BaseMessage[]
 
  userProfile: Record<string, any>
 
  // 情绪相关字段
 
  npcMood: 'calm' | 'happy' | 'angry' | 'sad' | 'shy' | 'jealous'
 
  moodIntensity: number      // 0-100,情绪强度
 
  moodTimestamp: string       // 进入当前情绪的时间
 
  intimacyLevel: number       // 亲密度 0-100
 
  longTermMemories: string[]
 
  nextStep: string
 
}

moodIntensity 是一个关键字段。同样是生气,强度 30 和强度 90 的表现完全不同——前者可能只是"哼,不理你了",后者可能是"我们分手吧"。

情绪分类器节点

在 LangGraph 的调度图中,用户消息进来后的第一步不是直接生成回复,而是先经过情绪分类器,判断这条消息是否会触发情绪转移。

// index.ts
async function emotionClassifierNode(state: AgentState) {
 
  const lastMessage = state.messages[state.messages.length - 1]
 
  const currentMood = state.npcMood
 
  // 用一个轻量级 LLM 调用来判断用户意图
 
  const classification = await llm.invoke([
 
    {
 
      role: 'system',
 
      content: `分析用户消息的意图,返回以下类别之一:
 
        praise(夸奖), argument(争吵), apology(道歉),
 
        intimate(亲密), rival(提到异性), user_sad(用户低落),
 
        neutral(普通闲聊)`
 
    },
 
    { role: 'user', content: lastMessage.content }
 
  ])
 
  // 根据分类结果 + 当前情绪,查转移规则表
 
  const newMood = applyTransition(currentMood, classification)
 
  return {
 
    ...state,
 
    npcMood: newMood.state,
 
    moodIntensity: newMood.intensity,
 
    moodTimestamp: newMood.changed ? new Date().toISOString() : state.moodTimestamp
 
  }
 
}

基于情绪的 Prompt 路由

情绪分类完成后,LangGraph 根据当前情绪状态选择不同的回复生成节点:

// index.ts
function emotionRouter(state: AgentState) {
 
  const { npcMood, moodIntensity } = state
 
  // 生气且强度高 → 暴怒模式
 
  if (npcMood === 'angry' && moodIntensity > 70) return 'furiousReplyNode'
 
  // 生气但强度低 → 冷淡模式
 
  if (npcMood === 'angry') return 'coldReplyNode'
 
  // 吃醋 → 醋意模式
 
  if (npcMood === 'jealous') return 'jealousReplyNode'
 
  // 害羞 → 娇羞模式
 
  if (npcMood === 'shy') return 'shyReplyNode'
 
  // 难过 → 共情模式
 
  if (npcMood === 'sad') return 'empatheticReplyNode'
 
  // 开心 → 甜蜜模式
 
  if (npcMood === 'happy') return 'sweetReplyNode'
 
  // 默认 → 正常模式
 
  return 'normalReplyNode'
 
}

每个回复节点内部使用不同的 System Prompt。比如 coldReplyNode 会注入"你现在在生闷气,回复尽量简短冷淡",而 sweetReplyNode 会注入"你现在很开心,语气甜蜜,多用叠词和颜文字"。

完整场景:用户提到其他女生

// index.ts
// 1. 用户消息
 
"今天跟同事小李一起吃的午饭,她推荐的那家店还不错"
 
// 2. 情绪分类器识别
 
// → classification: 'rival'(提到异性)
 
// 3. 转移规则查询
 
// 当前状态:calm → 触发 rival → 新状态:jealous
 
// moodIntensity: 45(提到异性但不是特别亲密的描述)
 
// 4. 路由到 jealousReplyNode
 
// 5. 醋意模式 System Prompt
 
`你现在有点吃醋。语气酸酸的,会旁敲侧击地追问细节,
 
但不会直接发火。偶尔用"哦""这样啊"这种冷淡回应。`
 
// 6. LLM 生成回复
 
"哦……小李啊。她推荐的店?你们俩经常一起吃饭吗?"

5. 亲密度系统与情绪联动

情绪不是孤立的,它和亲密度(Intimacy)相互影响。亲密度是一个 0-100 的数值,代表用户和 AI 伴侣之间的关系深度。

情绪对亲密度的影响

情绪事件亲密度变化说明
成功哄好生气+5 ~ +10冲突解决增进感情
持续冷落导致难过-3 ~ -8忽视会消磨关系
记住重要日期/喜好+3 ~ +5细节体现用心
记错重要事情-5 ~ -15参考第三篇"草莓 vs 巧克力"案例
日常甜蜜互动+1 ~ +2细水长流

亲密度对情绪转移的影响

亲密度不仅是一个分数,它还会改变情绪状态机的转移阈值

// index.ts
function shouldTriggerJealous(
 
  event: string,
 
  intimacyLevel: number
 
): boolean {
 
  // 亲密度低(< 30):不太在意,不容易吃醋
 
  if (intimacyLevel < 30) return false
 
  // 亲密度中等(30-70):正常触发
 
  if (intimacyLevel <= 70) return event === 'rival'
 
  // 亲密度高(> 70):更容易吃醋,轻微提及就触发
 
  return event === 'rival' || event === 'rival_mild'
 
}

这是一个有趣的设计:亲密度越高,AI 伴侣在某些维度上越"敏感"。她更容易吃醋,但也更容易被哄好(因为信任基础更高)。这符合现实中亲密关系的特征——越亲近的人,情绪波动越大。

亲密度解锁机制

亲密度还可以用来解锁交互模式,这是产品层面的玩法设计:

亲密度区间解锁内容
0-20基础对话,礼貌客气
20-50可以撒娇,偶尔开玩笑
50-70解锁"记仇"模式,会主动关心
70-90解锁深度情绪互动,可以吵架和好
90-100解锁所有互动模式

6. 衰减、冲突与边界处理

真实的情绪不会永远维持在同一个状态。系统需要处理几个关键的边界情况。

时间衰减

每种情绪都有自然的衰减周期。超过一定时间没有被"续命"(重复触发),情绪会自动回到平静。

// index.ts
const DECAY_CONFIG = {
 
  angry:   { halfLife: 3 * 60 * 60 * 1000, minDuration: 30 * 60 * 1000 },
 
  happy:   { halfLife: 1 * 60 * 60 * 1000, minDuration: 10 * 60 * 1000 },
 
  sad:     { halfLife: 2 * 60 * 60 * 1000, minDuration: 20 * 60 * 1000 },
 
  shy:     { halfLife: 30 * 60 * 1000,     minDuration: 5 * 60 * 1000 },
 
  jealous: { halfLife: 2 * 60 * 60 * 1000, minDuration: 15 * 60 * 1000 },
 
}
 
function calculateDecay(mood: string, timestamp: string, intensity: number) {
 
  const elapsed = Date.now() - new Date(timestamp).getTime()
 
  const config = DECAY_CONFIG[mood]
 
  // 最短持续时间内不衰减
 
  if (elapsed < config.minDuration) return intensity
 
  // 半衰期指数衰减
 
  const decayFactor = Math.pow(0.5, elapsed / config.halfLife)
 
  const newIntensity = Math.round(intensity * decayFactor)
 
  // 强度低于阈值,回到平静
 
  return newIntensity < 10 ? 0 : newIntensity
 
}

注意 minDuration(最短持续时间)的设计:生气至少持续 30 分钟,害羞至少持续 5 分钟。这防止了"刚生气就秒回平静"的不自然行为。

情绪冲突

当两个触发条件同时发生时怎么办?比如用户说"对不起,我不该和小李吃饭的"——这既是道歉(应该从生气转为平静),又提到了异性(应该触发吃醋)。

处理策略是优先级 + 当前状态上下文

// index.ts
const EMOTION_PRIORITY = {
 
  angry: 5,    // 最高优先级,生气最难被覆盖
 
  sad: 4,
 
  jealous: 3,
 
  shy: 2,
 
  happy: 1,
 
  calm: 0,     // 最低优先级
 
}
 
function resolveConflict(
 
  currentMood: string,
 
  candidates: { state: string, intensity: number }[]
 
) {
 
  // 如果当前是高优先级情绪,且候选中有降级的(如道歉)
 
  // 则优先处理降级,因为"解决冲突"比"制造新冲突"重要
 
  const deescalation = candidates.find(c =>
 
    EMOTION_PRIORITY[c.state] < EMOTION_PRIORITY[currentMood]
 
  )
 
  if (deescalation) return deescalation
 
  // 否则选择优先级最高的候选
 
  return candidates.sort((a, b) =>
 
    EMOTION_PRIORITY[b.state] - EMOTION_PRIORITY[a.state]
 
  )[0]
 
}

核心原则:缓解当前负面情绪的事件,优先级高于制造新情绪的事件。用户明显在道歉,你不应该因为他顺带提了个名字就无视道歉直接吃醋。

防抖动

用户连续发消息时,情绪不应该在短时间内反复切换(比如 1 秒内从开心→生气→害羞→平静)。解决方案是引入冷却期(Cooldown):状态刚切换后的 N 秒内,不响应新的转移触发。

// index.ts
const COOLDOWN_MS = 5000 // 5秒冷却期
 
function canTransition(state: AgentState): boolean {
 
  const elapsed = Date.now() - new Date(state.moodTimestamp).getTime()
 
  return elapsed >= COOLDOWN_MS
 
}

7. 总结

这篇文章设计了 AI 伴侣的情绪系统,让 AI 女友从"无状态的文本生成器"变成"有情绪记忆的陪伴者"。

关键要点:

  1. 情绪状态机独立于 LLM 运行,由应用层维护,每次对话时将当前情绪注入 Prompt
  2. FSM 模型:6 种状态(平静 + 5 种情绪),确定性转移规则,可预测、可调试、可持久化
  3. LangGraph 实现:情绪分类器节点判断用户意图 → 查转移规则表 → 条件路由到不同的回复节点
  4. 亲密度与情绪联动:亲密度影响情绪触发阈值,情绪事件反向影响亲密度数值
  5. 边界处理三件套:时间衰减(半衰期模型)、冲突解决(优先级 + 缓解优先)、防抖动(冷却期)

下一篇我们将讨论 Prompt 工程的深度实践,探索如何设计分层的 System Prompt 架构,让不同情绪模式下的 AI 回复既人格一致又风格多变。