【应用程序】基于 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设定了一个**"粘人、傲娇、偶尔皮"**的猫设:

  • 粘人:喜欢主人,经常撒娇,会主动求关注
  • 傲娇:有时会装作不在乎,但内心其实很在意主人
  • 偶尔调皮:会突然跑酷或藏起来,让主人哭笑不得

说话规则

  1. 每次回复不超过两句------保持简洁,符合猫咪"高冷"的形象
  2. 语气要可爱、拟人化,多用喵、呜等语气词
  3. 根据饥饿度/开心度实时调整情绪------状态决定语气
  4. 绝对不能输出 Markdown 格式或引号------保持自然对话感
  5. 可以用 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 搞定:

java 复制代码
// 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。
用户输入
Spring Boot Controller
构建 Prompt
System Prompt

猫设+当前状态
User Prompt

用户说的话
ChatMemory

历史对话
ChatClient
调用 OpenAI API
AI 生成回复
返回给用户
更新 ChatMemory

详细流程

  1. 用户输入:用户在网页上点击"喂食"或输入"小N,想我了吗?"
  2. Controller 接收PetController 接收到 HTTP 请求,解析 action 和 message
  3. 构建 Prompt :这是最关键的一步。Spring AI 会把三部分内容拼接起来:
    • System Prompt:告诉 AI "你是谁、什么性格、当前状态如何"
    • User Prompt:用户具体说了什么或做了什么
    • ChatMemory:之前的对话历史(让 AI 有记忆)
  4. 调用 OpenAIChatClient 把拼接好的 Prompt 发给 OpenAI API
  5. AI 生成回复:GPT 模型根据 Prompt 生成拟人化的猫咪回复
  6. 返回结果:把 AI 的回复 + 当前状态(饥饿度/开心度)返回给前端
  7. 更新记忆 :把这次对话加入 ChatMemory,下次就能记住了

2.2 ChatMemory 的原理

ChatMemory 是 Spring AI 提供的对话历史管理组件。它的核心作用是:让 AI 知道之前聊过什么
OpenAI API ChatMemory PetController 用户 OpenAI API ChatMemory PetController 用户 第1次:"小N,你好!" 获取历史(空) \[\] 发送 System Prompt + User Prompt "喵~ 主人好!(蹭手)" 保存对话 User: "小N,你好!" Assistant: "喵~ 主人好!" 显示回复 第2次:"今天过得怎么样?" 获取历史 第1次对话 System + History + User Prompt "喵~ 今天有点无聊,主人终于来陪我玩了!" 追加对话 显示回复

