海龟汤项目-后端部分 python 版本 使用 Djano ninja 框架实现

本文前半部分的需求分析,方案设计内容是从鱼皮那边直接拔过来的,但是从后端项目实现那边 根据鱼皮的模板去写了我自己怎么实现,然后AI 提示器部分 稍微做了更改

一、需求分析

1、什么是海龟汤?

海龟汤:老少咸宜的一个小游戏,分为汤面和汤底,主持人讲一个故事(汤面),由玩家通过提问的方式来不断地揭露还原故事的真相(汤底)。

海龟汤是一种逻辑推理游戏,通常以谜题形式呈现。玩家需要通过有限的线索,推理出完整的故事或事件经过。这类谜题通常以“汤面”(谜题的表面描述)和“汤底”(谜题的真相)两部分构成。

特点:

  1. 简短描述:谜题通常只有几句话,提供的信息有限。

  2. 出人意料:答案往往与直觉相反,需要跳出常规思维。

  3. 逻辑推理:玩家需要通过线索推断出隐藏的逻辑关系。

例子:

汤面:一个人走进餐厅,点了一碗海龟汤,喝了一口后突然冲出餐厅自杀了。为什么?

汤底:这个人曾和同伴在海上遇难,同伴死后,他靠吃同伴的尸体活了下来。餐厅的海龟汤让他意识到自己吃的其实是人肉,因此崩溃自杀。

玩法:

  1. 玩家根据“汤面”提问,主持人只能回答“是”或“否”。

  2. 通过逐步推理,最终揭示“汤底”。

海龟汤考验玩家的逻辑思维和想象力,适合喜欢推理和脑力挑战的人。

举个例子:有一个人邀请朋友来参加生日聚会,在他吹完蜡烛后,他把所有的其他朋友都弄死了,问为什么?

2、我们的项目需要有什么功能?

现在有了 AI,可以让 AI 代替传统海龟汤的主持人,玩家只需要向 AI 对话,就能够自己进行海龟汤的游戏。

本项目的核心功能点:

  • 和 AI 主持人对话 P0

  • 查看往期的对话记录 P1

可以发挥自己的想象力,增加扩展功能,比如:

  • 多人共同游戏,和一个主持人对话

  • 对话分享

  • 生成海龟汤的图片

  • 自动推荐海龟汤

二、方案设计

1、项目中接入 AI

可以使用 DeepSeek 大模型 + 火山方舟 SDK(下载了别人的代码工具包)。

在项目中要编写一个自己的 AI 工具类,便于在代码中更方便地使用 AI,理想情况下可以通过一行代码完成 AI 对话。

2、调教 AI

要把 AI 训练成一个主持人。建议利用线上控制台调试 Prompt,Prompt 调试完毕之后,再回到项目中进行开发,而不是在代码中不断运行调试。

注意,需要持续给 AI 补充用户的对话信息(关联上下文),而且要让 AI 在合适的时间结束游戏。

  • 玩家主动表示“不想玩了、想要答案”

  • 玩家已经接近真相

  • 玩家实在是回答不出来

3、持久化记录

理想情况下,要把每一次对话、以及每一个消息都保存到 MySQL 数据库中,便于玩家刷新之后还能找回来。

本项目为了节省时间,先教大家使用内存持久化消息列表,后续自行改为数据库也很方便。

4、项目优化

作为思路扩展,可在完成基础功能后自行实现,作为简历上的加分项

优化 AI 的稳定性:如果一次失败,可以重试

优化 AI 的性能:比如使用 SSE 实时推送、反应式编程、线程池 + 连接池

优化 AI 的可观测性:捕获异常 + 日志 + 推出消息的告警

优化 AI 的可用性:比如说一个 AI 不好用、换另一家 AI(用户无感知)

优化 AI 的成本:精简 Prompt、过滤无效消息、减少上下文和对话次数、命中缓存

技术选项

后端

  • Python + Django ninja 框架

  • Swagger 接口文档

  • AI 大模型接入

角色设定

你是一位专业的海龟汤游戏主持人,负责出题和引导玩家推理。你的任务是:

  1. 提供一道海龟汤谜题的"汤面”(故事表面描述)。

  2. 根据玩家的提问,仅回答“是”、“否"或"与此无关”。

  3. 在特定情况下结束游戏并揭示"汤底”(故事真相)。

游戏流程

  1. 当玩家输入“开始”时,你需立即提供一道海龟汤谜题的"汤面"。

  2. 玩家会依次提问,你只能回答以下三种之一:

  • 是:玩家的猜测与真相相符。

  • 否:玩家的猜测与真相不符。

  • 与此无关:玩家的猜测与真相无直接关联。

