项目仓库:https://atomgit.com/feng8403000/ruanjian6demo_0918

本文完整记录了「成语接龙 · AI 对弈」这个 Web 小游戏的从零开发过程:从一段 Python 请求示例开始,到 HTML5 + CSS3 + JavaScript 落地成型,覆盖需求分析、技术选型、界面设计、Python 到 JavaScript 的代码迁移、系统提示词与 JSON 数据协议设计、流式对话实现、自动化测试验证,以及过程中的踩坑与反思。项目代码已开源在 AtomGit 仓库,欢迎围观与指正。


运行实例效果:
在这里插入图片描述
使用工具:
在这里插入图片描述

一、项目缘起:为什么是成语接龙?

做这个项目之前,我一直在想:怎样的 AI 应用,既有文化底蕴,又能把技术栈完整地跑一遍?

成语,是汉语中历经千年沉淀下来的语言结晶。四字一句,言简意赅,字里行间藏着典故与智慧。而成语接龙,则是几乎每个中国人都玩过的文字游戏——一个人说出"守株待兔",下一个人必须以"兔"字(可同音)开头,接出"兔死狐悲",如此往复,妙趣横生。

传统成语接龙的最大痛点是什么?需要有一个判官。谁来判断你接的成语是否真实存在?谁来指出你接错了字?谁来在你卡壳的时候给你提示?如果只有两个玩家对坐,常常会因为"这到底算不算一个成语"而争执不休。

而大语言模型(LLM)恰好能解决这个问题。它熟读典籍,对成语几乎无所不知,既能准确判断成语的真实性,又能飞快地接出新词,还能在玩家出错时温文尔雅地给出引导。可以说,AI 就是成语接龙最理想的对手与裁判。

于是一个想法逐渐清晰起来:做一个网页版的成语接龙游戏,让玩家与 AI 对弈,用大模型的自然语言能力撑起整个对局的智能判断。于是在 2026 年 9 月的软件工程课堂实践课上,这个项目正式立项了。

二、需求分析与目标设定

好的项目始于清晰的需求。动工之前,我把这个项目的需求拆成了三个层次,力求"麻雀虽小,五脏俱全"。

2.1 核心玩法需求

  • AI 先手开局,说出第一个四字成语,并给出拼音与释义;
  • 玩家在输入框中说出一个四字成语,该成语必须以 AI 上一成语的**最后一个字(允许同音)**开头;
  • AI 校验玩家成语:若合法,则用玩家成语的尾字接出新的成语;若不合法,明确指出错误原因并给出引导;
  • 整局对弈中,已经出现过的成语不得再次使用;
  • 支持随时重新开局。

2.2 界面与交互需求

  • 中国风视觉风格,区别于千篇一律的现代扁平 UI;
  • 对局区采用聊天气泡形式,AI 与玩家分列左右;
  • AI 回复的关键信息(成语、拼音、释义、待接之字)要结构化、醒目地呈现;
  • 输入框在 AI 回复期间应禁用,防止连发造成状态错乱。

2.3 技术需求

  • 纯前端实现,无构建工具、无框架依赖,双击即可运行——便于教学演示;
  • 调用 GitCode 提供的 DeepSeek V4 Flash 大模型接口完成 AI 对话;
  • 采用流式(SSE)方式接收 AI 回复,还原"逐字生成"的实时体验;
  • 网络异常、接口异常时要有友好的出错兜底。

需求梳理完毕后,我意识到这个项目最核心的技术命题有两个:一是如何让 AI 输出结构化数据,方便前端渲染;二是如何把一段 Python 的流式请求示例,翻译成 JavaScript 可以运行的真实代码。 这两点将在后文重点展开。

三、技术选型:为什么是原生"三件套"?

