一、概述

在这里插入图片描述

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 搞定:

// 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)
// 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 数据流设计

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

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