人类游乐园
106.63M · 2026-04-16
Week 1 Day 4 做的英语 Agent 有这些功能:
| 功能 | 描述 | temperature |
|---|---|---|
| 今日单词 | 学习新单词,AI 讲解 | 0.3(准确讲解) |
| 单词测验 | 看中文猜英文 | 0.5(鼓励准确) |
| 造句练习 | 用单词造句,AI 检查 | 0.5(温和纠正) |
| 编故事 | 用单词编有趣故事 | 1.2(创意有趣) |
当时是在命令行里跑的,今天把它搬到浏览器上。
┌─────────────────────────────────────────┐
│ 英语单词学习 Agent │
├─────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ │ 今日单词 │ │ 单词测验 │ │ 造句练习 │ │ 编故事 │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │
│ ┌───────────────────────────────────┐ │
│ │ │ │
│ │ AI 回复区域(流式输出) │ │
│ │ │ │
│ └───────────────────────────────────┘ │
│ │
│ 输入: [________________________] [发送] │
└─────────────────────────────────────────┘
用户点击按钮后,前端会:
| 接口 | 方法 | 功能 |
|---|---|---|
/ | GET | 返回首页 HTML |
/word/learn | POST | 今日单词讲解 |
/word/test | POST | 单词测验 |
/word/sentence | POST | 造句练习 |
/word/story | POST | 编故事 |
/clear | POST | 清空对话历史 |
让 AI 有"英语老师"的感觉:
SYSTEM_PROMPT = """
你是教三年级小朋友的英语老师。
特点:
- 说话亲切可爱,像大姐姐一样
- 用简单的语言解释单词
- 多用表情符号,让学习更有趣
- 鼓励小朋友,夸他们聪明
格式:
- 讲解单词时,先说单词意思,再举个有趣的例子
- 测验时,先夸奖,再判断对错
- 编故事时,故事要有趣,能吸引小朋友
记住:你的学生是 8-9 岁的小朋友,要用他们能听懂的话。
"""
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 # 默认
# -*- 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)
<!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>
咋回事:按钮的事件没绑定,或者 selectAction() 函数没正确执行
咋办:检查按钮绑定,确保 onclick 有对应函数
咋回事:流结束时忘记恢复按钮状态
咋办:在 done: true 的处理里恢复:
if (done) {
inputEl.disabled = false;
sendBtn.disabled = false;
Object.values(buttons).forEach(btn => btn.disabled = false);
return;
}
咋回事:System Prompt 没设置,或者设置得太简单
咋办:完善 System Prompt,详细描述 AI 的身份和说话风格
咋回事: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},...}]}
发送内容: 你好!
流程:
reasoning_content(思考过程)content(实际要说的内容)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 层关闭思考,前端体验最好。
咋回事:Flask 开发服务器会缓冲流式响应,等缓冲区满才发送
咋办:在生成器开始时先 yield 一个空内容触发流:
def generate():
yield "n" # 先发送一个空内容,触发流开始
# 然后再处理实际内容...
这样第一次 yield 就立即触发响应开始,后续内容能实时发送。
咋回事:前端只显示提示"请输入你的答案",没有先获取随机单词展示给用户
咋办:
步骤一:后端添加随机单词接口
扩展单词库数据结构,包含英文和中文:
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 就知道正确答案是什么,能准确判断用户的答案。
咋回事: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: "):
# 处理数据...
咋回事:流结束后 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 版上线!