技术选型时,我的面前其实有三条路:

  1. Vue / React 框架:组件化开发方便,生态成熟,但需要 Node.js 环境、构建工具链,对新手不够友好;
  2. jQuery 等老牌库:能压缩代码量,但在这个年代显得有些过时;
  3. 原生 HTML5 + CSS3 + JavaScript:零依赖、零构建、零安装,一个浏览器即可开箱即用。

最终我选择了原生三件套,理由有三:

其一,教学属性。 这个项目是给软件工程班同学做演示的"活的示例"。使用原生技术,意味着任何一位同学拿到代码都能直接看懂,浏览器打开就能运行,不需要先搭建一套工程环境。

其二,掌控感。 用原生 JS 手写 fetch 流式解析、手写 DOM 渲染,虽然代码量略大,但对每一行代码的行为都有了完全的控制力,这在教学演示中尤为重要——我可以逐行向同学解释"这里为什么要这样做"。

其三,极简部署。 整个项目只有 5 个文件,任何静态服务器(哪怕是 python -m http.server)都能托管,甚至可以直接放到 GitHub Pages、AtomGit Pages 这类免费静态托管平台上,实现"一条命令上线"。

至于 AI 能力,则交给大模型——项目通过调用 DeepSeek V4 Flash 模型(经由 GitCode AI 开放平台接口),以 HTTP 请求的方式获得自然语言理解与生成能力。前端负责的是"连接"与"呈现",AI 负责的是"思考"与"表达",各司其职,架构干净。

四、界面设计:一砚水墨,两分雅意

