Files
gc-plan/week9/教案.md
2026-04-30 16:08:39 +08:00

627 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Week 9AI & 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 1AI/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 2Prompt 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 3LLM 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`
- Bodyraw 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 4Spring AI 框架入门
### Spring AI 是什么?
Spring AI 是 Spring 生态的 AI 集成框架。它做了一件和 Spring MVC / Spring Data 类似的事:**提供统一抽象,屏蔽底层差异**。
```
你的代码
↓ 调用
ChatClientSpring AI 统一 APIFluent 风格)
↓ 委托
OpenAiChatModelOpenAI 协议实现)
↓ HTTP
任何 OpenAI 兼容的 APIOpenAI / 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 6Streaming 流式响应 / SSE
### 为什么需要流式?
非流式:等 AI 完整生成回复 → 一次性返回 → 用户等着
流式AI 生成一个 Token → 立即返回一个 Token → 用户看到打字机效果
对于长回复,流式可以把"首次响应时间"从 30 秒降到 0.5 秒。
### SSEServer-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<String> —— 每个元素是一个 Token
public Flux<String> chatStream(String message, String conversationId) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
.stream() // ← 换成 .stream()
.content(); // 返回 Flux<String> 而不是 String
}
```
**对比**
| | 同步 | 流式 |
|------|------|------|
| 方法 | `.call().content()` | `.stream().content()` |
| 返回类型 | `String` | `Flux<String>` |
| 响应时机 | 全部生成完才返回 | 生成一个 token 返回一个 |
| 用户体验 | 等待 | 打字机效果 |
### Controller 中的 SSE 端点
```java
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatStream(@Valid @RequestBody ChatRequest request) {
return chatService.chatStream(...)
.map(token -> ServerSentEvent.<String>builder().data(token).build())
.concatWith(Mono.just(ServerSentEvent.<String>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"。