项目地址:https://gitcode.com/zhjdwk1018/zmjkk

码道 · 大学生英语速记 AI 助教开发全记录

从「一个想法」到「一个能跑能用的 AI 学习产品」,这篇文章完整记录了我使用 HTML5 + CSS3 + JavaScript 纯前端技术栈,接入 GitCode AI 大模型流式接口,打造「大学英语速记」AI 助教的全部过程——包括产品设计、系统提示词协议设计、流式响应实现、前端渲染与交互、测试验证,以及一路踩过的坑。

在这里插入图片描述
在这里插入图片描述

一、写在前面:为什么做这个项目

大学英语一直是很多同学心里的"老大难"。四六级、考研英语、雅思托福,每一场考试都像一座山。而背单词更是其中最枯燥、最劝退的环节:拿着一本厚厚的单词书,从 abandon 开始背,背到第三天还是 abandon。

我身边的很多同学都有类似的困扰:

  • 背单词纯靠死记硬背,没有词根、联想、谐音这些记忆技巧,背了就忘;
  • 语法知识碎片化,定语从句、非谓语动词、虚拟语气这些概念分开都认识,放在句子里就懵;
  • 长难句无从下手,考研英语里的句子动辄三四十个单词,主谓宾都找不到;
  • 写作文全是模板背不下来,或者背下了模板也套不进话题。

传统的解决方案是买课程、买书、找辅导机构,成本高、见效慢。而 2024 年开始,大语言模型的能力突飞猛进,它完全可以当一个随身英语助教。于是我想:能不能用最简单、最轻量、几乎零成本的方式,做一个纯前端的 AI 英语学习助手?不装 App、不建后端、不买服务器,打开浏览器就能用。

这就是「大学英语速记」项目的起点。

1.1 项目要解决的核心问题

经过一番思考,我把项目要解决的问题聚焦在以下四个方面:

第一,让记单词更高效。 传统的单词书只有词、音标和释义,缺少记忆钩子。而 AI 可以给每个单词都配上词根拆解、谐音联想、场景例句,把一个单词变成一组可记忆的信息网络。

第二,让语法学习更直观。 语法不是背出来的,是靠对比和语境理解出来的。AI 可以用对比的方式把两个容易混淆的知识点放在一起讲,比如"定语从句 vs 非谓语动词"。

第三,让学习过程有反馈。 很多同学学英语最大的问题是"以为自己会了"。如果每次学完都能有一道自测题,立刻检验学习效果,学习效率会高很多。

第四,让技术成本趋近于零。 我刻意选择了纯前端方案:不写后端、不买服务器、不用数据库,只靠浏览器原生能力加上一个 AI 大模型接口来完成整个产品。

这四点构成了项目最初的愿景,也直接决定了后面的技术选型。


二、技术选型:为什么是 HTML5 + CSS3 + JavaScript

2.1 没有后端,也能做 AI 产品

在做技术选型的时候,我其实面临很多选择:可以用 React + Node.js,可以用 Vue + Python FastAPI,也可以用 Next.js 全家桶。但最后我选择了最朴素的组合——HTML5 + CSS3 + JavaScript,三个文件搞定一切。

选择纯前端的主要理由有三点:

第一,部署成本最低。 纯静态站点可以免费部署到任何静态托管平台,也可以直接双击 index.html 在本地打开。对于学生和初学者来说,这是最友好的交付方式。

第二,传播最方便。 一个 HTML 文件、一个 CSS 文件、几个 JS 文件,打包起来不到 50KB,放哪都能跑,同学之间互相传着用非常方便。

第三,聚焦核心逻辑。 这个项目的核心不是复杂的服务端架构,而是「AI 对话 + 数据渲染」两件事。用原生 JavaScript 写,反而能让人把注意力完全集中在 AI 接口的对接和前端渲染上,不被框架的复杂度干扰。

2.2 为什么接入 GitCode AI 大模型

做 AI 对话产品,最核心的是模型能力。我选择了 GitCode AI 平台提供的 deepseek-ai/DeepSeek-V4-Flash 模型。

选择它的原因:

  • 接口规范:兼容 OpenAI Chat Completions 协议,/v1/chat/completions 端点,所有主流语言都有现成的对接方式;
  • 流式支持:原生支持 SSE(Server-Sent Events)流式输出,可以让 AI 回复逐字显示,体验接近真实的打字聊天;
  • 中文能力强:作为国产开源模型,DeepSeek 系列在中文理解与生成上的表现非常出色,非常适合面向中国大学生的英语学习场景;
  • 成本友好:Flash 版本定位轻量快速,适合对话型应用。

2.3 技术架构总览

整个项目的技术架构非常简单清晰:

┌─────────────────────────────────────────────┐
│              浏览器(前端)                    │
│  index.html ── 页面骨架                       │
│  css/style.css ── 视觉样式                    │
│  js/config.js ── API 配置(密钥隔离)          │
│  js/system-prompt.js ── 系统提示词(JSON协议) │
│  js/render.js ── 卡片渲染 + 自测题交互         │
│  js/app.js ── 流式请求与对话管理               │
└────────────────────┬────────────────────────┘
                     │ HTTPS + SSE 流式
                     ▼
┌─────────────────────────────────────────────┐
│      GitCode AI · /v1/chat/completions        │
│      DeepSeek-V4-Flash(流式)               │
└─────────────────────────────────────────────┘

前端的五个 JS 文件各司其职:配置、提示词、渲染、主逻辑分离,满足单一职责原则,方便维护和扩展。


三、系统提示词设计:让 AI 输出结构化的 JSON

3.1 为什么对话应用需要结构化的返回

这里要讲一个产品设计上的关键决策。

如果只是把聊天窗口接上大模型 API,用户问一句 AI 答一句,那这个项目只是一个"套壳聊天机器人",没有任何竞争力。作为英语学习工具,我们需要的是结构化、可交互、可复用的学习内容

比如用户说"帮我速记 6 个四级高频词",我们希望 AI 返回的不是一大段散文,而是一个包含以下结构的 JSON:

  • 这一组单词的主题是什么?
  • 每个单词的音标、释义、例句、记忆技巧是什么?
  • 有没有额外的学习建议?
  • 能不能出一道自测题来检验学习效果?

这种结构化的数据有两个好处:

  1. 前端可以把数据渲染成精美的卡片,而不是让用户去读一大段没有层次的文字;
  2. 数据可以被二次处理,比如用户下次想要复习,可以直接复用之前的结构化数据生成复习卡片。

所以我在系统提示词里做了两件事:一是明确告诉 AI"你是大学英语速记助教",设定角色;二是用 JSON Schema 约束回复格式,并反复强调"只能输出 JSON,不允许输出任何其他文字"。

