学习目标

  • 把 Week 1 的英语 Agent 整合到 Web 版(搞定!)
  • 设计一个好看的前端界面(AI生成的)(搞定!)
  • 实现所有功能:今日单词、单词测验、造句练习、编故事(搞定!)
  • 加入 System Prompt 让 AI 更有"老师"的感觉(搞定!)

一、回顾:Week 1 的英语 Agent

Week 1 Day 4 做的英语 Agent 有这些功能:

功能描述temperature
今日单词学习新单词,AI 讲解0.3(准确讲解)
单词测验看中文猜英文0.5(鼓励准确)
造句练习用单词造句,AI 检查0.5(温和纠正)
编故事用单词编有趣故事1.2(创意有趣)

当时是在命令行里跑的,今天把它搬到浏览器上。


二、Web 版界面设计

2.1 页面布局

┌─────────────────────────────────────────┐
│   英语单词学习 Agent                   │
├─────────────────────────────────────────┤
│                                         │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│  │ 今日单词 │ │ 单词测验 │ │ 造句练习 │ │ 编故事  │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘
│                                         │
│  ┌───────────────────────────────────┐ │
│  │                                   │ │
│  │  AI 回复区域(流式输出)            │ │
│  │                                   │ │
│  └───────────────────────────────────┘ │
│                                         │
│  输入: [________________________] [发送] │
└─────────────────────────────────────────┘

2.2 功能按钮设计

用户点击按钮后,前端会:

  1. 显示提示(比如"请输入一个单词")
  2. 禁用其他按钮,避免误操作
  3. 发送请求到对应的后端接口
  4. 流式显示 AI 的回复

三、Flask 后端设计

3.1 接口规划

接口方法功能
/GET返回首页 HTML
/word/learnPOST今日单词讲解
/word/testPOST单词测验
/word/sentencePOST造句练习
/word/storyPOST编故事
/clearPOST清空对话历史

3.2 System Prompt 设计

让 AI 有"英语老师"的感觉:

SYSTEM_PROMPT = """
你是教三年级小朋友的英语老师。

特点:
- 说话亲切可爱,像大姐姐一样
- 用简单的语言解释单词
- 多用表情符号,让学习更有趣
- 鼓励小朋友,夸他们聪明

格式:
- 讲解单词时,先说单词意思,再举个有趣的例子
- 测验时,先夸奖,再判断对错
- 编故事时,故事要有趣,能吸引小朋友

记住:你的学生是 8-9 岁的小朋友,要用他们能听懂的话。
"""

3.3 不同功能的 temperature 设置

def get_temperature(action):
    """不同功能用不同的温度"""
    if action == "learn":
        return 0.3  # 讲解要准确
    elif action == "test":
        return 0.5  # 测验要稳定
    elif action == "sentence":
        return 0.5  # 造句要温和
    elif action == "story":
        return 1.2  # 故事要有创意
    else:
        return 0.7  # 默认

四、核心代码实现

4.1 后端 app.py

# -*- coding: utf-8 -*-
from flask import Flask, request, Response, render_template
import requests
import json

app = Flask(__name__)

# ===== AI API 配置 =====
AI_URL = "https://coding.dashscope.aliyuncs.com/v1/chat/completions"
AI_HEADERS = {
    "Content-Type": "application/json",
    "Authorization": "Bearer your-api-key"
}
AI_MODEL = "glm-5"

# ===== System Prompt =====
SYSTEM_PROMPT = """
你是教三年级小朋友的英语老师。
说话亲切可爱,用简单的语言,多用表情符号。
"""

# ===== 单词库 =====
WORDS = {
    "unit1": ["cat", "dog", "bird", "fish"],
    "unit2": ["apple", "banana", "orange"],
}

# ===== 对话历史 =====
messages = []


