# Week 9:AI & LLM 基础 —— 从概念到聊天机器人 > **学习周期**:7 天 > **每日用时**:2-3 小时 > **最终产出**:一个支持流式输出和多轮对话的 AI 聊天机器人 API + 前端 --- ## 前置准备 | 事项 | 说明 | |------|------| | 注册 DeepSeek | [platform.deepseek.com](https://platform.deepseek.com) → API Keys → 创建(新用户送免费额度) | | 备选:阿里云百炼 | [bailian.console.aliyun.com](https://bailian.console.aliyun.com) → 开通 Qwen 模型 | | 安装 Postman | API 调试用(Day 3) | | IDEA 打开项目 | File → Open → 选择 `week9/pom.xml` | > **关于 API Key 的费用**:DeepSeek 注册送免费额度(约 10 元),Qwen 有免费额度。本周末用量极少,费用 < 1 元。如果你已经有 OpenAI / DeepSeek / Qwen 的 Key,直接复用即可。 --- ## 启动方式 ```bash # 1. 修改 application.yml 中的 api-key # 2. IDEA 中运行 Week9Application.main() # 3. 浏览器访问 http://localhost:8080/chat.html ``` --- ## Day 1:AI/ML 基本概念、LLM 是什么 ### 核心概念 | 术语 | 白话解释 | |------|---------| | **AI**(人工智能) | 让机器模拟人类智能的广义概念 | | **ML**(机器学习) | 让机器从数据中学习规律,而不是人工写规则 | | **DL**(深度学习) | 用多层神经网络学习,是 ML 的一个子集 | | **LLM**(大语言模型) | 用海量文本数据训练的超大规模神经网络,专门理解和生成语言 | | **NLP**(自然语言处理) | 让机器理解和使用人类语言的技术领域 | ### LLM 是怎么工作的?(简化理解) ``` 输入文字 → Tokenizer 切分为 Token → Transformer 模型逐 token 预测 → 输出文字 "你好,世界" → [你, 好, ,, 世界] → 预测下一个 Token → "!" ``` 关键概念: 1. **Token**:模型处理的最小文本单位。中文约 1-2 个汉字 = 1 个 Token,英文约 1 个单词 = 1-2 个 Token 2. **Transformer**:2017 年提出的神经网络架构,几乎所有现代 LLM 都基于它 3. **自回归生成**:逐 token 预测下一个 token,每次预测都基于之前已生成的所有 token 4. **训练 vs 推理**:训练 = 用海量数据调整模型参数(耗时数月、烧钱数千万);推理 = 用训练好的模型回答问题(秒级、按 Token 计费) ### 主流模型对比(2026 年) | 模型 | 开发公司 | 特点 | 免费额度 | |------|---------|------|---------| | GPT-4o / GPT-4o-mini | OpenAI | 综合能力强,多模态 | 少量 | | Claude 3.5 / Claude 4 | Anthropic | 安全、长上下文 | 少量 | | DeepSeek-V3 / DeepSeek-R1 | 深度求索 | 性价比高,中文优秀 | 注册送 10 元 | | Qwen3.5(通义千问) | 阿里云 | 中文理解好,生态丰富 | 百万 Token 免费 | | GLM-4(智谱清言) | 智谱 AI | 国产自主,多模态 | 注册送额度 | | Llama 4 | Meta | 开源,可本地部署 | 完全免费 | ### 动手 → 理解 1. 打开 [DeepSeek 网页版](https://chat.deepseek.com) 或 ChatGPT,问 5 个完全不同领域的问题,观察回答风格差异 2. 打开 [OpenAI Tokenizer](https://platform.openai.com/tokenizer),输入中英文混合文本,观察 Token 是如何切分的 3. 对比至少 2 个模型的免费版,用同一个问题提问,记录差异 ### 思考题 > 为什么同样的问题,不同模型给出的答案差异很大?这和"用了哪些数据训练"有什么关系? --- ## Day 2:Prompt Engineering 基础 ### 什么是 Prompt? Prompt 是你给模型的指令。好的 Prompt 和差的 Prompt,回答质量天差地别。 ### Prompt 五大要素 | 要素 | 说明 | 示例 | |------|------|------| | **角色(Role)** | 让 AI 扮演谁 | "你是一个资深的 Java 架构师" | | **任务(Task)** | 要做什么 | "请审查以下代码的安全漏洞" | | **格式(Format)** | 输出什么格式 | "请用 JSON 格式返回" | | **约束(Constraint)** | 有什么限制 | "答案不超过 200 字" | | **示例(Few-shot)** | 给一两个例子 | "示例输入: xxx → 示例输出: yyy" | ### Prompt 技巧速查 | 技巧 | 说明 | 适用场景 | |------|------|---------| | **Zero-shot** | 不给示例,直接提问 | 简单任务 | | **Few-shot** | 给 2-3 个示例 | 格式要求严格 | | **Chain of Thought** | 加一句"让我们一步步思考" | 推理/数学题 | | **角色扮演** | 设定专业角色 | 专业领域问答 | | **思维树** | 探索多个分支再综合 | 复杂决策 | | **反问澄清** | "如果不确定,请先提问澄清" | 需求不明确时 | ### 好 Prompt vs 差 Prompt ``` ❌ 差 Prompt: "写点代码" → AI 不知道你要什么语言、什么功能、什么风格,输出完全随机 ✅ 好 Prompt: "你是一个 Java 后端开发者。请用 Spring Boot 3.x 写一个 RESTful 接口, 实现用户注册功能。要求: (1)包含参数校验 (2)密码用 BCrypt 加密 (3)返回 JWT Token。 请给出完整的 Controller、Service、Config 代码。" → AI 明确知道要什么,输出精准可用 ``` ### 动手 → 理解 1. 用"解释 Spring Boot 的自动配置"这个任务,分别写一个差 Prompt 和好 Prompt,对比回答质量 2. 设计一个角色扮演 Prompt:让 AI 扮演"Java 面试官",向你提问 10 个 Spring 面试题 3. 用 Few-shot 让 AI 按照固定的 JSON 格式回答(给 2 个示例) 4. 尝试 Chain of Thought:在 Prompt 末尾加"请逐步思考后给出答案",观察推理过程的变化 5. 故意写一个模糊 Prompt(如"写点代码"),然后逐步添加约束,观察回答如何变化 ### 思考题 > 如果用户在前端输入"写一个学生管理系统",我们如何通过后端的 System Prompt 自动给这个请求加上角色设定和技术约束?这在 Day 4-5 的代码中会体现。 --- ## Day 3:LLM API 调用方式 ### Chat Completions API 结构 几乎所有大模型都兼容 OpenAI 的 API 格式。一次调用的 HTTP 请求长这样: ``` POST https://api.deepseek.com/v1/chat/completions Authorization: Bearer sk-xxxxxxxx Content-Type: application/json { "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一个有用的助手"}, {"role": "user", "content": "你好,1+1等于几?"} ], "temperature": 0.7, "max_tokens": 1000 } ``` ### 关键参数说明 | 参数 | 说明 | 典型值 | |------|------|--------| | `model` | 模型名称 | `deepseek-chat`, `gpt-4o-mini`, `qwen-turbo` | | `messages` | 对话消息数组 | system / user / assistant 三种角色 | | `temperature` | 随机性(0=确定, 1=创意) | 代码生成 0.1,聊天 0.7,写作 0.9 | | `max_tokens` | 最大输出 Token 数 | 视场景而定 | | `stream` | 是否流式输出 | `false`(默认), `true`(逐 token 返回) | ### messages 中的三种角色 ``` system: 设定 AI 的行为和角色("你是一个 Java 专家") user: 用户说的话 assistant: AI 之前回复的话(用于多轮对话的上下文) ``` ### 动手 → 理解 1. 注册 DeepSeek 获取 API Key(免费) 2. 用 Postman 发送你的第一个 API 请求: - Method: `POST` - URL: `https://api.deepseek.com/v1/chat/completions` - Headers: `Authorization: Bearer <你的API Key>`, `Content-Type: application/json` - Body(raw JSON): ```json { "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一个 JSON 生成器,所有回答必须是合法的 JSON 格式"}, {"role": "user", "content": "请用 JSON 格式返回你的名称、版本和擅长的编程语言"} ], "temperature": 0.0 } ``` 3. 修改 `temperature` 为 1.5,问同一个问题 3 次,对比回答的差异 4. 在 messages 中手动添加 assistant 回复 + 新的 user 消息,实现"手动多轮对话" 5. 故意传错误的 API Key,观察 401 错误响应的 JSON 结构 ### 常见 API 错误码 | 状态码 | 含义 | 处理方法 | |--------|------|---------| | 200 | 成功 | 从 `choices[0].message.content` 取回复 | | 401 | API Key 无效 | 检查 Key 是否正确/过期 | | 429 | 请求频率超限 | 等几秒重试,或升级套餐 | | 500 | 服务器错误 | 稍后重试 | ### 思考题 > 看 Postman 返回的完整 JSON。`choices[0].message.content` 和 `choices[0].delta.content` 有什么区别?(提示:后者是流式模式用的,引出 Day 6) --- ## Day 4:Spring AI 框架入门 ### Spring AI 是什么? Spring AI 是 Spring 生态的 AI 集成框架。它做了一件和 Spring MVC / Spring Data 类似的事:**提供统一抽象,屏蔽底层差异**。 ``` 你的代码 ↓ 调用 ChatClient(Spring AI 统一 API,Fluent 风格) ↓ 委托 OpenAiChatModel(OpenAI 协议实现) ↓ HTTP 任何 OpenAI 兼容的 API(OpenAI / DeepSeek / Qwen / GLM / Ollama …) ``` ### 和传统 Spring 模块的类比 | 传统 Spring | Spring AI | 说明 | |-------------|-----------|------| | `JdbcTemplate` | `ChatClient` | 高级封装,简化调用 | | `DataSource` | `ChatModel` | 底层连接,由配置自动创建 | | `TransactionManager` | `Advisor` | 拦截器,横切关注点 | | Redis / JPA Repository | `ChatMemory` | 数据持久化抽象 | ### 两个核心接口 | 接口 | 层级 | 用法 | |------|------|------| | `ChatModel` | 底层 | `.call(prompt)` — 发送 Prompt,返回完整 ChatResponse | | `ChatClient` | 高层 | `.prompt().user(msg).call().content()` — Fluent API | **推荐使用 ChatClient**。它是线程安全的,整个应用共用一个实例。ChatModel 由 `spring.ai.openai.*` 配置自动创建(OpenAiChatModel),我们不需要手动创建它。 ### 项目依赖的三个关键部分 1. **BOM**:`spring-ai-bom:1.0.6` 统一管理所有 AI 依赖的版本 2. **Starter**:`spring-ai-starter-model-openai` 自动配置 ChatModel 3. **WebFlux**:`spring-boot-starter-webflux` 提供 `Flux` 类型支持 SSE 流式(Day 6) ### 动手 → 理解 1. 用 IDEA 打开 `week9/pom.xml`,观察 BOM 和依赖的结构 2. 打开 `application.yml`: - 把 `api-key` 改成你的实际 Key - 如果用的不是 DeepSeek,修改 `base-url` 和 `model` 3. 打开 `AIConfig.java`,逐行理解三个 Bean 的创建: - `ChatMemory`:对话记忆的存储(内存实现,重启丢失) - `ChatClient`:包装 ChatModel,设置 System Prompt 和 Advisor 4. 运行 `Week9Application.main()`,观察启动日志: - `OpenAiApi` 初始化 → 确认 base-url 正确 - `ChatClient` 创建 → 确认 Bean 注入成功 5. 在 `Week9Application` 中临时添加一个 `CommandLineRunner` 测试: ```java @Bean CommandLineRunner test(ChatClient chatClient) { return args -> { String reply = chatClient.prompt().user("用一句话介绍 Spring AI").call().content(); System.out.println("AI: " + reply); }; } ``` 跑通后删掉,这只是验证配置是否正确。 ### 核心文件 | 文件 | 作用 | |------|------| | `pom.xml` | Maven 依赖,BOM 管理 | | `application.yml` | AI 服务商配置 | | `AIConfig.java` | 手动装配 ChatClient + ChatMemory | | `ApiResponse.java` | 统一响应格式(复用 Week 8 模式) | | `GlobalExceptionHandler.java` | 异常拦截 | ### 思考题 > 为什么要用 `ChatClient` 包装 `ChatModel`,而不是直接调用 `ChatModel.call()`?提示:Advisor 机制(拦截器链)让你想到了 Spring 的什么特性? --- ## Day 5:对话接口实现 / Chat Completion ### 最简单的 AI 调用 ```java // 在 Service 中 String reply = chatClient.prompt() .user("你好,1+1等于几?") .call() // 发送请求,等待完整回复 .content(); // 提取文本内容 ``` 这就是整个 Day 5 的核心。对比 Day 3 用 Postman 发请求的复杂度,Spring AI 把它简化到了一行链式调用。 ### ChatClient 的 Fluent API 流程 ``` chatClient .prompt() → 开始构建请求 .system("你是一个 xxx") → 覆盖默认的 System Prompt(可选) .user(message) → 设置用户消息 .advisors(a -> a.param(...)) → 传递 Advisor 参数(Day 7 用) .call() → 发送同步请求 .content() → 获取文本回复 .chatResponse() → 获取完整响应(含 Token 用量等元数据) .entity(MyClass.class) → 结构化输出(让 AI 返回 JSON → 自动解析为对象) ``` ### System Prompt 的作用 打开 `AIConfig.java`,看 `.defaultSystem(...)`: ```java .defaultSystem(""" 你是一个友好的 AI 助手,名字叫"小智"。 请用中文回答用户的问题。 如果你不知道答案,诚实地告诉用户,不要编造信息。 """) ``` `defaultSystem` 对整个 ChatClient 实例生效(全局默认)。如果想在单次请求中覆盖,可以用 `.system(...)`。 ### 动手 → 理解 1. 阅读 `ChatService.java` 的 `chat()` 方法(不到 5 行代码) 2. 阅读 `ChatController.java` 的 `POST /api/chat` 端点 3. 阅读 `ChatRequest.java` DTO — 只有 message + conversationId 两个字段 4. 启动项目,浏览器访问 `http://localhost:8080/chat.html`: - 输入"你好,介绍一下你自己"→ 观察 System Prompt 是否生效 - 输入"用 Java 写一个 Hello World"→ 观察回答 - 关掉"流式输出"复选框,对比流式和非流式的体验 5. 修改 `AIConfig.java` 的 System Prompt: - 把角色改成"你是一个只会用文言文回答的助手" - 重启,验证效果 ### 核心文件 | 文件 | 新增内容 | |------|---------| | `ChatRequest.java` | 入参 DTO | | `ChatService.java` | `chat(String message)` 方法 | | `ChatController.java` | `POST /api/chat` | | `static/chat.html` | 聊天界面骨架 | | `static/css/chat.css` | 聊天气泡样式 | | `static/js/chat.js` | `sendSyncMessage()` 同步请求 | ### 思考题 > 如果用户输入空字符串,`@NotBlank` 能拦截吗?如果用户输入" "(只有空格),会怎样?动手测试验证。 --- ## Day 6:Streaming 流式响应 / SSE ### 为什么需要流式? 非流式:等 AI 完整生成回复 → 一次性返回 → 用户等着 流式:AI 生成一个 Token → 立即返回一个 Token → 用户看到打字机效果 对于长回复,流式可以把"首次响应时间"从 30 秒降到 0.5 秒。 ### SSE(Server-Sent Events)协议 SSE 是 HTTP 标准的一部分,专门用于服务器向客户端推送事件流。 ``` HTTP Response Headers: Content-Type: text/event-stream HTTP Response Body: data: 你 data: 好 data: ! data: 我 data: 是 ... event: done data: [DONE] ``` ### Spring AI 的流式 API ```java // 返回 Flux —— 每个元素是一个 Token public Flux chatStream(String message, String conversationId) { return chatClient.prompt() .user(message) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .stream() // ← 换成 .stream() .content(); // 返回 Flux 而不是 String } ``` **对比**: | | 同步 | 流式 | |------|------|------| | 方法 | `.call().content()` | `.stream().content()` | | 返回类型 | `String` | `Flux` | | 响应时机 | 全部生成完才返回 | 生成一个 token 返回一个 | | 用户体验 | 等待 | 打字机效果 | ### Controller 中的 SSE 端点 ```java @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux> chatStream(@Valid @RequestBody ChatRequest request) { return chatService.chatStream(...) .map(token -> ServerSentEvent.builder().data(token).build()) .concatWith(Mono.just(ServerSentEvent.builder() .event("done").data("[DONE]").build())); } ``` 要点: - `produces = TEXT_EVENT_STREAM_VALUE` 告诉浏览器这是 SSE 流 - `ServerSentEvent` 封装 SSE 格式 - `[DONE]` 信号通知前端流结束 ### 前端 ReadableStream 解析 ```javascript const response = await fetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, conversationId }) }); const reader = response.body.getReader(); // 获取流读取器 const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; // 解析 SSE 格式: "data: xxx\n" // 逐 token 追加到气泡中 } ``` ### 动手 → 理解 1. 阅读 `ChatService.java` 的 `chatStream()` 方法 2. 阅读 `ChatController.java` 的 `POST /api/chat/stream` 端点 3. 阅读 `chat.js` 的 `sendStreamMessage()` 函数 4. 在浏览器中勾选"流式输出",发送消息,观察打字机效果 5. 打开 F12 → Network → 找到 `/api/chat/stream` 请求 → 点击 → EventStream 标签,观察每个 SSE 事件 6. 把浏览器的网速调慢(F12 → Network → Throttling → Slow 3G),观察流式输出的差异更明显 ### 核心文件 | 文件 | 变更 | |------|------| | `ChatService.java` | 新增 `chatStream()` 方法 | | `ChatController.java` | 新增 `POST /api/chat/stream` | | `chat.js` | 新增 `sendStreamMessage()` + `ReadableStream` | ### 思考题 > 如果流式响应中途网络断了(比如 Wi-Fi 被关掉),前端应该怎么处理?动手修改 `chat.js`,在 catch 块中显示"连接中断,请重试"。另外,`EventSource` API 和 `fetch + ReadableStream` 有什么区别?为什么聊天应用推荐用 fetch? --- ## Day 7:上下文管理与多轮对话 ### 核心问题:AI 是无状态的 ``` 第 1 轮: 用户: "我叫小明" AI: "好的小明!" 第 2 轮: 用户: "我刚才说我叫什么?" AI: "???我不知道" ``` 每次 API 请求是独立的。AI 不记得上一轮说了什么,除非你**把历史消息重新传给它**。 ### Spring AI 的 ChatMemory 机制 ``` ┌──────────────────────────────────────────┐ │ 用户: "我叫小明" │ │ ↓ │ │ MessageChatMemoryAdvisor │ │ ├─ 从 ChatMemory 读历史(此时为空) │ │ ├─ 把历史注入到 Prompt 的 messages 中 │ │ ├─ 调用 ChatModel │ │ └─ 把本轮对话写入 ChatMemory │ │ ↓ │ │ AI: "好的小明!" │ │ │ │ 用户: "我刚才说我叫什么?" │ │ ↓ │ │ MessageChatMemoryAdvisor │ │ ├─ 从 ChatMemory 读历史: │ │ │ user: "我叫小明" │ │ │ assistant: "好的小明!" │ │ ├─ 注入到 Prompt → AI 知道上下文 │ │ └─ 返回: "你叫小明!" │ └──────────────────────────────────────────┘ ``` ### conversationId —— 区分不同会话的关键 ```java chatClient.prompt() .user(message) .advisors(a -> a.param("chat_memory_conversation_id", conversationId)) .call() .content(); ``` 不同的 conversationId → 独立的记忆空间。这就是多用户 / 多会话隔离的基础。 ### ChatMemory 实现对比 | 实现 | 存储位置 | 重启丢失 | 适用场景 | |------|---------|---------|---------| | `InMemoryChatMemoryRepository` | JVM 堆内存 | 是 | 开发/测试(本周用这个) | | `MessageWindowChatMemory` | 滑动窗口策略 | - | 包装 Repository,只保留最近 N 条 | | `JdbcChatMemoryRepository` | MySQL/PostgreSQL | 否 | 生产环境 | > Spring AI 1.0.6 中,ChatMemory 被拆分为两个角色:**策略层**(MessageWindowChatMemory,滑动窗口)和 **存储层**(ChatMemoryRepository,持久化)。 ### 动手 → 理解 1. 阅读 `ChatService.java`: - `chat(String message, String conversationId)` — 多轮对话版 - `getHistory(String conversationId)` — 查看某会话的所有历史消息 - `clearHistory(String conversationId)` — 清除某会话 2. 阅读 `ChatController.java` 的 `GET /api/chat/history` 和 `DELETE /api/chat/history` 3. 阅读 `ChatHistoryVo.java` DTO 4. **完整测试多轮对话**: - 在浏览器中发送:"我叫小明,我是一名 Java 后端开发者,有 3 年经验" - 再发送:"我刚才说我叫什么名字?我做什么工作?几年经验?" - 确认 AI 能正确回忆上下文 5. 点击"+ 新建对话",问同样的问题"我叫什么?"——确认 AI "忘记"了(新对话 = 空白记忆) 6. 在侧边栏切换回之前的对话,再问一次——确认旧对话的记忆还在 7. 点击"清除历史"——确认记忆被清空 8. 访问 `GET http://localhost:8080/api/chat/history?conversationId=default` 查看记忆中的数据结构 ### 核心文件 | 文件 | 变更 | |------|------| | `ChatService.java` | 所有方法增加 conversationId;新增 `getHistory()`、`clearHistory()` | | `ChatController.java` | 新增 GET/DELETE `/history` 端点 | | `ChatHistoryVo.java` | 新增 | | `chat.html` | 新增侧边栏 + 新建/切换/删除对话 | | `chat.js` | 新增会话管理函数 | ### 思考题 > `InMemoryChatMemoryRepository` 在应用重启后会丢失所有记忆。如果要持久化到数据库,需要改哪些代码?提示:只需要把 `AIConfig.java` 中的 `InMemoryChatMemoryRepository` 换成 `JdbcChatMemoryRepository`,其他代码完全不需要改 —— 这就是面向接口编程的威力。 --- ## Week 9 总结 ### 技术栈全景 ``` 浏览器 (chat.html) │ fetch + SSE ▼ ChatController (/api/chat, /api/chat/stream, /api/chat/history) │ ▼ ChatService (chat, chatStream, getHistory, clearHistory) │ ▼ ChatClient (Spring AI Fluent API) ├─ ChatModel (OpenAiChatModel → HTTP → AI Service) └─ MessageChatMemoryAdvisor → ChatMemory (InMemory) ``` ### 能力清单 | 维度 | 掌握内容 | |------|---------| | LLM 概念 | Token、Transformer、自回归生成、主流模型对比 | | Prompt 设计 | 五大要素、Few-shot、CoT、角色扮演 | | API 调用 | REST API 格式、messages 结构、Temperature | | Spring AI | ChatClient、ChatModel、BOM、手动装配 | | 同步对话 | POST /api/chat、call().content() | | 流式对话 | SSE、Flux、ReadableStream、打字机效果 | | 多轮对话 | ChatMemory、conversationId、MessageChatMemoryAdvisor | ### vs Week 5 (Spring Security) | | Week 5 | Week 9 | |------|--------|--------| | 核心框架 | Spring Security + JWT | Spring AI | | 数据方向 | 客户端 → 服务器 → DB | 客户端 → 服务器 → AI API | | 状态管理 | JWT Token(客户端持有) | ChatMemory(服务端持有) | | 响应模式 | 同步 JSON | 同步 JSON + SSE 流 | | 拦截器 | SecurityFilterChain | Advisor Chain | ### 常见问题 | 问题 | 原因 | 解决 | |------|------|------| | 启动报 "API key not valid" | api-key 没改 | 在 `application.yml` 中填入真实 API Key | | 返回 "Connection refused" | base-url 不对 | 检查 `base-url` 是否与 provider 匹配 | | 返回 401 | API Key 无效或过期 | 登录平台重新生成 Key | | 返回 429 | 调用频率超限 | 免费版通常有 RPM 限制,等几秒再试 | | 流式没效果 | 前端没开流式开关 | 勾选"流式输出"复选框 | | 多轮对话不记得上下文 | 没传 conversationId | 检查请求体中是否包含 `conversationId` | | 中文显示乱码 | 编码问题 | 确认 `application.yml` 中有 `characterEncoding=utf-8`(虽本项目不连 MySQL) | | `mvn compile` 报找不到 Spring AI | BOM 未生效 | 确认 `dependencyManagement` 配置正确,`mvn clean compile` 重试 | | 前端页面 404 | 文件路径不对 | 确认 `chat.html` 在 `src/main/resources/static/` 下 | --- > **本周核心**:你第一次把 AI 集成到了自己的应用中。从最原始的 HTTP 调用,到 Spring AI 封装的一行 `.call().content()`,再到流式 SSE 和 ChatMemory 多轮对话——你掌握了让应用"拥有 AI 能力"的完整链路。这也是整个学习计划转折的一周:之前你学的是"如何写好代码",从今天开始你学的是"如何让代码拥有智能"。 --- > **下一阶段:Week 10 — AI Agent 核心概念**。在聊天机器人基础上,让 AI 能调用工具(Function Calling)、检索知识库(RAG)、持久化记忆(Redis)。从一个"会聊天的 AI"升级到"会干活的 Agent"。