3.2 JSON 协议的详细设计

我在 js/system-prompt.js 中定义了系统提示词。核心 JSON 结构如下:

{
  "type": "word|grammar|sentence|writing|exam|other",
  "title": "不超过15字的标题",
  "summary": "一句话总结本组内容(20字内)",
  "cards": [
    {
      "head": "主词条/语法点/标题短语",
      "phonetic": "音标(单词类必填)",
      "meaning": "中文释义/规则讲解",
      "example": "英文例句",
      "example_cn": "例句中文翻译",
      "memory_tip": "速记技巧:词根词缀/联想/谐音/场景"
    }
  ],
  "tips": ["拓展速记技巧1", "拓展速记技巧2"],
  "quiz": {
    "question": "围绕本次内容出1道自测题",
    "options": ["A. 选项", "B. 选项", "C. 选项", "D. 选项"],
    "answer": 0,
    "explain": "答案解析"
  }
}

这里我想重点讲几个设计细节:

第一,type 字段是整个渲染体系的调度中心。 我定义了六种类型:

  • word:单词速记,一次 3~6 个词,每个词都要有音标和记忆技巧;
  • grammar:语法讲解,1~3 个语法点,用对比方式把规则讲透;
  • sentence:长难句剖析,一句句拆主干、找修饰;
  • writing:写作模板,2~4 段直接可套用的句式和框架;
  • exam:考试技巧与真题解析;
  • other:兜底类型,处理闲聊等非英语学习类问题。

前端拿到 type 之后,就可以决定用什么样式、什么图标、什么排版来渲染。这有点像前端路由——同一个数据结构,根据类型分发到不同的"页面组件"。

第二,cards 数组是内容的主体。 每个卡片代表一个独立的词条或知识点。设计上我要求每个卡片必须包含 head(主词条)和 meaning(释义),单词类还必须包含 phonetic(音标)和 memory_tip(记忆技巧)。这是内容的"最小完整性约束"。

第三,quiz 是产品差异化的关键。 我强制要求 AI 每次回复都必须附带一道自测题,包含 questionoptionsanswer(正确答案下标)、explain(解析)。这让产品从"给你讲"变成"讲完考你",形成学习闭环。前端收到后渲染成可点击的单选题,用户点选答案立刻给出对错反馈。

3.3 系统提示词的调优过程

系统提示词不是一次写对的,我经历了多轮调优:

第一版问题:AI 忍不住输出代码块。 一开始我在提示词里说"返回 JSON",结果模型经常用 Markdown 的 ```json 代码块包裹输出。前端解析时就遇到麻烦。后来我在提示词里明确写"不允许输出 JSON 以外的文字、解释、Markdown 代码块标记(如 ```)",并且在前端做了兼容——即使模型真的输出了代码块,也会自动剥离。

第二版问题:字段内容溢出。 有时候模型会把 meaning 写得很长,把 summary 写成一段话。我在提示词里加了括号约束:“不超过2行”“20字内”“不超过15字”,让输出更克制。

第三版问题:tips 字段类型不稳定。 观察实机输出时发现,模型偶尔把 tips 数组里的元素写成对象 {"tip": "..."} 而不是字符串。我做了双重保险——既在提示词里明确数据类型,又在前端渲染时兼容两种形式。

调优原则总结: 系统提示词要做到"程序化约束 + 前端兜底"双保险。不能指望大模型 100% 遵循格式,前端必须有能力处理不完美的情况。


四、Python 到 JavaScript:流式接口的迁移

4.1 原版 Python 实现

GitCode AI 平台给的示例是 Python 代码,使用 requests 库实现流式读取:

import json
import requests

API_URL = "https://api-ai.gitcode.com/v1/chat/completions"

def query(payload):
    response = requests.post(API_URL, headers=headers, json=payload, stream=True)
    for line in response.iter_lines():
        if not line.startswith(b"data:"):
            continue
        if line.strip() == b"data: [DONE]":
            return
        yield json.loads(line.decode("utf-8").lstrip("data:"))

chunks = query({...})

for chunk in chunks:
    print(chunk["choices"])

这是一个典型的 SSE 流式解析:requests 开启流模式后,逐行读取响应;只处理以 data: 开头的数据行;遇到 data: [DONE] 就停止;每一行都是一段 JSON,用 json.loads 解析后 yield 出去。

我的任务,就是把这套逻辑用 JavaScript 完整复刻到浏览器里。

4.2 JavaScript 的关键差异

JavaScript 前端实现流式请求,有几个 Python 版本没有的问题:

第一,没有 requests 库,用什么发请求? 答案是浏览器原生的 fetch API。注意 fetch 默认拿到的 response.body 是一个 ReadableStream(可读流),这是实现流式的关键。

第二,iter_lines 怎么替代? Python 的 response.iter_lines() 会自动按行切分。而 JavaScript 的流是字节流,需要我们自己维护一个缓冲区,把不完整的行暂存起来,等收到换行符再切分。

第三,编码问题。 Python 的 iter_lines 已经处理了 UTF-8 解码。JS 需要手动用 TextDecoder("utf-8") 处理,并且要注意流式场景下的多字节字符边界问题——一个中文字符可能被拆到两个 chunk 里,缓冲区机制可以自然解决这个问题。

4.3 我的 JavaScript 实现

为了最大程度保持代码的清晰度,我实现了一个异步生成器 streamQuery。JavaScript 同样支持生成器,和 Python 的 yield 一一对应:

async function* streamQuery(payload) {
  const headers = {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${CONFIG.API_KEY}`
  };
  const response = await fetch(CONFIG.API_URL, {
    method: "POST",
    headers,
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    const errText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errText}`);
  }
  if (!response.body) throw new Error("当前浏览器不支持流式响应");

  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const lines = buffer.split("\n");
    buffer = lines.pop();

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith("data:")) continue;
      let data = trimmed.replace(/^data:\s*/, "");
      if (data === "[DONE]") return;
      try {
        yield JSON.parse(data);
      } catch (err) {
        // 跳过不完整的 JSON 行
      }
    }
  }
}

这段代码我在几个关键点上做了防御:

  1. 缓冲区设计buffer += decoder.decode(value, {stream: true}) 维护一个累积缓冲区,split("\n") 之后把最后一段(可能不完整)重新放回缓冲区,保证行切分永远不丢数据。
  2. [DONE] 处理:注意到这个端点的返回可能是不带空格或带空格的 data:[DONE] / data: [DONE],我用正则 replace(/^data:\s*/, "") 统一剥掉 data: 前缀和空白,确保两种情况都能识别。
  3. 容错解析json.loads 在 JS 里对应 JSON.parse,但流式场景下偶尔会解析失败,我用 try/catch 静默跳过,保证主流程不中断。