# ===== 路由 =====

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/word/<action>", methods=["POST"])
def word_action(action):
    """统一处理单词学习相关请求"""

    # 获取用户输入
    user_input = request.json.get("input", "")

    # 根据功能构建 prompt
    if action == "learn":
        prompt = f"请讲解单词 '{user_input}',告诉小朋友它的意思、怎么用,举一个有趣的例子。"
        temperature = 0.3

    elif action == "test":
        prompt = f"用户猜 '{user_input}',请判断对不对,并解释为什么。"
        temperature = 0.5

    elif action == "sentence":
        prompt = f"小朋友用 '{user_input}' 造了个句子:'{user_input}'。请检查句子对不对,如果有错,温和地告诉他怎么改。"
        temperature = 0.5

    elif action == "story":
        prompt = f"请用 '{user_input}' 这些单词编一个有趣的小故事,让小朋友喜欢读。"
        temperature = 1.2

    else:
        return Response("data: 功能不存在nndata: [DONE]nn", mimetype="text/event-stream")

    # 构建消息
    messages.clear()  # 每次新对话清空历史
    messages.append({"role": "system", "content": SYSTEM_PROMPT})
    messages.append({"role": "user", "content": prompt})

    # 调用 AI(流式)
    data = {
        "model": AI_MODEL,
        "messages": messages,
        "stream": True,
        "temperature": temperature
    }

    try:
        response = requests.post(AI_URL, headers=AI_HEADERS, json=data, stream=True, timeout=60)

        if response.status_code != 200:
            error_msg = response.json().get("error", {}).get("message", f"错误: {response.status_code}")
            return Response(f"data: {error_msg}nndata: [DONE]nn", mimetype="text/event-stream")

        def generate():
            for line in response.iter_lines():
                if line:
                    line = line.decode('utf-8')
                    if line.startswith("data: "):
                        json_str = line[6:]
                        if json_str == "[DONE]":
                            break
                        try:
                            chunk = json.loads(json_str)
                            content = chunk["choices"][0].get("delta", {}).get("content", "")
                            if content:
                                yield f"data: {content}nn"
                        except:
                            pass
            yield "data: [DONE]nn"

        return Response(generate(), mimetype="text/event-stream")

    except Exception as e:
        return Response(f"data: 网络错误: {str(e)}nndata: [DONE]nn", mimetype="text/event-stream")


if __name__ == "__main__":
    print("英语 Agent Web 版启动!")
    print("访问 0")
    app.run(debug=True)

4.2 前端 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>英语单词学习 Agent</title>
    <style>
        body {
            font-family: sans-serif;
            max-width: 700px;
            margin: 20px auto;
            background: #f5f5f5;
        }

        h1 {
            text-align: center;
            color: #333;
        }

        /* 功能按钮 */
        .btn-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            justify-content: center;
        }

        .btn-group button {
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 16px;
            background: #007bff;
            color: white;
        }

        .btn-group button:hover {
            background: #0056b3;
        }

        .btn-group button.active {
            background: #28a745;
        }

        .btn-group button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        /* AI 回复区域 */
        #ai-response {
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 20px;
            background: white;
            min-height: 200px;
            margin-bottom: 20px;
        }

        /* 输入区域 */
        #input-area {
            display: flex;
            gap: 10px;
        }

        #input-area input {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 8px;
            font-size: 16px;
        }

        #input-area button {
            padding: 10px 20px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            background: #007bff;
            color: white;
        }

        /* 提示文字 */
        .hint {
            text-align: center;
            color: #666;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <h1> 英语单词学习 Agent</h1>

    <!-- 功能按钮 -->
    <div class="btn-group">
        <button id="btn-learn">今日单词</button>
        <button id="btn-test">单词测验</button>
        <button id="btn-sentence">造句练习</button>
        <button id="btn-story">编故事</button>
    </div>

    <!-- 提示区域 -->
    <div class="hint" id="hint">选择一个功能开始学习吧!</div>

    <!-- AI 回复区域 -->
    <div id="ai-response">你好!我是你的英语老师,有什么想学的吗?</div>

    <!-- 输入区域 -->
    <div id="input-area">
        <input type="text" id="user-input" placeholder="输入单词或句子..." disabled>
        <button id="btn-send" disabled>发送</button>
    </div>

    <script>
        // ===== 状态管理 =====
        let currentAction = null;  // 当前选中的功能

        // ===== DOM 元素 =====
        const buttons = {
            learn: document.getElementById("btn-learn"),
            test: document.getElementById("btn-test"),
            sentence: document.getElementById("btn-sentence"),
            story: document.getElementById("btn-story")
        };
        const hintEl = document.getElementById("hint");
        const responseEl = document.getElementById("ai-response");
        const inputEl = document.getElementById("user-input");
        const sendBtn = document.getElementById("btn-send");

        // ===== 功能按钮点击 =====
        function selectAction(action) {
            currentAction = action;

            // 更新按钮样式
            Object.keys(buttons).forEach(key => {
                buttons[key].classList.toggle("active", key === action);
            });

            // 更新提示和输入框
            const hints = {
                learn: "请输入要学习的单词(如 cat)",
                test: "请输入你的答案(如 猫 → cat)",
                sentence: "请输入你造的句子",
                story: "请输入要编入故事的单词(多个用空格分隔)"
            };

            hintEl.textContent = hints[action];
            inputEl.disabled = false;
            inputEl.placeholder = hints[action];
            sendBtn.disabled = false;
            inputEl.focus();
        }

        // ===== 绑定按钮事件 =====
        buttons.learn.onclick = () => selectAction("learn");
        buttons.test.onclick = () => selectAction("test");
        buttons.sentence.onclick = () => selectAction("sentence");
        buttons.story.onclick = () => selectAction("story");

        // ===== 发送请求 =====
        sendBtn.onclick = sendMessage;
        inputEl.onkeypress = e => {
            if (e.key === "Enter" && !inputEl.disabled) sendMessage();
        };

        function sendMessage() {
            const input = inputEl.value.trim();
            if (!input || !currentAction) return;

            // 清空输入,禁用按钮
            inputEl.value = "";
            inputEl.disabled = true;
            sendBtn.disabled = true;
            Object.values(buttons).forEach(btn => btn.disabled = true);

            // 清空回复区域,准备接收流式数据
            responseEl.textContent = "";

            // 发送请求
            fetch(`/word/${currentAction}`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ input })
            })
            .then(response => {
                const reader = response.body.getReader();
                const decoder = new TextDecoder();

                function read() {
                    reader.read().then(({ done, value }) => {
                        if (done) {
                            // 流结束,恢复按钮
                            inputEl.disabled = false;
                            sendBtn.disabled = false;
                            Object.values(buttons).forEach(btn => btn.disabled = false);
                            currentAction = null;
                            return;
                        }

                        const text = decoder.decode(value);
                        const lines = text.split("n");

                        for (const line of lines) {
                            if (line.startsWith("data: ") && line.slice(6) !== "[DONE]") {
                                responseEl.textContent += line.slice(6);
                            }
                        }

                        read();
                    });
                }

                read();
            })
            .catch(() => {
                responseEl.textContent = "网络出错了,请重试";
                inputEl.disabled = false;
                sendBtn.disabled = false;
                Object.values(buttons).forEach(btn => btn.disabled = false);
            });
        }
    </script>
