作者:逆境不可逃

技术永无止境

希望我的内容可以帮助到你!!!!


一个带情绪状态机的对话 Agent 是怎么炼成的?从架构设计到六维评测,手把手拆解。

我用CC+DSv4pro通过几轮对话做了一个叫 EAgent 的项目 —— 一个有情绪的 AI 聊天 Agent

由于项目太过于玩具我就不开源了。。。。。。。。。。。。。。。。。。。。。。。。。。。

你跟它聊天,它不只回你文字。它有一个 8 维情绪向量(快乐😊、悲伤😢、愤怒😠、恐惧😨、惊讶😲、信任🤝、期待🤔、厌恶🤢),会随着你的话实时变化。你夸它,它开心;你骂它,它难过;你一直不理它,它慢慢回归平静。

前端能看到情绪雷达图变形强度条跳动时间线波动。选不同人设(暖心好友、毒舌评论家……),AI 的情绪基线和说话风格截然不同。

技术栈:Java 21 + Spring Boot 3.5 + Vue 3 + DeepSeek + SSE 流式

你说的话 Agent 的情绪变化
"今天升职了!太开心了!" joy↑ trust↑ sadness↓
"我的猫昨天走了……" sadness↑ joy↓
"老板又抢我功劳,太不公平了!" anger↑ disgust↑ trust↓
接下来 10 条消息都是闲聊 所有情绪逐渐回归人设基线

一、为什么要做这个项目?

现在市面上的 AI 聊天产品,基本都是「冷冰冰的回复机器」。你跟它说「我好难过」,它会给你一段共情的话,但它的「情绪」只存在于当前这条回复里 —— 下一条就忘了。

我想做的是:让 AI 真正拥有一个持续的情绪状态。不是靠提示词假装,而是用一个独立于 LLM 的情绪引擎来维护。

这就像一个正常人 —— 你不会因为朋友说了一句「我很好」就瞬间从悲伤切换到开心,情绪是有惯性、衰减、传染的。


二、整体架构(一图胜千言)

一条消息的完整旅程(9 步):

用户输入 "今天好开心!"
  ↓
① EventSource 连接 /api/chat/stream
  ↓
② ChatService 收到请求
  ↓
③ EmotionEngine 分析关键词:"开心" → joy+0.16
  ↓
④ PromptTemplates 构建提示词:「你是一个暖心好友...当前主要感到快乐...」
  ↓
⑤ HttpURLConnection 直连 DeepSeek,stream: true
  ↓
⑥ 逐行解析 SSE data: {"delta":{"content":"恭喜"}}
  ↓
⑦ Flux.create 推送给 Spring → 序列化为 SSE → 浏览器
  ↓
⑧ 前端 EventSource.onmessage → 实时追加文字 + 更新情绪可视化
  ↓
⑨ 流结束 → EmotionEngine 再根据 AI 回复二次调整情绪

三、核心模块逐个拆解

3.1 情绪引擎(EmotionEngine)—— 项目的灵魂

这是整个项目最有意思的部分。

情绪不是凭空产生的—— 每条用户消息经过一轮关键词匹配,触发情绪变化量(delta),然后加上衰减情绪传染

// EmotionEngine 处理流水线
processUserMessage(userMessage):
  ① 关键词分析 → 生成 EmotionDelta(8维变化量)
  ② currentState.applyDelta(delta)     // 应用变化
  ③ currentState.decay(baseline, 0.05) // 向人设基线衰减
  ④ currentState.applyContagion(0.1)   // 情绪间相互传染

为什么这么设计?

  • 衰减:真实情绪不会一直高涨。你开心了一阵,慢慢就平静了 —— 衰减率 0.05 就是这个「慢慢回归」的速度
  • 传染:心理学中的 Plutchik 情绪轮理论 —— 快乐会压制悲伤,信任会减少恐惧,愤怒和厌恶相互加强
  • 不对称敏感度:AI 自己的话对情绪的影响只有用户话的 3-5 折,防止「自我反馈循环」

关键词表(中英文双语,各 8 组):

// 快乐触发器(部分)
"happy","开心","太好了","哈哈","love","喜欢","wonderful"

// 悲伤触发器(部分)
"sad","难过","伤心","哭","lonely","孤独","bad day"

// 愤怒触发器(部分)
"angry","生气","不公平","hate","讨厌","frustrated"

