作业 8: 无限故事


作业设计者:Chris Piech,灵感来自Eric Roberts。讲义由Anjali Sreenivas、Yasmine Alonso、Katie Liu编写。伦理学部分由Javokhir Arifov和Dan Webber负责。测试脚本由Iddah Mlauzi和Tina Zheng编写。顾问包括Mehran Sahami和Ngoc Nguyen等!

截止日期:5月24日星期五下午2点(太平洋时间)

哇!这个作业是全新的!多么令人兴奋。事实上,据我们所知,这是入门计算机科学课程中首批使用DeepSeek的作业之一,甚至超越了斯坦福大学。如果你发现任何你认为可能是错误的地方,请告诉我们!如果你觉得很有趣也请告诉我们。联系我们最好的方式是通过课程讨论论坛的私密帖子。

📋 勘误:

5月14日:在里程碑2中添加了关于如何安装requests的文本

5月14日:将图片改为jpg格式。起始代码压缩包从100mb减少到4mb。

5月15日:小错误修复和重新措辞。

🔗 有用链接:

概述

你正在探索一个世界。几乎每个场景都很正常,但如果英雄探索得足够多,他们会慢慢开始发现神秘的部分...

在这个作业中,你将编写一个选择你自己的冒险游戏,利用生成式AI的力量。你将应用你学到的关于字典的知识,并将其与向DeepSeek发送请求相结合,最终帮助引导用户完成一个神秘的冒险。最后,你将思考在讲故事应用中 underlying 使用生成式AI的伦理影响。

当用户启动程序时,它会看起来像这样:

无限故事程序运行示例

图:无限故事程序运行示例 - 左侧显示AI生成的场景图像,右侧显示文本交互界面

用户可以选择下一步要采取什么行动。在这里,他们选择了1,走上山的路:

无限故事程序用户选择示例

图:用户选择"走上山的路"后的程序界面

到目前为止,这似乎是一个标准的讲故事应用程序。当用户到达一个尚未编写的场景时,魔法就发生了。程序不会崩溃或阻止用户继续,而是会向DeepSeek发送请求来生成下一个场景。故事继续!

无限故事程序AI生成场景示例

图:AI生成新场景时的程序界面 - 展示DeepSeek如何扩展故事

这个作业的目标之一是让你练习分解程序。在里程碑中,我们将给你有用的函数建议。然而,控制流决策最终掌握在你的好手中。

里程碑1:加载故事

任何此类故事的关键限制是你最终会碰到死胡同。编程warmup.py找到故事中的"死胡同"。死胡同是在故事中被引用但未定义的场景键。

为了完成这个热身,我们首先需要了解故事数据的存储方式。故事数据保存为嵌套字典。我们为你创建了两个故事字典,original_big.jsonoriginal_small.json。这些文件存储在名为data的文件夹中:

数据文件夹

图:数据文件夹

首先,加载故事字典。由于每个故事都是json格式,你可以用以下代码行加载它,这应该在你的main函数开始时:

story_data = json.load(open('data/original_small.json'))

你将在整个程序中经常访问这个字典。你应该只加载文件一次。

在继续之前,花一些时间了解story_data中数据的组织方式。以下是story_data字典的摘录:

{
  "plot": "你正在探索一个世界。几乎每个场景都很正常,但如果英雄探索了足够多的正常部分(例如超过10个场景),他们会慢慢开始发现神秘的部分。大部分基调只是设定一个美丽和令人振奋的充满奇迹的风景。",
  "scenes": {
    "start": {
      "text": "你站在一条路的尽头,面前是一座小砖房。一条小溪从建筑物中流出,向南流入一个沟壑。一条路向西通向一座小山。",
      "scene_summary": "故事的开始。站在砖房前。",
      "choices": [
        {
          "text": "走山上的路",
          "scene_key": "overlooking_valley"
        },
        {
          "text": "走向小溪",
          "scene_key": "next_to_gully"
        },
        {
          "text": "敲门",
          "scene_key": "knocking_on_small_brick_building"
        }
      ]
    },
    "knocking_on_small_brick_building": {
      "text": "你敲门,但没有人回答。你听到里面传来微弱的狗叫声。",
      "scene_summary": "没有人回答。",
      "choices": [
        {
          "text": "回到起点",
          "scene_key": "start"
        }
      ]
    }
    // ... 更多场景,为节省空间而隐藏
  }
}

