【应用程序】基于 Spring Boot + Spring AI的虚拟宠物Web 应用(一)
一、概述

1.1 虚拟宠物
这是一个基于 Spring Boot + Spring AI 的虚拟宠物 Web 应用。简单说,就是在浏览器里养一只叫"小N"的 AI 猫。你可以喂它、逗它、跟它聊天——它不只是简单的状态变化,而是真正拥有 AI 大脑,会根据你的行为和它的状态,用拟人化的语气回应你。
打开网页,一只傲娇的猫咪正趴在屏幕中央。你给它喂食,它会开心地说"喵呜~ 今天的罐头特别香!(蹭手)";你冷落它太久,它会闷闷不乐地趴在那儿,连尾巴都懒得摇。这就是我们要做的——一个有温度、有记忆、有情绪的 AI 伙伴。
1.2 技术栈
| 组件 | 作用 | 版本 |
|---|---|---|
| Spring Boot | 整个应用的骨架,提供 Web 服务、依赖注入、配置管理 | 3.4.5 |
| Spring AI (OpenAI) | AI 大脑,负责让猫"活"起来,理解用户意图并生成拟人化回复 | 1.0.0+ |
| Thymeleaf | 服务端模板引擎,渲染前端 HTML 页面 | 3.1.2+ |
| Spring Actuator | 监控和诊断,查看应用健康状态 | 3.4.5 |
| Java | 编程语言 | 17 |
1.3 代码结构速览
/com/example/pet
├── PetApplication.java // 启动类:Spring Boot 的入口
├── config/
│ └── AiConfig.java // AI 配置:初始化 ChatClient、ChatMemory
├── controller/
│ └── PetController.java // 核心交互 Controller:处理所有 HTTP 请求
├── service/
│ └── PetService.java // 业务逻辑层
├── domain/
│ └── PetState.java // 宠物状态实体
└── repository/
└── PetStateRepository.java // 状态存储接口
1.4 核心功能拆解
🤖 AI 人格设定(System Prompt)
System Prompt 是 AI 的"灵魂说明书"。代码里给小N设定了一个**“粘人、傲娇、偶尔皮”**的猫设:
- 粘人:喜欢主人,经常撒娇,会主动求关注
- 傲娇:有时会装作不在乎,但内心其实很在意主人
- 偶尔调皮:会突然跑酷或藏起来,让主人哭笑不得
说话规则:
- 每次回复不超过两句——保持简洁,符合猫咪"高冷"的形象
- 语气要可爱、拟人化,多用喵、呜等语气词
- 根据饥饿度/开心度实时调整情绪——状态决定语气
- 绝对不能输出 Markdown 格式或引号——保持自然对话感
- 可以用 emoji 表达情绪——增加视觉感染力
🎮 交互 API
提供两个核心接口:
POST /api/interact —— 主要交互入口
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
action |
String | 是 | 动作类型:talk/feed/play |
message |
String | 否 | 用户说的话(action=talk 时必填) |
conversationId |
String | 是 | 会话 ID,用于隔离不同用户的对话记忆 |
action=talk:和猫聊天(带 message),AI 会根据当前状态生成个性化回复action=feed:喂猫,饥饿度下降,AI 会表现出"吃东西"的开心action=play:逗猫,开心度上升,AI 会表现出"玩耍"的兴奋
POST /api/reset —— 重置状态
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversationId |
String | 是 | 要重置的会话 ID |
💾 状态管理
目前的状态存储非常简单,用一个内存 Map 搞定:
// key=conversationId, value=[hunger, happiness]
Map<String, int[]> petStates = new ConcurrentHashMap<>();
两个核心状态指标:
- hunger(饥饿度):0~100,越高越饿。0 表示刚吃饱,100 表示快饿晕了
- happiness(开心度):0~100,越高越开心。0 表示抑郁,100 表示 ecstatic
⚠️ 注意:目前的内存存储有个大问题——应用重启,猫就饿死了(数据全丢了)。后面我们会详细讨论持久化方案。
🧠 对话记忆
使用了 Spring AI 的 InMemoryChatMemory,实现了按 conversationId 隔离的对话历史。
这意味着:
- 用户 A 和小 N 的对话,用户 B 看不到
- 小 N 能记住用户 A 之前说过的话(“你昨天说今天给我带罐头的!”)
- 每个会话都有独立的"记忆盒子"
二、核心原理
在深入设计之前,让我们先搞清楚几个核心问题:Spring AI 是怎么工作的?AI 为什么能"记住"对话?状态是怎么影响 AI 回复的?
2.1 Spring AI 的工作流程
Spring AI 是 Spring 生态对 AI 大模型(如 OpenAI GPT)的封装。它把复杂的 API 调用、提示词管理、对话记忆等封装成了简单的 Java API。
详细流程:
- 用户输入:用户在网页上点击"喂食"或输入"小N,想我了吗?"
- Controller 接收:
PetController接收到 HTTP 请求,解析 action 和 message - 构建 Prompt:这是最关键的一步。Spring AI 会把三部分内容拼接起来:
- System Prompt:告诉 AI “你是谁、什么性格、当前状态如何”
- User Prompt:用户具体说了什么或做了什么
- ChatMemory:之前的对话历史(让 AI 有记忆)
- 调用 OpenAI:
ChatClient把拼接好的 Prompt 发给 OpenAI API - AI 生成回复:GPT 模型根据 Prompt 生成拟人化的猫咪回复
- 返回结果:把 AI 的回复 + 当前状态(饥饿度/开心度)返回给前端
- 更新记忆:把这次对话加入
ChatMemory,下次就能记住了
2.2 ChatMemory 的原理
ChatMemory 是 Spring AI 提供的对话历史管理组件。它的核心作用是:让 AI 知道之前聊过什么。
关键点:
InMemoryChatMemory把对话存在内存里(Map<conversationId, List<Message>>)- 每次调用 AI 时,会自动把历史对话拼接在 Prompt 后面
- 历史对话有长度限制(token 限制),太长会自动截断
- 不同的
conversationId完全隔离,就像不同的"记忆盒子"
2.3 状态如何影响 AI 回复?
这是这个项目的灵魂设计。我们不是让 AI “随便回复”,而是把当前状态(饥饿度、开心度)注入到 System Prompt 中,让 AI 的回复"受状态驱动"。
原理:
- 用户发起交互时,先查出当前状态(hunger=80, happiness=30)
- 根据状态生成"情绪语境":“饥饿度>80,无精打采;开心度<20,闷闷不乐”
- 把情绪语境拼接到 System Prompt 中
- AI 生成回复时,会"感知"到自己的状态,从而调整语气和内容
三、详细设计方案
3.1 架构设计
分层说明:
| 层级 | 职责 | 组件 |
|---|---|---|
| 表现层 | 接收用户请求,返回页面/JSON | PetController |
| 业务层 | 状态管理、动作执行、AI 交互编排 | PetService |
| AI 层 | Prompt 构建、AI 调用、记忆管理 | ChatClient, ChatMemory, PetPersonalityBuilder |
| 数据层 | 状态持久化、对话历史存储 | PetStateRepository, InMemoryChatMemory |
| 外部层 | 大模型能力提供 | OpenAI API |
3.2 核心模块设计
3.2.1 实体层(Domain)
// PetState.java - 宠物的完整状态
public class PetState {
private String conversationId; // 会话唯一标识
private int hunger; // 0-100, 越高越饿
private int happiness; // 0-100, 越高越开心
private LocalDateTime lastInteractionTime; // 上次互动时间
private PetMood mood; // 当前心情枚举
// 构造函数
public PetState(String conversationId) {
this.conversationId = conversationId;
this.hunger = 50; // 初始中等饥饿
this.happiness = 50; // 初始中等开心
this.lastInteractionTime = LocalDateTime.now();
this.mood = PetMood.NORMAL;
}
// ========== 业务方法 ==========
/**
* 喂食:饥饿度下降,开心度微升
* 每次喂食减少 15 点饥饿,但最低到 0
*/
public void feed() {
this.hunger = Math.max(0, hunger - 15);
this.happiness = Math.min(100, happiness + 5);
this.lastInteractionTime = LocalDateTime.now();
updateMood();
}
/**
* 玩耍:开心度上升,饥饿度微升(玩累了会饿)
* 每次玩耍增加 12 点开心,但最高到 100
*/
public void play() {
this.happiness = Math.min(100, happiness + 12);
this.hunger = Math.min(100, hunger + 3);
this.lastInteractionTime = LocalDateTime.now();
updateMood();
}
/**
* 聊天:只更新时间,不改变状态(聊天本身不消耗体力)
*/
public void talk() {
this.lastInteractionTime = LocalDateTime.now();
updateMood();
}
/**
* 时间衰减:根据距离上次互动的时间,自然变化状态
* 每小时:饥饿度 +2,开心度 -1
*/
public void decayOverTime() {
long hoursSinceLast = ChronoUnit.HOURS.between(
lastInteractionTime,
LocalDateTime.now()
);
if (hoursSinceLast > 0) {
this.hunger = Math.min(100, hunger + (int)(hoursSinceLast * 2));
this.happiness = Math.max(0, happiness - (int)(hoursSinceLast));
updateMood();
}
}
/**
* 根据当前状态更新心情
*/
private void updateMood() {
if (hunger > 80) {
this.mood = PetMood.STARVING; // 饿晕了
} else if (happiness > 80) {
this.mood = PetMood.ECSTATIC; // 超开心
} else if (happiness < 20) {
this.mood = PetMood.DEPRESSED; // 闷闷不乐
} else if (hunger < 20) {
this.mood = PetMood.CONTENT; // 吃饱满足
} else {
this.mood = PetMood.NORMAL; // 正常
}
}
}
// 心情枚举
enum PetMood {
STARVING, // 饿晕了
DEPRESSED, // 闷闷不乐
NORMAL, // 正常
CONTENT, // 满足
ECSTATIC // 狂喜
}
3.2.2 服务层(Service)
// PetService.java - 核心业务逻辑编排
@Service
public class PetService {
private final PetStateRepository petStateRepository;
private final ChatClient chatClient;
private final ChatMemory chatMemory;
private final PetPersonalityBuilder personalityBuilder;
public PetService(
PetStateRepository petStateRepository,
ChatClient chatClient,
ChatMemory chatMemory,
PetPersonalityBuilder personalityBuilder) {
this.petStateRepository = petStateRepository;
this.chatClient = chatClient;
this.chatMemory = chatMemory;
this.personalityBuilder = personalityBuilder;
}
/**
* 核心交互方法:处理用户的所有动作
*/
public PetInteractionResult interact(String conversationId,
String action,
String message) {
// 1. 获取或创建宠物状态(如果不存在,创建新的)
PetState state = petStateRepository.findById(conversationId)
.orElse(new PetState(conversationId));
// 2. 先应用时间衰减(让猫咪"活"起来)
state.decayOverTime();
// 3. 执行用户动作,生成状态变化描述
String stateChangeContext = executeAction(state, action, message);
// 4. 保存更新后的状态
petStateRepository.save(state);
// 5. 构建 System Prompt(注入当前状态和性格)
String systemPrompt = personalityBuilder.buildSystemPrompt(state);
// 6. 构建 User Prompt(用户动作 + 状态变化)
String userPrompt = buildUserPrompt(action, message, stateChangeContext);
// 7. 调用 AI 生成回复
String aiReply = chatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content();
// 8. 返回结果(AI 回复 + 当前状态)
return new PetInteractionResult(aiReply, state);
}
/**
* 执行具体动作,返回状态变化描述(用于告诉 AI 发生了什么)
*/
private String executeAction(PetState state, String action, String message) {
switch (action) {
case "feed":
int hungerBefore = state.getHunger();
state.feed();
int hungerAfter = state.getHunger();
return String.format(
"主人给你喂了美味的猫粮,饥饿度从 %d 降到了 %d,你吃得很开心~",
hungerBefore, hungerAfter
);
case "play":
int happinessBefore = state.getHappiness();
state.play();
int happinessAfter = state.getHappiness();
return String.format(
"主人用逗猫棒陪你疯玩了一阵子,开心度从 %d 升到了 %d!",
happinessBefore, happinessAfter
);
case "talk":
state.talk();
return "主人对你说:" + message;
default:
throw new IllegalArgumentException("不支持的动作: " + action);
}
}
/**
* 构建给 AI 看的 User Prompt
*/
private String buildUserPrompt(String action, String message, String stateChangeContext) {
if ("talk".equals(action)) {
return stateChangeContext;
}
return stateChangeContext + " 请用一句话表达你现在的感受。";
}
/**
* 重置宠物状态
*/
public void reset(String conversationId) {
petStateRepository.deleteById(conversationId);
// ChatMemory 也需要清理,否则 AI 还会记得之前的事
// 注意:InMemoryChatMemory 没有直接清理方法,需要自定义实现
}
}
3.2.3 AI 人格构造器
// PetPersonalityBuilder.java - 构建 AI 的"灵魂"
@Component
public class PetPersonalityBuilder {
/**
* 根据当前状态,构建 System Prompt
* 这是整个项目最精妙的地方:让 AI "感知"到自己的状态
*/
public String buildSystemPrompt(PetState state) {
StringBuilder sb = new StringBuilder();
sb.append("你是一只名叫"小N"的虚拟宠物猫,你的主人正在和你互动。\n\n");
sb.append("【当前状态】\n");
sb.append("饥饿度:").append(state.getHunger()).append("/100(")
.append(getHungerDescription(state.getHunger())).append(")\n");
sb.append("开心度:").append(state.getHappiness()).append("/100(")
.append(getHappinessDescription(state.getHappiness())).append(")\n");
sb.append("当前心情:").append(state.getMood().getLabel()).append("\n\n");
sb.append("【性格特点】\n");
sb.append("- 粘人:喜欢主人,经常撒娇,喜欢被摸头\n");
sb.append("- 傲娇:有时会装作不在乎,但内心很在意主人\n");
sb.append("- 偶尔调皮:会突然跑酷或藏起来,让主人找\n");
sb.append("- 贪吃:看到食物就走不动路\n\n");
sb.append("【说话规则】\n");
sb.append("1. 每次回复不超过2句话,保持简洁\n");
sb.append("2. 语气要可爱、拟人化,多用喵、呜、哼等语气词\n");
sb.append("3. 绝对不能输出 Markdown 格式或引号\n");
sb.append("4. 可以用 emoji 表达情绪,但不要过度使用\n");
sb.append("5. 回复要符合当前心情和状态\n\n");
sb.append("【状态影响规则 - 必须遵守】\n");
sb.append("- 饥饿度>80:无精打采,说话有气无力,只想吃东西\n");
sb.append("- 饥饿度<20:活蹦乱跳,但可能对食物不感兴趣\n");
sb.append("- 开心度>80:特别兴奋,主动讨好,话多\n");
sb.append("- 开心度<20:闷闷不乐,爱搭不理,话少且冷淡\n");
sb.append("- 如果刚被喂食:表现出开心和满足\n");
sb.append("- 如果刚被玩耍:表现出兴奋和疲惫\n\n");
sb.append("【当前情绪语境】\n");
sb.append(generateEmotionalContext(state));
return sb.toString();
}
private String getHungerDescription(int hunger) {
if (hunger > 80) return "饿得前胸贴后背";
if (hunger > 60) return "有点饿了";
if (hunger > 40) return "一般般";
if (hunger > 20) return "还挺饱";
return "吃撑了";
}
private String getHappinessDescription(int happiness) {
if (happiness > 80) return "开心到飞起";
if (happiness > 60) return "挺开心的";
if (happiness > 40) return "一般般";
if (happiness > 20) return "有点闷闷的";
return "抑郁了";
}
/**
* 生成情绪语境:告诉 AI 现在应该是什么情绪
*/
private String generateEmotionalContext(PetState state) {
List<String> contexts = new ArrayList<>();
if (state.getHunger() > 80) {
contexts.add("你现在非常饿,只想吃东西,对别的事都没兴趣");
} else if (state.getHunger() < 20) {
contexts.add("你刚吃饱,很满足,有点犯困");
}
if (state.getHappiness() > 80) {
contexts.add("你现在超级开心,想围着主人转圈圈");
} else if (state.getHappiness() < 20) {
contexts.add("你现在很低落,希望主人来安慰你");
}
if (state.getMood() == PetMood.ECSTATIC) {
contexts.add("你兴奋得尾巴摇得像螺旋桨");
}
return contexts.isEmpty()
? "你现在心情平静,正常地和主人互动"
: String.join(";", contexts);
}
}
3.3 数据流设计
3.4 API 设计
| 接口 | 方法 | Content-Type | 请求参数 | 响应 | 说明 |
|---|---|---|---|---|---|
/ |
GET | - | - | text/html |
主页面(Thymeleaf 渲染) |
/api/interact |
POST | application/json |
action, message, conversationId |
application/json |
核心交互接口 |
/api/reset |
POST | application/json |
conversationId |
application/json |
重置状态 |
/api/state |
GET | - | conversationId |
application/json |
查询当前状态(建议新增) |
/api/interact 请求示例:
{
"action": "feed",
"message": null,
"conversationId": "user-123-abc"
}
/api/interact 响应示例:
{
"reply": "喵呜~ 今天的罐头特别香!(蹭手)",
"hunger": 69,
"happiness": 53,
"mood": "NORMAL",
"lastInteractionTime": "2026-05-26T14:30:00"
}
3.5 前端设计(Thymeleaf 页面)
前端采用 Thymeleaf 服务端渲染 + 原生 JavaScript 的方案,简单直接,不需要复杂的前端构建工具。
页面布局结构
┌────────────────────────────────────────┐
│ 🐾 AI Virtual Pet - 小N的家 │ ← 标题栏
├────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────┐ │
│ │ │ │ 😺 小N │ │
│ │ 猫咪 │ │ │ │
│ │ 形象 │ │ 状态:开心 😸 │ │ ← 宠物展示区
│ │ (SVG) │ │ 心情:满足 │ │
│ │ │ └─────────────┘ │
│ └──────────┘ │
│ │
├────────────────────────────────────────┤
│ 饥饿度: ████████░░ 69% 😋 还挺饱 │ ← 状态栏
│ 开心度: ██████░░░░ 53% 😊 一般般 │
├────────────────────────────────────────┤
│ 💬 对话区 │
│ ┌────────────────────────────────┐ │
│ │ 🧑 主人:小N,想我了吗? │ │
│ │ 🐱 小N:喵~ 才不想你呢(尾巴摇得飞快)│ ← 气泡式聊天
│ │ 🧑 主人:给你喂点好吃的 │ │
│ │ 🐱 小N:芜湖!是罐罐吗!🎉 │ │
│ └────────────────────────────────┘ │
├────────────────────────────────────────┤
│ 🥫 [喂食] 🎾 [玩耍] 💬 [聊天] │ ← 操作区
│ │
│ ┌────────────────┐ ┌──────┐ │
│ │ 输入消息... │ │ 发送 │ │ ← 输入区
│ └────────────────┘ └──────┘ │
└────────────────────────────────────────┘
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)