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

25 KiB
Raw Blame History

Week 9AI & LLM 基础 —— 从概念到聊天机器人

学习周期7 天 每日用时2-3 小时 最终产出:一个支持流式输出和多轮对话的 AI 聊天机器人 API + 前端


前置准备

事项 说明
注册 DeepSeek platform.deepseek.com → API Keys → 创建(新用户送免费额度)
备选:阿里云百炼 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直接复用即可。


启动方式

# 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. Transformer2017 年提出的神经网络架构,几乎所有现代 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 网页版 或 ChatGPT问 5 个完全不同领域的问题,观察回答风格差异
  2. 打开 OpenAI 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:
      {
          "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.contentchoices[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. BOMspring-ai-bom:1.0.6 统一管理所有 AI 依赖的版本
  2. Starterspring-ai-starter-model-openai 自动配置 ChatModel
  3. WebFluxspring-boot-starter-webflux 提供 Flux 类型支持 SSE 流式Day 6

动手 → 理解

  1. 用 IDEA 打开 week9/pom.xml,观察 BOM 和依赖的结构
  2. 打开 application.yml
    • api-key 改成你的实际 Key
    • 如果用的不是 DeepSeek修改 base-urlmodel
  3. 打开 AIConfig.java,逐行理解三个 Bean 的创建:
    • ChatMemory:对话记忆的存储(内存实现,重启丢失)
    • ChatClient:包装 ChatModel设置 System Prompt 和 Advisor
  4. 运行 Week9Application.main(),观察启动日志:
    • OpenAiApi 初始化 → 确认 base-url 正确
    • ChatClient 创建 → 确认 Bean 注入成功
  5. Week9Application 中临时添加一个 CommandLineRunner 测试:
    @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 调用

// 在 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(...)

.defaultSystem("""
        你是一个友好的 AI 助手,名字叫"小智"。
        请用中文回答用户的问题。
        如果你不知道答案,诚实地告诉用户,不要编造信息。
        """)

defaultSystem 对整个 ChatClient 实例生效(全局默认)。如果想在单次请求中覆盖,可以用 .system(...)

动手 → 理解

  1. 阅读 ChatService.javachat() 方法(不到 5 行代码)
  2. 阅读 ChatController.javaPOST /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

// 返回 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 端点

@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 解析

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.javachatStream() 方法
  2. 阅读 ChatController.javaPOST /api/chat/stream 端点
  3. 阅读 chat.jssendStreamMessage() 函数
  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 —— 区分不同会话的关键

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.javaGET /api/chat/historyDELETE /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.htmlsrc/main/resources/static/

本周核心:你第一次把 AI 集成到了自己的应用中。从最原始的 HTTP 调用,到 Spring AI 封装的一行 .call().content(),再到流式 SSE 和 ChatMemory 多轮对话——你掌握了让应用"拥有 AI 能力"的完整链路。这也是整个学习计划转折的一周:之前你学的是"如何写好代码",从今天开始你学的是"如何让代码拥有智能"。


下一阶段Week 10 — AI Agent 核心概念。在聊天机器人基础上,让 AI 能调用工具Function Calling、检索知识库RAG、持久化记忆Redis。从一个"会聊天的 AI"升级到"会干活的 Agent"。