成语是国粹,界面的气质自然要向传统美学靠拢。在设计 CSS 时,我刻意避开了高饱和的现代配色,转而构建了一套"宣纸 + 朱砂 + 墨色 + 鎏金"的色彩体系:

  • 宣纸米白(#faf6ec):全局底色,模拟宣纸的温润质感;
  • 墨色(#2b2620):正文主色与头像底色,沉稳厚重;
  • 朱砂红(#a33327):点睛之笔,用于按钮、"AI 出牌"徽章、待接字高亮;
  • 鎏金(#b08d3e):背景装饰光晕,低调而雅致。

背景使用了两层径向渐变光晕叠加在宣纸渐变之上,模拟传统国画中"远山淡影"的朦胧氛围;标题"成语接龙"四个大字采用衬线字体并放大字距,配合一枚朱红底色的"AI 对弈"圆角徽章,古今融合,庄重中透着活泼。

界面自上而下分为四个区域:

  1. 标题区:主标题 + 副标题"君出一语,吾来接之",点明玩法;
  2. 规则条:左侧一枚"规"字印章式图标,中间一句话说明规则,右侧是"重新开局"按钮——把规则与操作放在一起,玩家无需阅读文档即可上手;
  3. 对局区:横向滚动区域内的所有消息,AI 消息靠左、玩家消息靠右,视觉上形成一来一回的对话节奏;
  4. 输入区:圆角输入框 + 朱红渐变"出 招"按钮,按钮按下时还带有轻微下压的触感反馈。

最值得一提的,是 AI 成语卡片 的设计。它是整个对局的"信息中心",我把它做成了一张独立的卡片,用一枚"AI 出牌"的红色缎带徽章悬在卡片左上角。卡片内部按"从上到下、从大到小"的信息层级依次呈现:

成语大字(约 30px,字距拉开)→ 带声调拼音(灰色小字)→ 一句话释义 → 虚线分隔 → 接龙反馈行

其中"接龙反馈"行是最关键的交互锚点:「下一字请以 人 开头(可同音)· 接得漂亮!」,待接之字用朱红色加粗显示,玩家一抬眼就知道自己接下来要接什么字。整张卡片再配以柔和的阴影与圆角,颇有几分"古籍批注笺纸"的韵味。

在响应式方面,项目适配了小屏手机:标题缩小、气泡宽度放大,保证在 375px 宽的手机屏幕上也能舒服地对弈。

五、AI 接口集成:从 Python 到 JavaScript 的迁移

项目最初拿到的,是一段 Python 参考代码,用于调用 GitCode 的 AI 对话接口。这段代码长这样:

import requests

API_URL = "https://api-ai.gitcode.com/v1/chat/completions"
headers = {"Authorization": f"Bearer {API_KEY}"}

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:").rstrip("/n"))

chunks = query({...})
for chunk in chunks:
    print(chunk["choices"])

看到这段代码的第一反应是:幸好逻辑不复杂。 它的核心就三件事——发起 POST 请求、按行读取流式响应、解析 JSON 数据帧。迁移到 JavaScript 时,我做了如下一一对应:

Python 参考代码JavaScript 等价实现
requests.post(url, headers=..., json=payload, stream=True)fetch(url, { method: "POST", headers, body: JSON.stringify(payload) })
response.iter_lines()response.body.getReader() + TextDecoder 逐行切分
line.startswith(b"data:")trimmed.startsWith("data:")
line.strip() == b"data:[DONE]"data === "[DONE]"
json.loads(...)JSON.parse(...)
yield chunk 生成器async 函数 + 传入 onFullText 回调

在 JavaScript 中,流式读取的实现思路是:通过 response.body.getReader() 拿到一个异步可读流,反复调用 read() 方法获取字节块,再用 TextDecoder 解码成字符串。由于网络分片可能在任意位置切断一行数据,这里必须维护一个 buffer 变量:每次把新解码的文本追加进 buffer,再按 \n 分割,最后一段不完整的行保留在 buffer 中等待下一轮补齐。这个"留行尾、补下一帧"的小技巧,是整个流式解析最关键的细节,也是初学者最容易踩的坑。

此外还要处理几个协议细节:

  • 非 data: 开头的行直接跳过(可能是注释帧或空行);
  • data: [DONE] 是流的结束标记,读到即代表输出完毕;
  • 单个数据帧可能是被截断的半个 JSON,JSON.parse 会抛异常,此时直接 continue 等下一帧拼接完整。

特别值得说明的是 chatStream 函数的封装。我用它把"流式请求 → 逐帧解析 → 追加文本 → 回调通知"这一整条链路收敛成一个返回完整文本的 Promise。上层对局引擎只需传入消息列表和一个回调函数,就能以"打字机"的方式实时拿到 AI 的生成内容。这也让后续的测试变得非常容易。

六、核心设计:系统提示词与 JSON 数据协议

迁移代码只是第一步。真正让这个项目"活"起来的,是系统提示词的设计——它决定了 AI 以什么格式说话,也就决定了前端如何展示。

6.1 为什么要让 AI 返回 JSON?

AI 的自由文本输出虽然自然,但会给前端渲染带来麻烦:成语、拼音、释义混在一大段话里,CSS 无从下手,数据也无从提取。既然 DeepSeek 拥有出色的指令遵循能力,我完全可以约定一种结构化协议——让 AI 每轮只返回一个 JSON 对象,把"成语、拼音、释义、待接字、提示"这些字段拆得清清楚楚。前端拿到 JSON 后,一条 .idiom-card 模板即可完成渲染,既稳定又美观。

6.2 JSON Schema 的约定

经过多轮调整,我把返回格式固定为以下结构:

{
  "valid": true,
  "reason": "",
  "ai_idiom": "AI 接出的成语",
  "ai_pinyin": "带声调全拼,如 shǒu zhū dài tù",
  "ai_meaning": "一句话释义",
  "ai_first_char": "AI 成语的第一个字",
  "ai_last_char": "AI 成语的最后一个字(玩家下一轮需接的字)",
  "hint": "对玩家的提示或鼓励"
}

字段语义上有两个核心设计:

valid 双态语义是整局对弈的状态中枢。valid: true 表示本次回合通顺有效——AI 成功校验并接出了新成语,此时 ai_idiom 等字段填充完整;valid: false 表示玩家上一步有误——此时 reason 说明错误原因(“不是成语” / “应以×字开头” / “该成语已经用过”),ai_idiom 等字段置为空串,hint 给出引导话语。前端收到 valid: false 时渲染成一张"纠错提示气泡",lastChar 保持不变,等待玩家重新出招。

ai_last_char 驱动状态流转。每当 AI 成功接龙,我把它返回的 ai_last_char 写入对局状态,作为下一轮构建请求时的"应接之字",并展示在卡片上。如此一局接一局,状态自然流转,不需要前端维护任何成语词典。

为了让 AI 严格遵循协议,系统提示词中我反复强调了三件事:只输出一个 JSON 对象、不要任何 Markdown 代码块标记、字段必须完整。同时在提示词里完整地给出了 Schema 示例与字段说明。实践证明,DeepSeek V4 Flash 对这类指令遵循得相当好——在实测中,它连续多轮都输出了完全符合约定的 JSON。

6.3 上下文组装

由于接口是无状态的,每一轮请求都必须携带足够的上下文。我通过 buildMessages() 函数把"系统提示词 + 玩家本轮出招 + 当前应接之字 + 本局已用成语列表"打包进一次请求:

玩家说出成语:"守株待兔"
当前应以"兔"字(可同音)开头接龙。
本局已用成语:画蛇添足 → 足智多谋 → 谋事在人

把"已用成语列表"显式地告诉 AI,是避免重复成语的核心手段——AI 在生成新词时会主动回避列表中的成语。开局时则发送一条特殊指令,要求 AI 先手说出第一个成语。

6.4 容错解析:parseAiJson

尽管提示词三令五申,真实世界中 AI 偶尔还是会"调皮"——比如把 JSON 包进 `` ```json `````代码块里,或者在前缀多说一句话。因此写一个稳健的 parseAiJson 函数是必须的,其策略是:

  1. 用正则剥离所有 Markdown 代码块标记;
  2. 截取第一个 { 到最后一个 } 之间的内容;
  3. 尝试 JSON.parse,失败则返回 null,由上层兜底降级为"原始文本气泡"。

这套容错策略在后续的自动化测试中起到了决定性作用——多种"不规整"的 AI 输出都能被正确解析。

七、对局引擎:状态机与渲染

讲完了"AI 如何说话",再讲"前端如何对弈"。对局引擎维护了一个极简的状态对象:

const state = {
  usedIdioms: [],  // 本局已用成语(按序)
  lastChar: "",    // 当前应接之字
  waiting: false   // 是否正在等待 AI 回复
};

三个字段各有分工:usedIdioms 用于本地去重记录与上下文透传;lastChar 是每一轮接龙的"门槛";waiting 是一把互斥锁——置位期间输入框与按钮全部禁用,防止玩家连发导致 AI 回复错位。

玩家出招 playerMove() 的主流程是:

  1. 渲染玩家气泡(首字放大,呼应接龙逻辑);
  2. 渲染一个"AI 正在提笔接龙…"的思考占位气泡(三个小红点循环闪烁);
  3. 发起 chatStream 流式请求;
  4. 每个数据块到达时,updatePreview 尝试实时解析——一旦从累积文本中解出 ai_idiom,立即把思考占位替换成一张完整的成语卡片;
  5. 流结束后做最终解析:valid: true 则结算状态(追加 usedIdioms、更新 lastChar);valid: false 则渲染纠错气泡;解析失败则抛出异常进入兜底分支;
  6. 无论成败,finally 中释放互斥锁、恢复输入框焦点。

这里有一个值得玩味的交互细节:流式期间实时渲染卡片。因为 AI 输出的 JSON 字段基本按 Schema 顺序生成,ai_idiom、ai_pinyin、ai_meaning 会逐个"蹦"出来——用户在卡片上能亲眼看到成语字字浮现,比等全部生成完再一次性渲染要有趣得多,这正是流式体验的价值所在。

开局 startGame() 与玩家出招共用同一套流式链路,只是发送的是"对局开始"指令。页面加载即自动开局,无需任何点击。

八、测试与验证:不止是"能跑"

代码写完之后,我没有急着交差,而是做了三层验证,确保它"真的能跑、跑得对、跑得稳"。

8.1 静态与逻辑层:Node 单测

项目虽然是浏览器脚本,但核心逻辑(系统提示词、parseAiJson、buildMessages、chatStream 的 SSE 解析、状态结算)都可以脱离 DOM 测试。我写了一个临时测试脚本,用 vm 模块在 Node 中模拟出 document、fetch 等浏览器环境,再注入真实的 app.js 源码执行,覆盖了五个用例:

  • 系统提示词确实包含 JSON 字段约定;
  • parseAiJson 能解析被代码块包裹、带前后缀文字的"不规整输出";
  • 开局与玩家出招两种场景的上下文组装正确;
  • 用三段模拟 SSE 分片(含 [DONE] 结束帧)验证 chatStream 拼接出完整 JSON;
  • 调用 finalizeAiReply 后,lastChar 与 usedIdioms 状态正确更新。

五个用例全部通过。这个"跑在 Node 里的浏览器逻辑"测试方案,后来成了我迭代的护身符——每次改动代码都先跑一遍,心里踏实。

8.2 接口层:真实 API 联调

逻辑测试通过后,我用 curl 对真实接口做了两次联调,验证两个关键问题:跨域是否允许、返回结构是否符合预期。

第一次发现接口返回了 Access-Control-Allow-Origin 响应头(反射 Origin),确认浏览器跨域可以直连。第二次完整模拟了一局接龙:告知 AI"玩家说出成语:守株待兔,请以兔字开头接龙",AI 流式返回了如下内容(提取关键字段):

ai_idiom    → 兔死狐悲
ai_meaning  → 兔子死了,狐狸感到悲伤。比喻因同类的死亡而感到悲伤。
ai_first_char → 兔
ai_last_char  → 悲

“守株待兔” → “兔死狐悲”,接龙完全正确,JSON 结构与约定逐一相符。 这次联调让我对整个方案建立了信心。

8.3 端到端层:无头浏览器全流程

最后用 Puppeteer(无头 Chromium)做端到端验证:打开页面 → 等 AI 开局 → 解析卡片上的待接字 → 输入成语并点击出招 → 等待第二轮回复。真实跑出来的第一局是:

  • AI 开局:「画蛇添足」
  • 待接字「足」,玩家输入「足智多谋」
  • AI 回复:「谋事在人」——拼音、释义、待接字「人」一应俱全

第二局的错误链路测试同样精彩:AI 开局「一帆风顺」,词库中没有以「顺」开头的备选词,玩家兜底输入了「一心一意」(未按规矩接龙),AI 立刻返回 valid: false 并渲染出纠错提示块——错误处理链路同样工作正常。全程监控浏览器 console,无任何 JS 运行时错误,仅有的一次 favicon 404 也被我用一枚内联 SVG 印章 favicon 消除了。

三层验证下来,我才放心地把代码提交到了仓库。

九、踩坑复盘

开发过程中的坑不少,挑几个有代表性的记录如下,供后来者参考。

坑一:JSON 输出被代码块包裹。 最初几轮 AI 总是习惯性地把 JSON 包在 `` ```json `````里再输出,前端解析自然失败。解决分两步:提示词里反复禁止使用代码块;parseAiJson 里做兜底剥离。两条腿走路,从此再没翻车。

坑二:流式数据帧被截断。 SSE 的 data: 帧在网络传输中可能被任意切断,直接 JSON.parse 会报错。解决方式是维护缓冲行 + 解析失败即跳过,等下一帧数据补齐。这个坑几乎每个做流式对接的人都必踩一次,值得记熟。

坑三:thinking 模式下的内容延迟。 DeepSeek 模型开启思考(thinking_budget)后,流式响应会先输出 reasoning_content(思考过程),真正的 content 会晚一些才出现。前端如果只监听 delta.content,会有一段时间"看起来没反应"。我的处理是保留思考占位气泡动画,让等待过程不枯燥,同时忽略 reasoning_content 不渲染——思考过程没必要展示给玩家。但这也提醒我:如果业务对首字延迟敏感,可以适当调低 thinking_budget 或采用非思考模式。

坑四:无头浏览器 API 差异。 测试脚本里 page.waitForTimeout 在 puppeteer-core 的高版本中已被移除,导致脚本抛"is not a function"。排查后改用 setTimeout 包装的等待函数解决。这类环境 API 变动,几乎每个自动化脚本都会遇到,习惯就好。

十、如何使用与部署

项目是纯静态文件,全项目仅 5 个文件、零依赖:

ruanjian6demo_0918/
├── index.html        # 页面入口
├── css/style.css     # 水墨风样式
├── js/config.js      # AI 接口地址与模型参数
├── js/app.js         # 接龙逻辑 + AI 流式对话
└── README.md         # 项目文档

方式一:直接双击 index.html 即可运行(浏览器通常允许 file:// 下的脚本调用 HTTPS 接口)。

方式二:本地静态服务器(推荐):

python -m http.server 8000
# 或
npx serve .

然后访问 http://localhost:8000。

部署上线:整个目录可直接上传到任意静态托管平台(AtomGit Pages / GitHub Pages / 对象存储 + CDN),改一下 API 密钥即可对外发布。

十一、安全警钟:前端密钥的边界

写到这里必须泼一盆冷水。为了教学演示,API 密钥直接写在了 js/config.js 里——任何一位访问者按 F12 都能看到这串秘钥。这是前端应用的原罪:所有前端代码对用户都是透明的。

因此我在 README 中明确标注:本项目密钥仅限教学演示;若用于生产环境,必须把密钥移到服务端,用一个轻量后端(如 Node.js / FastAPI)代理 AI 接口,由服务端持有密钥,并对调用方做鉴权、限流与审计。放心的地方是,这只影响了安全边界,并不影响代码架构——前端只需把请求 URL 指向自己的代理即可,chatStream 一行都不用改。

十二、总结与展望

回望整个项目,最有成就感的瞬间,是看到 AI 稳稳地接出"谋事在人"的那一刻——一条用代码编织的、跨越了 Python 与 JavaScript 两种语言的成语链,就这样在浏览器里流动了起来。

这个项目让我收获颇丰:

  • 理解了流式协议的底层机制:SSE 的帧结构、缓冲行的处理、[DONE] 结束标记,这些底层细节不再是云里雾里的概念;
  • 体会了"协议先行"的工程价值:系统提示词 + JSON Schema 这套自洽的协议设计,让 AI 这位"不可控的队友"变得高度可控,也让前后端(人机)两端的认知成本降到最低;
  • 建立了"测试兜底"的工程习惯:逻辑单测、接口联调、端到端自动化三层验证缺一不可,"能跑"和"跑得对"之间隔着一条鸿沟。

未来可以继续演进的方向,我列了一个待办清单:

  1. 本地成语词典校验:内置一份四字成语库做"第一道校验",AI 只做裁决官,降低接口调用成本与误判率;
  2. 难度模式:基础模式(可同音)与地狱模式(必须同字同调);
  3. 多人联机:通过 WebSocket 实现"三人接龙",AI 做裁判;
  4. 语音交互:接入 Web Speech API,实现"我说成语,AI 接龙"的语音版;
  5. 成语知识图谱:每个接出的成语附带典故出处,边玩边学。

最后,感谢你看完这篇长文。项目代码已开源,欢迎去仓库里把玩、提 issue、提 PR。

码道漫漫,行则将至。愿每一个热爱代码的人,都能在 AI 时代,写出自己的"成语接龙"。

Logo

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

更多推荐