4.4 前端的使用方式

app.jssendMessage 里,我用 for await 消费整个流:

for await (const chunk of streamQuery(payload)) {
  const delta = chunk.choices && chunk.choices[0] && chunk.choices[0].delta;
  if (delta && delta.content) {
    fullText += delta.content;
    typingUpdate("正在生成回复…");
  }
}

收到每个 chunk 时,从 choices[0].delta.content 累加内容。期间界面上显示打字动画;流结束后,把完整内容交给解析器,判断是结构化 JSON 还是普通文本,再走不同的渲染分支。

这里要特别说明:模型支持思维链(reasoning_content),第一个 chunk 里可能只有 reasoning_content 没有 content,所以判断条件里必须加 if (delta && delta.content),只有真正的内容增量才累加,否则会把思维过程拼进正文。


五、前端渲染:从 JSON 到精美卡片

5.1 渲染的整体架构

js/render.js 负责把 AI 返回的 JSON 渲染成 HTML。核心函数是 cardToMarkdown(data),流程是:

  1. 校验数据是否合法;
  2. 根据 type 字段决定类型标签的文案和图标;
  3. 依次渲染标题区、卡片列表、拓展技巧、自测题;
  4. 所有输出经过 escapeHtml 转义,防止 XSS 注入。

5.2 卡片渲染细节

以单词类型为例,渲染出来的每个卡片包含:

  • 词头行:单词本体(加粗高亮)+ 音标(灰色衬线字体);
  • 释义行:中文释义;
  • 例句区:英文例句(斜体)+ 中文翻译(浅色);
  • 速记技巧:左侧橙色色条 + 淡黄背景的记忆技巧框,视觉上与其他内容区分。

拓展技巧区用蓝色左边框的"速记小贴士"盒子,自测题用虚线边框的"考考你"盒子——不同内容区块有鲜明的视觉锚点。

5.3 自测题的交互实现

自测题渲染成四个可点击的选项,每个选项带 data-idx 属性记录下标。点击时调用全局函数 checkAnswer

function checkAnswer(el, answerIdx) {
  const box = el.closest(".quiz-box");
  const options = box.querySelectorAll(".quiz-option");
  const explain = box.querySelector(".quiz-explain");
  const chosen = Number(el.dataset.idx);
  options.forEach((o) => o.classList.remove("quiz-correct", "quiz-wrong"));
  if (chosen === answerIdx) {
    el.classList.add("quiz-correct");
  } else {
    el.classList.add("quiz-wrong");
    options[answerIdx] && options[answerIdx].classList.add("quiz-correct");
  }
  if (explain) explain.style.display = "block";
}

交互逻辑:选中正确项变绿,选中错误项变红并高亮正确答案,同时展开解析文字。这个"即时反馈"机制让学习变成游戏化的过程,对记忆强化非常有效。

5.4 降级渲染:AI 不听话的时候怎么办

再完美的提示词,也不能保证大模型 100% 按格式输出。所以 parseReply 做了两层降级:

  1. 如果 AI 用 Markdown 代码块包裹 JSON(json ... ),自动剥离包裹;
  2. 如果整个输出都不是合法 JSON(比如用户闲聊,模型直接回文本),就降级走 renderFallback,把纯文本做简易 Markdown 渲染(加粗行内代码、换行)。

这样的设计保证了:即使模型输出完全偏离协议,用户也永远看不到"解析失败"的报错弹窗,产品体验始终是平滑的。


六、视觉设计:美感和可用性并重

6.1 色彩体系

我以蓝色为主色调搭建了整套视觉体系:

  • 主色 #3b6cf6,渐变到 #6a5cf6(紫蓝渐变),用于按钮、Logo、用户气泡、类型标签;
  • 背景使用 #f0f4fb#eaf2ff 的柔和渐变,让页面不刺眼;
  • 内容卡片用白色 + 浅蓝描边 #e3e9f4,保证内容区干净利落;
  • 辅助色:橙色 #f59e0b(记忆技巧)、绿色 #22c55e(正确反馈)、红色 #ef4444(错误反馈)。

整个配色走"清爽、学院、年轻化"的路线,和"大学"这个用户群体贴合。

6.2 交互体验细节

  • 打字动画:AI 思考期间显示三个跳动的小圆点,用 CSS @keyframes blink 实现,模拟实时打字;
  • 流式占位:生成过程中显示"正在生成回复…",结束后替换为正式内容;
  • 快捷提问:四个高频场景按钮(速记高频词、语法辨析、长难句、作文模板),一键填充问题并发送,降低使用门槛;
  • 移动端适配:通过媒体查询,在窄屏下隐藏副标题、让快捷栏横向滚动、气泡宽度占 88%,保证手机上也能流畅使用。

七、安全与密钥管理

7.1 密钥绝不上传到仓库

这类项目的安全隐患,多半出在 API Key 泄露。我的处理方案是:

  1. 真实密钥放在 js/config.js,该文件被 .gitignore 明确忽略;
  2. 仓库里只提交 js/config.example.js 模板,里面的 API_KEY 是占位符;
  3. README 中专门用警示框标注:请复制模板再填入自己的令牌,不要把密钥提交到仓库。
cp js/config.example.js  js/config.js   # 然后编辑填入真实 Key

7.2 前端注入防护