哇!一个真正嵌套的字典。不要被它吓倒。相反,逐步理解它。

故事:每个故事都是一个字典,有两个键:一个键"plot"与整体故事情节的字符串描述相关联,一个键"scenes"与包含故事中所有场景数据的字典相关联(见下面的场景)。

场景:包含所有场景数据的字典。在场景字典中,场景键与给定场景的所有数据相关联。每个场景都有一个"text"描述、一个"scene_summary"和一个用户可以从场景中选择的选择列表(见下面的选择)。

选择:一个选择既有选择的"text",也有英雄选择这个选择后将前往的"scene_key"。

现在你已经加载了story_data,是时候编程warmup.py找到"死胡同"了:

warmup.py中:

例如,如果你用故事常量设置为"original_small.json"文件运行warmup.py,它应该打印出以下键:

next_to_gully
descend_into_valley
watching_sunset
continue_exploring_hilltop
return_to_small_brick_building

我们打印出这些是因为它们被选择引用,但它们不在场景字典中(它们是死胡同)。我们不打印出knocking_on_small_brick_building,因为它是场景字典中的一个键。回想一下,对于这个里程碑,你应该在warmup.py中编写代码。在两个故事original_smalloriginal_big上测试你的代码。死胡同的顺序无关紧要。

额外理解:

你还在试图理解story_data吗?查看Anjali Sreenivas制作的这个美丽的可视化:

story_data prezi

你也可以尝试这些可选挑战。你能使用story_data打印出这些值吗:

里程碑2:向控制台打印场景

安装包:

在接下来的作业中,你将在 infinitestory.py 文件中编程。在运行代码之前,你需要先安装 requests 包。你可以通过在终端中运行以下命令来安装:

python3 -m pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple/

在你的终端中。根据你的系统,你可能需要将 python3 替换为 pythonpy

编写一个函数来向控制台打印单个场景。它应该只接受一个参数,与该场景相关联的字典。在infinite_story.py中编写代码。在这个新程序中,你应该以与里程碑1相同的方式加载story_data json。你可以在大故事或小故事数据上测试代码。我们建议此时你开始使用original_big。

你的函数应该打印出场景的"text"以及与该场景数据中"choices"键相关联的每个选择的"text"。选择应该为用户编号,从1开始。例如,如果你用"start"场景的数据调用函数,它应该打印出以下内容:

你站在一条路的尽头,面前是一座小砖房。一条小溪从建筑物中流出,向南流入一个沟壑。一条路向西通向一座小山。
1. 走山上的路
2. 走向小溪
3. 敲门

旁白:我们最终将通过向控制台打印场景描述和渲染与场景相关联的图像来渲染场景。渲染图像将在未来的里程碑中。

回想一下,场景字典看起来像这样(这个是开始场景的):

{
  "text": "你站在一条路的尽头,面前是一座小砖房。一条小溪从建筑物中流出,向南流入一个沟壑。一条路向西通向一座小山。",
  "scene_summary": "故事的开始。站在砖房前。",
  "choices": [
    {
      "text": "走山上的路",
      "scene_key": "overlooking_valley"
    },
    {
      "text": "走向小溪",
      "scene_key": "next_to_gully"
    },
    {
      "text": "敲门",
      "scene_key": "knocking_on_small_brick_building"
    }
  ]
}

观察每个场景都有一个描述("text")以及用户可以选择的下一个场景选项列表("choices")。注意choices是一个列表,列表中的每个元素都是它自己的字典。每个内部字典中的"text"键是该选择的文本描述。

你的函数应该适用于任何场景,而不仅仅是开始场景。注意,你不需要使用"scene_summary"键,这在以后会派上用场。

里程碑3:获取有效选择

编写一个函数来从用户获取有效选择。这个函数也应该只接受一个参数,场景的数据字典。它应该返回用户做出的选择。

在你向控制台打印场景(包括选择)之后,你需要从用户获取有效选择。在这个里程碑中,你将编写获取该信息的函数。

用输入提示"What do you choose?"询问用户选择什么。他们的回答需要是对应于场景打印选择的整数之一。直到选择有效,打印出"Please enter a valid choice:"然后重新提示用户输入有效选择。当你获得有效选择时,返回所做的选择。

这里是一个例子,我们为"start"场景调用获取有效选择函数。注意场景已经使用前一个里程碑的函数打印了。用户输入用蓝色斜体显示。用户输入了两个无效选择,然后最终输入了有效选择1,这对应于文本"走山上的路":