优缺点

  • ✅ 零 API 成本,<1ms 响应
  • ✅ 中英文双语覆盖
  • ✅ 参数可配置(application.yml
  • ❌ 纯关键词匹配,没有语义理解(「今天下雨了,又是一个人」不会被识别为悲伤)
  • ❌ 魔法数字硬编码(0.3 上限、0.08 系数全是试出来的)

怎么改进? 已经预留了 buildEmotionAnalysisPrompt() 方法 —— 用 LLM 做情绪分析。可以作为关键词方案的第二级校验:关键词命中 → 快速响应;关键词未命中但怀疑有情绪 → 调 LLM 复核。

3.2 流式对话(ChatService)—— 踩过的坑

这里踩了一个大坑,写出来帮大家避雷。

问题 1:响应式框架中做阻塞 I/O

Spring WebFlux 基于 Netty,是 NIO 非阻塞的。但 DeepSeek 的 SSE 流需要同步逐行读取。直接用会阻塞 Netty 的 I/O 线程,导致 SSE 响应写不出去 ——后端日志显示成功,前端一片空白

解决方案Flux.create + Schedulers.boundedElastic()

return Flux.<StreamEvent>create(sink -> {
    // 阻塞式 HTTP 调用 DeepSeek SSE
    HttpURLConnection conn = ...
    BufferedReader reader = ...
    String line;
    while ((line = reader.readLine()) != null) {
        if (line.startsWith("data: ")) {
            // 解析 JSON chunk → sink.next(event)
        }
    }
    sink.next(doneEvent);
    sink.complete();
}, FluxSink.OverflowStrategy.BUFFER)
.subscribeOn(Schedulers.boundedElastic());  // ← 关键!移到弹性线程池

boundedElastic() 是 Reactor 专门给阻塞 I/O 准备的线程池,不会影响 Netty 的主 I/O 线程。

问题 2:Vue 响应式对象引用 Bug

前端 SSE 收到 chunk 后更新聊天气泡内容,结果文本在内存里变了,页面上纹丝不动

原因:

// ❌ 错误写法
const agentMsg = { role: 'agent', content: '' }
messages.value.push(agentMsg)
// ...
agentMsg.content = '新的内容'  // agentMsg 是原始对象,Vue 代理管不到它!

// ✅ 正确写法
messages.value.push({ role: 'agent', content: '' })
const idx = messages.value.length - 1
// ...
messages.value[idx].content = '新的内容'  // 通过代理数组更新,触发重渲染

Vue 3 的 ref 会对数组元素做响应式代理,但你手里那个 agentMsg 变量还是原始对象。必须通过 messages.value[index] 来更新

问题 3:上下文每轮被清空

前端每条消息都带 personaId,后端收到后无条件清了历史记录。导致 Agent 永远失忆,上一秒说「我叫李白」下一秒就不知道你是谁。

// ❌ 修复前
if (personaId != null && !personaId.isEmpty()) {
    conversationHistory.clear();  // 每条消息都清!
}

// ✅ 修复后
if (personaId != null && !personaId.isEmpty()
        && !personaId.equals(activePersona.getId())) {  // 真换了人设才清
    conversationHistory.clear();
}

这三个问题前后排查了两天,希望对你有帮助 😂

3.3 提示词工程(PromptTemplates)—— 情绪怎么「说出来」

不能让 LLM 直接念数值("我的快乐指数现在是 0.8")—— 太出戏了。要让情绪通过语言风格自然流露

你是一个 暖心好友 (🤗)。一个温暖、善解人意的朋友……

当前情绪状态:主要感到快乐,带着明显的快乐,充满信任。

情绪表达的重要指示:
- 让情绪通过措辞、语气来体现,不要直接说出来
- 如果感到快乐 → 更加热情积极
- 如果感到悲伤 → 更加内敛沉思
- 如果感到愤怒 → 更加尖锐直接(但不对用户)
- 情绪应逐渐变化,不要突兀
- 全程使用中文

设计要点

  • 人设定义「我是谁」,情绪定义「我现在感觉如何」—— 两者独立注入
  • 「不要说数值」这条指令非常关键 —— 几乎是提示词工程的必修课
  • 中文原生提示词,避免 LLM 的翻译损耗

3.4 人设系统 —— 6 种性格,一种代码

# personas.yml — 加新人设只需加一段配置
- id: grumpy-critic
  name: "毒舌评论家"
  emoji: "😼"
  baseline:
    joy: 0.2
    sadness: 0.3
    anger: 0.6      # ← 高愤怒基线
    disgust: 0.5     # ← 高厌恶基线
  traits:
    - "毒舌犀利"
    - "专说大实话"
    - "内心关心但嘴上不饶人"
  tone: "讽刺、毒舌、但内心温暖"
人设 情绪特征 跟你说话的画风
🤗 暖心好友 joy=0.7, trust=0.8 "没关系的,我一直在你身边"
🧘 斯多葛哲人 joy=0.3, sadness=0.3 "凡事皆有因果,不必过于执着"
💪 热情教练 joy=0.8, anticipation=0.7 "你可以的!再来一次!冲!"
🌙 忧郁诗人 sadness=0.6 "悲伤是灵魂深处的一首未完成的诗……"
🌟 好奇宝宝 joy=0.9, surprise=0.8 "哇!然后呢然后呢?为什么呀?"
😼 毒舌评论家 anger=0.6, disgust=0.5 "就这?我见过更差的,你这不算什么"

四、Agent 六维评测(含得分)

我按照一个通用的 Agent 评测框架,对 EAgent 当前版本做了自评。不吹不黑,每个维度都给了代码依据和当前得分。

维度 1:基础能力 ★★★☆☆

指标 得分 说明
指令遵循 ⭐⭐⭐⭐ 中文提示词分层清晰,LLM 执行到位
语义理解 ⭐⭐⭐ 依赖 DeepSeek,无额外处理层
多轮指代 ⭐⭐⭐ 上下文 Bug 已修复,支持 20 轮
多语言 ⭐⭐⭐⭐ 中英文双语关键词表

维度 2:Agent 特有能力 ★★☆☆☆

指标 得分 说明
工具调用 。这是从 Chat 到 Agent 的最大鸿沟
任务规划 无拆解、无 ReAct、无动态重规划
自主反思 情绪判断错了不会自我修正
边界控制 ⭐⭐ 依赖 LLM 原生拒绝能力

📌 这也是 EAgent 当前最大的成长空间——LangChain4j 的 Tool Calling 和 AiServices 已经引入了但还没用上。后续可以接入天气查询、音乐推荐、网页搜索等工具,让情绪不只是「聊出来」的,而是「感知环境」的。

维度 3:业务落地 ★★★☆☆

指标 得分 说明
情绪响应准确率 ⭐⭐⭐ 明显情绪词命中率高(~70%),隐晦表达遗漏
人设一致性 ⭐⭐⭐⭐ 基线情绪持续约束,切换流畅
效率 ⭐⭐⭐⭐ 情绪分析 <1ms,流式首 token ~1s
输出质量 ⭐⭐⭐ 简明扼要(1-4 句),但无 Markdown 渲染

维度 4:安全合规 ★★☆☆☆

指标 得分 说明
内容安全 ⭐⭐ 完全依赖 LLM,Agent 层无自定义过滤
隐私 API Key 硬编码在 yml 里 🚨
稳定性 ⭐⭐⭐ SSE 有降级处理,异常不崩溃
合规 无审计、无留痕、无权限

维度 5:用户与运维 ★★★☆☆

指标 得分 说明
交互体验 ⭐⭐⭐⭐ 情绪可视化丰富、流式打字效果、拟人感强
可调试 ⭐⭐⭐ 后端日志完善,前端 Console 有 SSE 追踪
可配置 ⭐⭐⭐⭐ 人设 YAML 驱动,情绪参数 yml 调优
可监控 ⭐⭐ 无 Metrics / Prometheus,零成本统计

维度 6:量化评测(框架搭建)

我建议构建以下测试集来量化 Agent 的表现:

情绪触发测试:50 条中英文混合 → 自动比对 delta
人设一致性测试:30 条 × 6 人设 → 人工打分 1-5
上下文记忆测试:20 轮对话 → 检查第 N 轮是否还记得第 1 轮信息
衰减回归测试:10 条中性消息 → 验证情绪是否回到基线 ±0.05

综合自评:3.5 / 5 分—— 对话体验是亮点,Agent 能力是短板。


五、改进路线图

✅ 已修复

  • 上下文每轮清空 Bug
  • 上下文窗口从 10 轮扩到 20 轮
  • Vue 响应式更新不生效问题
  • 后端阻塞导致 SSE 推不出去

🔲 短期(1-2 周)

  • API Key 改环境变量
  • 清理死代码 + 抽取重复逻辑
  • 加单元测试

🔲 中期(1-2 月)

  • Redis 持久化 + 用户事实档案
  • LLM 情绪分析作为关键词的二阶段校验
  • 接入 Tool Calling(天气、音乐、搜索)
  • 前端 Markdown 渲染

🔲 长期(3-6 月)

  • ReAct 规划模式
  • 多用户会话隔离
  • RAG 知识库
  • Prometheus 监控

怎么聊让情绪变化最明显?

情绪 试试这样说
😊 快乐 ↑ "今天升职了!太开心了!谢谢你的支持"
😢 悲伤 ↑ "我养的猫昨天走了,好难过,陪我聊聊吧"
😠 愤怒 ↑ "老板又抢我功劳了,这已经是第三次了!"
😨 恐惧 ↑ "明天要上台面对 500 人演讲,紧张到胃痛"
😲 惊讶 ↑ "天啊!!我居然中了彩票二等奖??"
🤝 信任 ↑ "我相信你,这个秘密我只跟你说了"

六、体验

感觉AI直接生成的还是比较蠢的,所以说AIcoding绝非一件简单的事情,需要一个合理的流程,而不是简单的对话。

七、项目总结

如果你问我 EAgent 到底算什么 ——

  • 不是一个生产级的客服机器人
  • 也不是一个简单的 ChatGPT 套壳
  • 它是一个有独立情绪状态的对话 Agent 实验项目,情绪引擎是真正的差异化

技术上有三个点我觉得最有价值:

  1. 情绪引擎的设计模式—— 衰减 + 传染 + 不对称敏感度,可以迁移到任何需要「状态管理」的 AI 应用
  2. Flux.create + boundedElastic 的 SSE 方案—— 在 WebFlux 中做流式 LLM 调用的正确姿势
  3. 提示词中情绪表达的策略——「别说数值,用语气表达」,适用于所有角色扮演类 AI

八、批判

无论如何,它终归是一个玩具项目,这个项目离"能用"都还差一截,离"能卖钱"更是十万八千里。

  第一,单用户玩具,不是产品。

  • 整个系统所有状态——对话历史、情绪向量、人设配置——全部塞在一个 JVM
  • 单例里。来第二个用户怎么办?两个人共享同一份情绪?A 把 Agent 骂生气了,B 打开页面看到一张臭脸,这算谁的?没有
  • Session、没有租户隔离、没有数据库、没有 Redis——服务一重启,所有记忆归零。你敢拿它给用户用?

  第二,情绪引擎经不起推敲。

  •   核心卖点是情绪模拟,但实现方式是什么?text.contains("开心")。就这么简单。用户说"我其实并不开心",Agent 的 joy 还是
  •   +0.16——它根本不懂否定句。说"今天下雨了,又是一个人",情绪引擎纹丝不动——它不认识隐晦表达。Plutchik 情绪轮、衰减系数
  •   0.05、传染因子 0.1,听着像那么回事,实际上全是拍脑袋的魔法数字,没有心理学实验支撑、没有用户调研验证、没有 A/B
  •   测试对照。Demo 唬人可以,生产环境你敢拿它做心理咨询?

  第三,安全性为零。

  • API Key 明文写在 application.yml 里,谁拿到代码谁就能盗刷你的 DeepSeek
  •   额度。没有认证、没有鉴权、没有限流、没有内容审核——用户发什么它回什么,发敏感内容没有拦截,发超长文本没有截断,打恶意请
  •   求没有防护。安全团队看一眼就得把项目毙了。

  第四,没有任何企业级能力。

  • 没有日志采集(ELK?没有)、没有指标监控(Prometheus?没有)、没有链路追踪(SkyWalking?没有)、没有灰度发布、没有熔断降级、没有健康检查探针。
  •  出了故障你怎么排查? 靠System.out.println?连常规日志都没有?运维看到这个架构图,连夜跑路。

  最后,也是最致命的问题——这个项目没有商业模式。

  • "有情绪的 AI 聊天"作为一个学术 Demo很酷,作为一篇博客文章很有话题性,但作为一个产品,谁买单?企业客服要的是准确率、效率、降本,不是客服对着客户"感到悲伤"。
  • 心理咨询场景需要专业资质和合规认证,不是一个关键词匹配引擎能 cover 的。
  • 娱乐聊天?用户用 ChatGPT、豆包、Kimi不比你这个强?情绪引擎是加分项,不是卖点。
  • 加分的项目可以没有商业模式,但卖点必须有。这个项目把加分项当成了卖点。

总结一句话:EAgent 是一份优秀的学习代码,但它距一个能上线、能见用户、能赚钱的产品,中间还差着一个完整的技术团队和一轮产品经理的毒打。

九、反思

  • Agent ≠ 套壳 LLM。 这是我犯的第一个认知错误。一个 Chatbot 只要加上系统提示词就能聊天,但一个 Agent 需要感知、决策、执行、反思四个环节。EAgent 只做了前面半个(感知到情绪、提示词决策),执行和反思完全是空白。没有工具调用 ——Agent 只能「想」不能「做」,就像一个人只有大脑没有手脚。如果你在搭 Agent,先把 Function Calling 跑通,再谈上层建筑。

  • 状态管理是 Agent 的及格线,不是加分项。 我花了很多精力设计情绪向量、衰减曲线、传染逻辑,但回过头看 —— 连最基本的上下文不丢失都花了两天才修好。Agent 的核心能力是「记住关键信息并在合适的时机用上」,而不是「记住所有信息」。做记忆的时候要区分三层:短期对话上下文(最近 N 轮,窗口 + 摘要)、长期用户档案(姓名、偏好、事实,结构化存储)、外部知识(RAG,按需检索)。三层分开管,互不污染。

  • 别指望 LLM 替你兜底。 情绪判断用关键词匹配太糙,于是你想「交给 LLM 分析不就行了」—— 但 LLM 会幻觉、会遗漏、会过度解读、会有偏见。规则引擎做第一道防线(快速、可控、零成本),LLM 做第二道复核(低置信度时才调),这才是正解。让 LLM 处理不确定性,让代码处理确定性。反过来也行不通 —— 全部走规则,复杂度爆炸且毫无泛化能力。

  • 安全不是最后才加的功能。 API Key 硬编码、无认证、无审计、无脱敏 —— 这些放在 Demo 里没人管,但如果是公司项目,第一天就应该上环境变量 + 密钥管理服务 + 操作日志。Agent 比普通应用更危险,因为它有自主行动能力 。工具调用权限要做最小化收口 —— 删数据的权限不给、发消息的权限要二次确认、敏感操作必须人类审批。Agent 越强大,安全边界越要收紧。

  • 先定义评测标准,再写代码。 这是我最后悔的事。情绪引擎写完了,效果好不好?不知道 —— 因为我没有建测试集。关键词改了会不 会引入新问题?也不知道 —— 因为没有回归测试。做 Agent 一定要提前定义:什么是好(准确率?召回率?用户满意度?)、怎么测(自动化用例?人工打分?A/B 对照?)、基线在哪(不用 Agent 的原始指标是多少?)。没有评测体系的 Agent 项目,等于闭着眼睛开车。

  • 单用户 Demo 和多用户产品的差距,比你想象的大 10 倍。 内存里存状态 → Redis 分 Session → 数据库持久化 → 多租户隔离 → 高可用集群 —— 每一步都是一层复杂度。不要在产品化的时候才想起来做架构升级,第一天就按多用户设计,哪怕只有一个用户用。

  • 最后一个也是最本质的问题:你的 Agent 解决了什么真问题? 我沉迷于情绪引擎的设计,觉得「有情绪的 AI 很酷」。但用户不会因为一个东西「酷」就付费 —— 他们只会因为一个东西「有用」而付费。技术是手段,解决问题是目的。不要爱上你 的方案,要盯着用户的问题。 做 Agent 之前先问自己三句话:谁在用?他遇到了什么麻烦?我的 Agent 做成了什么样他会说「值了」?答不上来这三个问题,先别写代码。

📌 如果你也在做 Agent 相关的项目,欢迎评论区聊聊:你觉得 AI 应该「有情绪」吗?情绪应该帮助 AI 更好地服务人类,还是会让它变得不可控?

更多推荐