AI 生成的内容直接插入页面 DOM,这是 XSS 的高危场景。我在渲染函数的输出端点统一做了 escapeHtml 转义(&<>"'),确保所有文本内容按字面显示,杜绝脚本注入。

这两点防御做完之后,项目的安全性就有了基本保障。当然,纯前端方案天然暴露 API Key 给用户,这在公开部署时仍然是一个权衡取舍,适合个人学习场景;生产级应用建议再加一层后端代理做密钥中转。


八、测试验证:用自动化浏览器跑真实对话

代码写完不能拍脑袋说"能用",我做了两层验证。

8.1 接口层验证

先用 curl 直接打 GitCode AI 接口,验证模型是否按照系统提示词返回合法 JSON:

curl -sN https://api-ai.gitcode.com/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -d '{"model":"deepseek-ai/DeepSeek-V4-Flash", ...}'

然后用 Node 脚本解析整个流,断言 JSON 合法、type 正确、cardsquiz 齐全。实测模型稳定返回 {"type":"word", ...} 的合法结构。

这是我实测的部分返回(节选):

{
  "type": "word",
  "title": "6个四级高频词速记",
  "cards": [
    {
      "head": "abundant",
      "phonetic": "/əˈbʌndənt/",
      "meaning": "adj. 丰富的;充裕的",
      "example": "The region is abundant in natural resources.",
      "example_cn": "该地区自然资源丰富。",
      "memory_tip": "词根记忆:ab(加强) + und(波浪) + ant(…的) → 像波浪一样涌来 → 丰富的。"
    }
  ],
  "quiz": {
    "question": "The country has ____ supplies of fresh water.",
    "options": ["A. abundant", "B. abandoned", "C. absent", "D. absurd"],
    "answer": 0,
    "explain": "abundant 意为“丰富的、充裕的”,符合题意。"
  }
}

8.2 浏览器端到端验证

我用 Playwright 驱动真实 Chromium 浏览器,模拟用户点击快捷按钮、等待 AI 回复、点击自测题选项的完整流程:

  • 断言页面上出现用户气泡;
  • 等待 AI 回复完成(等待 .quiz-box 元素出现);
  • 统计词汇卡片数量和自测选项数量;
  • 模拟点击第一个选项,验证对错反馈出现;
  • 再发送第二条文本消息,验证多轮对话正常;
  • 全程监听控制台错误。

验证结果:结构化卡片渲染成功(6 张词汇卡片 + 4 个自测选项)、选择题交互正常、控制台零报错。

8.3 验证中发现并修复的 Bug

自动化测试帮我发现了两个真实 Bug:

Bug 1:冗余的 renderCard 调用。 一度在 app.js 里残留了一行 if (result.data) renderCard(result.data),但 render.js 里根本没有这个函数定义,导致运行时抛出 ReferenceError: renderCard is not defined。测试日志抓到了它,随即删除。这也印证了自动化测试的重要性——单纯人工看代码很难发现这种问题。

Bug 2:tips 字段类型不稳定。 模型有时把 tips 数组元素返回成对象 {"tip": "..."},我在渲染层加了一行兼容逻辑:typeof t === "string" ? t : (t && t.tip) || "",彻底解决。


九、项目目录结构

最终的项目结构非常精简:

├── index.html              # 页面入口(顶栏/聊天区/快捷栏/输入框)
├── favicon.svg             # 站点图标
├── css/
│   └── style.css           # 全局样式(卡片/自测题/响应式)
├── js/
│   ├── config.example.js   # 配置模板(复制为 config.js)
│   ├── config.js           # 本地配置(含 API Key,不入库)
│   ├── system-prompt.js    # AI 系统提示词(JSON 协议)
│   ├── render.js           # JSON → HTML 卡片渲染
│   └── app.js              # 聊天逻辑:流式请求 + 解析
├── .gitignore              # 忽略本地密钥配置
├── README.md               # 项目说明文档
└── docs/blog/              # 博客与文档

五个 JS 文件按职责清晰拆分,每个文件单一职责,新手也能快速看懂。


十、使用方式

10.1 配置

cp js/config.example.js js/config.js

编辑 js/config.js,把 API_KEY 换成自己的 GitCode AI 令牌。

10.2 运行

python3 -m http.server 8080 --directory .
# 或
npx serve .

浏览器打开 http://localhost:8080 即可使用。

10.3 典型使用场景

  • 点「速记高频词」:一键获得 6 个高频词 + 记忆技巧 + 自测题;
  • 问"定语从句和非谓语动词有什么区别":获得对比式语法解析;
  • 点「长难句」:获得主干拆解 + 翻译策略;
  • 点「作文模板」:获得万能句型和框架。

十一、踩坑记录

开发过程中积累了一些经验,写在这里供后来者参考:

  1. SSE 必须用 response.body.getReader(),不要用 response.text() 一次性读完——那会丢失流式体验;
  2. data: 前缀可能带空格也可能不带,用正则统一剥离最稳妥;
  3. 多字节字符会被拆到不同 chunk,缓冲区 + TextDecoder(stream:true) 是关键;
  4. 首次 chunk 可能只有 reasoning_content,判断内容增量必须检查 delta.content 是否存在;
  5. 模型输出的 tips 等数组元素类型可能漂移,前端渲染要做类型兼容;
  6. 浏览器直接 file:// 打开可能因 CORS 失败,本地开发务必起 HTTP 服务;
  7. API Key 必须通过 .gitignore 隔离,一旦提交到公开仓库就成了安全事故;
  8. 自动化测试帮助发现了手测发现不了的报错,比如那个残留的 renderCard 调用。

十二、总结与展望

12.1 项目亮点回顾

「大学英语速记」用最轻量的技术栈,做出了一个体验完整的 AI 学习产品:

  • 纯前端、零依赖、零后端:三个技术,五个 JS 文件,最后打包不过几十 KB;
  • 结构化 AI 协议:用系统提示词把大模型的输出约束成 JSON,让 UI 可以渲染成精美的学习卡片;
  • 学习闭环:速记 → 技巧 → 自测题 → 解析反馈,一个很完整的学习动线;
  • 全流程验证:接口层 + 浏览器端自动化双保险,实测通过;
  • 安全合规:密钥隔离、XSS 防御,好事做在前面。

12.2 后续演进方向

这个项目还留有充分的扩展空间,列几个我考虑过的方向:

  1. 加入记忆曲线复习:利用本地 localStorage 存储学过的词,按艾宾浩斯遗忘曲线定时提醒复习;
  2. 支持语音朗读:接入 Web Speech API,让 AI 生成的内容可以发音,练听力练口语;
  3. 多模型切换:把模型名做成可选项,方便对比不同模型的效果;
  4. 导出学习笔记:把生成的单词卡片一键导出为 Markdown 或图片,方便打印;
  5. 用户画像与错题本:记录每次自测题的错误,自动生成错题本延后重测。

12.3 写在最后

这个项目从立项到上线,走完了一个完整的小产品闭环:想法 → 技术选型 → 协议设计 → 编码 → 测试 → 部署 → 文档。它很好地印证了一件事:AI 时代,做产品的最小成本可以非常低。 不需要服务器,不需要团队,一个人、一台电脑、一份热情,加上一个靠谱的大模型接口,就能做出对大家有真实帮助的工具。

如果你也是大学生,或者正想学前端、学 AI 应用开发,我建议你也从这样一个小而美的项目开始:找一个具体的痛点,用最简单的技术栈做出来,跑起来,让真实用户给你反馈。技术能力是在一次又一次"把它做出来"的过程中增长的,而不是看一百遍教程。

愿我们都能在码道上稳步前行,用代码解决真实世界的问题。

项目地址:https://gitcode.com/zhjdwk1018/zmjkk
技术栈:HTML5 · CSS3 · JavaScript · GitCode AI (DeepSeek-V4-Flash) · SSE 流式


本文由「码道 · 大学英语速记」项目作者撰写,记录 AI 学习类 Web 应用从零到一的完整开发过程。> 项目地址:https://gitcode.com/zhjdwk1018/zmjkk

码道 · 大学生英语速记 AI 助教开发全记录

从「一个想法」到「一个能跑能用的 AI 学习产品」,这篇文章完整记录了我使用 HTML5 + CSS3 + JavaScript 纯前端技术栈,接入 GitCode AI 大模型流式接口,打造「大学英语速记」AI 助教的全部过程——包括产品设计、系统提示词协议设计、流式响应实现、前端渲染与交互、测试验证,以及一路踩过的坑。


一、写在前面:为什么做这个项目

大学英语一直是很多同学心里的"老大难"。四六级、考研英语、雅思托福,每一场考试都像一座山。而背单词更是其中最枯燥、最劝退的环节:拿着一本厚厚的单词书,从 abandon 开始背,背到第三天还是 abandon。

我身边的很多同学都有类似的困扰:

  • 背单词纯靠死记硬背,没有词根、联想、谐音这些记忆技巧,背了就忘;
  • 语法知识碎片化,定语从句、非谓语动词、虚拟语气这些概念分开都认识,放在句子里就懵;
  • 长难句无从下手,考研英语里的句子动辄三四十个单词,主谓宾都找不到;
  • 写作文全是模板背不下来,或者背下了模板也套不进话题。

传统的解决方案是买课程、买书、找辅导机构,成本高、见效慢。而 2024 年开始,大语言模型的能力突飞猛进,它完全可以当一个随身英语助教。于是我想:能不能用最简单、最轻量、几乎零成本的方式,做一个纯前端的 AI 英语学习助手?不装 App、不建后端、不买服务器,打开浏览器就能用。

这就是「大学英语速记」项目的起点。

1.1 项目要解决的核心问题

经过一番思考,我把项目要解决的问题聚焦在以下四个方面:

第一,让记单词更高效。 传统的单词书只有词、音标和释义,缺少记忆钩子。而 AI 可以给每个单词都配上词根拆解、谐音联想、场景例句,把一个单词变成一组可记忆的信息网络。

第二,让语法学习更直观。 语法不是背出来的,是靠对比和语境理解出来的。AI 可以用对比的方式把两个容易混淆的知识点放在一起讲,比如"定语从句 vs 非谓语动词"。

第三,让学习过程有反馈。 很多同学学英语最大的问题是"以为自己会了"。如果每次学完都能有一道自测题,立刻检验学习效果,学习效率会高很多。

第四,让技术成本趋近于零。 我刻意选择了纯前端方案:不写后端、不买服务器、不用数据库,只靠浏览器原生能力加上一个 AI 大模型接口来完成整个产品。

这四点构成了项目最初的愿景,也直接决定了后面的技术选型。


二、技术选型:为什么是 HTML5 + CSS3 + JavaScript

2.1 没有后端,也能做 AI 产品

在做技术选型的时候,我其实面临很多选择:可以用 React + Node.js,可以用 Vue + Python FastAPI,也可以用 Next.js 全家桶。但最后我选择了最朴素的组合——HTML5 + CSS3 + JavaScript,三个文件搞定一切。

选择纯前端的主要理由有三点:

第一,部署成本最低。 纯静态站点可以免费部署到任何静态托管平台,也可以直接双击 index.html 在本地打开。对于学生和初学者来说,这是最友好的交付方式。

第二,传播最方便。 一个 HTML 文件、一个 CSS 文件、几个 JS 文件,打包起来不到 50KB,放哪都能跑,同学之间互相传着用非常方便。

第三,聚焦核心逻辑。 这个项目的核心不是复杂的服务端架构,而是「AI 对话 + 数据渲染」两件事。用原生 JavaScript 写,反而能让人把注意力完全集中在 AI 接口的对接和前端渲染上,不被框架的复杂度干扰。

2.2 为什么接入 GitCode AI 大模型

做 AI 对话产品,最核心的是模型能力。我选择了 GitCode AI 平台提供的 deepseek-ai/DeepSeek-V4-Flash 模型。

选择它的原因:

  • 接口规范:兼容 OpenAI Chat Completions 协议,/v1/chat/completions 端点,所有主流语言都有现成的对接方式;
  • 流式支持:原生支持 SSE(Server-Sent Events)流式输出,可以让 AI 回复逐字显示,体验接近真实的打字聊天;
  • 中文能力强:作为国产开源模型,DeepSeek 系列在中文理解与生成上的表现非常出色,非常适合面向中国大学生的英语学习场景;
  • 成本友好:Flash 版本定位轻量快速,适合对话型应用。

2.3 技术架构总览

整个项目的技术架构非常简单清晰:

┌─────────────────────────────────────────────┐
│              浏览器(前端)                    │
│  index.html ── 页面骨架                       │
│  css/style.css ── 视觉样式                    │
│  js/config.js ── API 配置(密钥隔离)          │
│  js/system-prompt.js ── 系统提示词(JSON协议) │
│  js/render.js ── 卡片渲染 + 自测题交互         │
│  js/app.js ── 流式请求与对话管理               │
└────────────────────┬────────────────────────┘
                     │ HTTPS + SSE 流式
                     ▼
┌─────────────────────────────────────────────┐
│      GitCode AI · /v1/chat/completions        │
│      DeepSeek-V4-Flash(流式)               │
└─────────────────────────────────────────────┘

前端的五个 JS 文件各司其职:配置、提示词、渲染、主逻辑分离,满足单一职责原则,方便维护和扩展。


三、系统提示词设计:让 AI 输出结构化的 JSON

3.1 为什么对话应用需要结构化的返回

这里要讲一个产品设计上的关键决策。

如果只是把聊天窗口接上大模型 API,用户问一句 AI 答一句,那这个项目只是一个"套壳聊天机器人",没有任何竞争力。作为英语学习工具,我们需要的是结构化、可交互、可复用的学习内容

比如用户说"帮我速记 6 个四级高频词",我们希望 AI 返回的不是一大段散文,而是一个包含以下结构的 JSON:

  • 这一组单词的主题是什么?
  • 每个单词的音标、释义、例句、记忆技巧是什么?
  • 有没有额外的学习建议?
  • 能不能出一道自测题来检验学习效果?

这种结构化的数据有两个好处:

  1. 前端可以把数据渲染成精美的卡片,而不是让用户去读一大段没有层次的文字;
  2. 数据可以被二次处理,比如用户下次想要复习,可以直接复用之前的结构化数据生成复习卡片。

所以我在系统提示词里做了两件事:一是明确告诉 AI"你是大学英语速记助教",设定角色;二是用 JSON Schema 约束回复格式,并反复强调"只能输出 JSON,不允许输出任何其他文字"。

3.2 JSON 协议的详细设计

我在 js/system-prompt.js 中定义了系统提示词。核心 JSON 结构如下:

{
  "type": "word|grammar|sentence|writing|exam|other",
  "title": "不超过15字的标题",
  "summary": "一句话总结本组内容(20字内)",
  "cards": [
    {
      "head": "主词条/语法点/标题短语",
      "phonetic": "音标(单词类必填)",
      "meaning": "中文释义/规则讲解",
      "example": "英文例句",
      "example_cn": "例句中文翻译",
      "memory_tip": "速记技巧:词根词缀/联想/谐音/场景"
    }
  ],
  "tips": ["拓展速记技巧1", "拓展速记技巧2"],
  "quiz": {
    "question": "围绕本次内容出1道自测题",
    "options": ["A. 选项", "B. 选项", "C. 选项", "D. 选项"],
    "answer": 0,
    "explain": "答案解析"
  }
}

这里我想重点讲几个设计细节:

第一,type 字段是整个渲染体系的调度中心。 我定义了六种类型:

  • word:单词速记,一次 3~6 个词,每个词都要有音标和记忆技巧;
  • grammar:语法讲解,1~3 个语法点,用对比方式把规则讲透;
  • sentence:长难句剖析,一句句拆主干、找修饰;
  • writing:写作模板,2~4 段直接可套用的句式和框架;
  • exam:考试技巧与真题解析;
  • other:兜底类型,处理闲聊等非英语学习类问题。

前端拿到 type 之后,就可以决定用什么样式、什么图标、什么排版来渲染。这有点像前端路由——同一个数据结构,根据类型分发到不同的"页面组件"。

第二,cards 数组是内容的主体。 每个卡片代表一个独立的词条或知识点。设计上我要求每个卡片必须包含 head(主词条)和 meaning(释义),单词类还必须包含 phonetic(音标)和 memory_tip(记忆技巧)。这是内容的"最小完整性约束"。

第三,quiz 是产品差异化的关键。 我强制要求 AI 每次回复都必须附带一道自测题,包含 questionoptionsanswer(正确答案下标)、explain(解析)。这让产品从"给你讲"变成"讲完考你",形成学习闭环。前端收到后渲染成可点击的单选题,用户点选答案立刻给出对错反馈。

3.3 系统提示词的调优过程

系统提示词不是一次写对的,我经历了多轮调优:

第一版问题:AI 忍不住输出代码块。 一开始我在提示词里说"返回 JSON",结果模型经常用 Markdown 的 ```json 代码块包裹输出。前端解析时就遇到麻烦。后来我在提示词里明确写"不允许输出 JSON 以外的文字、解释、Markdown 代码块标记(如 ```)",并且在前端做了兼容——即使模型真的输出了代码块,也会自动剥离。

