🤖
第4课:AI 金句工坊
接入 AI API,完成最终全栈应用
2 课时全栈需API Key
← → 键切换 · 底部导航跳转
1 / 28
🎯 这节课做什么
🤖 输入主题 → AI 实时生成金句 → 精美展示
2
app.py 升级 — Flask + OpenAI + 环境变量
3
index.html 升级 — POST + 输入框 + 快捷标签
4
配置并启动 — 设置 API Key,跑通全链路
2 / 28
📖 回顾:前三课的知识
🃏
第1课
HTML + CSS 卡片
JS 数组/事件/DOM
☁️
第2课
API 概念
fetch + JSON + async
今天全部合在一起,做成一个真正的 AI 产品!
3 / 28
📁 两个文件
🐍
app.py(90行)
后端:Flask + OpenAI API
模拟模式/真实模式
🌐
index.html(205行)
前端:漂亮卡片 + 输入框
快捷标签 + POST fetch
💡
和前几课的区别
第3课:app.py 只返回 JSON,前端单独用 test.html
第4课:app.py 同时 serve index.html(一站式)
打开 http://localhost:5000 就能看到完整页面!
4 / 28
🏗️ 整体架构
POST /api/quote
{topic:"坚持"}
调 DeepSeek API
← JSON ←
5 / 28
🐍 app.py ① — 导入(第1-11行)
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
| 6 | send_from_directory | ⚠️ 新导入! Flask 用来发送静态文件(index.html) |
| 8 | import os | ⚠️ 新! 读取环境变量(API Key) |
| 9 | import json | ⚠️ 新! json.loads() 解析 AI 返回的数据 |
| 10 | from openai import OpenAI | ⚠️ 新! OpenAI 官方 SDK,调用 AI 模型 |
| 11 | from dotenv import load_dotenv | ⚠️ 新! 从 .env 文件读取配置 |
💡
这节课要装额外的依赖
pip install openai python-dotenv
加上之前的 flask flask-cors,共4个包
6 / 28
🐍 app.py ② — 配置加载(第13-25行)
load_dotenv()
app = Flask(__name__, static_folder='.')
CORS(app)
# ===== 配置 =====
MOCK_MODE = os.getenv("MOCK", "true").lower() == "true"
API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
client = OpenAI(
api_key=API_KEY,
base_url="https://api.deepseek.com/v1"
)
| 13 | load_dotenv() | ⚠️ 新! 读取 .env 文件里的配置 |
| 15 | static_folder='.' | ⚠️ 新! 静态文件目录设为当前目录 这样 / 就能返回 index.html |
| 19 | os.getenv("MOCK", "true") | ⚠️ 新! 读环境变量 如果没设置 MOCK,默认 "true" |
| 22-25 | client = OpenAI(...) | ⚠️ 新! 创建 AI 客户端 api_key 从 .env 读取 base_url 是 DeepSeek 的 API 地址 |
7 / 28
🔑 .env 文件 — 配置 API Key
DEEPSEEK_API_KEY=sk-你的密钥
MOCK=true
💡
什么是 .env 文件?
.env = environment(环境)
专门存放密钥和配置的文件
不会上传到 Git(在 .gitignore 里)
Python 用 load_dotenv() 读取
代码里用 os.getenv("KEY") 拿到值
🔐
为什么用 .env?
❌ 不好的做法:
直接把 API Key 写在代码里
传到 GitHub 上 → 人人可见 → 被盗用
✅ 好的做法:
密钥放 .env,代码里用变量引用
.env 不上传,密钥安全
8 / 28
💡 MOCK 模式的设计
🎯
MOCK=true(模拟模式)
• 不需要 API Key
• 从内置的 8 条金句随机返回
• 适合课堂先跑通流程
🤖
MOCK=false(真实模式)
• 需要设置 API Key
• 调用 DeepSeek AI 实时生成
• 每个主题都得到不同的金句
💡
.env 设置方法
没 API Key 时:MOCK=true(默认)
有 API Key 时:MOCK=false + 填 DEEPSEEK_API_KEY
代码逻辑:if MOCK_MODE or not API_KEY → 用模拟数据
9 / 28
🐍 app.py ③ — 模拟数据 + 首页(第27-43行)
MOCK_QUOTES = [
{"quote": "不积跬步...", "author": "荀子"},
... 共8条
]
@app.route('/')
def index():
return send_from_directory('.', 'index.html')
| 28-37 | MOCK_QUOTES = [ ... ] | 8条内置金句(没有 tag 字段了) 当没有 API Key 时使用 |
| 41-43 | send_from_directory('.', 'index.html') | ⚠️ 新! 返回 index.html 给浏览器 所以访问 / 就能看到完整页面 |
💡
和第3课的区别
第3课:访问 / 返回一段文字
第4课:访问 / 返回整个 HTML 页面!
因为 static_folder='.' 让 Flask 能读取 index.html
10 / 28
🐍 app.py ④ — POST 接口(第46-52行)
@app.route('/api/quote', methods=['POST'])
def generate_quote():
data = request.get_json() # 拿到前端发来的 JSON
topic = data.get('topic', '').strip() # 取 topic 字段并去掉空格
if not topic:
return jsonify({'error': '请输入一个主题'}), 400
| 46 | methods=['POST'] | ⚠️ 新! 第3课用 GET(URL 传参) 这节课用 POST(JSON 传参) POST 适合传大量/敏感数据 |
| 48 | request.get_json() | ⚠️ 从请求体(body)读取 JSON 前端 fetch 时 body: JSON.stringify({topic}) |
| 49 | data.get('topic', '').strip() | 取 topic 字段,默认空字符串 .strip() 去掉首尾空格 |
| 51-52 | if not topic: return 400 | 没传主题 → 返回 400 错误 |
11 / 28
💡 GET vs POST
📥
GET — 第3课用的
数据在 URL 里:
?tag=坚持
浏览器地址栏直接可见
适合:获取数据、筛选
📤
POST — 这节课用的
数据在请求体里:
{"topic": "坚持"}
URL 里看不见
适合:提交数据、发送内容
💡
前端 POST 写法
fetch('/api/quote', {
method: 'POST', // 指定为 POST
headers: { 'Content-Type': 'application/json' }, // 告诉后端发的是 JSON
body: JSON.stringify({ topic }) // 把 JS 对象转成 JSON 字符串
})
12 / 28
🐍 app.py ⑤ — 模拟模式(第54-57行)
# 模拟模式(无需 API Key)
if MOCK_MODE or not API_KEY:
import random
return jsonify(random.choice(MOCK_QUOTES))
💡
逻辑拆解
条件:MOCK_MODE 为 true 或 API_KEY 为空
→ 使用模拟数据
random.choice(MOCK_QUOTES)
从 8 条预设金句中随机选一条
和第3课一样,但数据量少一些
13 / 28
🐍 app.py ⑥ — 调用 AI API(第59-76行)
# 真实模式(调用 AI API)
try:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "你是一个金句生成器..."},
{"role": "user", "content": f"主题:{topic}"}
],
temperature=0.8,
max_tokens=200
)
content = response.choices[0].message.content.strip()
if content.startswith("```"):
content = content.split("\n", 1)[1].rsplit("\n", 1)[0]
result = json.loads(content)
return jsonify(result)
} except Exception as e:
return jsonify({'error': f'调用 AI 失败:{str(e)}'}), 500
14 / 28
🤖 AI API 调用详解
| 61 | client.chat.completions.create() | ⚠️ 调用 AI 对话模型 传入参数,AI 返回回复 |
| 62 | model="deepseek-chat" | 指定模型名字 也可以换成 gpt-4 / claude 等 |
| 64 | {"role": "system", "content": "..."} | ⚠️ system prompt 告诉 AI 它的角色和任务 "你是金句生成器..." |
| 65 | {"role": "user", "content": f"主题:{topic}"} | 用户输入(学生输入的主题) |
| 67 | temperature=0.8 | ⚠️ 创造力参数 0=严谨 · 1=天马行空 0.8 适合创意生成 |
| 68 | max_tokens=200 | 最多生成200个 token 防止 AI 回复太长 |
💡
第71行:从返回结果中提取文字
response.choices[0].message.content
= AI 回复的文字内容
.strip() 去掉首尾空格
15 / 28
💡 System Prompt 是什么?
{"role": "system", "content": "你是一个金句生成器。根据主题生成一句有哲理的名言金句,并注明作者。作者可以是真实或虚构的。只返回JSON:{\"quote\": \"...\", \"author\": \"...\"}"}
🧠
System Prompt = AI 的"人设"
告诉 AI:
1. 你是谁 → 金句生成器
2. 你要做什么 → 根据主题生成金句
3. 输出格式 → 只返回 JSON
好的 system prompt 决定输出质量!
✏️
学生可以改的地方
改成不同风格:
• "你是一个搞笑版金句生成器"
• "用古诗风格生成金句"
• "你是一个毒鸡汤生成器"
改了 Prompt 就能改变 AI 的输出风格!
16 / 28
🐍 app.py ⑦ — 错误处理 + 启动(第78-90行)
} except Exception as e:
return jsonify({'error': f'调用 AI 失败:{str(e)}'}), 500
if __name__ == '__main__':
mode = "🎯 模拟模式" if (MOCK_MODE or not API_KEY) else "🤖 AI 实时生成模式"
...
app.run(host='0.0.0.0', port=5000, debug=True)
🛡️
第78-79行:异常捕获
如果 AI API 调用失败(网络问题/Key无效)
→ 返回错误信息 + HTTP 500
前端 JS 的 catch 会收到这个错误
▶️
第83行:模式提示
启动时在终端显示当前模式
让学生立刻知道自己用的是模拟还是真实 AI
17 / 28
🌐 index.html CSS 概览(第1-118行)
前端代码 205 行,其中 CSS 118 行,HTML 35 行,JS 50 行
🆕
新增 CSS
.quick-tags — 快捷主题标签(第32-46行)
.input-group — 输入框+按钮水平排列(第48-62行)
.quote-card — 带入场动画的金句卡片(第75-84行)
.status-bar — 底部状态栏(第105-110行)
@media (max-width: 500px) — 手机适配(第112-117行)
💡
重点 CSS 技巧
.quote-card 的入场动画:
opacity: 0 + transform: translateY(20px)
→ .show 时 opacity:1 + translateY(0)
transition: all 0.5s ease → 平滑入场
@media 响应式
屏幕小于 500px 时
输入框变成上下排列
18 / 28
🌐 index.html HTML 详解(第120-152行)
<div class="container">
<div class="logo">✨</div>
<h1>AI 金句工坊</h1>
<p class="subtitle">...</p>
<!-- 快捷标签 -->
<div class="quick-tags">
<span onclick="setTopic('坚持')">💪 坚持</span>
... 共6个
</div>
<!-- 输入框 + 按钮 -->
<div class="input-group">
<input id="topicInput" onkeydown="..." />
<button onclick="generate()">✨ 生成金句</button>
</div>
<!-- 金句展示 -->
<div class="quote-card" id="quoteCard">...</div>
<div class="status-bar">🟢 POST /api/quote → AI → JSON</div>
</div>
🆕
快捷标签(第127-133行)
onclick 直接调用 JS 函数
点击标签 → 自动填入主题并生成
🆕
输入框(第136-139行)
onkeydown 监听键盘
按 Enter 键也触发生成
🆕
状态栏(第151行)
显示当前 API 接口信息
让用户知道数据流向
19 / 28
🌐 HTML — 输入框 + 按钮(第135-141行)
<div class="input-group">
<input type="text" id="topicInput"
placeholder="输入主题..."
maxlength="30"
onkeydown="if(event.key==='Enter') generate()">
<button id="genBtn" onclick="generate()">✨ 生成金句</button>
</div>
| 136 | type="text" | 文本输入框 |
| 137 | placeholder | 灰色提示文字(用户输入后消失) |
| 138 | maxlength="30" | 限制最多30个字 |
| 139 | onkeydown | ⚠️ 新! HTML 直接绑定键盘事件 按 Enter 键 → 执行 generate() |
| 140 | onclick="generate()" | ⚠️ 新! HTML 直接绑定点击事件 和 addEventListener 功能一样 |
💡
onclick vs addEventListener
onclick="generate()" = 写在 HTML 里,简单直接
addEventListener('click', generate) = 写在 JS 里,更灵活
这节课两种都用了,让学生都接触一下
20 / 28
⚡ JS ① — generate() 函数(第155-191行)
async function generate() {
const topic = document.getElementById('topicInput').value.trim();
if (!topic) { showError('请输入一个主题 ✍️'); return; }
// 显示加载状态
card.classList.remove('show');
card.classList.add('loading');
btn.disabled = true;
btn.textContent = '生成中...';
try {
// ★ 调用自己的后端 API(POST) ★
const response = await fetch('/api/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic })
});
const data = await response.json();
if (!response.ok) { showError(data.error); return; }
// 展示结果
document.getElementById('quoteText').textContent = data.quote;
document.getElementById('quoteAuthor').textContent = data.author;
card.classList.remove('loading');
requestAnimationFrame(function() { card.classList.add('show'); });
} catch (err) {
showError('网络错误,请检查后端是否启动');
} finally {
btn.disabled = false;
btn.textContent = '✨ 生成金句';
}
}
21 / 28
⚡ JS ② — 新知识点
🔍
input.value(第156行)
.value 获取输入框的文字
.trim() 去掉首尾空格
如果用户只输入空格 → 也算空
🔄
requestAnimationFrame(第183行)
⚠️ 浏览器优化的动画触发方式
比 setTimeout 更流畅
等当前渲染完成后再加 .show 类
💡
和前几课 JS 的对比
第1b课:数组存金句 → 随机选一条 → 展示
第2课:fetch 公网 API → 展示
第3课:fetch 自己 API(GET)→ 展示
第4课:fetch 自己 API(POST)→ AI 生成 → 展示带动画
22 / 28
⚡ JS ③ — 辅助函数(第193-202行)
function setTopic(topic) {
document.getElementById('topicInput').value = topic;
generate();
}
function showError(msg) {
const el = document.getElementById('errorMsg');
el.textContent = '❌ ' + msg;
el.style.display = 'block';
document.getElementById('quoteCard').classList.remove('loading');
}
| 193-196 | setTopic(topic) | 快捷标签调用的函数 先设置输入框文字 → 再调用 generate() |
| 198-202 | showError(msg) | 显示错误提示 • 设置错误文字 • 把隐藏的 errorMsg 显示出来 • 移除 loading 状态 |
23 / 28
🔑 .env 配置步骤
在 project 目录下新建 .env 文件:
# 方式1:模拟模式(推荐先试)
MOCK=true
# 方式2:真实模式(有 API Key 时)
MOCK=false
DEEPSEEK_API_KEY=sk-你的密钥
📝
获取 DeepSeek API Key
1. 打开 platform.deepseek.com
2. 注册/登录
3. 进入 API Keys 页面
4. 创建新 Key → 复制
💰
费用
DeepSeek 非常便宜
课堂使用几乎免费
生成一条金句大约 0.001 元
24 / 28
▶️ 完整启动流程
1
安装依赖 — pip install flask flask-cors openai python-dotenv
2
创建 .env — 设置 MOCK=true(或填 API Key)
3
启动后端 — python app.py(终端显示运行模式)
4
打开浏览器 — 访问 http://localhost:5000
25 / 28
⚠️ 可能遇到的问题
❌
ModuleNotFoundError
没有 openai 模块
→ pip install openai python-dotenv
❌
401 Authentication Error
API Key 无效
→ 检查 .env 里的 Key 是否正确
❌
Connection refused
后端没启动
→ 确认终端正在运行 python app.py
❌
TypeError: Failed to fetch
跨域或网络问题
→ 检查 CORS(app) 是否在代码里
26 / 28
🚀 自由发挥方向
✏️
改 Prompt 换风格
修该 system prompt 里的描述
变成「搞笑版」「古诗版」「英文版」
🎨
美化界面
改 CSS 颜色、字体、背景
加新动画效果
📋
加历史记录
生成过的金句存到数组里
用一个列表展示出来
🔌
接硬件
ESP32 连 WiFi 调这个 API
用 OLED 屏显示金句
27 / 28
🏆
恭喜完成全部课程!
🃏 第1a课 卡片的骨架 — HTML + CSS 基础
💻 第1b课 让卡片动起来 — JS 交互入门
☁️ 第2课 云上金句 — 调用公网 API
🐍 第3课 自己的 API — Flask 后端
🤖 第4课 AI 金句工坊 — 全栈 AI 应用
🎊 你从一个静态 HTML 页面做到了 AI 全栈应用!
学会调用 API → 你就打开了互联网无限可能性的大门 🚪✨
28 / 28