码道 : AI 海龟汤推理小游戏:当脑筋急转弯遇上大模型流式对话


一、缘起:一碗汤引发的头脑风暴
如果你是第一次听说"海龟汤",可能会以为这是一道菜谱。实际上,海龟汤是一种风靡已久的文字推理游戏,它的玩法非常简单,却能让一群人围坐一整个晚上:
出题人先抛出一个离奇、荒诞、几乎无法用常识解释的"汤面"——比如"一个男人走进餐厅,点了一碗海龟汤,喝了一口之后,他走出餐厅,回家自杀了。为什么?“。玩家需要不断向出题人提问,而出题人只能回答"是”“否"或"无关”。玩家通过一个个闭合式问题,逐步拼凑出事件的全貌,最终还原出令人恍然大悟的"汤底"。
这类游戏最大的魅力在于:答案往往就藏在最不起眼的细节里。男人的味觉出了问题?他和餐厅老板认识?那碗汤有什么特别?——当你终于问出"他是不是以前尝过这种味道?"并得到"是"的回答时,那种头皮发麻的顿悟感,是任何视频或小说都无法替代的。
而"海龟汤"这个名字本身就来自一个著名的经典谜题——关于一碗海龟汤的味道,隐藏着一个关于人性与灾难的悲伤故事。这也是它的中文译名被广为流传的原因。
那么问题来了:如果没有人愿意当出题人呢? 如果深夜独处,突然想玩一局海龟汤,身边没有朋友,该怎么办?
答案是:找一个永远不会厌烦、题库永远用不完的出题人——大语言模型。
于是便有了本项目:AI 海龟汤推理小游戏。它让 DeepSeek-V4-Flash 扮演那个神秘的海龟汤主持人,为你随机出题、严格执行"是/否/无关"的裁判规则,并在你放弃推理时给出完整的故事真相。
本文将从项目动机、技术选型、架构设计、核心实现、踩坑经验五个维度,完整记录这个项目的诞生过程,希望能给同样想做 AI 对话类 Web 应用的同学一些启发。
二、为什么选择纯前端?——一次"零依赖"的冒险
项目最初收到的需求很简单:用 HTML5 + CSS3 + JavaScript 写一个 AI 对话类项目,集成 DeepSeek 模型的流式接口。
很多人可能会问:现在前端框架百花齐放,Vue、React、Svelte 一个比一个热闹,为什么还要用"三大件"纯手写?
这个选择背后有几个非常现实的考量:
第一,部署成本趋近于零。 一个纯静态项目,不需要 Node.js 服务、不需要构建工具链、不需要包管理器。把三个文件扔到任何一台能联网的电脑上,双击 index.html 就能跑。对于课堂作业、作品展示、Demo 演示这类场景,这个特性是碾压级的优势。
第二,学习路径最清晰。 当一块代码的复杂度完全暴露在你眼前时,你才能真正理解它。没有了框架的"魔法",fetch 到底怎么发请求、ReadableStream 到底怎么读数据、DOM 到底怎么更新——每一个环节都清清楚楚,这对初学者和教学场景极其友好。
第三,构建工具洁癖。 很多小型项目其实用不到 Vite/Webpack 那套复杂的依赖体系,npm install 动辄几百兆的 node_modules,对一个小游戏来说完全是负担。零依赖意味着零维护成本、零版本冲突、零供应链风险。
当然,纯前端方案也有它的代价,最典型的就是 CORS 跨域限制和密钥暴露。这个坑我们留到后文专门讨论,因为它几乎决定了一个 AI 对话类项目能否"纯前端化"。
三、先设计游戏规则,再写代码
任何一个好的交互项目,都应该先把"产品规则"想清楚。海龟汤的规则虽然简单,但把它抽象成 AI 的出题逻辑时,需要非常明确的行为约束,否则大模型就会"自由发挥",破坏游戏体验。
我们为 AI 主持人定义了以下六条铁律:
- 开局只给"汤面":随机挑选题库中的谜题,用两到三句话描述一个离奇场景,绝不提前泄露真相。
- 严格三选一回答:对玩家的任何提问,只能回答"是"“否"或"无关”。
- 有限补充:最多附加一句不超过 15 字的补充说明,防止 AI 无意间剧透。
- 揭晓机制:当玩家说出"揭晓答案""公布汤底"等指令时,一次性给出逻辑自洽的完整真相。
- 重启机制:当玩家说"重新开始""新游戏"时,换一道新题。
- 身份锁定:始终保持出题人身份,不跳出游戏、不反问玩家。
这六条规则在实现层面变成了两件事:前端的指令识别和后端的系统提示词约束。
前端指令识别非常简单粗暴但有效——用正则匹配用户输入:
if (/揭晓|答案|汤底|揭秘/.test(text)) {
// 触发揭晓逻辑
} else if (/重新开始|新游戏|再来一局|换一题/.test(text)) {
// 触发重开逻辑
}
而系统提示词的约束力,则远比想象中重要。大模型在没有强约束的情况下,很容易把规则"玩坏":比如直接说出汤底、追问玩家问题、或者回答"可能"这种模糊词。所以提示词工程在此项目中不是锦上添花,而是核心功能的一部分。
四、从 Python 到 JavaScript:一次流式思维的重构
项目拿到了一份现成的 Python 参考代码,它非常标准地演示了如何调用 GitCode AI 的 OpenAI 兼容接口:
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]" or line.strip() == b"data: [DONE]":
return
yield json.loads(line.decode("utf-8").lstrip("data:").rstrip("/n"))
这段代码的核心是 SSE(Server-Sent Events)流式解析:服务端不会一次性吐出完整回答,而是以 data: 前缀的行分片推送,最后以 data: [DONE] 收尾。Python 端通过 iter_lines() 逐行读取,配合生成器 yield 逐块产出。
要把这套逻辑搬到浏览器里,我们需要完成三个"翻译":
1. HTTP 请求:requests.post → fetch
fetch 是浏览器原生提供的 HTTP API,返回一个 Promise。与 requests 最大的不同是:浏览器端必须通过 response.body 暴露的 ReadableStream 来读取流式响应。
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`,
},
body: JSON.stringify(payload),
stream: true,
});
2. 逐行解析:iter_lines() → reader.read() + 手动切行
JavaScript 没有现成的 iter_lines(),我们需要自己维护一个缓冲区,把每次读到的数据块按换行符切分成行:
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; // 丢弃非 data 行
const data = trimmed.slice(5).trim();
if (data === "[DONE]") return; // 流结束标志
// ...解析 JSON
}
}
这里有一个关键细节:SSE 的数据块边界并不等于行边界。一次 reader.read() 可能返回半行、一行、或好几行。所以我们必须把不完整的尾部(buffer.pop())留到下一轮拼接,这正是"流式思维"与普通"一次性响应"思维的差异所在。
3. 增量渲染:yield → 回调函数
Python 的 yield 在 JavaScript 里最贴切的对应物是回调。我们把"每收到一个 content 片段"这个事件回传给渲染层,让打字机效果自然产生:
async function streamChat(renderDelta) {
// ...
if (delta.content) renderDelta(delta.content);
}
经过这三个"翻译",原本 20 行 Python 就变成了一个健壮的前端流式 HTTP 客户端。这条路径也为其他 AI 对话类项目提供了可复制的范式:任何 OpenAI 兼容接口(DeepSeek、通义、文心等)都可以用同一套代码接入。
五、核心难点:让 AI 只输出 JSON
海龟汤游戏与普通聊天最大的不同在于:AI 的回复不是自由文本,而是一种需要被程序结构化消费的"数据"。HTML 要根据回复的类型做不同展示:
- 开局时:展示一个带高亮边框的"汤面"卡片;
- 回答问题时:展示"是/否/无关"的核心回答;
- 揭晓时:展示带回绿的"汤底"故事。
如果 AI 乱说话,前端将无法区分这三种场景。所以我们必须设计一套 JSON 输出协议,并通过系统提示词把 AI 约束在一个非常窄的输出空间里。
5.1 三段式 JSON Schema
我们定义了三种消息类型,分别对应游戏的三个状态,全部放在一条系统提示词中:
// ① 开局出题
{ "type": "intro", "scenario": "汤面内容", "hint": "一句玩法提示" }
// ② 回答提问
{ "type": "reply", "answer": "是", "comment": "不超过15字的补充" }
// ③ 揭晓汤底
{ "type": "reveal", "scenario": "汤面回顾", "soup_bottom": "完整汤底", "hint": "点睛之笔" }
提示词中有一句非常关键的硬约束:
“你每一次回复必须且只能输出一个 JSON 对象,禁止输出 JSON 以外的任何文字、空格、解释或 Markdown 代码块标记。”
5.2 为什么不能用 response_format: { "type": "json_object" }?
看到这里,熟悉 OpenAI 接口的同学可能会问:接口不是自带 response_format: {"type": "json_object"} 参数吗?直接用不就行了?
诚然,这个参数是结构化的正规军,它能从解码层面强制模型输出合法 JSON。但本项目放弃它的原因有二:
其一,它与流式输出的兼容性在不同模型实现上并不一致——有的模型开启 JSON 模式后流式行为异常,有的模型对非 JSON Schema 的约束力度不够。
其二,thinking_budget 与我们"自己掌控解析"的诉求更匹配。既然我们已经在提示词层面给出了精确的三段式模板,再叠加 JSON 模式反而可能让模型在"模板"和"格式强制"之间摇摆。
所以最终我们选择了"提示词约束 + 前端容错解析"的组合拳,实测稳定性完全够用。
5.3 流式 JSON 的实时解析:括号配对算法
这里有一个非常有意思的工程细节:SSE 流式输出意味着 JSON 是"残缺的"。模型先吐出 {"type":"intro", 再吐出 "scenario":"一个男人走 再吐出 进餐厅...",前端收到的是被任意切分的碎片。
如果等整个流结束再 JSON.parse,打字机效果就没了;如果每收到一个字符就尝试 JSON.parse,绝大多数时候都会因为 JSON 不完整而报错。
解决方案是一个经典的配平扫描算法:
function extractJson(acc) {
const start = acc.indexOf("{"); // 找到对象起点
if (start === -1) return null;
let depth = 0;
for (let i = start; i < acc.length; i++) {
const ch = acc[i];
if (ch === "{") depth++;
else if (ch === "}") {
depth--;
if (depth === 0) { // 花括号配平,说明 JSON 完整
try {
return JSON.parse(acc.slice(start, i + 1));
} catch (_) {
return null; // 还不完整,继续等
}
}
}
}
return null;
}
它的思路是:用计数的方式扫描花括号的配对情况。只有当我们扫描到 depth 重新归零的那个 } 时,才认为 { ... } 区间内的内容是一个完整的 JSON 对象,此时才进行 JSON.parse。如果解析失败(说明 JSON 内部还不完整,比如字符串引号还没闭合),就返回 null,等待后续分片继续累积。
这个算法还有三个额外的好处:
- 自动忽略前缀杂质:哪怕模型调皮地在 JSON 前加了几个空格或一行"好的,我来回答",只要找到第一个
{,就能精准截取。 - 自动截断尾部多余内容:如果模型在 JSON 结束后又多说了几个字,我们只取配平区间,多余文字会被自然丢弃。
- 时机精准:一旦解析成功,立即渲染并锁定
handled标志,后续分片不再重复解析。
六、系统提示词的写作技巧
很多人以为系统提示词就是"告诉 AI 做个好人",其实它是一项有章法的写作工程。本项目这条提示词,实际上隐含了四个层次:
第一层:角色设定。 “你是海龟汤推理游戏的主机(出题人)”——一句话锁定身份,让模型的整个回答立场都围绕出题人展开。
第二层:行为规则。 数字编号的六条规则,明确"什么能做、什么不能做"。注意,规则采用了"肯定句 + 否定句"交替的结构:"只能回答是/否/无关"是对行为边界的肯定式定义,而"不要跳出游戏、不要反问玩家"是对越界行为的显式封堵。大模型对边界的理解,往往靠这种正反结合才最牢靠。
第三层:输出约束。 这是最关键的一段。我们把三种 JSON 结构"逐字段"写进了提示词,甚至标注了字段类型("是"、"不超过15字的补充说明"),这等于给了模型一张带示例的填空表,比抽象描述"请返回 JSON"有效得多。
第四层:极端强调。 “极其重要!你每一次回复必须且只能输出一个 JSON 对象,禁止输出 JSON 以外的任何文字、空格、解释或 Markdown 代码块标记。”——注意这句强调为什么要写在提示词末尾?因为大模型对文件末尾的内容往往有更强的记忆权重,把最重要的约束放在最后,是为了给它"压轴"的地位。
这套写作方法论可以泛化到任何"AI 输出结构化数据"的场景:购物推荐、情报整理、表格抽取、甚至代码生成,都可以沿用"角色 → 规则 → 模板 → 强调"的四段式结构。
七、界面设计:让对话拥有"剧情"
海龟汤是一个讲究氛围的游戏,界面设计直接影响沉浸感。我们采用了深夜蓝 + 琥珀色的配色方案:深邃的暗蓝背景模拟"午夜推理"的场景,琥珀色的标题与强调色呼应"汤"的暖意,也暗示"真相在迷雾中闪光"。
布局上把页面分成四个区域,形成一条清晰的心智路径:
- 规则区:"汤面 / 提问 / 线索 / 汤底"四个步骤卡片,让第一次玩的玩家 10 秒内理解玩法。
- 对话区:信息流气泡,AI 答语靠左、玩家提问靠右、系统提示居中,视觉层级分明。
- 操作区:输入框 + 三个按钮——发送、揭晓答案、新游戏。
- 提示区:一行小字说明"输入『重新开始』也能开新局"。
三个按钮各有不同的视觉性格:发送是明亮的琥珀色主按钮;揭晓答案是带红边描边的"危险"样式,暗示这是"放弃推理"的操作;新游戏低调沉稳。按钮的视觉权重差异,其实是在用设计语言告诉用户"主流程是什么、应急操作是什么"。
移动端适配也做得足够细心:视口小于 560px 时,规则卡片从两列收成单列,对话区高度改为 52vh,按钮从"横排一行"变成"等宽下排",保证在手机上也顺手。还专门隐藏了底部小字提示,避免关键按钮被挤占。
甚至还有几个彩蛋级的小细节:AI 回答用了 border-bottom-left-radius: 4px 的"气泡尾巴";汤面的卡片左侧加了 3px 的琥珀色竖线作为"重要信息"标记;CSS 里通过 @keyframes pop 给每条新消息加了 0.18 秒的淡入上移动画,让信息流有"涌现"感。
八、运行效果与技术验证
在正式提交仓库之前,我们对项目做了两轮验证:
第一轮:API 层验证。 用 Node.js 编写了一个模拟浏览器行为的测试脚本,把系统提示词原封不动地喂给 DeepSeek-V4-Flash,验证三种场景的 JSON 输出。
实测输出(开局):
{"type":"intro","scenario":"一个男人走进餐厅,点了一碗海龟汤。喝了一口后,
他走出餐厅,回家自杀了。","hint":"试着问问汤的味道、他为什么点汤、他之前发生了什么。"}
实测输出(提问):{"type":"reply","answer":"无关","comment":"汤面场景是酒吧,并非餐厅。"} ——回答精准、补充克制,符合预期。
实测输出(揭晓):
{"type":"reveal","scenario":"...","soup_bottom":"男人一直在打嗝,想喝水止嗝。
酒保看出他的问题,于是掏枪吓他,把他治好了...","hint":"..."}
三局全绿,JSON 全部合法。 这证明了"提示词约束 + 前端容错解析"这条路线是工程上站得住的。
第二轮:静态资源验证。 本地起了一个 python3 -m http.server,逐一请求 index.html、style.css、script.js、README.md,全部返回 200,页面标题与关键元素确认无误。
九、踩过的坑与复盘
9.1 CORS 跨域的"纯前端之痛"
这是 AI 对话类纯前端项目绕不开的坎。浏览器出于安全策略,会拦截来自不同源的 XHR/fetch 请求——除非服务端在响应头里带上 Access-Control-Allow-Origin。
GitCode AI 的接口实测对浏览器直连是放开的(跨域头齐全),所以项目可以直接双击 index.html 运行。但如果你换用其他厂商的接口,很可能会碰一鼻子灰。这类问题的三个递进解法:
- 本地开发:用任意静态服务器(
python3 -m http.server)起服务,很多 CORS 问题在http://localhost而非file://协议下会消失; - 部署期:把调用挪到自己的后端(Node/FastAPI/Spring Boot),由后端转发请求;
- 终极方案:走云函数 / Serverless,让密钥和请求都留在服务端。
9.2 密钥暴露:一次必须被点名的妥协
script.js 里硬编码了 API Key。这是本项目明确已知的安全隐患,README 里也做了醒目提醒。任何打开浏览器开发者工具的人都能看到这个密钥。好在项目定位是课堂展示,且密钥有额度限制,风险可控。
如果未来要正式上线,正确姿势是:服务端代理 + 环境变量。把密钥放进服务器的 .env,前端只请求自己的后端接口,并且后端要做调用频率限制、请求域名白名单、甚至按用户分配流量配额。网络安全没有"一次到位",只有"持续加固"。
9.3 max_tokens 与 thinking_budget 的关系
项目沿用了参考代码中的 thinking_budget: 2048。这在带推理能力(thinking)的模型上意味着:模型会先花一部分 token 做内部思考,再输出最终答案。我们的 JSON 提取逻辑只消费 choices[0].delta.content,思考过程(reasoning_content)被天然忽略,绝不污染 UI——这是个很舒服的设计。
但要注意:如果某个模型的 max_tokens 同时覆盖思考与输出,思考太"上头"可能会导致最终回答被截断。表现为"JSON 一直解析不完整"。此时要么调大 max_tokens,要么在提示词里要求"简短思考"。这也是未来兼容其他模型时需要重点回归的点。
9.4 让 AI 自己"猜"意图,不如让前端"判断"意图
最初的设计里,“是否揭晓答案"的判断完全交给模型。但模型偶尔会在玩家说"这不可能吧"时误判为"揭晓指令”,直接抖出汤底。后来我们决定把指令识别放回前端:只有正则命中"揭晓/答案/汤底"等明确关键词,前端才构造"请揭晓完整汤底"这句指令发给模型。
这个改动的本质是把不可控的模型行为,替换为可控的程序判断——AI 负责"内容生成",程序负责"流程控制"。这种分工原则,值得在所有 AI 应用里推广。
十、项目结构一图胜千言
ruanjian6_zsy/
├── index.html # 页面骨架:规则区 + 对话区 + 操作栏
├── style.css # 深色主题样式 + 移动端响应式
├── script.js # 核心:SSE 流式客户端 + JSON 协议解析 + UI 渲染
└── README.md # 项目文档:玩法、部署、转换对照、安全提示
script.js 内部按职责清晰地分成了 11 个区块:配置、系统提示词、状态管理、DOM 引用、渲染函数、流式 API、JSON 抽取、请求编排、结果渲染、交互事件、自动开局。代码总行书不到 300 行,却串联了一次完整的 AI 对话闭环——从用户输入到模型推理,再到结构化渲染。
十一、体验一下
把整个项目 clone 到本地,用浏览器打开 index.html,一局游戏就开始了:
- 页面自动开局,AI 抛出一碗热腾腾的"汤面";
- 在输入框提问:“死者是不是被毒死的?”“他和餐厅老板认识吗?”“那碗汤是什么肉做的?”
- AI 只回"是 / 否 / 无关",偶尔附上一句撩拨你的补充;
- 推理逐渐清晰时——“那碗汤是不是他亲人做的?”“是。”
- 点击【揭晓答案】,真相大白,恍然大悟。
如果你愿意,甚至可以把它当做一个午夜 AI 谜题机:设定一个 24 小时不间断的谜题大转盘,想玩随时来一局。这就是大模型时代的小确幸——最好的出题人,永远在场。
十二、未来展望
项目虽然"已完成",但可以演进的方向还有很多:
- 难度分级:让 AI 根据题目难度标注"烧脑指数",玩家可以自选"入门 / 进阶 / 地狱"模式;
- 题库本地化:内置一批高质量经典汤底作为"保底题库",避免依赖网络时无题可玩;
- 多模态汤面:结合文生图能力,把汤面渲染成一张诡异的概念图,进一步拉满氛围;
- 推理记录:支持把整局推理过程导出为文字,分享给朋友"复盘";
- 计时与计分:记录每位玩家从开局到揭晓用掉的提问数,形成排行榜;
- 服务端化:把密钥后移,做成真正的多人在线海龟汤社区,玩家之间互相出题、互相挑战。
AI 应用的下半场,拼的往往不是模型本身,而是围绕模型的体验设计。海龟汤这样一个小项目,恰好把"流式交互、结构化输出、提示词工程、前端工程"几个议题串成了一条完整的实践链路,性价比极高。
十三、结语
从一碗海龟汤的脑洞,到一个可运行、可演示、可分享的 AI 应用,整个过程最有价值的收获并不是那几百行代码,而是三件事:
- 流式思维——理解了 SSE 的本质,也就理解了几乎所有 AI 对话应用的底层脉搏;
- 结构化输出——学会了用提示词协议约束一个"自由散漫"的大模型,让它变成听话的"数据服务员";
- 工程折中——在安全、性能、体验之间做权衡,并在 README 里如实交代所有取舍。
如果你也想做一个 AI 小应用,不妨从海龟汤开始。它规则简单、场景明确、反馈即时有爽感,是练习 AI 工程能力的绝佳沙盒。
最后,用项目页面上那句话收尾吧:
🐢 一句离奇的汤面,一场烧脑的推理。最好的出题人,永远在线。
感谢阅读。欢迎 Star 与 Fork,也欢迎在评论区分享你见过的最离谱一碗海龟汤。
本文由「码道」社区成员撰写,用于展示 AI 海龟汤推理小游戏项目(GitCode: AB123___/ruanjian6_zsy)。文中演示所用 API Key 为示例密钥,正式使用请替换为你自己的密钥,并务必部署为服务端代理。
更多推荐


所有评论(0)