第二版问题:字段内容溢出。 有时候模型会把 meaning 写得很长,把 summary 写成一段话。我在提示词里加了括号约束:“不超过2行”“20字内”“不超过15字”,让输出更克制。

第三版问题:tips 字段类型不稳定。 观察实机输出时发现,模型偶尔把 tips 数组里的元素写成对象 {"tip": "..."} 而不是字符串。我做了双重保险——既在提示词里明确数据类型,又在前端渲染时兼容两种形式。

调优原则总结: 系统提示词要做到"程序化约束 + 前端兜底"双保险。不能指望大模型 100% 遵循格式,前端必须有能力处理不完美的情况。


四、Python 到 JavaScript:流式接口的迁移

4.1 原版 Python 实现

GitCode AI 平台给的示例是 Python 代码,使用 requests 库实现流式读取:

import json
import requests

API_URL = "https://api-ai.gitcode.com/v1/chat/completions"

def query(payload):
    response = requests.post(API_URL, headers=headers, json=payload, stream=True)
    for line in response.iter_lines():
        if not line.startswith(b"data:"):
            continue
        if line.strip() == b"data: [DONE]":
            return
        yield json.loads(line.decode("utf-8").lstrip("data:"))

chunks = query({...})

for chunk in chunks:
    print(chunk["choices"])