你站在一条路的尽头,面前是一座小砖房。一条小溪从建筑物中流出,向南流入一个沟壑。一条路向西通向一座小山。
 1. 走山上的路
 2. 走向小溪
 3. 敲门
 你选择什么?cat
 请输入有效选择:5
 请输入有效选择:1

专业提示:Python列表是0索引的,但我们的选择从1开始索引!你如何照顾这个?

里程碑4:场景流程

现在你已经有了打印场景和获取场景有效选择的功能,是时候将我们的故事应用程序缝合在一起了!在main()中,你的工作是基于用户输入的内容,通过从一个场景转换到下一个场景来无限运行场景。

专业提示:你应该有一个变量来跟踪当前场景键。第一个场景总是有场景键"start"。

在无限循环中你应该:

建议:为了让用户更容易阅读,在向控制台打印场景之前,打印一个额外的(空白)行。

哇!看看你的无限故事正在形成!如果你玩得足够久,你最终会到达一个不在你故事中的scene_key。你的程序可能会崩溃,出现错误说KeyError并抱怨场景键不在你的场景字典中。那么如果我们没有场景描述怎么办?!我们将在下一个里程碑中解决这个问题!😊

里程碑5:处理无限

当我们到达一个没有场景数据的场景键时(即键在我们的story_data字典中不存在,一个死胡同),我们不会崩溃。相反,我们将调用DeepSeek来帮助生成一个新场景,包括适当的选择!然后,我们将继续使用新创建的场景运行程序。

对于这个里程碑,定义一个函数,当场景键在你的story_data字典中不存在时创建新场景。具体来说你应该:

提示应该格式如下:

"为键 [scene key] 返回故事的下一个场景。示例场景应该按照以下json格式:[example scene]。故事的主要情节线是 [plot]。"

scene_key是要生成的场景的键。plot应该是story_data中与"plot"相关联的值。示例场景应该是开始场景的数据转换为字符串。你可以像将整数转换为字符串一样,使用像str(start_scene_data)这样的命令将字典转换为字符串。给DeepSeek一个示例json很重要,这样它就知道你期望场景的格式。

那么我们如何与DeepSeek通信来生成下一个场景,当我们字典中没有数据时?要做到这一点,我们首先需要理解一个新概念:API。

API(应用程序编程接口)允许软件应用程序相互通信和交互。它们定义了应用程序可以用来请求和交换信息的方法和数据格式,允许计算机科学家将各种服务(如DeepSeek)无缝集成到他们自己的项目中,而无需大量额外的复杂性。将它们想象成远程计算机上的函数,你可以通过互联网调用。它们是一个超级强大的工具!

参见example_request.py了解向DeepSeek发送API"调用"的完整示例:

chat_completion = CLIENT.chat.completions.create(
  messages=[{
    "role": "user",
    "content": """What is the capital of all countries in east africa?
    Reply in json where keys are countries""",
  }],
  model="deepseek-chat", # 使用的DeepSeek模型
  response_format={"type": "json_object"} # 我们希望响应是json格式
)

这是API"调用"的一个例子,我们向DeepSeek的聊天API(客户端)发送提示,基于提示"What is the capital of all countries in east africa? Reply in json where keys are countries"生成响应。它还指定输入消息来自用户("role": "user"),指示要使用的DeepSeek模型("deepseek-chat"),并指定响应应该格式化为json(response_format={"type": "json_object"})。

为了使调用工作,我们首先需要初始化API客户端。注意你程序中的这一行:

CLIENT = NotDeepSeek(api_key="yourapikey")

你需要用你自己的API密钥替换"yourapikey"。要获取你的API密钥,请访问https://platform.deepseek.com/api_keys

你从API调用返回的chat_completion是一个相当复杂的对象。要只获取json响应,你可以使用以下两行:

# 获取响应的内容
response_str = chat_completion.choices[0].message.content
# 使用loads将字符串转换为字典
new_scene_data = json.loads(response_str)

如果DeepSeek完成了它的工作,new_scene_data现在将是一个新场景的字典,包含文本、摘要和选择。你现在可以将这个新场景添加到你的story_data字典中。