关键点

  • InMemoryChatMemory 把对话存在内存里(Map<conversationId, List<Message>>
  • 每次调用 AI 时,会自动把历史对话拼接在 Prompt 后面
  • 历史对话有长度限制(token 限制),太长会自动截断
  • 不同的 conversationId 完全隔离,就像不同的"记忆盒子"

2.3 状态如何影响 AI 回复?

这是这个项目的灵魂设计 。我们不是让 AI "随便回复",而是把当前状态(饥饿度、开心度)注入到 System Prompt 中,让 AI 的回复"受状态驱动"。
当前状态
构建 System Prompt
饥饿度: 80/100

开心度: 30/100
生成情绪语境
无精打采,说话有气无力
AI 回复
喵呜...(趴在地上,尾巴都没力气摇了)

主人... 我好饿...

原理

  1. 用户发起交互时,先查出当前状态(hunger=80, happiness=30)
  2. 根据状态生成"情绪语境":"饥饿度>80,无精打采;开心度<20,闷闷不乐"
  3. 把情绪语境拼接到 System Prompt 中
  4. AI 生成回复时,会"感知"到自己的状态,从而调整语气和内容

三、详细设计方案

3.1 架构设计

外部服务
数据层
SpringBoot应用层
客户端层
HTTP
更新状态
获取历史
构建 Prompt
调用
AI 回复
返回结果
JSON/HTML
浏览器

Thymeleaf + JS
PetController

REST API 入口
PetService

业务逻辑编排
AI ChatClient

  • System Prompt 构建器
    ChatMemory

对话记忆管理
PetStateRepository

状态存储接口
Redis

持久化存储
InMemoryChatMemory

内存对话历史
OpenAI API

GPT 模型

分层说明

层级 职责 组件
表现层 接收用户请求,返回页面/JSON PetController
业务层 状态管理、动作执行、AI 交互编排 PetService
AI 层 Prompt 构建、AI 调用、记忆管理 ChatClient, ChatMemory, PetPersonalityBuilder
数据层 状态持久化、对话历史存储 PetStateRepository, InMemoryChatMemory
外部层 大模型能力提供 OpenAI API

3.2 核心模块设计

3.2.1 实体层(Domain)
java 复制代码
// 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)
java 复制代码
// 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 人格构造器
java 复制代码
// 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 数据流设计

ChatMemory OpenAI API AI ChatClient PetStateRepository PetService PetController 前端页面 用户 ChatMemory OpenAI API AI ChatClient PetStateRepository PetService PetController 前端页面 用户 假设过了2小时 hunger: 80→84 happiness: 50→48 hunger: 84→69 happiness: 48→53 注入当前状态: 饥饿度69,开心度53 心情:NORMAL 点击"喂食"按钮 1 POST /api/interact {action:"feed", conversationId:"user-123"} 2 interact("user-123", "feed", null) 3 findById("user-123") 4 PetState(hunger=80, happiness=50) 5 decayOverTime() 应用时间衰减 6 feed() 执行喂食动作 7 save(state) 8 保存成功 9 buildSystemPrompt(state) 构建 AI 人格提示 10 获取历史对话 11 之前的对话记录 12 prompt() .system(systemPrompt) .user("主人给你喂了美味的猫粮...") .call() 13 发送完整 Prompt (System + History + User) 14 "喵呜~ 今天的罐头特别香!(蹭手)" 15 AI 回复内容 16 保存新对话到记忆 17 PetInteractionResult (reply, state) 18 JSON 响应 {reply:"喵呜~...", hunger:69, happiness:53} 19 更新进度条 显示 AI 回复气泡 播放吃东西动画 20 展示结果 21

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 请求示例

json 复制代码
{
  "action": "feed",
  "message": null,
  "conversationId": "user-123-abc"
}

/api/interact 响应示例

json 复制代码
{
  "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:芜湖!是罐罐吗!🎉       │   │
│  └────────────────────────────────┘   │
├────────────────────────────────────────┤
│  🥫 [喂食]  🎾 [玩耍]  💬 [聊天]      │  ← 操作区
│                                        │
│  ┌────────────────┐ ┌──────┐          │
│  │ 输入消息...      │ │ 发送 │          │  ← 输入区
│  └────────────────┘ └──────┘          │
└────────────────────────────────────────┘

相关推荐
Shockang8 小时前
AI 设计工作流全景拆解:Figma MCP / Claude Design / Codex / Google Stitch
人工智能
To_OC9 小时前
数据集划分不是随便切:手把手切分大众点评情感数据集
人工智能·llm·agent
冬奇Lab10 小时前
每日一个开源项目(第142篇):android/skills - Google 官方 Android 开发 AI Skill 库
人工智能·开源·资讯
冬奇Lab10 小时前
Skill 系列(06):Skill 工程化与治理——路由准确率 38%、压缩节省 76%
人工智能·开源·agent
IT_陈寒12 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
新新技术迷12 小时前
Node给AI接口做SSE代理与鉴权
人工智能
人活一口气12 小时前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc
redreamSo13 小时前
大模型是不是到顶了?瓶颈到底在哪
人工智能·openai
Oo92013 小时前
Tool Use 背后的技术逻辑
人工智能