这是一个典型的 SSE 流式解析:requests 开启流模式后,逐行读取响应;只处理以 data: 开头的数据行;遇到 data: [DONE] 就停止;每一行都是一段 JSON,用 json.loads 解析后 yield 出去。

我的任务,就是把这套逻辑用 JavaScript 完整复刻到浏览器里。

4.2 JavaScript 的关键差异

JavaScript 前端实现流式请求,有几个 Python 版本没有的问题:

第一,没有 requests 库,用什么发请求? 答案是浏览器原生的 fetch API。注意 fetch 默认拿到的 response.body 是一个 ReadableStream(可读流),这是实现流式的关键。

第二,iter_lines 怎么替代? Python 的 response.iter_lines() 会自动按行切分。而 JavaScript 的流是字节流,需要我们自己维护一个缓冲区,把不完整的行暂存起来,等收到换行符再切分。

第三,编码问题。 Python 的 iter_lines 已经处理了 UTF-8 解码。JS 需要手动用 TextDecoder("utf-8") 处理,并且要注意流式场景下的多字节字符边界问题——一个中文字符可能被拆到两个 chunk 里,缓冲区机制可以自然解决这个问题。

4.3 我的 JavaScript 实现

为了最大程度保持代码的清晰度,我实现了一个异步生成器 streamQuery。JavaScript 同样支持生成器,和 Python 的 yield 一一对应:

async function* streamQuery(payload) {
  const headers = {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${CONFIG.API_KEY}`
  };
  const response = await fetch(CONFIG.API_URL, {
    method: "POST",
    headers,
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    const errText = await response.text();
    throw new Error(`HTTP ${response.status}: ${errText}`);
  }
  if (!response.body) throw new Error("当前浏览器不支持流式响应");

  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const lines = buffer.split("\n");
    buffer = lines.pop();

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith("data:")) continue;
      let data = trimmed.replace(/^data:\s*/, "");
      if (data === "[DONE]") return;
      try {
        yield JSON.parse(data);
      } catch (err) {
        // 跳过不完整的 JSON 行
      }
    }
  }
}

这段代码我在几个关键点上做了防御:

  1. 缓冲区设计buffer += decoder.decode(value, {stream: true}) 维护一个累积缓冲区,split("\n") 之后把最后一段(可能不完整)重新放回缓冲区,保证行切分永远不丢数据。
  2. [DONE] 处理:注意到这个端点的返回可能是不带空格或带空格的 data:[DONE] / data: [DONE],我用正则 replace(/^data:\s*/, "") 统一剥掉 data: 前缀和空白,确保两种情况都能识别。
  3. 容错解析json.loads 在 JS 里对应 JSON.parse,但流式场景下偶尔会解析失败,我用 try/catch 静默跳过,保证主流程不中断。

4.4 前端的使用方式

app.jssendMessage 里,我用 for await 消费整个流:

for await (const chunk of streamQuery(payload)) {
  const delta = chunk.choices && chunk.choices[0] && chunk.choices[0].delta;
  if (delta && delta.content) {
    fullText += delta.content;
    typingUpdate("正在生成回复…");
  }
}

收到每个 chunk 时,从 choices[0].delta.content 累加内容。期间界面上显示打字动画;流结束后,把完整内容交给解析器,判断是结构化 JSON 还是普通文本,再走不同的渲染分支。

这里要特别说明:模型支持思维链(reasoning_content),第一个 chunk 里可能只有 reasoning_content 没有 content,所以判断条件里必须加 if (delta && delta.content),只有真正的内容增量才累加,否则会把思维过程拼进正文。


五、前端渲染:从 JSON 到精美卡片

5.1 渲染的整体架构

js/render.js 负责把 AI 返回的 JSON 渲染成 HTML。核心函数是 cardToMarkdown(data),流程是:

  1. 校验数据是否合法;
  2. 根据 type 字段决定类型标签的文案和图标;
  3. 依次渲染标题区、卡片列表、拓展技巧、自测题;
  4. 所有输出经过 escapeHtml 转义,防止 XSS 注入。

5.2 卡片渲染细节

以单词类型为例,渲染出来的每个卡片包含:

  • 词头行:单词本体(加粗高亮)+ 音标(灰色衬线字体);
  • 释义行:中文释义;
  • 例句区:英文例句(斜体)+ 中文翻译(浅色);
  • 速记技巧:左侧橙色色条 + 淡黄背景的记忆技巧框,视觉上与其他内容区分。

拓展技巧区用蓝色左边框的"速记小贴士"盒子,自测题用虚线边框的"考考你"盒子——不同内容区块有鲜明的视觉锚点。

5.3 自测题的交互实现

自测题渲染成四个可点击的选项,每个选项带 data-idx 属性记录下标。点击时调用全局函数 checkAnswer

function checkAnswer(el, answerIdx) {
  const box = el.closest(".quiz-box");
  const options = box.querySelectorAll(".quiz-option");
  const explain = box.querySelector(".quiz-explain");
  const chosen = Number(el.dataset.idx);
  options.forEach((o) => o.classList.remove("quiz-correct", "quiz-wrong"));
  if (chosen === answerIdx) {
    el.classList.add("quiz-correct");
  } else {
    el.classList.add("quiz-wrong");
    options[answerIdx] && options[answerIdx].classList.add("quiz-correct");
  }
  if (explain) explain.style.display = "block";
}

交互逻辑:选中正确项变绿,选中错误项变红并高亮正确答案,同时展开解析文字。这个"即时反馈"机制让学习变成游戏化的过程,对记忆强化非常有效。

5.4 降级渲染:AI 不听话的时候怎么办

再完美的提示词,也不能保证大模型 100% 按格式输出。所以 parseReply 做了两层降级:

  1. 如果 AI 用 Markdown 代码块包裹 JSON(json ... ),自动剥离包裹;
  2. 如果整个输出都不是合法 JSON(比如用户闲聊,模型直接回文本),就降级走 renderFallback,把纯文本做简易 Markdown 渲染(加粗行内代码、换行)。

这样的设计保证了:即使模型输出完全偏离协议,用户也永远看不到"解析失败"的报错弹窗,产品体验始终是平滑的。


六、视觉设计:美感和可用性并重

6.1 色彩体系

我以蓝色为主色调搭建了整套视觉体系:

  • 主色 #3b6cf6,渐变到 #6a5cf6(紫蓝渐变),用于按钮、Logo、用户气泡、类型标签;
  • 背景使用 #f0f4fb#eaf2ff 的柔和渐变,让页面不刺眼;
  • 内容卡片用白色 + 浅蓝描边 #e3e9f4,保证内容区干净利落;
  • 辅助色:橙色 #f59e0b(记忆技巧)、绿色 #22c55e(正确反馈)、红色 #ef4444(错误反馈)。

整个配色走"清爽、学院、年轻化"的路线,和"大学"这个用户群体贴合。

6.2 交互体验细节

  • 打字动画:AI 思考期间显示三个跳动的小圆点,用 CSS @keyframes blink 实现,模拟实时打字;
  • 流式占位:生成过程中显示"正在生成回复…",结束后替换为正式内容;
  • 快捷提问:四个高频场景按钮(速记高频词、语法辨析、长难句、作文模板),一键填充问题并发送,降低使用门槛;
  • 移动端适配:通过媒体查询,在窄屏下隐藏副标题、让快捷栏横向滚动、气泡宽度占 88%,保证手机上也能流畅使用。

七、安全与密钥管理

7.1 密钥绝不上传到仓库

这类项目的安全隐患,多半出在 API Key 泄露。我的处理方案是:

  1. 真实密钥放在 js/config.js,该文件被 .gitignore 明确忽略;
  2. 仓库里只提交 js/config.example.js 模板,里面的 API_KEY 是占位符;
  3. README 中专门用警示框标注:请复制模板再填入自己的令牌,不要把密钥提交到仓库。
cp js/config.example.js  js/config.js   # 然后编辑填入真实 Key

7.2 前端注入防护

AI 生成的内容直接插入页面 DOM,这是 XSS 的高危场景。我在渲染函数的输出端点统一做了 escapeHtml 转义(&<>"'),确保所有文本内容按字面显示,杜绝脚本注入。

这两点防御做完之后,项目的安全性就有了基本保障。当然,纯前端方案天然暴露 API Key 给用户,这在公开部署时仍然是一个权衡取舍,适合个人学习场景;生产级应用建议再加一层后端代理做密钥中转。


八、测试验证:用自动化浏览器跑真实对话

代码写完不能拍脑袋说"能用",我做了两层验证。

8.1 接口层验证

先用 curl 直接打 GitCode AI 接口,验证模型是否按照系统提示词返回合法 JSON:

curl -sN https://api-ai.gitcode.com/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -d '{"model":"deepseek-ai/DeepSeek-V4-Flash", ...}'

然后用 Node 脚本解析整个流,断言 JSON 合法、type 正确、cardsquiz 齐全。实测模型稳定返回 {"type":"word", ...} 的合法结构。

这是我实测的部分返回(节选):

{
  "type": "word",
  "title": "6个四级高频词速记",
  "cards": [
    {
      "head": "abundant",
      "phonetic": "/əˈbʌndənt/",
      "meaning": "adj. 丰富的;充裕的",
      "example": "The region is abundant in natural resources.",
      "example_cn": "该地区自然资源丰富。",
      "memory_tip": "词根记忆:ab(加强) + und(波浪) + ant(…的) → 像波浪一样涌来 → 丰富的。"
    }
  ],
  "quiz": {
    "question": "The country has ____ supplies of fresh water.",
    "options": ["A. abundant", "B. abandoned", "C. absent", "D. absurd"],
    "answer": 0,
    "explain": "abundant 意为“丰富的、充裕的”,符合题意。"
  }
}

8.2 浏览器端到端验证

我用 Playwright 驱动真实 Chromium 浏览器,模拟用户点击快捷按钮、等待 AI 回复、点击自测题选项的完整流程:

  • 断言页面上出现用户气泡;
  • 等待 AI 回复完成(等待 .quiz-box 元素出现);
  • 统计词汇卡片数量和自测选项数量;
  • 模拟点击第一个选项,验证对错反馈出现;
  • 再发送第二条文本消息,验证多轮对话正常;
  • 全程监听控制台错误。

验证结果:结构化卡片渲染成功(6 张词汇卡片 + 4 个自测选项)、选择题交互正常、控制台零报错。

8.3 验证中发现并修复的 Bug

自动化测试帮我发现了两个真实 Bug:

Bug 1:冗余的 renderCard 调用。 一度在 app.js 里残留了一行 if (result.data) renderCard(result.data),但 render.js 里根本没有这个函数定义,导致运行时抛出 ReferenceError: renderCard is not defined。测试日志抓到了它,随即删除。这也印证了自动化测试的重要性——单纯人工看代码很难发现这种问题。

Bug 2:tips 字段类型不稳定。 模型有时把 tips 数组元素返回成对象 {"tip": "..."},我在渲染层加了一行兼容逻辑:typeof t === "string" ? t : (t && t.tip) || "",彻底解决。


九、项目目录结构

最终的项目结构非常精简:

├── index.html              # 页面入口(顶栏/聊天区/快捷栏/输入框)
├── favicon.svg             # 站点图标
├── css/
│   └── style.css           # 全局样式(卡片/自测题/响应式)
├── js/
│   ├── config.example.js   # 配置模板(复制为 config.js)
│   ├── config.js           # 本地配置(含 API Key,不入库)
│   ├── system-prompt.js    # AI 系统提示词(JSON 协议)
│   ├── render.js           # JSON → HTML 卡片渲染
│   └── app.js              # 聊天逻辑:流式请求 + 解析
├── .gitignore              # 忽略本地密钥配置
├── README.md               # 项目说明文档
└── docs/blog/              # 博客与文档

五个 JS 文件按职责清晰拆分,每个文件单一职责,新手也能快速看懂。


十、使用方式

10.1 配置

cp js/config.example.js js/config.js

编辑 js/config.js,把 API_KEY 换成自己的 GitCode AI 令牌。

10.2 运行

python3 -m http.server 8080 --directory .
# 或
npx serve .

浏览器打开 http://localhost:8080 即可使用。

10.3 典型使用场景

  • 点「速记高频词」:一键获得 6 个高频词 + 记忆技巧 + 自测题;
  • 问"定语从句和非谓语动词有什么区别":获得对比式语法解析;
  • 点「长难句」:获得主干拆解 + 翻译策略;
  • 点「作文模板」:获得万能句型和框架。

十一、踩坑记录

开发过程中积累了一些经验,写在这里供后来者参考:

  1. SSE 必须用 response.body.getReader(),不要用 response.text() 一次性读完——那会丢失流式体验;
  2. data: 前缀可能带空格也可能不带,用正则统一剥离最稳妥;
  3. 多字节字符会被拆到不同 chunk,缓冲区 + TextDecoder(stream:true) 是关键;
  4. 首次 chunk 可能只有 reasoning_content,判断内容增量必须检查 delta.content 是否存在;
  5. 模型输出的 tips 等数组元素类型可能漂移,前端渲染要做类型兼容;
  6. 浏览器直接 file:// 打开可能因 CORS 失败,本地开发务必起 HTTP 服务;
  7. API Key 必须通过 .gitignore 隔离,一旦提交到公开仓库就成了安全事故;
  8. 自动化测试帮助发现了手测发现不了的报错,比如那个残留的 renderCard 调用。

十二、总结与展望

12.1 项目亮点回顾

「大学英语速记」用最轻量的技术栈,做出了一个体验完整的 AI 学习产品:

  • 纯前端、零依赖、零后端:三个技术,五个 JS 文件,最后打包不过几十 KB;
  • 结构化 AI 协议:用系统提示词把大模型的输出约束成 JSON,让 UI 可以渲染成精美的学习卡片;
  • 学习闭环:速记 → 技巧 → 自测题 → 解析反馈,一个很完整的学习动线;
  • 全流程验证:接口层 + 浏览器端自动化双保险,实测通过;
  • 安全合规:密钥隔离、XSS 防御,好事做在前面。

12.2 后续演进方向

这个项目还留有充分的扩展空间,列几个我考虑过的方向:

  1. 加入记忆曲线复习:利用本地 localStorage 存储学过的词,按艾宾浩斯遗忘曲线定时提醒复习;
  2. 支持语音朗读:接入 Web Speech API,让 AI 生成的内容可以发音,练听力练口语;
  3. 多模型切换:把模型名做成可选项,方便对比不同模型的效果;
  4. 导出学习笔记:把生成的单词卡片一键导出为 Markdown 或图片,方便打印;
  5. 用户画像与错题本:记录每次自测题的错误,自动生成错题本延后重测。

12.3 写在最后

这个项目从立项到上线,走完了一个完整的小产品闭环:想法 → 技术选型 → 协议设计 → 编码 → 测试 → 部署 → 文档。它很好地印证了一件事:AI 时代,做产品的最小成本可以非常低。 不需要服务器,不需要团队,一个人、一台电脑、一份热情,加上一个靠谱的大模型接口,就能做出对大家有真实帮助的工具。

如果你也是大学生,或者正想学前端、学 AI 应用开发,我建议你也从这样一个小而美的项目开始:找一个具体的痛点,用最简单的技术栈做出来,跑起来,让真实用户给你反馈。技术能力是在一次又一次"把它做出来"的过程中增长的,而不是看一百遍教程。

愿我们都能在码道上稳步前行,用代码解决真实世界的问题。

项目地址:https://gitcode.com/zhjdwk1018/zmjkk
技术栈:HTML5 · CSS3 · JavaScript · GitCode AI (DeepSeek-V4-Flash) · SSE 流式


本文由「码道 · 大学英语速记」项目作者撰写,记录 AI 学习类 Web 应用从零到一的完整开发过程。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