第01阶段:AI应用基础与 OpenAI 入门
先看懂 AI 应用的工作流程,再完成第一次 API 调用,最后学会写清楚提示词。
进入学习长回答不用等全部生成完,可以像打字一样逐步显示。
本课项目:流式 AI 聊天页面
学习重点:Streaming、异步、用户体验。工具重点:Streaming Responses。
页面里加入 OpenAI、Node.js、Python 和 JSON 图标,帮助学生把 AI 能力、后端调用、脚本实验和结构化输出放在同一条学习路线里理解。
先用阶段卡片看清大方向,再用周课卡片进入具体项目。每节课都保留理论、例子、Node.js、Python、练习和自测,学生可以直接按卡片推进。
先看懂 AI 应用的工作流程,再完成第一次 API 调用,最后学会写清楚提示词。
进入学习AI 不只要会说话,还要按固定格式交答案,这样程序才能稳定处理。
进入学习AI 负责理解语言,函数负责精确计算;两者配合,结果更可靠。
进入学习把图片也交给 AI 看,再让它用清楚的文字或 JSON 结果回答。
进入学习需要最新消息时查网页;需要班级资料时查文件。先找资料,再回答。
进入学习把 AI 能力做成网页:前端收集输入,后端保护密钥,页面展示结果。
进入学习Agent 会按步骤完成任务,但你要给它工具、边界、检查规则和预算意识。
进入学习把前面学过的提示词、JSON、函数、检索、网页和安全设计合在一个作品里。
进入学习流式输出就像水流。模型生成一点,页面就显示一点,用户会觉得应用反应更快。
理论不是背概念,而是帮你判断项目为什么这样设计。下面这些规则会在代码里反复出现。
这一课的项目是“流式 AI 聊天页面”,重点是“Streaming、异步、用户体验”。你可以把它当成一个小实验:先给它一个清楚输入,再观察代码里哪些地方用到了 Streaming Responses。课堂里我们不会一上来就追求复杂功能,而是先把最小版本做出来。最小版本跑通以后,你再改输入、改提示词、改输出格式,变化就会看得很清楚。
这几课把能力做成网页。网页项目会让你看到真实用户体验:按钮、等待、错误提示、结果展示,一个都不能少。
本课有一条很实用的学习线索:先问“用户到底给了什么”,再问“程序希望拿到什么”。比如你可以试这些输入:写一个 300 字故事;解释递归,慢慢显示;生成一份 5 步学习计划。这些输入故意有简单的,也有容易出问题的。正常输入能帮你确认功能;短输入、空输入、奇怪输入能帮你发现系统边界。
写代码时建议你分三轮。第一轮只跑通官方调用,不加自己的想法;第二轮把输入换成自己的例子,看看结果是否还合理;第三轮才开始改结构,比如增加字段、加错误提示、做网页交互。这个顺序有点慢,但很稳。真正浪费时间的不是慢,而是一下子改太多,最后不知道错在哪里。
前端不能保存 API Key。浏览器里的代码别人能看到,密钥一旦放进去,就像把钥匙贴在门上。
理论部分要和代码一起看。比如“输入契约”不是一个漂亮词,它在代码里就是长度检查、必填字段、表单校验;“输出契约”也不是空话,它在代码里就是 JSON Schema、固定字段或页面渲染规则。你每写一行检查代码,都是在告诉系统:什么结果可以接受,什么结果需要退回去重新处理。
课堂里可以把同桌当成第一个用户。你把项目跑给同桌看,让对方换一个输入,观察系统会不会乱。很多问题都是别人随手一试才出现的,比如输入太短、问题太模糊、连续点击按钮、图片看不清。能处理这些小麻烦,作品就会从“我电脑上能跑”变成“别人也能用”。
最后做复盘时,不要只写“我学会了调用 API”。可以写得更具体:我学会了怎样限制输入,怎样让输出固定,怎样判断结果不可靠,怎样把报错变成用户看得懂的提示。这样的复盘有用,因为下一课你真的会再次用到它。
先把例子看懂,再动手写代码。你不需要一次记住所有概念,先能说清楚“输入是什么、输出是什么、程序要检查什么”。
等完整故事生成完,页面一次性显示。
标题先出现,第一句出现,第二句出现……用户不用一直等。
按钮变成“生成中”,输入框暂时禁用,生成完成后恢复。
下面同时给出 Node.js 和 Python 两套完整最小实现。先任选一种原样跑通,再改输入、改提示词、改输出格式。
import express from "express";
import OpenAI from "openai";
const app = express();
const client = new OpenAI();
const MODEL = process.env.OPENAI_MODEL || "gpt-5.5";
app.get("/", (req, res) => {
res.send(`
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>流式 AI 聊天页面</title>
<style>
body { font-family: sans-serif; max-width: 760px; margin: 40px auto; line-height: 1.7; }
input, button { width: 100%; padding: 10px; margin: 6px 0 14px; }
#result { white-space: pre-wrap; background: #f4f7ff; padding: 16px; border-radius: 8px; min-height: 120px; }
</style>
</head>
<body>
<h1>流式 AI 聊天页面</h1>
<input id="question" value="用简单例子解释递归。">
<button id="btn">开始生成</button>
<div id="result"></div>
<script>
const btn = document.querySelector("#btn");
const result = document.querySelector("#result");
btn.onclick = () => {
btn.disabled = true;
result.textContent = "";
const q = encodeURIComponent(document.querySelector("#question").value);
const events = new EventSource("/api/stream?question=" + q);
events.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.delta) result.textContent += data.delta;
if (data.done) {
events.close();
btn.disabled = false;
}
};
events.onerror = () => {
events.close();
btn.disabled = false;
result.textContent += "\\n[连接中断,请重试]";
};
};
</script>
</body>
</html>`);
});
app.get("/api/stream", async (req, res) => {
const question = String(req.query.question || "").slice(0, 300);
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const stream = await client.responses.create({
model: MODEL,
input: `请给 10-14 岁学生回答:${question}`,
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") {
res.write(`data: ${JSON.stringify({ delta: event.delta })}\n\n`);
}
}
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
res.end();
});
app.listen(3000, () => {
console.log("打开 http://localhost:3000");
});
from flask import Flask, Response, request
from openai import OpenAI
import json
import os
app = Flask(__name__)
client = OpenAI()
MODEL = os.getenv("OPENAI_MODEL", "gpt-5.5")
@app.get("/")
def index():
return """
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>流式 AI 聊天页面</title>
<style>
body { font-family: sans-serif; max-width: 760px; margin: 40px auto; line-height: 1.7; }
input, button { width: 100%; padding: 10px; margin: 6px 0 14px; }
#result { white-space: pre-wrap; background: #f4f7ff; padding: 16px; border-radius: 8px; min-height: 120px; }
</style>
</head>
<body>
<h1>流式 AI 聊天页面</h1>
<input id="question" value="用简单例子解释递归。">
<button id="btn">开始生成</button>
<div id="result"></div>
<script>
const btn = document.querySelector("#btn");
const result = document.querySelector("#result");
btn.onclick = () => {
btn.disabled = true;
result.textContent = "";
const q = encodeURIComponent(document.querySelector("#question").value);
const events = new EventSource("/api/stream?question=" + q);
events.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.delta) result.textContent += data.delta;
if (data.done) {
events.close();
btn.disabled = false;
}
};
events.onerror = () => {
events.close();
btn.disabled = false;
result.textContent += "\\n[连接中断,请重试]";
};
};
</script>
</body>
</html>
"""
@app.get("/api/stream")
def stream_answer():
question = request.args.get("question", "")[:300]
def generate():
stream = client.responses.create(
model=MODEL,
input=f"请给 10-14 岁学生回答:{question}",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
payload = json.dumps({"delta": event.delta}, ensure_ascii=False)
yield f"data: {payload}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
return Response(generate(), mimetype="text/event-stream")
if __name__ == "__main__":
app.run(port=3000, debug=True, threaded=True)