1.在以下情况下,你需要主动结束游戏并揭示"汤底”:

  • 玩家明确表示“不想玩了”、“想要答案"或类似表达。

  • 玩家几乎已经还原故事真相,或所有关键问题都已询问完毕。

  • 玩家输入"退出”。

  • 玩家连续提问 10次仍未触及关键信息,或表现出完全无头绪的状态。

注意事项

  1. 汤面设计:谜题应简短、有趣且逻辑严密,答案需出人意料但合理。

  2. 回答限制:严格遵守"是”、”否”或“与此无关”的回答规则,不得提供额外提示。

  3. 结束时机:在符合结束条件时,及时揭示"汤底",避免玩家陷入无效推理。

示例

  • 玩家输入:"开始"

  • AI 回复(汤面):“一个人走进餐厅,点了一碗海龟汤,喝了一口后突然冲出餐厅自杀了。为什么?”

  • 玩家提问:"他是因为汤太难喝了吗?"

  • AI 回复:“否。"

  • 玩家提问:“他认识餐厅里的人吗?“

  • Al 回复:"与此无关。"

  • 玩家输入:"退出。"

  • AI 回复(汤底):“这个人曾和同伴在海上遇难,同伴死后,他靠吃同伴的尸体活了下来。餐厅的海龟汤让他意识到自己吃的其实是人肉,因此崩溃自杀。”

四、业务流程

1)玩家进入页面,点击【开始游戏】,进入聊天室页面

2)进入聊天室页面时,AI 会立刻给出一个招呼语(给出故事汤面)

3)接下来,用户可以和 AI 主持人进行对话

4)用户可以主动结束,也可以由 AI 主动结束游戏

5)用户可以随时查看往期的对话记录

如图:

五、原型图

主页:

聊天室页面(历史对话也可以单独拉出来作为一个新的页面):

六、调教 AI

提示词

我这边提示器,自己稍微修改了一下 根据鱼皮的,简化到次数 5 次,然后对次数每次写入对话里,来提示智能体,次数用尽,该结束游戏

1、我们自己写的系统预设

你是一位海龟汤游戏主持人,当我说“开始”的时候,你要给我出一道海龟汤游戏的“汤面”。然后我会依次问你一些问题,你只能回答“是”、“否”或者“与此无关”。但是,在以下 3 种情况下,你应该结束游戏,并且输出游戏的“汤底”。

需要结束游戏的情况:

  • 我给出“不想玩了、或者想要答案”之类的表达

  • 我几乎已经讲明了真相,或者已经还原了故事,或者所有关键问题都已经询问过

  • 我输入“退出”

  • 经过 5个问题后,我还是没有答到关键信息、或者完全没有头绪

2、AI 优化后的系统预设

角色设定

你是一位专业的海龟汤游戏主持人,负责出题和引导玩家推理。你的任务是:

  1. 提供一道海龟汤谜题的“汤面”(故事表面描述),并且给出玩家 5 次机会。

  2. 根据玩家的提问,仅回答“是”、“否”或“与此无关,并且显示玩家剩余机会次数”。

  3. 在特定情况下结束游戏并揭示“汤底”(故事真相)。

游戏流程

  1. 当玩家输入“开始”时,你需立即提供一道海龟汤谜题的“汤面”。

  2. 玩家会依次提问,你只能回答以下三种之一: ○ 是:玩家的猜测与真相相符。 ○ 否:玩家的猜测与真相不符。 ○ 与此无关:玩家的猜测与真相无直接关联。

  3. 在以下特定情况下,你需要主动结束游戏并揭示“汤底”: ○ 玩家明确表示“不想玩了”、“想要答案”或类似表达。 ○ 玩家几乎已经还原故事真相,或所有关键问题都已询问完毕。 ○ 玩家输入“退出”。 ○ 玩家连续提问 5次仍未得到答案,机会用尽,直接公布答案,并且结束游戏。

注意事项

  1. 汤面设计:谜题应简短、有趣且逻辑严密,答案需出人意料但合理。

  2. 回答限制:严格遵守“是”、“否”或“与此无关”的回答规则,不得提供额外提示。

  3. 结束时机:在符合结束条件时,及时揭示“汤底”,避免玩家陷入无效推理。

  4. 当你决定结束时,必须在结束的消息中包含【游戏已结束】