</body>
</html>

五、踩坑记录

Q1: 点击按钮后没反应

咋回事:按钮的事件没绑定,或者 selectAction() 函数没正确执行

咋办:检查按钮绑定,确保 onclick 有对应函数

Q2: 发送请求后所有按钮都禁用了,流结束没恢复

咋回事:流结束时忘记恢复按钮状态

咋办:在 done: true 的处理里恢复:

if (done) {
    inputEl.disabled = false;
    sendBtn.disabled = false;
    Object.values(buttons).forEach(btn => btn.disabled = false);
    return;
}

Q3: AI 回复没有"老师"的感觉

咋回事:System Prompt 没设置,或者设置得太简单

咋办:完善 System Prompt,详细描述 AI 的身份和说话风格

Q4: 发送消息后,终端打印了很多日志,但浏览器等了很久才有输出

咋回事:GLM-5 模型会先输出"思考过程"(reasoning_content),思考完成后才输出真正的内容(content

终端日志是这样的

收到行: data: {"choices":[{"delta":{"content":null,"reasoning_content":"收到用户..."},"finish_reason":null,...}]}
收到行: data: {"choices":[{"delta":{"content":null,"reasoning_content":"需要讲解..."},"finish_reason":null,...}]}
...(很多 reasoning_content,但 content 都是 null)...
收到行: data: {"choices":[{"delta":{"content":"你好!","reasoning_content":null},...}]}
发送内容: 你好!

流程

  1. AI 开始思考 → 返回 reasoning_content(思考过程)
  2. 思考完成 → 返回 content(实际要说的内容)
  3. 前端只处理 content,所以思考期间看不到任何输出

咋办

两种方案:

方案一:API 参数关闭思考过程(推荐)

在 API 请求中加 reasoning_effort: "none" 参数:

data = {
    "model": "glm-5",
    "messages": messages,
    "stream": True,
    "reasoning_effort": "none"  # 关闭思考,直接输出
}

reasoning_effort 参数值:

效果
"none"关闭思考,直接输出内容
"low"少量思考
"medium"中等思考(默认)
"high"深度思考

方案二:只取 content,接受延迟

content = delta.get("content", "")
if content:
    yield f"data: {content}nn"  # 只有 content 才发给前端

优点:用户看不到思考过程,输出干净 缺点:思考期间前端空白,体验不好

方案三:同时显示思考过程

# 显示思考过程
reasoning = delta.get("reasoning_content", "")
if reasoning:
    yield f"data: [思考] {reasoning}nn"

# 显示正式内容
content = delta.get("content", "")
if content:
    yield f"data: {content}nn"

优点:用户能看到 AI 在思考,不会觉得卡住 缺点:思考过程可能很长,显示出来有点乱

推荐用方案一:直接在 API 层关闭思考,前端体验最好。

Q5: Flask 流式输出有缓冲延迟

咋回事:Flask 开发服务器会缓冲流式响应,等缓冲区满才发送

咋办:在生成器开始时先 yield 一个空内容触发流:

def generate():
    yield "n"  # 先发送一个空内容,触发流开始
    # 然后再处理实际内容...

这样第一次 yield 就立即触发响应开始,后续内容能实时发送。

Q6: 点击"单词测验"后没有随机单词

咋回事:前端只显示提示"请输入你的答案",没有先获取随机单词展示给用户

咋办

步骤一:后端添加随机单词接口

扩展单词库数据结构,包含英文和中文:

WORDS = [
    {"en": "cat", "cn": "猫"},
    {"en": "dog", "cn": "狗"},
    ...
]

添加随机单词接口:

@app.route("/word/random", methods=["GET"])
def word_random():
    import random
    word = random.choice(WORDS)
    return {"en": word["en"], "cn": word["cn"]}

步骤二:前端点击测验按钮时先获取随机单词

if (action === "test") {
    // 先获取随机单词
    fetch("/word/random")
        .then(res => res.json())
        .then(data => {
            currentTestWord = data;
            hintEl.innerHTML = `请猜英文:看到「${data.cn}」后,输入对应的英文单词`;
            // 启用输入...
        });
    return;
}

步骤三:发送答案时把正确答案也发给后端

let requestBody = { input };
if (currentAction === "test" && currentTestWord) {
    requestBody.cn = currentTestWord.cn;
    requestBody.correctAnswer = currentTestWord.en;
}
fetch(`/word/${currentAction}`, {
    body: JSON.stringify(requestBody)
});

步骤四:后端修改测验 prompt,包含正确答案信息

"test": f"中文意思是「{request.json.get('cn', '')}」,正确答案是「{request.json.get('correctAnswer', '')}」。小朋友猜的是「{user_input}」。请判断对不对..."

这样 AI 就知道正确答案是什么,能准确判断用户的答案。

Q7: AI 只返回问候语,没有完整讲解单词

咋回事:AI 返回内容包含 emoji(、),但 print(f"收到行: {line}") 在 Windows 终端用 GBK 编码无法打印 emoji,抛 UnicodeEncodeError 异常,导致生成器中断,流式输出提前结束。

终端日志

UnicodeEncodeError: 'gbk' codec can't encode character 'U0001f44b' in position 45

咋办:移除或用 try-except 包裹 print 语句:

def generate():
    for line in response.iter_lines():
        if line:
            line = line.decode('utf-8')
            # 不要直接 print,可能包含 emoji 导致编码错误
            if line.startswith("data: "):
                # 处理数据...

Q8: 再次发送消息没有触发请求

咋回事:流结束后 currentAction = null,导致 sendMessage() 里检查 if (!currentAction) return; 直接返回,不发送请求。

咋办:流结束后不要清空 currentAction,保持当前功能:

if (done) {
    // 恢复交互,但保持当前功能
    inputEl.disabled = false;
    sendBtn.disabled = false;
    Object.values(buttons).forEach(btn => btn.disabled = false);
    // 不要清空 currentAction
    hintEl.innerHTML = "学习完成!可以继续输入,或选择其他功能 ";
    return;
}

这样用户可以继续用同一功能学习多个单词。

六、今日总结

今天干了啥:

把命令行英语 Agent 搬到了浏览器

设计了好看的界面,有功能按钮

用 System Prompt 让 AI 更有"老师"的感觉

不同功能用不同的 temperature,输出更符合预期

明天干嘛:可以加更多功能,比如发音、积分系统、错题本、优化页面!


记于 2026-04-15,英语 Agent Web 版上线!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com