旁白:为什么API叫NotDeepSeek?这只是个玩笑。DeepSeek是创建DeepSeek-chat的公司。为了让你免费使用,我们通过我们的CS106A付费账户路由所有请求。我们称它为NotDeepSeek,因为我们不是DeepSeek😊。API与DeepSeek API相同,如果你想切换到自己的付费DeepSeek密钥,你可以!详情请参见API页面。

里程碑6:可视化场景图像

也许你正在沿着蜿蜒的河流在郁郁葱葱的山谷中旅行,经过古雅迷人的砖砌小屋。或者,也许一旦夜幕降临,你继续跟随这条发光的、明亮的溪流,遇到一座神秘的建筑,光线从中倾泻而出。这些视觉效果为整个作业带来了魔法——现在由你来展示它们以增强用户体验!对于original_bigoriginal_small中的所有场景,我们要DallE生成相应的图像。它们都保存在img文件夹中。

重要:我们可能没有每个需要显示的场景的图像,特别是当你进入新创建的场景时。如果我们没有当前场景的图像,只需在画布上显示一个黑色矩形(或者随意添加一些其他选择作为扩展!)。你不需要自己生成图像!

注意:你可能需要在PyCharm终端中运行python3 infinite_story.py(Windows)或python3 infinite_story.py(Mac)才能使图像出现。

了解这些图像的位置以及我们如何访问它们很重要。注意在你的无限故事作业文件夹中,有一个名为img的子文件夹。在img文件夹中,我们将有所有可以用来制作故事的图像(存储为.jpg图像)!每个图像都命名为"[scene_key].jpg",其中scene_key是original_big story_data字典"scenes"中的键。例如"img/start.jpg"有这个图像:

开始场景图像

图:开始场景图像 - 对应original_big story_data字典中的start场景

图像路径是一个字符串,告诉你如何从起始目录(在这种情况下,你的无限故事作业的当前文件夹)到你的目标目的地(所需图像)。我们用斜杠(/)分隔文件夹(目录)来指示必须"点击通过"什么目录才能到达我们的目的地。这就是为什么"img/start.jpg"是这个上面图像的路径。

如前所述:我们有两种潜在情况,要么我们有场景的图像(在这种情况下我们将显示它),要么我们没有场景的图像(在这种情况下我们将显示黑屏,或者你选择的其他东西)。这取决于你!你将使用你方便的画布库函数来显示图像。😊

这部分作业的一些有用提示:

可选建议:作为一个小而诚实的扩展,在每个图像上写文字说"由DallE生成"。否则用户可能不知道这些是AI生成的。

恭喜!!

看看你的程序,多么神奇!你刚刚实现了你的第一个嵌套字典和你的第一个生成式AI项目!我们为你感到骄傲。♥️

里程碑7:反思

太棒了!现在,我们已经将讲故事的创意控制权交给了我们的代码和AI模型。但是,真的那么简单吗?生成式AI是一个令人兴奋的新工具,但它带来了很多复杂的问题。AI做得"好"吗?AI是否从人类内容创作者那里复制故事?如果我们无法区分AI内容和人类内容,这对我们的社会意味着什么。最后:这个生成式AI使用什么价值观和偏见?在最后一个里程碑中,我们将思考最后一个问题。

考虑你在讲故事时做出的所有选择。叙事、设置,甚至名字都是你作为讲故事者会决定的事情。当我们提示我们的AI模型时,它为我们做出什么选择?它真正在讲谁的故事?

将故事改为engineer_story.json。这个故事只有一个场景,所以用户做出选择后应该立即开始生成。

运行故事几次,注意它为你的同事生成的名字。你可以使用这个网站探索任何生成名字背后的统计数据:https://forebears.io/forenames。随意创建自己的故事或更改提示。之后,在infinite_ethics.txt文件中回答这些问题:

Q1:你应该注意到DeepSeek为这个故事生成的这些名字在很多国家都很受欢迎。这些名字经常出现的国家/地区类型是什么?你觉得它们在世界人口中分布均匀吗?作为提示,将你故事中生成的名字与这个讲义的作者列表进行比较!证明你的答案。至少写两句话。

Q2:你让AI模型决定故事中使用的名字有多舒服?你愿意信任模型做其他讲故事的决定吗?没有错误的立场,但请证明你的答案。至少写两句话。

Q3:考虑到DeepSeek在这种情况下如何处理名字,如果你不是要求DeepSeek生成故事,而是使用DeepSeek来评估求职者,你能想象会出现什么样的问题?