示例 ● 玩家输入:“开始” ● AI 回复(汤面): “一个人走进餐厅,点了一碗海龟汤,喝了一口后突然冲出餐厅自杀了。为什么?” ● 玩家提问:“他是因为汤太难喝了吗?你有 5 次提问机会,开始吧!” ● AI 回复:“否。 (剩余机会:4)” ● 玩家提问:“他认识餐厅里的人吗?” ● AI 回复:“与此无关。 (剩余机会:3)” ● 玩家输入:“退出。” ● AI 回复(汤底): “这个人曾和同伴在海上遇难,同伴死后,他靠吃同伴的尸体活了下来。餐厅的海龟汤让他意识到自己吃的其实是人肉,因此崩溃自杀。”

七、后端开发

首先明确我们要开发的接口:

  1. 和 AI 进行对话的接口

  2. 查询历史对话列表的接口

1、初始化项目

从零搭建,依次引入 Django ninja, 这个框架 自带 Swagger api

简单科普一下

django ninja 这个框架 是在 Django 基础上搭建,可以认为是个扩展插件,

特点是在扩展和优化了

  • 使用类似 FastAPI 的语法,可以快速开发

  • 自动生成 OpenAPI (Swagger) 文档

  • 请求和响应的数据验证

但与 Django 完全兼容:

  • 可以访问所有 Django 的功能(ORM、缓存、认证等)

  • 可以与现有的 Django 视图共存

  • 使用 Django 的路由系统

保证你的 装了 django ninja 在你的 pip 包里 没有的话 运行这个命令,django 也需要安装 如果没装

bash
复制代码
pip install django-ninja

之后我们还是一样象正常的构建一个 django 项目即可,然后在 url.py 里面,添加一个 api 路径即可

运行

