🤖

第4课:AI 金句工坊

接入 AI API,完成最终全栈应用

2 课时全栈需API Key

← → 键切换 · 底部导航跳转

1 / 28

🎯 这节课做什么

🤖 输入主题 → AI 实时生成金句 → 精美展示
1
回顾前三课 — 看看我们要把哪些东西合起来
2
app.py 升级 — Flask + OpenAI + 环境变量
3
index.html 升级 — POST + 输入框 + 快捷标签
4
配置并启动 — 设置 API Key,跑通全链路
5
自由创作 — 改 Prompt、换风格、加功能
2 / 28

📖 回顾:前三课的知识

🃏

第1课

HTML + CSS 卡片
JS 数组/事件/DOM

☁️

第2课

API 概念
fetch + JSON + async

🐍

第3课

Flask 后端
路由 + JSON 返回

今天全部合在一起,做成一个真正的 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

🏗️ 整体架构

🌐 浏览器
index.html
POST /api/quote
{topic:"坚持"}
🐍 Flask
app.py:5000
调 DeepSeek API
🤖 AI
deepseek-chat
← JSON ←
🌐 浏览器
显示金句
🌐

前端

POST 发送主题

🐍

后端

转发给 AI API

🤖

AI

生成金句返回

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
6send_from_directory⚠️ 新导入! Flask 用来发送静态文件(index.html)
8import os⚠️ 新! 读取环境变量(API Key)
9import json⚠️ 新! json.loads() 解析 AI 返回的数据
10from openai import OpenAI⚠️ 新! OpenAI 官方 SDK,调用 AI 模型
11from 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"
)
13load_dotenv()⚠️ 新! 读取 .env 文件里的配置
15static_folder='.'⚠️ 新! 静态文件目录设为当前目录
这样 / 就能返回 index.html
19os.getenv("MOCK", "true")⚠️ 新! 读环境变量
如果没设置 MOCK,默认 "true"
22-25client = 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-37MOCK_QUOTES = [ ... ]8条内置金句(没有 tag 字段了)
当没有 API Key 时使用
41-43send_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
46methods=['POST']⚠️ 新! 第3课用 GET(URL 传参)
这节课用 POST(JSON 传参)
POST 适合传大量/敏感数据
48request.get_json()⚠️ 从请求体(body)读取 JSON
前端 fetch 时 body: JSON.stringify({topic})
49data.get('topic', '').strip()取 topic 字段,默认空字符串
.strip() 去掉首尾空格
51-52if 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 调用详解

61client.chat.completions.create()⚠️ 调用 AI 对话模型
传入参数,AI 返回回复
62model="deepseek-chat"指定模型名字
也可以换成 gpt-4 / claude 等
64{"role": "system", "content": "..."}⚠️ system prompt
告诉 AI 它的角色和任务
"你是金句生成器..."
65{"role": "user", "content": f"主题:{topic}"}用户输入(学生输入的主题)
67temperature=0.8⚠️ 创造力参数
0=严谨 · 1=天马行空
0.8 适合创意生成
68max_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>
136type="text"文本输入框
137placeholder灰色提示文字(用户输入后消失)
138maxlength="30"限制最多30个字
139onkeydown⚠️ 新! HTML 直接绑定键盘事件
按 Enter 键 → 执行 generate()
140onclick="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-196setTopic(topic)快捷标签调用的函数
先设置输入框文字 → 再调用 generate()
198-202showError(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
5
输入主题 — 点快捷标签或自己输入
6
生成金句 — 看到 AI 生成的结果 🎉
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 屏显示金句

🎙️

加语音

调用 TTS API 让金句读出来

27 / 28
🏆

恭喜完成全部课程!

🃏 第1a课 卡片的骨架 — HTML + CSS 基础

💻 第1b课 让卡片动起来 — JS 交互入门

☁️ 第2课 云上金句 — 调用公网 API

🐍 第3课 自己的 API — Flask 后端

🤖 第4课 AI 金句工坊 — 全栈 AI 应用

🎊 你从一个静态 HTML 页面做到了 AI 全栈应用!

学会调用 API → 你就打开了互联网无限可能性的大门 🚪✨

28 / 28