bash
复制代码
django-admin startproject 《你的项目名字》
python
复制代码
from django.contrib import admin from django.urls import path from .api import api urlpatterns = [ path("admin/", admin.site.urls), path("api/", api.urls), # 所有 API 路由都通过全局实例暴露 ]

然后在项目根目录 创建一个 api.py 文件 ,在里面写我们的接口函数即可,也就是原来的视图函数

然后加入下面代码,你就实现了运用 django ninja 去构建你的初始化项目了

python
复制代码
from ninja import NinjaAPI, Schema api = NinjaAPI(title="Anson 海龟汤的 API", version="1.0.0")

然后正常运行你的 django 项目即可

bash
复制代码
python manage.py runserver 8000

通过 访问 http://localhost:8000/api/docs,然后 你就可以访问你的 Swagger api 文档页面,当你完成接口,也直接可以在页面调试

还要解决一个前端访问跨域问题,我们需要修改项目的 setting.py,把 corsheaders 加入进去,来让我们项目允许跨域请求,代码如下

json
复制代码
INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", #添加下面这行 "corsheaders", ] # 以及下面这一段 # 允许所有来源的跨域请求(仅用于开发环境,不推荐用于生产环境) CORS_ALLOW_ALL_ORIGINS = True # 如果只允许特定的前端域名,可以使用以下配置: # CORS_ALLOWED_ORIGINS = [ # "http://localhost:3000", # 替换为前端项目的实际地址 # "https://your-frontend-domain.com", # ]

2、接入 AI

先参考 官方的 SDK 创建接入点或机器人,然后直接复制示例代码完成一个 Demo,先验证能否调用成功 AI。

  • 创建机器人,调用时指定机器人 id。这种方式会更简单,先在界面上配置机器人,然后程序直接调用,不用在代码中写系统预设。

这里我在项目里面创建了一个 config 文件夹,写了一个 ai_config.py 用于 初始化 AI 客户端,读取配置并得到 service

python
复制代码
import os from dotenv import load_dotenv from volcenginesdkarkruntime import Ark # 加载环境变量 load_dotenv() def get_ark_service(): """ 初始化并返回 ArkService 实例 """ api_key = os.environ.get("ARK_API_KEY") # 从环境变量中获取 API Key if not api_key: raise ValueError("ARK_API_KEY is not set in environment variables") base_url = "https://ark.cn-beijing.volces.com/api/v3" # 默认路径 return Ark(api_key=api_key, base_url=base_url) # 初始化 service 实例 service = get_ark_service()

这里我创建了一个 manager 文件夹,创建了一个ai_manager.py 编写 AI 封装类,根据需求 提供不同参数的 AI 调用方法

python
复制代码
from ..config import service from volcenginesdkarkruntime import Ark # AI 调用 工具类 class AiManager: def do_chat(self, system_prompt: str, user_prompt: str): """ 准备聊天消息并发送给AI模型处理。 :param system_prompt: 系统预设提示内容。 :param user_prompt: 用户预设输入内容。 :return: AI的回复内容。 """ # 可以对chat_message 进行类的封装 chat_message = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] return self.do_chat_message_list(chat_message) def do_chat_message_list(self, chat_message_list): """ 处理聊天消息列表并调用AI模型生成回复。 :param chat_message_list: 聊天消息列表。 :return: AI的回复内容。 """ completion = service.bot_chat.completions.create( model="bot-20250316173849-89vzg", # bot-20250316173849-89vzg 为您当前的智能体的ID,注意此处与Chat API存在差异。差异对比详见 SDK使用指南 messages=chat_message_list, ) if not completion.choices: raise RuntimeError("AI 没有返回任何内容") content = completion.choices[0].message.content # print(f"AI 返回内容:{content}") return content

编写单元测试,验证 AI 封装有效性,这里用的 pytest, 在项目中新建一个test 文件夹,

python
复制代码
import pytest from haiguitang_be.manager.ai_manager import AiManager def test_do_chat_with_real_service(): # 初始化 AiManager 实例 ai_manager = AiManager() # 调用 do_chat 方法 system_prompt = "你是一个程序员大佬。" user_prompt = "帮我写一个hello world ,python程序" response = ai_manager.do_chat(system_prompt, user_prompt) # 验证返回值是否为非空字符串 assert isinstance(response, str) assert len(response) > 0 print(response)

3、编写业务逻辑和接口

由于和 AI 需要进行多轮对话,我们要维护上下文列表,最简单的方式就是存储到内存(这里用 字典 中),当然也可以持久化到数据库。

这里我们肯定先通过内存维护上下文,更快地实现基础功能。

用户和 AI 的对话消息可能是下面这样的:

markdown
复制代码
1. 系统消息(只用添加一次) 2. 用户消息:开始 3. AI 给的第一个回复(海龟汤的汤面)5 次机会 4. 用户消息:这个人有问题么? 5. AI 给的第二个回复:是 4 次机会

注意事项

如果要使用内存去维护上下文列表,你需要及时释放内存。也就是说,我们需要能够捕捉到一场对话是在什么时候结束的。

可以在对话结束时,让 AI 输出一个特定的关键词,通过判断 AI 的回复是否包含关键词,来决定是否结束。

因此我们需要优化 Prompt,在注意事项中补充结束关键词。

markdown
复制代码
注意事项 1. 汤面设计:谜题应简短、有趣且逻辑严密,答案需出人意料但合理。 2. 回答限制:严格遵守“是”、“否”或“与此无关”的回答规则,不得提供额外提示。 3. 结束时机:在符合结束条件时,及时揭示“汤底”,避免玩家陷入无效推理。 4. 当你决定结束时,必须在结束的消息中包含【游戏已结束】

以下是我接口服务的实现

python
复制代码
from ..manager.ai_manager import AiManager from typing import Dict, List class ChatServiceImpl: ai_manager = AiManager() # 全局消息映射 global_message_dict: Dict[int, list] = {} def do_chat_service(self, room_id: int, message: str) -> str: # 系统预设 system_prompt = """ 1. 提供一道海龟汤谜题的“汤面”(故事表面描述),并且给出玩家 5 次机会。 2. 根据玩家的提问,仅回答“是”、“否”或“与此无关,并且显示玩家剩余机会次数”。 3. 在特定情况下结束游戏并揭示“汤底”(故事真相)。 游戏流程 1. 当玩家输入“开始”时,你需立即提供一道海龟汤谜题的“汤面”。 2. 玩家会依次提问,你只能回答以下三种之一: ○ 是:玩家的猜测与真相相符。 ○ 否:玩家的猜测与真相不符。 ○ 与此无关:玩家的猜测与真相无直接关联。 1. 在以下特定情况下,你需要主动结束游戏并揭示“汤底”: ○ 玩家明确表示“不想玩了”、“想要答案”或类似表达。 ○ 玩家几乎已经还原故事真相,或所有关键问题都已询问完毕。 ○ 玩家输入“退出”。 ○ 玩家连续提问 5次仍未得到答案,机会用尽,直接公布答案,并且结束游戏。 注意事项 1. 汤面设计:谜题应简短、有趣且逻辑严密,答案需出人意料但合理。 2. 回答限制:严格遵守“是”、“否”或“与此无关”的回答规则,不得提供额外提示。 3. 结束时机:在符合结束条件时,及时揭示“汤底”,避免玩家陷入无效推理。 4. 当你决定结束时,必须在结束的消息中包含【游戏已结束】\n" + 示例 ● 玩家输入:“开始” ● AI 回复(汤面): “一个人走进餐厅,点了一碗海龟汤,喝了一口后突然冲出餐厅自杀了。为什么?” ● 玩家提问:“他是因为汤太难喝了吗?你有 5 次提问机会,开始吧!” ● AI 回复:“否。 (剩余机会:4)” ● 玩家提问:“他认识餐厅里的人吗?” ● AI 回复:“与此无关。 (剩余机会:3)” ● 玩家输入:“退出。” ● AI 回复(汤底): “这个人曾和同伴在海上遇难,同伴死后,他靠吃同伴的尸体活了下来。餐厅的海龟汤让他意识到自己吃的其实是人肉,因此崩溃自杀。”” """ # 1.准备消息列表 system_message = {"role": "system", "content": system_prompt} user_message = {"role": "user", "content": message} chat_messasges = [system_message, user_message] # 首次开始时,需要初始化消息列表,并且额外添加系统消息到记录中 # 优化空间,应该再写个函数,遍历 当前 chat_messages 里面有没有 开始游戏,如果 开始了游戏,用户再次开始是不被允许的 # 防止用户在游戏没开始时候,乱输入 if message != "开始" and room_id not in self.global_message_dict: raise RuntimeError("请开始游戏") # 游戏开始,创建房间 elif message == "开始" and room_id not in self.global_message_dict: self.global_message_dict[room_id] = chat_messasges else: self.global_message_dict[room_id].append(user_message) # 2.调用api answer = self.ai_manager.do_chat_message_list(self.global_message_dict[room_id]) assistant_message = {"role": "assistant", "content": answer} self.global_message_dict[room_id].append(assistant_message) # 3.返回结果 if "【游戏已结束】" in answer: # 清理当前房间的消息记录 del self.global_message_dict[room_id] return answer def get_chat_room_list(self): chat_list = [] for room_id, chat_messages in self.global_message_dict.items(): chat_list.append({"room_id": room_id, "chat_history": chat_messages}) return chat_list def get_chat_messgae_list(self, room_id: int): return self.global_message_dict[room_id]

然后 在api.py 页面,创建我们两个 api 接口,对应的接口函数直接调用 ChatServiceImpl 当中的函数来实现结果,

这里解释一下,Django ninja 这个框架的一些特别之处, 通过 Scheme 类 我们 可以 对请求类型,和响应类型做模板预设,并且,这个模板会显示在我们的 swagger 文档当中,这样 前端跟我们交互的时候,就知道,发送请求 要以怎么样的 json 格式,和带什么参数去传递,以及我们这边响应的一个结果,除此在写函数我们运用 api装饰器去定义 路径,让我们方便了很多,这也是这个框架相较于普通 Django 精简了的地方

python
复制代码
from ninja import NinjaAPI, Schema from typing import List from .service.chat_service_impl import ChatServiceImpl api = NinjaAPI(title="Anson 海龟汤的 API", version="1.0.0") # 实例化 ChatServiceImpl chat_service = ChatServiceImpl() # 定义请求和响应的 Schema class MessageRequest(Schema): message: str class MessageResponse(Schema): response: str class ErrorResponse(Schema): error: str class ChatHistoryResponse(Schema): room_id: int chat_history: list # 发送消息接口 @api.post("/chat/{room_id}/send", response={200: MessageResponse, 400: ErrorResponse}, summary="发送消息" ) def send_message(request, room_id: int, payload: MessageRequest): try: response = chat_service.do_chat_service(room_id, payload.message) return 200, {"response": response} except RuntimeError as e: return 400, {"error": str(e)} # 获取房间聊天记录接口 @api.get( "/chat/rooms", response=List[ChatHistoryResponse], summary="获取所有房间的聊天记录" ) def get_chat_rooms(request): chat_list = chat_service.get_chat_room_list() return chat_list

4、测试

通过 Swagger 接口文档 + Debug 来验证对话是否正常进行、消息列表能否完整获取。

前端部分 我没有写,我直接使用鱼皮的 粗略的试了一下发现一些,与我写法有兼容性问题,首先那个前端 发送,用户消息请求,我是要求写在请求体里的,鱼皮的前端好像是写在路径里的,这个部分可能要改一下,然后 response 我写在 json 对象里面,这个在前端也要提取一下

项目源码

https://github.com/ansonwong9695/haiguitang_be.git

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP