{
 "version": 1,
 "written": "2026-10-10",
 "units": [
  {"id": "python_start", "group": "第 1 单元：Windows 和 macOS 上的命令行", "label": "开始学 Python：终端、vim、Jupyter", "at": "python_start.html"},
  {"id": "it__it__it_windows", "group": "第 1 单元：Windows", "label": "Windows 基础：屏幕上的各个部分", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_files", "group": "第 1 单元：Windows", "label": "Windows 中的文件管理", "at": "it/it/it_files.html"},
  {"id": "mac_terminal", "group": "第 1 单元：macOS", "label": "在 Mac 上使用 sed、awk、curl、> 和 |", "at": "mac_terminal.html"},
  {"id": "it__net__net_types", "group": "第 2 单元：网络基础", "label": "网络：类型和拓扑", "at": "it/net/net_types.html"},
  {"id": "it__net__net_ip", "group": "第 2 单元：网络基础", "label": "IP 地址与子网划分", "at": "it/net/net_ip.html"},
  {"id": "it__net__net_wifi", "group": "第 2 单元：网络基础", "label": "Wi-Fi 和无线网络词汇", "at": "it/net/net_wifi.html"},
  {"id": "it__net__net_dns", "group": "第 2 单元：网络基础", "label": "DNS 和域名", "at": "it/net/net_dns.html"},
  {"id": "it__net__net_troubleshoot", "group": "第 2 单元：网络基础", "label": "网络故障排查工具", "at": "it/net/net_troubleshoot.html"},
  {"id": "tailscale", "group": "第 2 单元：网络基础", "label": "Tailscale：你自己的私有网络", "at": "tailscale.html"},
  {"id": "easy_variables", "group": "第 3 单元：Python 基础（简单）", "label": "变量、字符串和数字", "at": "python_piggy.html#step1"},
  {"id": "easy_lists", "group": "第 3 单元：Python 基础（简单）", "label": "列表：[0]、[-1]、append 和 len", "at": "python_piggy.html#step2"},
  {"id": "easy_dicts", "group": "第 3 单元：Python 基础（简单）", "label": "字典：键和值", "at": "python_piggy.html#step4"},
  {"id": "easy_loops", "group": "第 3 单元：Python 基础（简单）", "label": "for 循环和前面的空格", "at": "python_piggy.html#step5"},
  {"id": "easy_if", "group": "第 3 单元：Python 基础（简单）", "label": "True、False、if、elif 和 ==", "at": "python_piggy.html#step6"},
  {"id": "easy_functions", "group": "第 3 单元：Python 基础（简单）", "label": "函数：def、return 和 main()", "at": "python_piggy.html#step8"},
  {"id": "easy_modules", "group": "第 3 单元：Python 基础（简单）", "label": "import 和 Python 自带的模块", "at": "python_piggy.html#step2"},
  {"id": "easy_comprehensions", "group": "第 3 单元：Python 基础（简单）", "label": "推导式、集合和生成器", "at": "python_browser.html#step3"},
  {"id": "python_toolkit", "group": "第 3 单元：Python 基础（较难）", "label": "Python 工具箱：七个工具", "at": "python_toolkit.html"},
  {"id": "python_classes", "group": "第 3 单元：Python 基础（较难）", "label": "Python 类：用一个窗口来学", "at": "python_classes.html"},
  {"id": "python_data_classes", "group": "第 3 单元：Python 基础（较难）", "label": "dataclasses 模块里的数据类", "at": "python_data_classes.html"},
  {"id": "catch_your_own_bugs", "group": "第 3 单元：需要安装的 Python 库", "label": "用 pytest 抓住自己的 bug", "at": "catch_your_own_bugs.html"},
  {"id": "python_libraries", "group": "第 3 单元：需要安装的 Python 库", "label": "requests、attrs 和 Pydantic", "at": "python_data_classes.html"},
  {"id": "send_email", "group": "第 4 单元：真正能用的小程序", "label": "用 Python 处理邮件、文件和 AI", "at": "send_email.html"},
  {"id": "python_six_libraries", "group": "第 4 单元：真正能用的小程序", "label": "六个免费的数据类 Python 库", "at": "python_six_libraries.html"},
  {"id": "mailbot", "group": "第 4 单元：真正能用的小程序", "label": "每小时读一次你的邮件的机器人", "at": "mailbot.html"},
  {"id": "apis", "group": "第 4 单元：真正能用的小程序", "label": "什么是 API？", "at": "apis.html"},
  {"id": "html_intro", "group": "第 5 单元：GitHub Pages 上的网站", "label": "什么是 HTML：你的第一个网页", "at": "html_intro.html"},
  {"id": "javascript_lab", "group": "第 5 单元：GitHub Pages 上的网站", "label": "JavaScript Lab：九个基本概念", "at": "javascript_lab.html"},
  {"id": "it__ai__ai_code", "group": "第 5 单元：GitHub Pages 上的网站", "label": "AI 代码生成", "at": "it/ai/ai_code.html"},
  {"id": "prompt_lab", "group": "第 5 单元：GitHub Pages 上的网站", "label": "Prompt Lab：提示词的各个部分", "at": "prompt_lab.html"},
  {"id": "it__git_basics", "group": "第 5 单元：GitHub Pages 上的网站", "label": "Git 基础：提交、分支、推送、拉取", "at": "it/git_basics.html"},
  {"id": "it__github_pages_lab", "group": "第 5 单元：GitHub Pages 上的网站", "label": "Git 和 GitHub Pages：发布一个网站", "at": "it/github_pages_lab.html"},
  {"id": "javascript_space_game", "group": "第 5 单元：GitHub Pages 上的网站", "label": "用 JavaScript 做一个太空游戏", "at": "javascript_space_game.html"},
  {"id": "github", "group": "第 6 单元：私有仓库", "label": "Git 和 GitHub 基础", "at": "github.html"},
  {"id": "free_local_ai", "group": "第 7 单元：Ollama：你电脑上的模型", "label": "用 Ollama 免费运行本地 AI", "at": "free_local_ai.html"},
  {"id": "it__ai__ai_ollama", "group": "第 7 单元：Ollama：你电脑上的模型", "label": "Ollama：在自己电脑上运行 AI 模型", "at": "it/ai/ai_ollama.html"},
  {"id": "make_an_ai_tutor", "group": "第 7 单元：Ollama：你电脑上的模型", "label": "用 Python 和 Ollama 做一个 AI 辅导老师", "at": "make_an_ai_tutor.html"},
  {"id": "tkinter_chat", "group": "第 7 单元：Ollama：你电脑上的模型", "label": "用 tkinter 和 Ollama 做一个聊天窗口", "at": "tkinter_chat.html"},
  {"id": "it__ai__ai_windows_stack", "group": "第 7 单元：Windows 上的 Ollama", "label": "Windows 上的本地 AI 技术栈", "at": "it/ai/ai_windows_stack.html"},
  {"id": "it__ai__ai_mac_stack", "group": "第 7 单元：macOS 上的 Ollama", "label": "Mac 上的本地 AI 技术栈", "at": "it/ai/ai_mac_stack.html"},
  {"id": "decider", "group": "第 8 单元：Decider：会做选择的模型", "label": "Decider：一个从选项中做选择的模型", "at": "decider.html"},
  {"id": "jev", "group": "第 8 单元：Decider：会做选择的模型", "label": "Jev 和 Kev：决策模型", "at": "jev.html"},
  {"id": "agno_agents", "group": "第 9 单元：Agno：一支智能体团队", "label": "Agno：一家由 AI 智能体组成的公司", "at": "agno_agents.html"},
  {"id": "smolagents", "group": "第 9 单元：Agno：一支智能体团队", "label": "smolagents：会使用你的工具的智能体", "at": "smolagents.html"},
  {"id": "docker_lab", "group": "第 10 单元：Docker", "label": "Docker Lab：装程序的盒子", "at": "docker_lab.html"},
  {"id": "it__ai__dockerfile_basics", "group": "第 10 单元：Docker", "label": "自己写一个 Dockerfile", "at": "it/ai/dockerfile_basics.html"},
  {"id": "linux_lab", "group": "第 10 单元：Docker", "label": "Linux Lab：Docker 里的 Ubuntu 和 Rocky", "at": "linux_lab.html"},
  {"id": "bentoml", "group": "第 11 单元：BentoML：把模型变成网络服务", "label": "BentoML：把函数变成网络服务", "at": "bentoml.html"},
  {"id": "reverse_engineer", "group": "第 12 单元：逆向工程", "label": "用 REA 学逆向工程", "at": "reverse_engineer.html"},
  {"id": "flask", "group": "第 11 单元：BentoML：把模型变成网络服务", "label": "Flask：用 Python 做网页", "at": "flask.html"},
  {"id": "gradio", "group": "第 11 单元：BentoML：把模型变成网络服务", "label": "Gradio：用一个函数做出网页", "at": "gradio.html"},
  {"id": "rag_llm", "group": "第 13 单元：你自己的项目", "label": "在笔记本电脑上用 Python 做 RAG", "at": "rag_llm.html"},
  {"id": "keyestudio_smart_home", "group": "第 13 单元：你自己的项目", "label": "用 MicroPython 玩 Keyestudio Smart Home", "at": "keyestudio_smart_home.html"},
  {"id": "thai_speech_to_text", "group": "第 13 单元：你自己的项目", "label": "在自己的电脑上免费做泰语语音转文字", "at": "thai_speech_to_text.html"},
  {"id": "fine_tune_llm", "group": "第 13 单元：你自己的项目", "label": "微调 LLM：全量微调与 LoRA", "at": "fine_tune_llm.html"},
  {"id": "python_to_exe", "group": "第 13 单元：在 Windows 上把 Python 变成应用", "label": "用 PyInstaller 把 Python 变成 EXE", "at": "python_to_exe.html"},
  {"id": "python_to_mac_app", "group": "第 13 单元：在 macOS 上把 Python 变成应用", "label": "从 Python 脚本到 Mac 应用", "at": "python_to_mac_app.html"},
  {"id": "space_invaders", "group": "Start Python 里的更多课程", "label": "用类写 Space Invaders 游戏", "at": "space_invaders.html"},
  {"id": "chiang_mai_map", "group": "Start Python 里的更多课程", "label": "用 Python 和 folium 绘制清迈地图", "at": "chiang_mai_map.html"},
  {"id": "grokking_algorithms", "group": "Start Python 里的更多课程", "label": "Grokking Algorithms：用 Python 写 16 个算法", "at": "grokking_algorithms.html"},
  {"id": "make_a_flashcard_app", "group": "Start Python 里的更多课程", "label": "用 tkinter 做一个单词卡应用", "at": "make_a_flashcard_app.html"},
  {"id": "make_a_homework_app", "group": "Start Python 里的更多课程", "label": "会记住东西的应用：sqlite3、argparse", "at": "make_a_homework_app.html"},
  {"id": "make_a_school_board", "group": "Start Python 里的更多课程", "label": "Flask：学校 wifi 上的作业应用", "at": "make_a_school_board.html"},
  {"id": "is_the_air_safe", "group": "Start Python 里的更多课程", "label": "空气安全吗：从 API 获取 PM2.5", "at": "is_the_air_safe.html"},
  {"id": "teach_the_computer_to_speak", "group": "Start Python 里的更多课程", "label": "用 edge-tts 在 Python 中把文字转成语音", "at": "teach_the_computer_to_speak.html"},
  {"id": "robot_python", "group": "Start Python 里的更多课程", "label": "用 Python 开机器人小车", "at": "robot_python.html"},
  {"id": "control_your_phone", "group": "Start Python 里的更多课程", "label": "用笔记本电脑控制 Android 手机", "at": "control_your_phone.html"},
  {"id": "crewai_autogen", "group": "Start Python 里的更多课程", "label": "CrewAI 和 AutoGen 智能体团队", "at": "crewai_autogen.html"},
  {"id": "langchain", "group": "Start Python 里的更多课程", "label": "LangChain：提示词、模型、解析器", "at": "langchain.html"},
  {"id": "face_recognition", "group": "Start Python 里的更多课程", "label": "用 OpenCV 做人脸识别", "at": "face_recognition.html"},
  {"id": "physics_with_python", "group": "Start Python 里的更多课程", "label": "用 Python 学物理：检验公式", "at": "physics_with_python.html"},
  {"id": "telescope_data", "group": "Start Python 里的更多课程", "label": "用 Python 查询太空望远镜档案库", "at": "telescope_data.html"},
  {"id": "cloakbrowser", "group": "Start Python 里的更多课程", "label": "用 CloakBrowser 抓取网页", "at": "cloakbrowser.html"},
  {"id": "rust_start", "group": "Start Python 里的更多课程", "label": "Rust 入门：变量、结构体、错误", "at": "rust_start.html"}
 ],
 "questions": [
  {"id": "python_start_01", "unit": "python_start", "d": -0.8, "type": "mcq", "q": "在终端里，`mkdir Code` 做什么？", "o": ["打开名为 Code 的文件夹", "删除名为 Code 的文件夹", "创建名为 Code 的文件夹", "列出 Code 里面的文件"], "a": 2, "why": "`mkdir` 的意思是 Make Directory（创建目录）。`mkdir Code` 会创建一个名为 Code 的文件夹。", "at": "python_start.html#s5"},
  {"id": "python_start_02", "unit": "python_start", "d": -0.5, "type": "mcq", "q": "在终端里使用 `rm` 之前，为什么一定要检查两遍？", "o": ["它没有废纸篓，也不能撤销", "它还会删除上一级的文件夹", "它需要连上互联网才能工作", "它会停止正在运行的程序"], "a": 0, "why": "`rm` 删除文件时没有废纸篓，也不能撤销。用鼠标删除的文件还能恢复，但用 `rm` 删除的不能。", "at": "python_start.html#s5"},
  {"id": "python_start_03", "unit": "python_start", "d": -0.4, "type": "mcq", "q": "你在 vim 里打开一个文件，然后按 `i`。什么变了？", "o": ["文件被保存，vim 关闭", "整行一下子被删除", "你上一次的修改被撤销", "键盘现在可以输入字母了"], "a": 3, "why": "`i` 进入 INSERT 模式。在 INSERT 模式下键盘正常输入，按 Esc 退出该模式。", "at": "python_start.html#s7"},
  {"id": "python_start_04", "unit": "python_start", "d": 0.0, "type": "mcq", "q": "你在 vim 里迷路了，想退出并丢掉你的修改。按下 Esc 之后，你要输入哪条命令？", "o": ["`:wq`", "`:q!`", "`:w`", "`:x`"], "a": 1, "why": "`:q!` 退出 vim 并丢掉你的修改。它总是有效，所以迷路的时候就用它脱身。", "at": "python_start.html#s7"},
  {"id": "python_start_05", "unit": "python_start", "d": -0.1, "type": "mcq", "q": "提示符显示 `>>>`。怎样回到终端的提示符？", "o": ["输入 `:wq` 然后按 Return 键", "输入 `exit()` 然后按 Return 键", "输入 `cd ..` 然后按 Return 键", "输入 `pwd` 然后按 Return 键"], "a": 1, "why": "`>>>` 表示你在 Python 里面。要离开，输入 `exit()` 然后按 Return 键。", "at": "python_start.html#s8"},
  {"id": "python_start_06", "unit": "python_start", "d": 0.3, "type": "mcq", "q": "为什么要按顺序运行 Jupyter notebook 里的单元格？", "o": ["单元格共享内存，所以顺序会改变结果", "Jupyter 会删除任何不按顺序运行的单元格", "每个单元格运行时都从空的内存开始", "浏览器一次只能显示一个单元格的结果"], "a": 0, "why": "单元格共享内存：在一个单元格里创建的变量，在后面的单元格里仍然存在。几乎所有奇怪的结果都是不按顺序运行单元格造成的。", "at": "python_start.html#s11"},
  {"id": "python_start_07", "unit": "python_start", "d": 0.4, "type": "mcq", "q": "`git push` 要你输入密码。你必须在那里粘贴什么？", "o": ["你的 GitHub 账号密码", "你电脑的登录密码", "个人访问令牌（personal access token）", "你的仓库的名字"], "a": 2, "why": "GitHub 不接受通过 HTTPS 使用账号密码。你要创建一个个人访问令牌（personal access token），在 git 要密码时把它粘贴进去。", "at": "python_start.html#s11"},
  {"id": "python_start_08", "unit": "python_start", "d": 0.5, "type": "mcq", "q": "最后一行会打印什么？", "code": "class Creep:\n    def __init__(self, speed, hit_points):\n        self.speed = speed\n        self.hit_points = hit_points\n\na = Creep(2.0, 3)\nb = Creep(0.5, 1)\na.hit_points -= 1\nprint(b.hit_points)", "o": ["2", "1", "3", "0"], "a": 1, "why": "点号进入的是某一个对象的内部。只有 `a` 变了，所以 `b.hit_points` 仍然是 1。", "at": "python_start.html#dots"},
  {"id": "it__it__it_windows_01", "unit": "it__it__it_windows", "d": -0.9, "type": "mcq", "q": "Windows 是什么？", "o": ["一个用来写信和文档的程序", "电脑内部的一个硬件部件", "一个用来下载新应用的网站", "Microsoft 开发的一个操作系统"], "a": 3, "why": "Windows 是 Microsoft 开发的操作系统。操作系统是管理电脑上一切的主要软件。", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_windows_02", "unit": "it__it__it_windows", "d": -0.6, "type": "mcq", "q": "你想更改 Wi-Fi、显示或语言。要打开哪个应用？", "o": ["设置（Settings）", "回收站（Recycle Bin）", "任务栏（Taskbar）", "壁纸（Wallpaper）"], "a": 0, "why": "设置（Settings）是用来更改 Wi-Fi、显示和语言等电脑选项的应用。", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_windows_03", "unit": "it__it__it_windows", "d": -0.3, "type": "mcq", "q": "桌面上的快捷方式是什么？", "o": ["一条告诉你有新邮件的消息", "所有图标后面的那张背景图片", "一个能快速打开程序的小图标", "一个保存你已删除文件的文件夹"], "a": 2, "why": "快捷方式是桌面上的一个小图标，能快速打开某个文件或程序。", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_windows_04", "unit": "it__it__it_windows", "d": -0.1, "type": "fill", "q": "桌面上的背景图片用哪个英文单词表示？请输入一个单词。", "answers": ["wallpaper"], "why": "wallpaper（壁纸）就是桌面上的背景图片。", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_windows_05", "unit": "it__it__it_windows", "d": 0.3, "type": "mcq", "q": "你不小心删除了一个文件。在哪里还能找到它？", "o": ["在“开始”菜单里", "在回收站里", "在设置应用里", "在任务栏上"], "a": 1, "why": "回收站会保存已删除的文件，直到你把它们永久删除，所以你可以从那里还原这个文件。", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_windows_06", "unit": "it__it__it_windows", "d": 0.5, "type": "mcq", "q": "哪两种方法可以打开“开始”菜单？", "o": ["点击齿轮图标，或者按 Ctrl 键", "点击壁纸，或者按 Alt 键", "点击回收站，或者按 Shift 键", "点击 Windows 图标，或者按 Windows 键"], "a": 3, "why": "点击任务栏左下角的 Windows 图标，或者按 Windows 键。", "at": "it/it/it_windows.html"},
  {"id": "it__it__it_files_01", "unit": "it__it__it_files", "d": -0.8, "type": "mcq", "q": "在 Windows 里删除一个文件时，首先会发生什么？", "o": ["它被复制到一个备份里", "它进入回收站", "它被移到“文档”文件夹", "它得到一个新的扩展名"], "a": 1, "why": "删除会移除一个文件。在 Windows 里，被删除的文件会先进入回收站。", "at": "it/it/it_files.html"},
  {"id": "it__it__it_files_02", "unit": "it__it__it_files", "d": -0.4, "type": "mcq", "q": "“复制”和“移动”有什么区别？", "o": ["复制改变名称；移动改变类型", "复制用于文件夹；移动用于文件", "复制会移除原文件；移动会保留它", "复制会保留原文件；移动会把它拿走"], "a": 3, "why": "“复制”做出一个副本，原文件不动。“移动”把文件从一个位置转到另一个位置。", "at": "it/it/it_files.html"},
  {"id": "it__it__it_files_03", "unit": "it__it__it_files", "d": 0.0, "type": "mcq", "q": "一个文件名叫 `budget.xlsx`。扩展名告诉你什么？", "o": ["它是给 Media Player 用的视频", "它是一个压缩文件", "它是给 Excel 用的电子表格", "它是一个程序安装包"], "a": 2, "why": "扩展名就是点后面的那几个字母。`.xlsx` 是电子表格，用 Microsoft Excel 打开。", "at": "it/it/it_files.html"},
  {"id": "it__it__it_files_04", "unit": "it__it__it_files", "d": 0.3, "type": "mcq", "q": "在文件资源管理器里，怎样新建一个文件夹？", "o": ["右键单击，然后选“新建”，再选“文件夹”", "按两次 Windows + E", "把一个文件重命名，让它以 `.zip` 结尾", "搜索“文件夹”这个词"], "a": 0, "why": "在文件资源管理器里：右键单击，“新建”，“文件夹”。然后你就可以把文件移到新文件夹里。", "at": "it/it/it_files.html"},
  {"id": "it__it__it_files_05", "unit": "it__it__it_files", "d": 0.5, "type": "mcq", "q": "你想在第二个文件夹里放一份 `report.docx` 的副本。然后你想移除一张旧照片。你要用哪两个操作？", "o": ["移动，然后重命名", "重命名，然后搜索", "复制，然后删除", "移动，然后备份"], "a": 2, "why": "“复制”做出一个副本，并留下原文件。“删除”会移除一个文件。", "at": "it/it/it_files.html"},
  {"id": "mac_terminal_01", "unit": "mac_terminal", "d": -0.8, "type": "mcq", "q": "文件 `hello.txt` 里已经有两行。你运行 `echo \"This wipes it\" > hello.txt`。原来的两行会怎样？", "o": ["它们还在，新的一行被加上去", "它们没有了，而且没有任何警告", "shell 会先请你确认", "它们被移到废纸篓"], "a": 1, "why": "一个 `>` 会重新创建这个文件，所以原来的内容就没有了。没有警告，也不进废纸篓。`>>` 则是加到末尾。", "at": "mac_terminal.html#step1"},
  {"id": "mac_terminal_02", "unit": "mac_terminal", "d": -0.3, "type": "mcq", "q": "`2> problems.txt` 接住的是什么？", "o": ["只有错误信息", "只有正常的输出", "两种输出都有", "只有输出的第二行"], "a": 0, "why": "每个命令都有两个出口。`>` 接住正常的输出，`2>` 接住错误。", "at": "mac_terminal.html#step2"},
  {"id": "mac_terminal_03", "unit": "mac_terminal", "d": -0.5, "type": "mcq", "q": "`|` 这个符号对一个命令的输出做什么？", "o": ["把它保存到一个新文件里", "把它加到一个文件的末尾", "把它送给另一个程序", "把输出扔掉"], "a": 2, "why": "`>` 把输出送到一个文件。`|` 把输出送给另一个程序，这样小工具就连成了一串。", "at": "mac_terminal.html#step3"},
  {"id": "mac_terminal_04", "unit": "mac_terminal", "d": 0.0, "type": "mcq", "q": "少了一个选项，即使网页不存在，`curl` 也会报告成功。哪个选项能让 `curl` 在遇到 404 时明确地报错？", "o": ["`-s`", "`-L`", "`-o`", "`-f`"], "a": 3, "why": "`-f` 让 `curl` 承认网页不存在：它打印出 404 错误，而且退出码是 22，不是 0。", "at": "mac_terminal.html#step5"},
  {"id": "mac_terminal_05", "unit": "mac_terminal", "d": 0.2, "type": "mcq", "q": "`echo \"one one one\" | sed 's/one/two/'` 会打印什么？", "o": ["two two two", "one one two", "one two one", "two one one"], "a": 3, "why": "没有 `g` 时，`sed` 只改这一行里第一个匹配的地方，然后就停下。有了 `g`，它会改每一个匹配的地方。", "at": "mac_terminal.html#step6"},
  {"id": "mac_terminal_06", "unit": "mac_terminal", "d": 0.5, "type": "mcq", "q": "一个 shell 脚本用 `sed 's/first/FIRST/' lines.txt > lines.new && mv lines.new lines.txt` 来修改文件，而不用 `sed -i`。先写入再重命名的做法有什么好处？", "o": ["它在 Mac 和 Linux 上运行起来都一样", "它是保留一份备份的唯一办法", "它处理很大的文件时要快得多", "它让 `sed` 一次修改两个文件"], "a": 0, "why": "在 Mac 上，`sed -i` 需要一对空引号（`sed -i ''`），在 Linux 上则不需要。写一个新文件再给它重命名，在两边都行得通。", "at": "mac_terminal.html#step8"},
  {"id": "mac_terminal_07", "unit": "mac_terminal", "d": 0.4, "type": "mcq", "q": "`echo \"Nari teaches English in Chiang Mai\" | awk '{print $1, $NF}'` 会打印什么？", "o": ["Nari Chiang Mai", "Nari Mai", "Nari 6", "Nari teaches"], "a": 1, "why": "`$NF` 是最后一段。`awk` 按空格切分，所以最后一段是 Mai，而不是 Chiang Mai。", "at": "mac_terminal.html#step9"},
  {"id": "mac_terminal_08", "unit": "mac_terminal", "d": 0.6, "type": "mcq", "q": "这个 shell 脚本每天运行一次。它的最后一行是 `>> price_log.csv`。如果只用一个箭头，写成 `> price_log.csv`，会出什么问题？", "code": "curl -sS -f \"https://api.bitkub.com/api/v3/market/ticker?sym=BTC_THB\" \\\n  | sed -E 's/.*\"last\":\"([0-9.]+)\".*/\\1/' \\\n  | awk -v day=\"$(date +%F)\" '{printf \"%s,%.2f\\n\", day, $1}' \\\n  >> price_log.csv", "o": ["每次运行都会把价格打印两遍", "每次运行都会把错误写进日志", "每次运行都会让 `awk` 无法运行", "每次运行都会抹掉前几天的记录"], "a": 3, "why": "两个箭头是在文件末尾添加一行。一个箭头会重新创建这个文件，所以前几天的那些行就没有了。", "at": "mac_terminal.html#step11"},
  {"id": "it__net__net_types_01", "unit": "it__net__net_types", "d": -0.8, "type": "mcq", "q": "在计算机网络里，“拓扑”这个词是什么意思？", "o": ["能发送的最大数据量", "一台请求服务的设备", "一个覆盖很大区域的网络", "设备之间如何连接的布局"], "a": 3, "why": "拓扑是网络中设备之间如何连接的布局或排列方式。", "at": "it/net/net_types.html"},
  {"id": "it__net__net_types_02", "unit": "it__net__net_types", "d": -0.5, "type": "mcq", "q": "一个网络覆盖一座城市或一个大校园。它是哪种类型？", "o": ["LAN", "WAN", "MAN", "Mesh"], "a": 2, "why": "MAN（Metropolitan Area Network，城域网）覆盖一座城市或一个大校园。LAN 覆盖小范围，WAN 覆盖很大的地理范围。", "at": "it/net/net_types.html"},
  {"id": "it__net__net_types_03", "unit": "it__net__net_types", "d": 0.0, "type": "mcq", "q": "在哪一种拓扑里，每台设备都和其他每一台设备相连？", "o": ["网状", "环形", "星形", "总线"], "a": 0, "why": "在网状拓扑里，每台设备都和其他每一台设备相连。路径这么多，所以网状拓扑很可靠。", "at": "it/net/net_types.html"},
  {"id": "it__net__net_types_04", "unit": "it__net__net_types", "d": 0.2, "type": "fill", "q": "在网络里，有一个词指任何连到网络上的设备，比如一台电脑或一台打印机。这个词有四个字母，以 “n” 开头。请输入这个英文单词。", "answers": ["node", "a node", "nodes"], "why": "node（节点）是任何连到网络上的设备，比如一台电脑或打印机。", "at": "it/net/net_types.html"},
  {"id": "it__net__net_types_05", "unit": "it__net__net_types", "d": 0.4, "type": "mcq", "q": "在 bus（总线）拓扑中，设备是怎样连接的？", "o": ["每台设备都连到一台中央交换机", "所有设备共用一条通信线路", "它们连成一条环形的链", "每台设备都和其他所有设备相连"], "a": 1, "why": "在 bus 拓扑中，所有设备共用一条通信线路。连到中央交换机的是 star（星形），环形的链是 ring（环形）。", "at": "it/net/net_types.html"},
  {"id": "it__net__net_ip_01", "unit": "it__net__net_ip", "d": -0.7, "type": "mcq", "q": "地址 127.0.0.1 叫作回环地址，也叫 localhost。它指向什么？", "o": ["你所在网络的路由器", "你自己的电脑", "DHCP 服务器", "你的网络运营商"], "a": 1, "why": "127.0.0.1 是一个特殊地址，指的是本机设备自身。它永远指向你自己的电脑。", "at": "it/net/net_ip.html"},
  {"id": "it__net__net_ip_02", "unit": "it__net__net_ip", "d": -0.3, "type": "mcq", "q": "什么是静态 IP？", "o": ["一个手动设置、不会改变的地址", "一个由 DHCP 服务器分配的地址", "一个指向本机设备的地址", "一个由 ISP 展示给互联网的地址"], "a": 0, "why": "静态 IP 是手动分配的，不会改变。动态 IP 是由 DHCP 服务器自动分配的。", "at": "it/net/net_ip.html"},
  {"id": "it__net__net_ip_03", "unit": "it__net__net_ip", "d": 0.0, "type": "mcq", "q": "子网掩码的作用是什么？", "o": ["它连接两个不同的网络", "它给新设备分配地址", "它让设备在互联网上隐藏", "它标出网络部分和主机部分"], "a": 3, "why": "子网掩码规定了 IP 地址中哪一部分是网络，哪一部分是主机。", "at": "it/net/net_ip.html"},
  {"id": "it__net__net_ip_04", "unit": "it__net__net_ip", "d": 0.3, "type": "fill", "q": "在 Windows 上，你在命令提示符里输入哪条命令，可以看到你的 IP 地址、子网掩码和默认网关？请输入这条命令。", "answers": ["ipconfig"], "why": "在 Windows 上，`ipconfig` 显示这台电脑的网络设置：它的 IP 地址、子网掩码和默认网关。", "at": "it/net/net_ip.html"},
  {"id": "it__net__net_ip_05", "unit": "it__net__net_ip", "d": 0.6, "type": "mcq", "q": "家里的一台笔记本电脑接入网络时，自动得到了它的地址。哪一行描述的是这种情况？", "o": ["静态 IP，由网关设定", "公网 IP，由子网掩码设定", "动态 IP，来自 DHCP 服务器", "环回 IP，来自 ISP"], "a": 2, "why": "动态 IP 由 DHCP 服务器自动分配。家里的设备通常就是这样得到地址的。", "at": "it/net/net_ip.html"},
  {"id": "it__net__net_wifi_01", "unit": "it__net__net_wifi", "d": -0.7, "type": "mcq", "q": "什么是接入点（access point）？", "o": ["一种增强弱信号的设备", "一种创建无线网络的设备", "一个无线网络所用的名称", "Wi-Fi 信号较弱的一片区域"], "a": 1, "why": "接入点是一种创建无线网络、供其他设备连接的设备。", "at": "it/net/net_wifi.html"},
  {"id": "it__net__net_wifi_02", "unit": "it__net__net_wifi", "d": -0.5, "type": "fill", "q": "哪个词表示无线网络的名称，也就是设备搜索时看到的名称？请输入这四个字母。", "answers": ["ssid"], "why": "SSID 是 Service Set Identifier 的缩写。它是无线网络的名称。", "at": "it/net/net_wifi.html"},
  {"id": "it__net__net_wifi_03", "unit": "it__net__net_wifi", "d": 0.0, "type": "mcq", "q": "5 GHz 的 Wi-Fi 和 2.4 GHz 的 Wi-Fi 有什么不同？", "o": ["覆盖范围更大，但更慢", "覆盖范围更大，也更快", "覆盖范围更小，也更慢", "覆盖范围更小，但更快"], "a": 3, "why": "2.4 GHz 的覆盖范围更大，速度更慢。5 GHz 的覆盖范围更小，但更快。", "at": "it/net/net_wifi.html"},
  {"id": "it__net__net_wifi_04", "unit": "it__net__net_wifi", "d": 0.3, "type": "mcq", "q": "什么是信道（channel）？", "o": ["一个 Wi-Fi 频段内的某个特定频率范围", "设备搜索网络时看到的那个名称", "把你连接到互联网的那台设备", "在设备之间传送数据的无线电波"], "a": 0, "why": "信道是一个 Wi-Fi 频段内的某个特定频率范围。频段本身指的是频率，即 2.4 GHz 或 5 GHz。", "at": "it/net/net_wifi.html"},
  {"id": "it__net__net_wifi_05", "unit": "it__net__net_wifi", "d": 0.5, "type": "mcq", "q": "中继器通过增强信号来扩大覆盖范围。mesh Wi-Fi 有什么特别之处？", "o": ["它使用网线，完全不用无线电波", "它只能配合旧的 2.4 GHz 设备工作", "它使用多个协同工作的单元", "它是一种安全标准，不是一种设备"], "a": 2, "why": "mesh Wi-Fi 是由多个 Wi-Fi 单元组成的系统，它们协同工作，实现全面覆盖。", "at": "it/net/net_wifi.html"},
  {"id": "it__net__net_dns_01", "unit": "it__net__net_dns", "d": -0.7, "type": "mcq", "q": "DNS 的工作是什么？", "o": ["它把域名翻译成 IP 地址。", "它把网站的文件存在服务器上。", "它为公司注册新的域名。", "它让网站在用户那里加载得更快。"], "a": 0, "why": "DNS 是 Domain Name System（域名系统）。它把域名翻译成 IP 地址。", "at": "it/net/net_dns.html"},
  {"id": "it__net__net_dns_02", "unit": "it__net__net_dns", "d": -0.3, "type": "mcq", "q": "A 记录的作用是什么？", "o": ["它把一个域名指向另一个域名。", "它把一个域名对应到一个 IPv4 地址。", "它规定一条记录在缓存里保留多久。", "它把网站的文件存在服务器上。"], "a": 1, "why": "A 记录把一个域名对应到一个 IPv4 地址，比如 example.com → 93.184.216.34。指向另一个域名的别名是 CNAME。", "at": "it/net/net_dns.html"},
  {"id": "it__net__net_dns_03", "unit": "it__net__net_dns", "d": -0.2, "type": "fill", "q": "命令提示符里的哪个工具会向 DNS 服务器查询一个名字的记录，比如 `google.com` 的 IP 地址？请只输入命令的名称。", "answers": ["nslookup", "nslookup google.com"], "why": "`nslookup` 向 DNS 查询一个名字的记录。在命令提示符里，`nslookup google.com` 会显示 `google.com` 的 IP 地址。", "at": "it/net/net_dns.html"},
  {"id": "it__net__net_dns_04", "unit": "it__net__net_dns", "d": 0.2, "type": "mcq", "q": "一条 DNS 记录的 TTL 是 3600。这说明什么？", "o": ["这条记录指向 3600 个不同的 IP 地址。", "这个域名是 3600 天前注册的。", "这条记录会被缓存 3600 秒（1 小时）。", "这次改动要 3600 小时才能传遍全世界。"], "a": 2, "why": "TTL 是 Time to Live（存活时间）：一条 DNS 记录在刷新之前被缓存多久。它以秒为单位，3600 就是 1 小时。", "at": "it/net/net_dns.html"},
  {"id": "it__net__net_dns_05", "unit": "it__net__net_dns", "d": 0.5, "type": "mcq", "q": "resolver（解析器）和 nameserver（域名服务器）有什么区别？", "o": ["它们是同一种 DNS 服务器的两个名字。", "resolver 注册域名；nameserver 托管网站。", "resolver 保存记录；nameserver 查找 IP。", "resolver 查找 IP；nameserver 保存记录。"], "a": 3, "why": "resolver 接收 DNS 查询，并找到正确的 IP 地址。nameserver 保存 DNS 记录，并回答查询。", "at": "it/net/net_dns.html"},
  {"id": "it__net__net_troubleshoot_01", "unit": "it__net__net_troubleshoot", "d": -0.7, "type": "fill", "q": "你想测试网络上的一台设备能不能连通。哪条命令做这件事？请输入它的名称。", "answers": ["ping"], "why": "`ping` 是一条用来测试网络上的设备能不能连通的命令。", "at": "it/net/net_troubleshoot.html"},
  {"id": "it__net__net_troubleshoot_02", "unit": "it__net__net_troubleshoot", "d": -0.3, "type": "mcq", "q": "`traceroute` 显示什么？", "o": ["数据到达目的地所走的路径", "DNS 为某个域名记录的 IP 地址", "现在正在监听的端口", "Windows 上的网络设置"], "a": 0, "why": "`traceroute` 显示数据从你的电脑到目的地所走的路径。", "at": "it/net/net_troubleshoot.html"},
  {"id": "it__net__net_troubleshoot_03", "unit": "it__net__net_troubleshoot", "d": 0.0, "type": "mcq", "q": "哪个命令向 DNS 查询某个域名的 IP 地址？", "o": ["`netstat`", "`ipconfig`", "`nslookup`", "`traceroute`"], "a": 2, "why": "`nslookup` 是查询 DNS、找出域名对应 IP 地址的命令。", "at": "it/net/net_troubleshoot.html"},
  {"id": "it__net__net_troubleshoot_04", "unit": "it__net__net_troubleshoot", "d": 0.4, "type": "mcq", "q": "TTL（Time to Live）限制的是什么？", "o": ["一个请求等待回复的时间有多长", "一个数据包最多能经过多少跳", "一台设备最多能打开多少个端口", "一个监控程序最多能监视多少流量"], "a": 1, "why": "TTL 限制一个数据包在被丢弃之前最多能经过多少跳。", "at": "it/net/net_troubleshoot.html"},
  {"id": "it__net__net_troubleshoot_05", "unit": "it__net__net_troubleshoot", "d": 0.6, "type": "mcq", "q": "一个网络请求花的时间太长，被取消了。这叫什么？", "o": ["延迟", "丢包", "TTL", "超时"], "a": 3, "why": "超时是指请求花的时间太长而被取消。延迟只是时间上的滞后，丢包是数据没有到达。", "at": "it/net/net_troubleshoot.html"},
  {"id": "tailscale_01", "unit": "tailscale", "d": -0.8, "type": "mcq", "q": "在 Tailscale 里，是什么让你的设备处在同一个 tailnet 里？", "o": ["把它们都连到同一个 Wi-Fi 上", "在每台设备上用同一个账号登录", "给它们都起同一个设备名称", "在你的路由器上打开同一个端口"], "a": 1, "why": "一个 tailnet 就是同一个账号下的所有设备。所以在每台设备上用同一个账号登录，它们就在同一个 tailnet 里。", "at": "tailscale.html#install"},
  {"id": "tailscale_02", "unit": "tailscale", "d": -0.5, "type": "fill", "q": "补全这条列出 tailnet 里每一台设备的命令：`tailscale ____`。请输入缺少的那个英文单词。", "answers": ["status", "tailscale status", "sudo tailscale status"], "why": "`tailscale status` 列出每一台设备。每一行显示它的地址、名字和连接情况。", "at": "tailscale.html#look"},
  {"id": "tailscale_03", "unit": "tailscale", "d": -0.3, "type": "mcq", "q": "在 Tailscale 里，Serve 和 Funnel 有什么区别？", "o": ["Serve 只面向 tailnet；Funnel 面向整个互联网", "Serve 是给手机用的；Funnel 是给 Linux 服务器用的", "Serve 用普通的 HTTP；只有 Funnel 才有 HTTPS", "Serve 是免费的；Funnel 需要 Standard 套餐"], "a": 0, "why": "Serve 只把一个应用分享给你 tailnet 里的设备。Funnel 把它分享给互联网上的所有人。", "at": "tailscale.html#share"},
  {"id": "tailscale_04", "unit": "tailscale", "d": 0.0, "type": "mcq", "q": "`tailscale status` 对某台设备显示 `active; direct`。这是什么意思？", "o": ["这台设备关机了，或者它上面的 Tailscale 停了", "流量经过一个 DERP 中继", "流量在两台设备之间直接传输", "这台设备的密钥很快就要过期"], "a": 2, "why": "`active; direct` 是最好的结果：流量在设备之间直接传输，不经过中继。", "at": "tailscale.html#look"},
  {"id": "tailscale_05", "unit": "tailscale", "d": 0.3, "type": "mcq", "q": "`tailscale ping` 能通，但另一台设备上的应用打不开。要检查的一件事是什么？", "o": ["两台设备用的是不是完全相同的 Google 账号", "另一台设备的密钥是不是还没有过期", "用 `tailscale netcheck` 检查 UDP 是不是能用", "应用是不是没有只监听 `127.0.0.1`"], "a": 3, "why": "如果 ping 能通，网络就没有问题。应用必须监听网络，而不是只监听 localhost。", "at": "tailscale.html#trouble"},
  {"id": "tailscale_06", "unit": "tailscale", "d": 0.5, "type": "mcq", "q": "在 Tailscale 里，要改一项设置时，为什么用 `tailscale set` 比用 `tailscale up` 更好？", "o": ["`set` 在 Linux 上不用 `sudo` 就能运行，`up` 则不行", "`set` 只改你指明的那些设置", "`set` 还会用一个新密钥让你重新登录", "`set` 把改动一次发送到每一台设备"], "a": 1, "why": "`tailscale set` 只改你指明的那些设置。用 `tailscale up` 时，你想保留的每一项设置都必须再写一遍。", "at": "tailscale.html#configure"},
  {"id": "tailscale_07", "unit": "tailscale", "d": 0.6, "type": "mcq", "q": "这个 Docker Compose 文件在一个应用旁边的容器里运行 Tailscale。如果你去掉 `ts-state` 卷，会发生什么？", "code": "services:\n  app:\n    image: nginx:alpine\n  app-tailscale:\n    image: tailscale/tailscale:latest\n    environment:\n      TS_AUTHKEY: ${TS_AUTHKEY}\n      TS_HOSTNAME: myapp\n      TS_STATE_DIR: /var/lib/tailscale\n    volumes:\n      - ts-state:/var/lib/tailscale\nvolumes:\n  ts-state:", "o": ["应用会丢失它的 HTTPS 证书", "Docker 无法按名字找到 `app`", "每次重启都会多出一台新机器", "认证密钥会被写进 compose 文件里"], "a": 2, "why": "`TS_STATE_DIR` 和 `ts-state` 卷让 Tailscale 的登录状态在重启之间保留下来。没有这个卷，每次重启都会作为一台新机器加入 tailnet。", "at": "tailscale.html#docker"},
  {"id": "ev_01", "unit": "easy_variables", "d": -1.0, "type": "mcq", "q": "`pig = \"Princess Taiping\"` 里的 `=` 做什么？", "o": ["把值存进这个名字", "问两边是否相等", "把值打印在屏幕上", "把两个词连在一起"], "a": 0, "why": "一个 `=` 给名字一个值：可以读作“现在是”。两个，`==`，是问两个值是否相同。", "at": "python_piggy.html#step1"},
  {"id": "ev_02", "unit": "easy_variables", "d": -0.9, "type": "fill", "q": "放在引号里的文字，比如 `\"mango\"`，是一种值。这种值叫什么？请输入一个单词。", "answers": ["string", "str", "strings"], "why": "引号里的文字是字符串，简称 `str`。Python 会完全按你输入的样子保存它。", "at": "python_piggy.html#step1"},
  {"id": "ev_03", "unit": "easy_variables", "d": -0.8, "type": "mcq", "q": "`age` 一开始是 3，然后过了一个生日。这段代码打印什么？", "code": "age = 3\nage = age + 1\nprint(age)", "o": ["3", "4", "age + 1", "1"], "a": 1, "why": "Python 先算右边，3 + 1，然后把 4 放进 `age`。变量的值是可以改变的。", "at": "python_piggy.html#step1"},
  {"id": "ev_04", "unit": "easy_variables", "d": -0.7, "type": "fill", "q": "像 `3` 这样的整数是一种值。它在 Python 里的简称是什么？请输入。", "answers": ["int", "integer"], "why": "整数是 `int`，是 integer（整数）的缩写。数字没有引号，所以 Python 能用它们做计算。", "at": "python_piggy.html#step1"},
  {"id": "ev_05", "unit": "easy_variables", "d": -0.6, "type": "mcq", "q": "这行 `print` 显示什么？", "code": "print(\"Princess Taiping\", \"is\", 3, \"years old\")", "o": ["Princess Taipingis3years old", "\"Princess Taiping\" \"is\" 3 \"years old\"", "Princess Taiping is 3 years old", "Princess Taiping, is, 3, years old"], "a": 2, "why": "`print` 显示它的值时，值与值之间隔一个空格，并且不带引号。", "at": "python_piggy.html#step1"},
  {"id": "ev_06", "unit": "easy_variables", "d": -0.5, "type": "mcq", "q": "在 `age = 3  # a whole number` 这一行里，`#` 做什么？", "o": ["把这个数变成整数", "把这一行剩下的部分变成文字", "把这一行剩下的部分打印在屏幕上", "开始一条 Python 会跳过的注释"], "a": 3, "why": "`#` 后面的所有内容都是注释：写给人看的说明。Python 会跳过它。", "at": "python_piggy.html#step1"},
  {"id": "ev_07", "unit": "easy_variables", "d": -0.4, "type": "mcq", "q": "这个 f-string 打印什么？", "code": "name = \"Mimi\"\nprint(f\"Hello {name}\")", "o": ["Hello Mimi", "Hello {name}", "f\"Hello Mimi\"", "Hello name"], "a": 0, "why": "f-string 在引号前面有一个 `f`。Python 会把 `{curly brackets}` 里每个名字的值放进文字里。", "at": "python_song.html#step2"},
  {"id": "ev_08", "unit": "easy_variables", "d": -0.3, "type": "mcq", "q": "这个字符串前面没有 `f`。它打印什么？", "code": "name = \"Mimi\"\nprint(\"Hello {name}\")", "o": ["Hello Mimi", "Hello {name}", "Hello name", "Hello"], "a": 1, "why": "引号前面没有 `f`，所以它是普通字符串。Python 会原样打印花括号。", "at": "python_song.html#step2"},
  {"id": "ev_09", "unit": "easy_variables", "d": -0.6, "type": "mcq", "q": "`animal` 先后得到两个值。这段代码打印什么？", "code": "animal = \"pig\"\nanimal = \"dog\"\nprint(animal)", "o": ["pig", "pig dog", "dog", "先 pig，再 dog"], "a": 2, "why": "一个名字一次只保存一个值。第二行给了 `animal` 一个新值，所以旧值就没了。", "at": "python_song.html#step1"},
  {"id": "ev_10", "unit": "easy_variables", "d": -0.3, "type": "mcq", "q": "`int(\"15\")` 得到什么？", "o": ["文字 \"15\"", "数字 1 和 5", "出错：文字永远不是数字", "数字 15"], "a": 3, "why": "`int()` 把看起来像整数的文字变成那个数字本身。", "at": "python_piggy.html#step7"},
  {"id": "ev_11", "unit": "easy_variables", "d": -0.2, "type": "mcq", "q": "`str(2147483647)` 得到什么？", "o": ["文字 \"2147483647\"", "数字 2147483647", "数字 10，即它的位数", "由这十个数字组成的列表"], "a": 0, "why": "`str()` 把一个值变成文字。然后 `len()` 就能数出它的字符，也就是这个数的各位数字。", "at": "python_piggy.html#step8"},
  {"id": "ev_12", "unit": "easy_variables", "d": -0.1, "type": "mcq", "q": "关于 2026 年，这段代码打印什么？", "code": "print(len(str(2026)))", "o": ["2026", "4", "1", "\"2026\""], "a": 1, "why": "`str(2026)` 是文字 \"2026\"，`len()` 数出它的四个字符。", "at": "python_piggy.html#step8"},
  {"id": "ev_13", "unit": "easy_variables", "d": -0.4, "type": "mcq", "q": "`print(7 // 2)` 打印什么？", "o": ["3.5", "4", "3", "1"], "a": 2, "why": "`//` 做除法并只保留整数部分：7 除以 2 得 3，余 1。", "at": "python_song.html#step6"},
  {"id": "ev_14", "unit": "easy_variables", "d": -0.5, "type": "mcq", "q": "`print(\"ha\" * 3)` 打印什么？", "o": ["ha3", "ha ha ha", "出错：文字不能相乘", "hahaha"], "a": 3, "why": "`*` 用在字符串和整数之间时，会把字符串重复几遍，中间什么也不加。", "at": "python_piggy.html#puzzle3"},
  {"id": "ev_15", "unit": "easy_variables", "d": -0.4, "type": "mcq", "q": "`print(\"mango\" + \"roti\")` 打印什么？", "o": ["mangoroti", "mango roti", "mango + roti", "出错：文字不能相加"], "a": 0, "why": "`+` 把两个字符串连成一个。它不会加空格：想要空格就把它写进文字里。", "at": "python_piggy.html#puzzle1"},
  {"id": "ev_16", "unit": "easy_variables", "d": -0.2, "type": "mcq", "q": "`print(\"Mango\".upper())` 打印什么？", "o": ["Mango", "MANGO", "mango", "出错：upper() 需要一个数字"], "a": 1, "why": "`upper()` 给出同样的文字，但每个字母都变成大写。`lower()` 让每个字母都变成小写。", "at": "python_piggy.html#step5"},
  {"id": "ev_17", "unit": "easy_variables", "d": 0.1, "type": "mcq", "q": "`print(\"3.14.2\".split(\".\"))` 打印什么？", "o": ["[3, 14, 2]", "3 14 2", "['3', '14', '2']", "['3.14.2']"], "a": 2, "why": "`split(\".\")` 在每个点的地方把文字切开，变成一个由小段组成的列表。这些小段仍然是文字，所以带引号。", "at": "python_piggy.html#step7"},
  {"id": "ev_18", "unit": "easy_variables", "d": 0.0, "type": "mcq", "q": "Python 在 `print(\"Hello)` 这一行因 `SyntaxError` 停了下来。为什么？", "o": ["print 需要两个值", "Hello 必须用小写 h 开头", "字符串不能被打印", "缺少结尾的引号"], "a": 3, "why": "字符串两端都需要引号。没有第二个引号，Python 就不知道文字在哪里结束。", "at": "python_song.html#step1"},
  {"id": "ev_19", "unit": "easy_variables", "d": -0.3, "type": "mcq", "q": "哪一行创建一个叫 `my_age` 的变量，保存数字 10？", "o": ["`my_age = 10`", "`10 = my_age`", "`my_age == 10`", "`my age = 10`"], "a": 0, "why": "名字写在左边，值写在右边。`==` 是比较，而且名字里不能有空格。", "at": "python_piggy.html#step1"},
  {"id": "ev_20", "unit": "easy_variables", "d": 0.2, "type": "mcq", "q": "字符串可以写在三个引号 `\"\"\"` 之间。它能做什么，是写在一对引号里的字符串做不到的？", "o": ["既能装数字也能装文字", "可以写很多行", "创建之后还能修改", "能装中文和泰文"], "a": 1, "why": "三个引号让一段文字可以跨越很多行。任何字符串都能装数字或任何语言。", "at": "python_piggy.html#step10"},
  {"id": "el_01", "unit": "easy_lists", "d": -1.0, "type": "mcq", "q": "哪种括号用来创建列表？", "o": ["圆括号：( )", "花括号：{ }", "方括号：[ ]", "尖括号：< >"], "a": 2, "why": "列表写在方括号里，各项之间用逗号隔开。", "at": "python_piggy.html#step2"},
  {"id": "el_02", "unit": "easy_lists", "d": -0.8, "type": "mcq", "q": "这里 `snacks[0]` 打印什么？", "code": "snacks = [\"mango\", \"sticky rice\", \"khao soi\"]\nprint(snacks[0])", "o": ["sticky rice", "khao soi", "出错：没有第 0 项", "mango"], "a": 3, "why": "Python 从 0 开始数，所以 `[0]` 是第一项。", "at": "python_piggy.html#step2"},
  {"id": "el_03", "unit": "easy_lists", "d": -0.6, "type": "mcq", "q": "这里 `snacks[-1]` 打印什么？", "code": "snacks = [\"mango\", \"sticky rice\", \"khao soi\"]\nprint(snacks[-1])", "o": ["khao soi", "mango", "sticky rice", "出错：没有小于 0 的索引"], "a": 0, "why": "`[-1]` 是列表的最后一项，`[-2]` 是它前面的那一项。", "at": "python_piggy.html#step2"},
  {"id": "el_04", "unit": "easy_lists", "d": -0.7, "type": "fill", "q": "哪个函数能数出列表里有几项，比如 `___(snacks)`？请输入它的名字。", "answers": ["len"], "why": "`len()` 数出列表的项数。给它一个字符串，它就数字符。", "at": "python_piggy.html#step2"},
  {"id": "el_05", "unit": "easy_lists", "d": -0.6, "type": "mcq", "q": "`snacks.append(\"roti\")` 做什么？", "o": ["把 \"roti\" 放在最前面", "在末尾加上 \"roti\"", "用 \"roti\" 换掉最后一项", "创建一个只有 \"roti\" 的列表"], "a": 1, "why": "`append()` 在列表末尾再加一项。原来的各项位置不变。", "at": "python_piggy.html#step2"},
  {"id": "el_06", "unit": "easy_lists", "d": -0.5, "type": "mcq", "q": "`append()` 之后，`len(snacks)` 打印什么？", "code": "snacks = [\"mango\", \"sticky rice\", \"khao soi\"]\nsnacks.append(\"roti\")\nprint(len(snacks))", "o": ["3", "5", "4", "roti"], "a": 2, "why": "列表原来有三项，`append()` 加了第四项。", "at": "python_piggy.html#step2"},
  {"id": "el_07", "unit": "easy_lists", "d": -0.4, "type": "mcq", "q": "这里 `snacks[1]` 打印什么？", "code": "snacks = [\"mango\", \"sticky rice\", \"khao soi\"]\nprint(snacks[1])", "o": ["mango", "khao soi", "sticky", "sticky rice"], "a": 3, "why": "从 0 开始数，`[1]` 是第二项。这一项是整个字符串 `sticky rice`。", "at": "python_piggy.html#step2"},
  {"id": "el_08", "unit": "easy_lists", "d": -0.3, "type": "mcq", "q": "`snacks[2]` 方括号里的数字叫什么？", "o": ["索引", "键", "值", "参数"], "a": 0, "why": "列表中一项的位置编号叫索引。字典用的是键。", "at": "python_piggy.html#step2"},
  {"id": "el_09", "unit": "easy_lists", "d": -0.2, "type": "mcq", "q": "列表里有两种小吃，代码却要 `snacks[2]`。会发生什么？", "code": "snacks = [\"mango\", \"roti\"]\nprint(snacks[2])", "o": ["打印 `roti`，也就是最后一个", "Python 因 `IndexError` 停止", "绕回开头，打印 `mango`", "打印一个空行"], "a": 1, "why": "这个列表只有位置 0 和 1。要位置 2 会让 Python 因 `IndexError` 停止。", "at": "python_piggy.html#step2"},
  {"id": "el_10", "unit": "easy_lists", "d": -0.3, "type": "mcq", "q": "用 `+` 把两个列表连起来。这段代码打印什么？", "code": "a = [\"do\", \"re\"]\nb = [\"mi\"]\nprint(len(a + b))", "o": ["2", "1", "3", "5"], "a": 2, "why": "`+` 把两个列表连成一个更长的列表，就像火车车厢一样：`['do', 're', 'mi']` 有三项。", "at": "python_song.html#step5"},
  {"id": "el_11", "unit": "easy_lists", "d": -0.1, "type": "mcq", "q": "这个 `join()` 打印什么？", "code": "print(\"-\".join([\"do\", \"re\", \"mi\"]))", "o": ["do re mi", "['do', 're', 'mi']", "-do-re-mi-", "do-re-mi"], "a": 3, "why": "`join()` 把列表的各项粘成一个字符串，点号前面的文字放在每两项之间。", "at": "python_song.html#step5"},
  {"id": "el_12", "unit": "easy_lists", "d": 0.0, "type": "mcq", "q": "`\"\\n\".join(lines)` 把一个由行组成的列表粘成一个字符串。行与行之间放的是什么？", "o": ["换行", "一个空格", "一个逗号", "什么都没有"], "a": 0, "why": "`\\n` 是换行符，所以列表里的每一行都从新的一行开始。", "at": "python_song.html#step5"},
  {"id": "el_13", "unit": "easy_lists", "d": -0.2, "type": "mcq", "q": "这里 `sorted()` 打印什么？", "code": "print(sorted([\"mango\", \"banana\", \"longan\"]))", "o": ["['mango', 'banana', 'longan']", "['banana', 'longan', 'mango']", "['mango', 'longan', 'banana']", "banana longan mango"], "a": 1, "why": "`sorted()` 创建一个从 A 到 Z 排好序的新列表。", "at": "python_piggy.html#step2"},
  {"id": "el_14", "unit": "easy_lists", "d": 0.1, "type": "mcq", "q": "一个名字列表被排序。为什么 `_abc` 排在 `array` 前面？", "o": ["带 _ 的名字总是放在最前面", "sorted() 把短的名字放在前面", "_ 排在所有小写字母前面", "_abc 是最先放进列表的"], "a": 2, "why": "`sorted()` 比较的是字符，而 `_` 排在所有小写字母 a 到 z 前面。", "at": "python_piggy.html#step2"},
  {"id": "el_15", "unit": "easy_lists", "d": -0.7, "type": "mcq", "q": "在列表里，各项之间放什么？", "o": ["空格", "冒号", "句号", "逗号"], "a": 3, "why": "各项之间用逗号隔开：`[\"mango\", \"roti\"]`。漏掉逗号是常见的错误。", "at": "python_song.html#step3"},
  {"id": "el_16", "unit": "easy_lists", "d": 0.2, "type": "mcq", "q": "往列表里追加了两项。它打印什么？", "code": "fruits = [\"mango\", \"longan\"]\nfruits.append(\"banana\")\nfruits.append(\"mango\")\nprint(fruits)", "o": ["['mango', 'longan', 'banana', 'mango']", "['mango', 'longan', 'banana']", "['banana', 'mango', 'mango', 'longan']", "['mango', 'mango', 'longan', 'banana']"], "a": 0, "why": "每次 `append()` 都按顺序加在末尾。列表可以把同一项放两次。", "at": "python_piggy.html#step2"},
  {"id": "el_17", "unit": "easy_lists", "d": 0.0, "type": "mcq", "q": "`platform.architecture()` 回答两样东西，就像一个有两项的列表。怎样拿到第一样？", "o": ["`platform.architecture()[1]`", "`platform.architecture()[0]`", "`platform.architecture(0)`", "`platform.architecture[0]()`"], "a": 1, "why": "先调用函数，带上它的括号，然后用 `[0]` 取它答案的位置 0。", "at": "python_piggy.html#step3"},
  {"id": "el_18", "unit": "easy_lists", "d": -0.1, "type": "mcq", "q": "`word[0]` 打印什么？", "code": "word = \"Pigs\"\nprint(word[0])", "o": ["Pigs", "s", "P", "i"], "a": 2, "why": "字符串可以像列表一样数：`[0]` 是它的第一个字符。", "at": "python_piggy.html#puzzle1"},
  {"id": "el_19", "unit": "easy_lists", "d": 0.2, "type": "mcq", "q": "先 `sorted()`，然后是原来的列表。这段代码分两行打印什么？", "code": "numbers = [3, 1, 2]\nprint(sorted(numbers))\nprint(numbers)", "o": ["先 [1, 2, 3]，再 [1, 2, 3]", "先 [3, 1, 2]，再 [1, 2, 3]", "先 [3, 2, 1]，再 [3, 1, 2]", "先 [1, 2, 3]，再 [3, 1, 2]"], "a": 3, "why": "`sorted()` 创建一个排好序的新列表。原来的列表没有改变。", "at": "python_piggy.html#step2"},
  {"id": "el_20", "unit": "easy_lists", "d": 0.3, "type": "mcq", "q": "对一个列表和它的第一项分别用 `len()`。这段代码打印什么？", "code": "verse = [\"la\", \"la\", \"la\"]\nprint(len(verse), len(verse[0]))", "o": ["3 2", "3 3", "2 3", "6 2"], "a": 0, "why": "`len(verse)` 数出三项。`verse[0]` 是字符串 `la`，`len()` 数出它的两个字符。", "at": "python_song.html#step3"},
  {"id": "ed_01", "unit": "easy_dicts", "d": -0.9, "type": "mcq", "q": "哪种括号用来创建字典？", "o": ["方括号：[ ]", "花括号：{ }", "圆括号：( )", "尖括号：< >"], "a": 1, "why": "字典写在花括号里，里面是一对一对的：一个键、一个冒号，再加上它的值。", "at": "python_piggy.html#step4"},
  {"id": "ed_02", "unit": "easy_dicts", "d": -0.8, "type": "mcq", "q": "`pig[\"food\"]` 打印什么？", "code": "pig = {\"name\": \"Princess Taiping\", \"food\": \"mango\"}\nprint(pig[\"food\"])", "o": ["food", "Princess Taiping", "mango", "{'food': 'mango'}"], "a": 2, "why": "`pig[\"food\"]` 查找键 `food`，给出它的值。", "at": "python_piggy.html#step4"},
  {"id": "ed_03", "unit": "easy_dicts", "d": -0.6, "type": "mcq", "q": "在 `{\"name\": \"Mali\", \"age\": 10}` 里，哪些是键？", "o": ["`\"Mali\"` 和 `10`", "`\"name\"` 和 `\"Mali\"`", "只有 `\"name\"`", "`\"name\"` 和 `\"age\"`"], "a": 3, "why": "在每一对里，键在冒号前面，值在冒号后面。", "at": "python_piggy.html#step4"},
  {"id": "ed_04", "unit": "easy_dicts", "d": -0.6, "type": "mcq", "q": "怎样把键 `\"colour\"` 和值 `\"pink\"` 加到字典 `pig` 里？", "o": ["`pig[\"colour\"] = \"pink\"`", "`pig.append(\"colour\", \"pink\")`", "`pig[\"pink\"] = \"colour\"`", "`pig(\"colour\") = \"pink\"`"], "a": 0, "why": "用方括号和 `=` 给一个新键赋值。字典没有 `append()`。", "at": "python_piggy.html#step4"},
  {"id": "ed_05", "unit": "easy_dicts", "d": -0.5, "type": "mcq", "q": "列表按位置找一项：0、1、2。字典怎样找一个值？", "o": ["按位置，从 0 开始", "按键", "按长度", "按加入的时间"], "a": 1, "why": "字典按键找值，就像价目表按水果的名字找价格一样。", "at": "python_browser.html#step5"},
  {"id": "ed_06", "unit": "easy_dicts", "d": -0.3, "type": "mcq", "q": "一个已经存在的键得到一个新值。这段代码打印什么？", "code": "prices = {\"mango\": 60}\nprices[\"mango\"] = 55\nprint(prices)", "o": ["{'mango': 60, 'mango': 55}", "{'mango': 60}", "{'mango': 55}", "{'mango': [60, 55]}"], "a": 2, "why": "同一个键在字典里不会出现两次。已经存在的键会得到新的值。", "at": "python_browser.html#step5"},
  {"id": "ed_07", "unit": "easy_dicts", "d": -0.2, "type": "mcq", "q": "代码查找一个不存在的键。会发生什么？", "code": "prices = {\"mango\": 60}\nprint(prices[\"durian\"])", "o": ["打印 `0`，表示没有价格", "打印 `None` 并继续", "加入没有值的 `durian`", "Python 因 `KeyError` 停止"], "a": 3, "why": "没有键 `durian`，所以用方括号查找它会让程序因 `KeyError` 停止。", "at": "python_browser.html#step5"},
  {"id": "ed_08", "unit": "easy_dicts", "d": -0.1, "type": "mcq", "q": "这里 `get()` 打印什么？", "code": "prices = {\"mango\": 60}\nprint(prices.get(\"durian\", 0))", "o": ["0", "60", "durian", "KeyError"], "a": 0, "why": "键不存在时，`get()` 给出第二个值，这里是 0，而且不会让程序停止。", "at": "python_browser.html#step5"},
  {"id": "ed_09", "unit": "easy_dicts", "d": -0.3, "type": "mcq", "q": "这里 `in` 打印什么？", "code": "prices = {\"mango\": 60, \"longan\": 50}\nprint(\"durian\" in prices)", "o": ["True", "False", "0", "durian"], "a": 1, "why": "`in` 问一个键是否在字典里。`durian` 不是其中的键。", "at": "python_browser.html#step5"},
  {"id": "ed_10", "unit": "easy_dicts", "d": -0.4, "type": "mcq", "q": "一个循环遍历字典。它一行一行地打印什么？", "code": "pig = {\"name\": \"Taiping\", \"food\": \"mango\"}\nfor key in pig:\n    print(key)", "o": ["先 Taiping，再 mango", "先 name: Taiping，再 food: mango", "先 name，再 food", "只有 name"], "a": 2, "why": "遍历字典的循环一次给你一个键。", "at": "python_piggy.html#step5"},
  {"id": "ed_11", "unit": "easy_dicts", "d": -0.2, "type": "mcq", "q": "把字典交给 `len()` 时，它数的是什么？", "o": ["所有键里的字母", "所有值加起来", "最长的值", "键的个数"], "a": 3, "why": "`len()` 数字典的键，就像它数列表的项一样。", "at": "python_piggy.html#step4"},
  {"id": "ed_12", "unit": "easy_dicts", "d": -0.7, "type": "mcq", "q": "`python = {}` 创建了什么？", "o": ["一个空字典", "一个空列表", "一个空集合", "一个空字符串"], "a": 0, "why": "单独的 `{}` 是一个空字典，可以一个键一个键地往里填。空集合写作 `set()`。", "at": "python_piggy.html#step4"},
  {"id": "ed_13", "unit": "easy_dicts", "d": 0.1, "type": "mcq", "q": "库存数量被修改，又加了一个键。这段代码打印什么？", "code": "stock = {\"mango\": 3}\nstock[\"mango\"] = stock[\"mango\"] + 2\nstock[\"lime\"] = 1\nprint(stock)", "o": ["{'mango': 3, 'mango': 5, 'lime': 1}", "{'mango': 5, 'lime': 1}", "{'mango': 2, 'lime': 1}", "{'mango': 3, 'lime': 1}"], "a": 1, "why": "`mango` 得到 3 + 2，也就是 5，`lime` 是一个新键，值为 1。", "at": "python_browser.html#step5"},
  {"id": "ed_14", "unit": "easy_dicts", "d": 0.1, "type": "mcq", "q": "哪个循环能给出 `prices` 的每个键以及它的值？", "o": ["`for fruit in prices.keys(1):`", "`for fruit, price in prices:`", "`for fruit, price in prices.items():`", "`for price in prices.values(fruit):`"], "a": 2, "why": "`items()` 给出每个键和它的值，所以循环可以用两个名字。", "at": "python_browser.html#step5"},
  {"id": "ed_15", "unit": "easy_dicts", "d": 0.2, "type": "mcq", "q": "一个循环统计卖出了什么。这段代码打印什么？", "code": "sold = {}\nfor fruit in [\"mango\", \"lime\", \"mango\"]:\n    sold[fruit] = sold.get(fruit, 0) + 1\nprint(sold)", "o": ["{'mango': 1, 'lime': 1}", "{'mango': 1, 'lime': 1, 'mango': 1}", "{'mango': 3, 'lime': 3}", "{'mango': 2, 'lime': 1}"], "a": 3, "why": "`sold.get(fruit, 0) + 1` 每次多数一个，从 0 开始。mango 出现两次，lime 出现一次。", "at": "python_browser.html#step9"},
  {"id": "ed_16", "unit": "easy_dicts", "d": 0.0, "type": "mcq", "q": "`sold = {\"mango\": 2, \"lime\": 1}`。`sum(sold.values())` 得到什么？", "o": ["3", "2", "2 1", "mangolime"], "a": 0, "why": "`values()` 只给出值，2 和 1，然后 `sum()` 把它们加起来。", "at": "python_browser.html#step9"},
  {"id": "ed_17", "unit": "easy_dicts", "d": 0.3, "type": "mcq", "q": "寻宝游戏跟着两条线索走。这段代码打印什么？", "code": "clues = {\"door\": \"sofa\", \"sofa\": \"lamp\"}\nplace = \"door\"\nplace = clues[place]\nplace = clues[place]\nprint(place)", "o": ["sofa", "lamp", "door", "KeyError"], "a": 1, "why": "第一次查找把 `door` 变成 `sofa`，第二次把 `sofa` 变成 `lamp`。", "at": "python_piggy.html#puzzle4"},
  {"id": "ed_18", "unit": "easy_dicts", "d": -0.1, "type": "mcq", "q": "`pig[\"best friend\"] = \"Mali\"` 可以吗？键里面有一个空格。", "o": ["不行：键必须是一个单词", "不行：键必须是数字", "可以：任何字符串都能当键", "可以，但它就没有值了"], "a": 2, "why": "键可以是任何字符串，带空格也行。变量名不能有空格；键可以。", "at": "python_piggy.html#step4"},
  {"id": "lo_01", "unit": "easy_loops", "d": -0.9, "type": "mcq", "q": "这个循环打印什么？", "code": "for snack in [\"mango\", \"roti\"]:\n    print(\"Taiping eats\", snack)", "o": ["Taiping eats ['mango', 'roti']，在同一行", "Taiping eats snack，两次", "Taiping eats mango，只有一次", "先 Taiping eats mango，再 Taiping eats roti"], "a": 3, "why": "循环一次取一项。每转一圈，`snack` 保存下一项，它下面的那行运行一次。", "at": "python_piggy.html#step5"},
  {"id": "lo_02", "unit": "easy_loops", "d": -0.8, "type": "mcq", "q": "Python 怎样知道 `for` 循环要重复哪些行？", "o": ["它们在它下面缩进", "它们在花括号里", "它们以分号结尾", "它们在 for 那一行之前"], "a": 0, "why": "在以冒号结尾的那一行下面、缩进四个空格的行，就是要重复的部分。Python 看的是空格。", "at": "python_piggy.html#step5"},
  {"id": "lo_03", "unit": "easy_loops", "d": -0.7, "type": "fill", "q": "`for snack in snacks` 这一行必须以哪个符号结尾？请输入这个符号。", "answers": [":", "colon"], "why": "`for` 这一行以冒号结尾。`if`、`else`、`def` 以及其他下面有缩进行的行也都一样。", "at": "python_piggy.html#step5"},
  {"id": "lo_04", "unit": "easy_loops", "d": -0.5, "type": "mcq", "q": "一个循环，后面还有一行。这段代码打印什么？", "code": "for word in [\"la\", \"la\"]:\n    print(word)\nprint(\"end\")", "o": ["la、end、la、end：共四行", "la、la、end：共三行", "la、la、end、end：共四行", "end、la、la：共三行"], "a": 1, "why": "只有缩进的那一行会重复。`print(\"end\")` 没有缩进，所以它只在循环结束后运行一次。", "at": "python_song.html#step3"},
  {"id": "lo_05", "unit": "easy_loops", "d": -0.4, "type": "mcq", "q": "一个循环数名字。它打印什么？", "code": "count = 0\nfor name in [\"sys\", \"math\", \"time\"]:\n    count = count + 1\nprint(count)", "o": ["0", "1", "3", "time"], "a": 2, "why": "一共三个名字，循环为每个名字运行一次，每次给 `count` 加 1。", "at": "python_piggy.html#step5"},
  {"id": "lo_06", "unit": "easy_loops", "d": -0.3, "type": "mcq", "q": "一个循环调用 `upper()`。它打印什么？", "code": "for snack in [\"mango\", \"roti\"]:\n    print(snack.upper())", "o": ["先 mango，再 roti", "MANGO ROTI，在同一行", "SNACK，两次", "先 MANGO，再 ROTI"], "a": 3, "why": "每转一圈，`snack` 保存下一项，`upper()` 把它变成大写。", "at": "python_piggy.html#step5"},
  {"id": "lo_07", "unit": "easy_loops", "d": -0.2, "type": "mcq", "q": "一个循环用首字母拼出一条消息。它打印什么？", "code": "message = \"\"\nfor word in [\"Pigs\", \"Yell\"]:\n    message = message + word[0]\nprint(message)", "o": ["PY", "Pigs Yell", "PigsYell", "P"], "a": 0, "why": "`word[0]` 是每个单词的第一个字母，`+` 把它接到 `message` 的末尾。", "at": "python_piggy.html#puzzle1"},
  {"id": "lo_08", "unit": "easy_loops", "d": -0.1, "type": "mcq", "q": "这个遍历 `range(3)` 的循环打印什么？", "code": "for number in range(3):\n    print(number)", "o": ["1, 2, 3", "0, 1, 2", "0, 1, 2, 3", "3"], "a": 1, "why": "`range(3)` 从 0 开始数，在 3 之前停下：0、1 和 2。", "at": "python_browser.html#step7"},
  {"id": "lo_09", "unit": "easy_loops", "d": 0.0, "type": "mcq", "q": "一个循环把价格加起来。它打印什么？", "code": "total = 0\nfor price in [60, 50, 25]:\n    total = total + price\nprint(total)", "o": ["25", "60 50 25", "135", "0"], "a": 2, "why": "每转一圈，下一个价格就加进 `total`：60，然后 110，然后 135。", "at": "python_piggy.html#step5"},
  {"id": "lo_10", "unit": "easy_loops", "d": 0.1, "type": "mcq", "q": "一个循环找最长的名字。它打印什么？", "code": "longest = \"\"\nfor name in [\"sys\", \"math\", \"json\"]:\n    if len(name) > len(longest):\n        longest = name\nprint(longest)", "o": ["json", "sys", "mathjson", "math"], "a": 3, "why": "`json` 和 `math` 一样长，并不更长，所以对它来说 `>` 是 False，`math` 留了下来。", "at": "python_piggy.html#puzzle2"},
  {"id": "lo_11", "unit": "easy_loops", "d": -0.6, "type": "mcq", "q": "遍历列表的 `for` 循环做什么？", "o": ["一次取一项", "把各项从 A 到 Z 排序", "数出项数然后停下", "只取第一项"], "a": 0, "why": "`for` 循环按顺序一次取一项，并且为每一项运行一次它下面的那些行。", "at": "python_piggy.html#step5"},
  {"id": "lo_12", "unit": "easy_loops", "d": 0.2, "type": "mcq", "q": "循环下面有两行缩进。它打印什么？", "code": "for fruit in [\"mango\", \"lime\"]:\n    print(fruit)\n    print(\"~\")", "o": ["mango, lime, ~", "mango, ~, lime, ~", "mango, lime, ~, ~", "~, mango, ~, lime"], "a": 1, "why": "两行都缩进了，所以两行都会重复：每转一圈，先打印一个水果，再打印一个 `~`。", "at": "python_song.html#step3"},
  {"id": "lo_13", "unit": "easy_loops", "d": 0.2, "type": "mcq", "q": "循环下面只有一行缩进。它打印什么？", "code": "for fruit in [\"mango\", \"lime\"]:\n    print(fruit)\nprint(\"~\")", "o": ["mango, ~, lime, ~", "~, mango, lime", "mango, lime, ~", "mango, lime, ~, ~"], "a": 2, "why": "`print(\"~\")` 没有缩进，所以它不属于循环。它只在循环结束后运行一次。", "at": "python_song.html#step3"},
  {"id": "lo_14", "unit": "easy_loops", "d": 0.1, "type": "mcq", "q": "循环里的 `break` 做什么？", "o": ["跳过循环里的一行", "从头重新开始循环", "让 Python 报错停止", "立刻结束循环"], "a": 3, "why": "`break` 马上结束循环。程序接着运行循环后面的第一行。", "at": "python_browser.html#step9"},
  {"id": "lo_15", "unit": "easy_loops", "d": 0.3, "type": "mcq", "q": "一个循环里有 `break`。它打印什么？", "code": "for number in range(10):\n    if number == 3:\n        break\n    print(number)", "o": ["0, 1, 2", "0, 1, 2, 3", "3", "0 到 9"], "a": 0, "why": "当 `number` 是 3 时，`break` 在打印之前就结束了循环，所以 3 从来没有被打印。", "at": "python_browser.html#step9"},
  {"id": "lo_16", "unit": "easy_loops", "d": -0.3, "type": "mcq", "q": "一个循环遍历字符串：`for letter in \"pig\":`。每转一圈，`letter` 保存什么？", "o": ["整个单词 pig，只有一次", "一个字母：先 p，再 i，再 g", "字母的个数，3", "什么都没有：单词不是列表"], "a": 1, "why": "遍历字符串的循环一次取一个字符。", "at": "python_piggy.html#puzzle5"},
  {"id": "lo_17", "unit": "easy_loops", "d": 0.0, "type": "mcq", "q": "在 Jupyter 里，你输入一行以冒号结尾的代码，然后按 Enter。会发生什么？", "o": ["这一行马上运行", "冒号又被删掉了", "下一行自动帮你缩进", "在它下面开始一个新单元格"], "a": 2, "why": "冒号之后，Jupyter 会帮你加上空格，因为它下面的行必须缩进。", "at": "python_piggy.html#step5"},
  {"id": "lo_18", "unit": "easy_loops", "d": 0.2, "type": "mcq", "q": "一个循环数出有几个 True 答案。它打印什么？", "code": "points = 0\nfor answer in [True, False, True]:\n    if answer:\n        points = points + 1\nprint(\"*\" * points)", "o": ["***", "*", "2", "**"], "a": 3, "why": "三个答案中有两个是 True，所以 `points` 是 2，`\"*\" * 2` 把星号重复两次。", "at": "python_piggy.html#puzzle3"},
  {"id": "lo_19", "unit": "easy_loops", "d": -0.1, "type": "mcq", "q": "一个循环遍历由三个名字组成的列表，为每个名字运行 `print(\"name:\", name)`。它打印几行？", "o": ["3", "1", "6", "4"], "a": 0, "why": "每转一圈 print 运行一次，而三个名字让循环转三圈。", "at": "python_piggy.html#step5"},
  {"id": "lo_20", "unit": "easy_loops", "d": 0.3, "type": "mcq", "q": "这个循环本来要打印每种小吃，但 Python 因 `IndentationError` 停了下来。哪里错了？", "code": "for snack in [\"mango\", \"roti\"]:\nprint(snack)", "o": ["列表需要圆括号", "print 没有在 for 下面缩进", "snack 必须叫 item", "循环不能打印文字"], "a": 1, "why": "循环要重复的行必须在带冒号的那一行下面缩进。这里一行都没有。", "at": "python_piggy.html#step5"},
  {"id": "ei_01", "unit": "easy_if", "d": -1.0, "type": "mcq", "q": "布尔值只有两个值。是哪两个？", "o": ["Yes 和 No", "1 和 2", "True 和 False", "On 和 Off"], "a": 2, "why": "布尔值要么是 `True`，要么是 `False`，首字母大写。", "at": "python_piggy.html#step6"},
  {"id": "ei_02", "unit": "easy_if", "d": -0.9, "type": "mcq", "q": "`print(5 > 3)` 打印什么？", "o": ["False", "5", "2", "True"], "a": 3, "why": "像 `5 > 3` 这样的问题会给出一个布尔值。5 比 3 大，所以是 True。", "at": "python_piggy.html#step6"},
  {"id": "ei_03", "unit": "easy_if", "d": -0.8, "type": "mcq", "q": "`print(\"roti\" in [\"mango\", \"roti\"])` 打印什么？", "o": ["True", "False", "roti", "1"], "a": 0, "why": "`in` 问某样东西是否在列表里，回答 True 或 False。", "at": "python_piggy.html#step6"},
  {"id": "ei_04", "unit": "easy_if", "d": -0.6, "type": "mcq", "q": "`print(\"Taiping\" == \"taiping\")` 打印什么？", "o": ["True", "False", "Taiping", "出错"], "a": 1, "why": "大写的 T 和小写的 t 是不同的字母，所以两个字符串不相同。", "at": "python_piggy.html#step7"},
  {"id": "ei_05", "unit": "easy_if", "d": -0.5, "type": "mcq", "q": "`print(\"15\" == 15)` 打印什么？", "o": ["True", "15", "False", "出错：文字和数字不能比较"], "a": 2, "why": "文字永远不是数字，即使它看起来像数字，所以它们不相等。", "at": "python_piggy.html#step7"},
  {"id": "ei_06", "unit": "easy_if", "d": -0.4, "type": "mcq", "q": "`print(int(\"15\") == 15)` 打印什么？", "o": ["False", "15", "\"15\"", "True"], "a": 3, "why": "`int(\"15\")` 把文字变成数字 15，而 15 等于 15。", "at": "python_piggy.html#step7"},
  {"id": "ei_07", "unit": "easy_if", "d": -0.6, "type": "mcq", "q": "`=` 和 `==` 有什么不同？", "o": ["`=` 给一个值；`==` 比较两个值", "`=` 比较两个值；`==` 给一个值", "它们作用一样；`==` 用于数字", "`==` 是 `=` 的打字错误"], "a": 0, "why": "一个 `=` 给名字一个值。两个，`==`，问两个值是否相同，并给出 True 或 False。", "at": "python_piggy.html#step7"},
  {"id": "ei_08", "unit": "easy_if", "d": -0.3, "type": "mcq", "q": "写 `if sys.platform = \"darwin\":` 会发生什么？", "o": ["sys.platform 变成 darwin", "Python 因 SyntaxError 停止", "if 部分总是运行", "if 部分永远不运行"], "a": 1, "why": "`if` 需要一个问题，也就是 `==`。在那里只写一个 `=` 是错误的，Python 什么都不运行就停下了。", "at": "python_piggy.html#step7"},
  {"id": "ei_09", "unit": "easy_if", "d": -0.5, "type": "mcq", "q": "`!=` 问的是什么？", "o": ["它们相同吗？", "第一个更大吗？", "它们不同吗？", "它不是数字吗？"], "a": 2, "why": "`!=` 问的和 `==` 相反：这两个值不同吗？", "at": "python_piggy.html#step7"},
  {"id": "ei_10", "unit": "easy_if", "d": -0.3, "type": "mcq", "q": "`language` 是 `\"th\"`。这段代码打印什么？", "code": "language = \"th\"\nif language == \"en\":\n    print(\"English\")\nelif language == \"zh\":\n    print(\"Chinese\")\nelse:\n    print(\"something else\")", "o": ["English", "Chinese", "什么都不打印", "something else"], "a": 3, "why": "对 `th` 来说两个测试都不是 True，所以 `else` 部分运行。", "at": "python_song.html#step6"},
  {"id": "ei_11", "unit": "easy_if", "d": -0.2, "type": "mcq", "q": "`elif` 是什么意思？", "o": ["否则，如果（else, if）", "否则，结束（else, finish）", "每一行，如果（every line, if）", "结束 if（end the if）"], "a": 0, "why": "`elif` 的意思是“else, if”（否则，如果）：只有在它上面的测试都是 False 时才会尝试它。", "at": "python_piggy.html#step7"},
  {"id": "ei_12", "unit": "easy_if", "d": 0.0, "type": "mcq", "q": "`x` 是 10，有两个测试可能是 True。这段代码打印什么？", "code": "x = 10\nif x > 5:\n    print(\"big\")\nelif x > 2:\n    print(\"middle\")\nelse:\n    print(\"small\")", "o": ["先 big，再 middle", "big", "middle", "big、middle、small"], "a": 1, "why": "Python 从上往下尝试每个测试，只运行第一个为 True 的。10 > 2 也是 True，但它根本不会被检查。", "at": "python_piggy.html#step7"},
  {"id": "ei_13", "unit": "easy_if", "d": -0.4, "type": "mcq", "q": "这个 `if` 和 `else` 打印什么？", "code": "if 2 + 2 == 5:\n    print(\"yes\")\nelse:\n    print(\"no\")", "o": ["yes", "先 yes，再 no", "no", "什么都不打印"], "a": 2, "why": "2 + 2 是 4，所以测试是 False，`else` 部分运行。", "at": "python_piggy.html#step7"},
  {"id": "ei_14", "unit": "easy_if", "d": -0.1, "type": "mcq", "q": "`print(\"Mango\".lower() == \"mango\")` 打印什么？", "o": ["False", "mango", "Mango", "True"], "a": 3, "why": "`lower()` 把每个字母都变成小写，所以两个字符串现在完全一样了。", "at": "python_piggy.html#step7"},
  {"id": "ei_15", "unit": "easy_if", "d": -0.1, "type": "mcq", "q": "`print(\"_abc\".startswith(\"_\"))` 打印什么？", "o": ["True", "False", "_", "abc"], "a": 0, "why": "`startswith()` 问文字是否以你给的那一段开头，回答 True 或 False。", "at": "python_piggy.html#step6"},
  {"id": "ei_16", "unit": "easy_if", "d": 0.0, "type": "mcq", "q": "`print(\"wasm\" in \"wasm32\")` 打印什么？", "o": ["False", "True", "32", "wasm"], "a": 1, "why": "`in` 也能用在文字上：它问一段文字是否在另一段文字里面。", "at": "python_piggy.html#step6"},
  {"id": "ei_17", "unit": "easy_if", "d": 0.1, "type": "mcq", "q": "`print(len(\"mango\") == 5)` 打印什么？", "o": ["False", "5", "True", "mango"], "a": 2, "why": "`len(\"mango\")` 是 5，而 `5 == 5` 是 True。", "at": "python_piggy.html#step7"},
  {"id": "ei_18", "unit": "easy_if", "d": 0.2, "type": "mcq", "q": "`mood` 是 `\"sleepy\"`。这段代码打印什么？", "code": "mood = \"sleepy\"\nif mood == \"happy\":\n    bpm = 140\nelif mood == \"sleepy\":\n    bpm = 60\nelse:\n    bpm = 300\nprint(bpm)", "o": ["140", "300", "sleepy", "60"], "a": 3, "why": "第一个测试是 False，第二个是 True，所以 `bpm` 变成 60。", "at": "python_song.html#step6"},
  {"id": "ei_19", "unit": "easy_if", "d": 0.2, "type": "fill", "q": "哪个词把两个测试连起来，只要有一个是 True 就够了？请输入它。", "answers": ["or"], "why": "只要至少有一个是 True，`a or b` 就是 True。`and` 需要两个都是 True。", "at": "python_piggy.html#puzzle5"},
  {"id": "ei_20", "unit": "easy_if", "d": 0.3, "type": "mcq", "q": "三个 `if` 测试在加分。这段代码打印什么？", "code": "points = 0\nif 3 > 1:\n    points = points + 1\nif \"a\" in \"cat\":\n    points = points + 1\nif 2 == \"2\":\n    points = points + 1\nprint(points)", "o": ["2", "3", "1", "0"], "a": 0, "why": "前两个测试是 True。最后一个是 False：数字 2 不是文字 \"2\"。", "at": "python_piggy.html#puzzle3"},
  {"id": "ei_21", "unit": "easy_if", "d": -0.7, "type": "mcq", "q": "一个 `if` 带有一个 `else`。两部分中有几部分会运行？", "o": ["两部分都运行，一个接一个", "正好其中一个", "都不运行，除非你按 Enter", "只有 else 部分"], "a": 1, "why": "测试为 True 时运行 `if` 部分，为 False 时运行 `else` 部分。两部分中永远只有一个会运行。", "at": "python_piggy.html#step6"},
  {"id": "ei_22", "unit": "easy_if", "d": 0.1, "type": "mcq", "q": "在 `version.split(\".\")[1] == \"14\"` 里，为什么 14 带引号？", "o": ["引号让数字变大", "== 只能用在文字上", "split() 给出的是文字，不是数字", "14 是字典里的一个键"], "a": 2, "why": "`split()` 切出的小段是文字，所以要和文字比较。数字 14 永远不会和它相等。", "at": "python_piggy.html#step7"},
  {"id": "ef_01", "unit": "easy_functions", "d": -0.9, "type": "fill", "q": "哪个词开始一个新函数，比如 `___ greet(name):`？请输入它。", "answers": ["def"], "why": "`def` 创建一个函数并给它起名字。", "at": "python_piggy.html#step8"},
  {"id": "ef_02", "unit": "easy_functions", "d": -0.8, "type": "mcq", "q": "`def greet(name):` 下面的行什么时候运行？", "o": ["Python 读到 def 时运行一次", "保存文件之前永远不运行", "只在第一次运行", "每次调用 greet 时"], "a": 3, "why": "Python 读到函数体里的行时，它们还不会运行。每次你调用它，比如 `greet(\"Mali\")`，它们才运行。", "at": "python_piggy.html#step8"},
  {"id": "ef_03", "unit": "easy_functions", "d": -0.6, "type": "mcq", "q": "在 `def greet(name):` 里，`name` 是什么？", "o": ["参数", "键", "模块", "字符串"], "a": 0, "why": "参数是一个变量，保存你调用函数时放在括号里的东西。", "at": "python_piggy.html#step8"},
  {"id": "ef_04", "unit": "easy_functions", "d": -0.6, "type": "mcq", "q": "`return` 做什么？", "o": ["把值打印在屏幕上", "把一个值交回给调用它的地方", "重新开始函数", "结束整个程序"], "a": 1, "why": "`return` 把一个值交回给调用这个函数的那一行，并在那里结束函数。", "at": "python_piggy.html#step8"},
  {"id": "ef_05", "unit": "easy_functions", "d": -0.5, "type": "mcq", "q": "`double(21)` 打印什么？", "code": "def double(number):\n    return number * 2\n\nprint(double(21))", "o": ["21", "2", "42", "double(21)"], "a": 2, "why": "`double(21)` 把 21 放进 `number`，函数返回 21 * 2。", "at": "python_piggy.html#step8"},
  {"id": "ef_06", "unit": "easy_functions", "d": -0.4, "type": "mcq", "q": "`double(\"ha\")` 打印什么？", "code": "def double(x):\n    return x + x\n\nprint(double(\"ha\"))", "o": ["ha", "x + x", "出错：文字不能相加", "haha"], "a": 3, "why": "`x` 保存文字 `ha`，而 `+` 把两个字符串连起来：`haha`。", "at": "python_song.html#step4"},
  {"id": "ef_07", "unit": "easy_functions", "d": -0.2, "type": "mcq", "q": "`sale_price(60)` 打印什么？", "code": "def sale_price(price, off=10):\n    return price - off\n\nprint(sale_price(60))", "o": ["50", "60", "10", "70"], "a": 0, "why": "`off` 有一个默认值：除非你给另一个数字，否则它就是 10。60 - 10 是 50。", "at": "python_browser.html#step1"},
  {"id": "ef_08", "unit": "easy_functions", "d": -0.1, "type": "mcq", "q": "`sale_price(50, 20)` 打印什么？", "code": "def sale_price(price, off=10):\n    return price - off\n\nprint(sale_price(50, 20))", "o": ["40", "30", "20", "70"], "a": 1, "why": "第二个数字替换了默认值，所以 `off` 是 20，50 - 20 是 30。", "at": "python_browser.html#step1"},
  {"id": "ef_09", "unit": "easy_functions", "d": 0.0, "type": "mcq", "q": "一个函数会打印，但没有 `return`。这段代码打印什么？", "code": "def greet(name):\n    print(\"Hello\", name)\n\nresult = greet(\"Mali\")\nprint(result)", "o": ["Hello Mali，两次", "先 None，再 Hello Mali", "先 Hello Mali，再 None", "只有 Hello Mali"], "a": 2, "why": "`greet` 会打印，但它没有 `return`，所以它交回的是 `None`。", "at": "python_piggy.html#step8"},
  {"id": "ef_10", "unit": "easy_functions", "d": -0.3, "type": "mcq", "q": "一个函数没有 `return` 这一行。它交回什么？", "o": ["0", "一个空字符串", "它最后打印的值", "None"], "a": 3, "why": "没有 `return` 的函数交回 `None`，这是 Python 表示“什么都没有”的词。", "at": "python_piggy.html#step8"},
  {"id": "ef_11", "unit": "easy_functions", "d": -0.2, "type": "mcq", "q": "这段代码打印几行？", "code": "def twice(phrase):\n    print(phrase)\n    print(phrase)\n\ntwice(\"la\")\ntwice(\"do\")", "o": ["4", "2", "1", "3"], "a": 0, "why": "函数每次打印两行，而它被调用了两次。", "at": "python_song.html#step4"},
  {"id": "ef_12", "unit": "easy_functions", "d": 0.0, "type": "mcq", "q": "函数是用来做什么的？", "o": ["让 Python 跑得更快", "通过名字重复使用同样的几行", "让 print 能处理数字", "防止变量名打错"], "a": 1, "why": "函数给几行代码起一个名字，这样你就能一次又一次地用它们，而不用再写一遍。", "at": "python_piggy.html#step8"},
  {"id": "ef_13", "unit": "easy_functions", "d": 0.1, "type": "mcq", "q": "`is_open(\"json\")` 交回的是哪种值？", "code": "import sys\n\ndef is_open(module):\n    return module in sys.modules", "o": ["一个模块", "一个模块列表", "一个布尔值：True 或 False", "一个像 json 这样的字符串"], "a": 2, "why": "`module in sys.modules` 是一个问题，所以它的答案，也就是函数返回的东西，是 True 或 False。", "at": "python_piggy.html#step8"},
  {"id": "ef_14", "unit": "easy_functions", "d": 0.2, "type": "mcq", "q": "`if __name__ == \"__main__\":` 检查什么？", "o": ["这个文件有没有 main() 函数", "Python 是不是最新版本", "另一个文件有没有导入 main()", "这个文件是不是你运行的那个"], "a": 3, "why": "当你把一个文件当作程序运行时，Python 把它命名为 `\"__main__\"`。当另一个文件导入它时，它的名字就是它的文件名。", "at": "python_browser.html#step1"},
  {"id": "ef_15", "unit": "easy_functions", "d": 0.2, "type": "mcq", "q": "文件 `stall.py` 的结尾是 `if __name__ == \"__main__\":` 和 `main()`。另一个文件运行 `import stall`。`main()` 会运行吗？", "o": ["不会：在那里 `__name__` 是 `\"stall\"`", "会：import 会运行每一行", "会，但它的打印被隐藏了", "不会：import 从不运行任何一行"], "a": 0, "why": "导入时会运行这个文件，但这时它的 `__name__` 是 `\"stall\"`，所以 `if` 是 False，`main()` 不会运行。", "at": "python_browser.html#step2"},
  {"id": "ef_16", "unit": "easy_functions", "d": -0.1, "type": "mcq", "q": "在有 `main()` 的 Python 文件里，函数通常放在哪里？", "o": ["放在最后，main() 之后", "先放函数，然后 main()，然后 if 那一行", "放在最后的 if 那一行里面", "每个函数放在自己的文件里"], "a": 1, "why": "几乎每个程序的布局都一样：先是函数，然后是 `main()`，然后是运行它的 `if __name__` 那一行。", "at": "python_browser.html#step1"},
  {"id": "ef_17", "unit": "easy_functions", "d": 0.1, "type": "mcq", "q": "`len(chorus(\"Peppa\"))` 打印什么？", "code": "def chorus(name):\n    return [\"[chorus]\", name + \" is a pig\"]\n\nprint(len(chorus(\"Peppa\")))", "o": ["1", "5", "2", "[chorus]"], "a": 2, "why": "函数返回一个有两个字符串的列表，`len()` 数出它的两项。", "at": "python_song.html#step4"},
  {"id": "ef_18", "unit": "easy_functions", "d": 0.3, "type": "mcq", "q": "`return` 后面有一个 `print`。这段代码打印什么？", "code": "def add(a, b):\n    return a + b\n    print(\"done\")\n\nprint(add(2, 3))", "o": ["先 5，再 done", "先 done，再 5", "出错：return 后面有 print", "只有 5：return 结束了函数"], "a": 3, "why": "`return` 交回答案并在那里结束函数，所以它后面的那一行永远不会运行。", "at": "python_piggy.html#step8"},
  {"id": "ef_19", "unit": "easy_functions", "d": 0.3, "type": "mcq", "q": "`digits(2147483647)` 打印什么？", "code": "def digits(number):\n    return len(str(number))\n\nprint(digits(2147483647))", "o": ["10", "2147483647", "1", "4"], "a": 0, "why": "`str()` 把数字变成文字，`len()` 数出它的十个字符。", "at": "python_piggy.html#step8"},
  {"id": "ef_20", "unit": "easy_functions", "d": 0.2, "type": "mcq", "q": "在 `play(\"G A B G\", bpm=bpm)` 里，`bpm=bpm` 是什么？", "o": ["让两个变量相等", "play 用的名字，配上你的值", "一个测试：两个相同吗？", "Python 会跳过的打字错误"], "a": 1, "why": "第一个 `bpm` 是 `play` 使用的名字，第二个是你的变量，里面装着那个数字。", "at": "python_song.html#step6"},
  {"id": "em_01", "unit": "easy_modules", "d": -0.9, "type": "mcq", "q": "`import platform` 做什么？", "o": ["从互联网安装一个程序", "创建一个叫 platform 的新文件", "打开一个代码工具箱，也就是模块", "打印关于这台电脑的信息"], "a": 2, "why": "`import` 打开一个模块：一个代码工具箱。`platform` 告诉你运行 Python 的这台电脑的情况。", "at": "python_piggy.html#step1"},
  {"id": "em_02", "unit": "easy_modules", "d": -0.7, "type": "mcq", "q": "Python 自带的那些模块，合起来叫什么？", "o": ["Python 商店", "软件包索引", "模块商店", "标准库"], "a": 3, "why": "Python 自带几百个模块，叫做它的标准库。这些模块都不用你安装。", "at": "python_piggy.html#step2"},
  {"id": "em_03", "unit": "easy_modules", "d": -0.5, "type": "fill", "q": "在 Mac 上，`sys.platform` 是什么？请输入。", "answers": ["darwin"], "why": "`sys.platform` 用一个单词表示 Python 在哪里运行：Mac 上是 `darwin`，Windows 上是 `win32`，Linux 上是 `linux`。", "at": "python_piggy.html#step1"},
  {"id": "em_04", "unit": "easy_modules", "d": -0.5, "type": "fill", "q": "在 Windows 上，`sys.platform` 是什么？请输入。", "answers": ["win32"], "why": "在 Windows 上它是 `win32`，即使在 64 位电脑上也是。在 Mac 上它是 `darwin`。", "at": "python_piggy.html#step1"},
  {"id": "em_05", "unit": "easy_modules", "d": -0.3, "type": "mcq", "q": "为什么 `sys.platform` 后面没有括号，而 `platform.system()` 有？", "o": ["它是一个值，不是要调用的函数", "它是一个不需要输入的函数", "在 Python 里括号可写可不写", "sys 比 platform 更老"], "a": 0, "why": "`sys.platform` 是 `sys` 保存的一个值。`platform.system()` 是一个函数：括号就是在向它提问。", "at": "python_piggy.html#step1"},
  {"id": "em_06", "unit": "easy_modules", "d": -0.4, "type": "mcq", "q": "`from tune import play` 做什么？", "o": ["大声播放文件 tune.py", "从 tune.py 借来函数 play", "把 tune.py 复制成一个新文件", "导入除了 play 以外的所有函数"], "a": 1, "why": "`from ... import ...` 从另一个文件里拿出一个名字，这样你就能直接调用 `play()`。", "at": "python_song.html#step1"},
  {"id": "em_07", "unit": "easy_modules", "d": -0.2, "type": "mcq", "q": "在网页浏览器里，`sys.platform` 显示 `emscripten`。Emscripten 是什么？", "o": ["一个专为 Python 做的网页浏览器", "一种电脑芯片", "把 Python 变成 WebAssembly 的工具", "一个用于音乐的 Python 模块"], "a": 2, "why": "Emscripten 把 Python 变成了 WebAssembly，这是每个网页浏览器都能运行的一种程序。", "at": "python_browser.html#inside"},
  {"id": "em_08", "unit": "easy_modules", "d": -0.1, "type": "mcq", "q": "在导入 `statistics` 之前和之后各查找它一次。这段代码打印什么？", "code": "import sys\nprint(\"statistics\" in sys.modules)\nimport statistics\nprint(\"statistics\" in sys.modules)", "o": ["先 True，再 True", "先 False，再 False", "先 True，再 False", "先 False，再 True"], "a": 3, "why": "`sys.modules` 保存 Python 到目前为止打开过的每个模块。`statistics` 只有在导入时才被打开。", "at": "python_piggy.html#step6"},
  {"id": "em_09", "unit": "easy_modules", "d": 0.0, "type": "mcq", "q": "`sys.modules` 保存什么？", "o": ["Python 到目前为止打开过的每个模块", "Python 自带的每个模块", "只有你安装的模块", "在互联网上找到的模块"], "a": 0, "why": "`sys.modules` 是一个字典，装着 Python 打开过的模块：键是名字，值是模块。", "at": "python_piggy.html#step4"},
  {"id": "em_10", "unit": "easy_modules", "d": 0.1, "type": "mcq", "q": "`platform.python_version()` 给出 `\"3.14.2\"`。这是哪种值？", "o": ["数字", "字符串", "列表", "布尔值"], "a": 1, "why": "它带引号：是字符串。中间用点隔开的三个数字不是一个数。", "at": "python_piggy.html#step1"},
  {"id": "em_11", "unit": "easy_modules", "d": -0.3, "type": "mcq", "q": "下面哪个是 Python 自带的，不需要安装？", "o": ["requests", "pandas", "json", "pydantic"], "a": 2, "why": "`json` 在标准库里。`requests`、`pandas` 和 `pydantic` 要用 pip 安装。", "at": "python_piggy.html#step2"},
  {"id": "em_12", "unit": "easy_modules", "d": 0.0, "type": "fill", "q": "Python 自带的哪个模块能随机挑选东西？请输入它的名字。", "answers": ["random"], "why": "`random` 是 Python 自带的：`\"random\" in sys.stdlib_module_names` 是 True。", "at": "python_piggy.html#step6"},
  {"id": "em_13", "unit": "easy_modules", "d": 0.1, "type": "fill", "q": "模块就是一个装着 Python 代码的文件。这个文件的名字以什么结尾？请输入。", "answers": [".py", "py"], "why": "Python 文件以 `.py` 结尾，比如 `tune.py`。`import tune` 会找到那个文件。", "at": "python_browser.html#boxes"},
  {"id": "em_14", "unit": "easy_modules", "d": 0.2, "type": "mcq", "q": "在 Python 3.14 里，`print(sys.version_info.minor > 12)` 打印什么？", "o": ["False", "14", "12", "True"], "a": 3, "why": "`sys.version_info.minor` 是版本号中间的那个数，14，而 14 > 12 是 True。", "at": "python_piggy.html#puzzle3"},
  {"id": "em_15", "unit": "easy_modules", "d": 0.3, "type": "mcq", "q": "你在一个文件夹里输入 `python -m http.server 8000`。它做什么？", "o": ["把文件夹里的文件当作网页提供出来", "从互联网下载一个网页服务器", "打开 Python 的 >>> 提示符", "把文件夹上传到一个网站"], "a": 0, "why": "`http.server` 是 Python 自带的。它提供文件夹里的文件，让浏览器可以通过一个网址打开它们。", "at": "python_piggy.html#avatar"},
  {"id": "ec_01", "unit": "easy_comprehensions", "d": -0.6, "type": "mcq", "q": "这个列表推导式打印什么？", "code": "prices = [60, 50, 25]\nprint([price - 10 for price in prices])", "o": ["[60, 50, 25]", "[50, 40, 15]", "[50]", "125"], "a": 1, "why": "列表推导式创建一个新列表：“对 prices 里的每个 price，取 price 减 10”。", "at": "python_browser.html#step3"},
  {"id": "ec_02", "unit": "easy_comprehensions", "d": -0.5, "type": "mcq", "q": "一个列表推导式加上 `!`。它打印什么？", "code": "words = [\"hi\", \"bye\"]\nprint([word + \"!\" for word in words])", "o": ["hi! bye!", "['hi', 'bye', '!']", "['hi!', 'bye!']", "['hi bye!']"], "a": 2, "why": "每个单词后面都接上 `!`，新列表装着这些结果。", "at": "python_browser.html#step3"},
  {"id": "ec_03", "unit": "easy_comprehensions", "d": -0.3, "type": "mcq", "q": "一个列表推导式带有 `if`。它打印什么？", "code": "prices = [5, 50, 15, 80]\nprint([price for price in prices if price > 10])", "o": ["[50, 80]", "[5]", "[5, 50, 15, 80]", "[50, 15, 80]"], "a": 3, "why": "末尾有 `if` 时，只有通过测试的项才会进入新列表。15 也比 10 大。", "at": "python_browser.html#step4"},
  {"id": "ec_04", "unit": "easy_comprehensions", "d": -0.4, "type": "mcq", "q": "列表推导式用哪种括号？", "o": ["方括号：[ ]", "圆括号：( )", "花括号：{ }", "根本不用括号"], "a": 0, "why": "列表推导式写在方括号里。写在圆括号里是生成器；写在花括号里，是集合或字典。", "at": "python_browser.html#step3"},
  {"id": "ec_05", "unit": "easy_comprehensions", "d": -0.2, "type": "mcq", "q": "一个有重复项的列表变成集合。这段代码打印什么？", "code": "basket = [\"mango\", \"longan\", \"mango\"]\nprint(len(set(basket)))", "o": ["3", "2", "1", "5"], "a": 1, "why": "集合里每样东西只保存一次，所以第二个 mango 被去掉了：剩下两种。", "at": "python_browser.html#step6"},
  {"id": "ec_06", "unit": "easy_comprehensions", "d": -0.1, "type": "mcq", "q": "单独的 `{}` 创建什么？", "o": ["一个空集合", "一个空列表", "一个空字典", "什么都不创建：它是错误"], "a": 2, "why": "`{}` 是一个空字典。空集合写作 `set()`。", "at": "python_browser.html#step6"},
  {"id": "ec_07", "unit": "easy_comprehensions", "d": 0.0, "type": "mcq", "q": "这里 `&` 打印什么？", "code": "north = {\"mango\", \"longan\"}\nsouth = {\"mango\", \"banana\"}\nprint(north & south)", "o": ["{'mango', 'longan', 'banana'}", "{'longan', 'banana'}", "{'longan'}", "{'mango'}"], "a": 3, "why": "`&` 保留两个集合里都有的东西。`|` 会把它们合起来，`-` 会减去一些。", "at": "python_browser.html#step6"},
  {"id": "ec_08", "unit": "easy_comprehensions", "d": 0.1, "type": "mcq", "q": "一个集合在循环里被填充。这段代码打印什么？", "code": "seen = set()\nfor fruit in [\"mango\", \"lime\", \"mango\"]:\n    seen.add(fruit)\nprint(len(seen))", "o": ["2", "3", "1", "0"], "a": 0, "why": "第二次加入 `mango` 不会改变任何东西：集合里每样东西只保存一次。", "at": "python_browser.html#step6"},
  {"id": "ec_09", "unit": "easy_comprehensions", "d": 0.1, "type": "mcq", "q": "这里 `next()` 打印什么？", "code": "sale = (price - 10 for price in [60, 50])\nprint(next(sale))", "o": ["40", "50", "60", "[50, 40]"], "a": 1, "why": "圆括号创建一个生成器。`next()` 向它要一项：第一项，60 - 10。", "at": "python_browser.html#step7"},
  {"id": "ec_10", "unit": "easy_comprehensions", "d": 0.2, "type": "mcq", "q": "一个生成器两次交给 `sum()`。这段代码打印什么？", "code": "squares = (number * number for number in [1, 2, 3])\nprint(sum(squares))\nprint(sum(squares))", "o": ["先 14，再 14", "先 6，再 6", "先 14，再 0", "先 0，再 14"], "a": 2, "why": "1 + 4 + 9 是 14。生成器只能用一次，所以第二次 `sum()` 什么都找不到：0。", "at": "python_browser.html#step7"},
  {"id": "ec_11", "unit": "easy_comprehensions", "d": 0.0, "type": "mcq", "q": "这个字典推导式打印什么？", "code": "prices = {\"mango\": 60, \"lime\": 20}\nprint({fruit: price - 10 for fruit, price in prices.items()})", "o": ["{'mango': 60, 'lime': 20}", "[50, 10]", "{50, 10}", "{'mango': 50, 'lime': 10}"], "a": 3, "why": "字典推导式保留同样的键，并生成新的值：每个价格减 10。", "at": "python_browser.html#step5"},
  {"id": "ec_12", "unit": "easy_comprehensions", "d": 0.2, "type": "mcq", "q": "为什么你可能会用生成器而不用列表？", "o": ["它只记住自己进行到哪里", "它可以一次又一次地使用", "它在生成各项时就排好序", "它能装文字，列表不能"], "a": 0, "why": "列表把每一项同时放在内存里。生成器只记住自己进行到哪里，被要求时才生成下一项。", "at": "python_browser.html#step7"},
  {"id": "ec_13", "unit": "easy_comprehensions", "d": 0.3, "type": "mcq", "q": "函数里的 `yield` 做什么？", "o": ["永远结束这个函数", "交出一项然后暂停", "在屏幕上打印一项", "一次交回所有项"], "a": 1, "why": "`yield` 交出一项，并让函数在那里暂停。当循环要下一项时，它就接着运行。", "at": "python_browser.html#step8"},
  {"id": "ec_14", "unit": "easy_comprehensions", "d": 0.1, "type": "mcq", "q": "一个列表推导式使用了 `len()`。它打印什么？", "code": "print([len(fruit) for fruit in [\"fig\", \"mango\"]])", "o": ["[2]", "8", "[3, 5]", "['fig', 'mango']"], "a": 2, "why": "新列表装着每种水果名字的长度：fig 是 3，mango 是 5。", "at": "python_browser.html#step3"},
  {"id": "ec_15", "unit": "easy_comprehensions", "d": 0.3, "type": "fill", "q": "生成器已经没有项了，`next()` 还向它要一项。这个错误叫什么？请输入它的名字。", "answers": ["stopiteration"], "why": "`StopIteration` 是生成器表示“没有了”的方式。", "at": "python_browser.html#step7"},
  {"id": "python_toolkit_01", "unit": "python_toolkit", "d": -0.7, "type": "mcq", "q": "在这个 Python 类里，`self` 是什么意思？", "code": "class Dog:\n    def __init__(self, name, age):\n        self.name = name\n        self.age = age\n\n    def bark(self):\n        return f\"{self.name} says woof!\"", "o": ["类 `Dog` 本身，也就是模板。", "这一只具体的狗，这一个对象。", "程序创建的第一只狗。", "存放这个类的代码的文件。"], "a": 1, "why": "`self` 就是调用这个方法的那一个对象。每个方法都把它作为第一个输入，Python 会替你传进去。", "at": "python_toolkit.html#s1"},
  {"id": "python_toolkit_02", "unit": "python_toolkit", "d": -0.5, "type": "mcq", "q": "`json.dumps(student)` 做什么？", "o": ["它把一个 JSON 文件读成字典。", "它把这个字典从内存里删除。", "它把一个 Python 对象变成 JSON 文本。", "它把这个字典发送到网络服务器。"], "a": 2, "why": "`json.dumps` 是从 Python 到文本：dump to string（转存为字符串）。`json.loads` 则是反方向。", "at": "python_toolkit.html#s6"},
  {"id": "python_toolkit_03", "unit": "python_toolkit", "d": -0.3, "type": "fill", "q": "路径 `p` 指向文件 `notes.txt`。`p.stem` 给出什么？请输入。", "answers": ["notes", "\"notes\"", "'notes'"], "why": "`p.stem` 是不带扩展名的文件名。`p.name` 给出 `notes.txt`，`p.suffix` 给出 `.txt`。", "at": "python_toolkit.html#s5"},
  {"id": "python_toolkit_04", "unit": "python_toolkit", "d": -0.1, "type": "mcq", "q": "函数 `greet` 带有类型提示 `name: str`。调用 `greet(42)` 时会发生什么？", "o": ["Python 立刻报 `TypeError` 并停止。", "Python 拒绝加载整个文件。", "这个函数每次都返回 `None`。", "它照样运行。Python 不强制执行类型提示。"], "a": 3, "why": "类型提示是写给人和工具看的备注，不是给解释器的规则。`greet(42)` 照样运行。", "at": "python_toolkit.html#s4"},
  {"id": "python_toolkit_05", "unit": "python_libraries", "d": 0.1, "type": "mcq", "q": "为什么要在 `requests.get(...)` 之后调用 `r.raise_for_status()`？", "o": ["如果服务器返回了 404 或 500，它会抛出错误。", "如果服务器 10 秒没有反应，它就放弃。", "它把回复从 JSON 变成字典。", "它把你的 API 密钥加到请求头里。"], "a": 0, "why": "没有 `raise_for_status()`，失败的请求看起来和成功的一样。遇到 404 或 500，它会抛出错误。", "at": "python_toolkit.html#s7"},
  {"id": "python_toolkit_06", "unit": "python_toolkit", "d": 0.4, "type": "mcq", "q": "你对同一个生成器对象循环了两次。第二次循环得到什么？", "o": ["同样的元素再来一遍，顺序相同。", "什么也没有。生成器只能用一次。", "元素再来一遍，但顺序相反。", "一个 `TypeError`，因为它没有 `len()`。"], "a": 1, "why": "生成器只能用一次。对它循环两次，第二次循环什么也得不到。", "at": "python_toolkit.html#s2"},
  {"id": "python_toolkit_07", "unit": "python_toolkit", "d": 0.5, "type": "mcq", "q": "在这个装饰器里，有人忘了写 `wrapper` 里面的 `return result` 这一行。现在，一个用 `@timed` 包装的函数会返回什么？", "code": "def timed(fn):\n    def wrapper(*args, **kwargs):\n        start = time.time()\n        result = fn(*args, **kwargs)\n        print(f\"{fn.__name__} took {time.time() - start:.2f}s\")\n    return wrapper", "o": ["它返回时间，单位是秒。", "它返回函数 `fn`。", "它每次都返回 `None`。", "它返回和以前一样的值。"], "a": 2, "why": "`wrapper` 取代了原来的函数，所以调用者拿到的是 `wrapper` 返回的东西。一个没有 `return` 行的 Python 函数返回 `None`。", "at": "python_toolkit.html#s3"},
  {"id": "python_classes_01", "unit": "python_classes", "d": -0.8, "type": "mcq", "q": "在 Python 的 `>>>` 提示符下一行一行创建的 tkinter 窗口，不用 `root.mainloop()` 也能工作。为什么脚本文件需要这一行？", "o": ["没有它，文件就不能导入 `tkinter`", "文件运行到末尾就会关闭", "没有它，文件的窗口就没有标题", "没有它，文件就不会 pack 标签"], "a": 1, "why": "提示符在等你输入下一行的时候，让窗口一直活着。文件没有什么可等的，所以它运行到末尾就关闭了。`mainloop` 画出窗口，并等待按键和点击。", "at": "python_classes.html#step1"},
  {"id": "python_classes_02", "unit": "python_classes", "d": -0.6, "type": "mcq", "q": "你创建了一个 `tk.Label`，但它一直不显示。Python 也没有报错。你忘了什么？", "o": ["对窗口调用 `title`", "先导入 `scrolledtext`", "对标签调用 `pack`", "给标签设置字体"], "a": 2, "why": "在调用 `pack` 之前，什么都不会显示。从未 pack 过的控件是看不见的，而 Python 什么也不会说。", "at": "python_classes.html#step1"},
  {"id": "python_classes_03", "unit": "python_classes", "d": -0.3, "type": "mcq", "q": "这个 tkinter 程序没有类。控件 `box` 和 `transcript` 是在文件顶部作为全局变量创建的，而不是在 `main()` 这样的函数里面。为什么？", "code": "import tkinter as tk\nfrom tkinter import scrolledtext\n\nroot = tk.Tk()\ntranscript = scrolledtext.ScrolledText(root, state=\"disabled\")\nrow = tk.Frame(root)\nrow.pack(side=\"bottom\", fill=\"x\", padx=10, pady=10)\nbox = tk.Entry(row, font=(\"Segoe UI\", 11))\nbox.pack(side=\"left\", fill=\"x\", expand=True, ipady=6)\n\ndef send(event=None):\n    said = box.get().strip()\n    transcript.configure(state=\"normal\")\n    transcript.insert(\"end\", \"You: \" + said + \"\\n\")\n    transcript.configure(state=\"disabled\")\n\ntk.Button(row, text=\"Send\", width=8, command=send).pack(side=\"left\", padx=(8, 0))\ntranscript.pack(side=\"top\", fill=\"both\", expand=True)\nroot.mainloop()", "o": ["`send()` 看不到另一个函数里面的名字", "tkinter 只能画出保存在全局名字里的控件", "全局名字让窗口打开得快得多", "没有类的文件不能有 `main()`"], "a": 0, "why": "`send()` 是一个单独的函数，而一个函数看不到另一个函数里面的名字。没有类来保存这些控件，全局变量就是文件的两半都能访问到的唯一东西。", "at": "python_classes.html#step2"},
  {"id": "python_classes_04", "unit": "python_classes", "d": 0.0, "type": "mcq", "q": "在这个 Python 文件里，`if __name__ == \"__main__\":` 这一行是做什么的？", "code": "import tkinter as tk\n\ndef main():\n    root = tk.Tk()\n    root.title(\"Chat\")\n    root.geometry(\"520x420\")\n    root.mainloop()\n\nif __name__ == \"__main__\":\n    main()", "o": ["它给 `tkinter` 起了一个短名字 `tk`", "它让窗口一直开着，直到你把它关掉", "当另一个文件导入它时，它运行 `main()`", "只有当你运行的是这个文件时，它才运行 `main()`"], "a": 3, "why": "只有当这个文件是你运行的那个文件时，Python 才把 `__name__` 设为 `\"__main__\"`。当另一个文件导入它时，这个判断为假，`main()` 不运行，也不会打开窗口。", "at": "python_classes.html#step3"},
  {"id": "python_classes_05", "unit": "python_classes", "d": 0.1, "type": "fill", "q": "在 Python 类的 `__init__` 里，第一个参数是正在被创建的对象。你从不传入它，因为 Python 会替你把它放进去。Python 程序员总是给它起哪个名字？请输入这个名字。", "answers": ["self"], "why": "`self` 是正在被创建的对象。调用者只传入其他参数，`self` 由 Python 提供。", "at": "python_classes.html#step4"},
  {"id": "python_classes_06", "unit": "python_classes", "d": 0.2, "type": "mcq", "q": "在这个 tkinter 类里，输入框保存为 `self.box`，但框架只是一个普通变量 `row`。为什么？", "code": "class ChatWindow:\n    def __init__(self, root):\n        self.root = root\n        row = tk.Frame(root)\n        row.pack(side=\"bottom\", fill=\"x\", padx=10, pady=10)\n        self.box = tk.Entry(row, font=(\"Segoe UI\", 11))\n        self.box.pack(side=\"left\", fill=\"x\", expand=True, ipady=6)\n        tk.Button(row, text=\"Send\", width=8, command=self.send).pack(\n            side=\"left\", padx=(8, 0))\n\n    def send(self, event=None):\n        said = self.box.get().strip()\n        if not said:\n            return\n        self.box.delete(0, \"end\")", "o": ["`Frame` 不能保存在 `self` 上", "普通变量会比其他所有变量先被 pack", "`send()` 之后需要输入框，不需要框架", "`self.box` 让输入框变成只读的"], "a": 2, "why": "只有当某个方法之后还需要一样东西时，才把它保存在 `self` 上。`send()` 要读取输入框，但 `__init__` 结束之后，再也没有什么会去找那个框架。", "at": "python_classes.html#step5"},
  {"id": "python_classes_07", "unit": "python_classes", "d": 0.4, "type": "mcq", "q": "在 tkinter 里，一个按钮是用 `tk.Button(row, text=\"Send\", command=self.send())` 创建的，`send` 后面带着括号。会发生什么？", "o": ["`send` 现在就运行一次；点击没有任何作用", "Python 在那一行出错并停止", "每点击一次，`send` 运行两次", "按钮能用，但 Enter 键不能用"], "a": 0, "why": "带上括号，Python 就会在窗口还在创建的时候立刻调用 `send`。按钮拿到的是 `send` 返回的东西，也就是 `None`，所以点击没有任何作用。", "at": "python_classes.html#step6"},
  {"id": "python_classes_08", "unit": "python_classes", "d": 0.3, "type": "mcq", "q": "在 tkinter 里，同一个框架里的控件可以同时用 `pack` 和 `grid` 来摆放吗？", "o": ["可以，只要 `grid` 在 `pack` 之前调用", "可以，只要每个控件只用两者中的一个", "不可以。`grid` 只在主窗口里有效", "不可以。一个框架只用两者中的一个，不能都用"], "a": 3, "why": "每个框架只有一种摆放控件的方式。在一个框架里把两者混用，tkinter 会报错并停下。两个不同的框架可以各用不同的一种。", "at": "python_classes.html#step7"},
  {"id": "python_classes_09", "unit": "python_classes", "d": 0.6, "type": "mcq", "q": "这些是一个 tkinter 窗口类的方法。`self.answers` 是一个 `queue.Queue()`，`ask` 在一个工作线程上运行。为什么 `ask` 调用的是 `self.answers.put(said)`，而不是 `self.write(said)`？", "code": "    def send(self, event=None):\n        said = self.box.get().strip()\n        self.write(\"You: \" + said + \"\\n\")\n        self.history.append({\"role\": \"user\", \"content\": said})\n        threading.Thread(target=self.ask, args=(list(self.history),),\n                         daemon=True).start()\n\n    def ask(self, history):\n        # ... a slow call to a model puts its reply in said ...\n        self.answers.put(said)\n\n    def drain(self):\n        while not self.answers.empty():\n            said = self.answers.get()\n            self.write(\"Bot: \" + said + \"\\n\\n\")\n        self.root.after(250, self.drain)", "o": ["队列比往控件里写内容更快", "工作线程绝不能碰控件", "工作线程不能调用 `self` 上的方法", "队列是保存聊天记录的地方"], "a": 1, "why": "在 tkinter 里，只有主线程可以碰控件。工作线程把文本放进队列，而 `drain` 是用 `after` 安排在主线程上运行的，它把文本取出来并写上去。", "at": "python_classes.html#step8"},
  {"id": "python_classes_10", "unit": "python_classes", "d": 0.8, "type": "mcq", "q": "这个 Python 函数用 `type()` 创建一个类。最后一行打印出 `AskName can send?  False`。为什么？", "code": "def window_class(name, title, kind):\n    # __init__, close, run, build_form, submit, build_chat, write,\n    # send, think and drain are plain functions written here\n    inside = {\"__init__\": __init__, \"close\": close, \"run\": run}\n    if kind == \"form\":\n        inside.update({\"build_form\": build_form, \"submit\": submit})\n    else:\n        inside.update({\"build_chat\": build_chat, \"write\": write,\n                       \"send\": send, \"think\": think, \"drain\": drain})\n    return type(name, (), inside)\n\nAskName = window_class(\"AskName\", \"Before we start\", \"form\")\nprint(\"AskName can send? \", hasattr(AskName, \"send\"))", "o": ["每个对象上的一个标志在表单里隐藏了 `send`", "传给 `type()` 的字典里没有 `send`", "表单窗口关闭时，`send` 就被删除了", "`AskName` 继承自一个没有 `send` 的父类"], "a": 1, "why": "`type(name, bases, dict)` 根据一个写明类里有什么的字典来创建一个类。对于 `\"form\"` 这一种，字典里没有 `send`，所以这个类根本没有这个属性。之后不会检查任何标志。", "at": "python_classes.html#step8"},
  {"id": "catch_your_own_bugs_01", "unit": "catch_your_own_bugs", "d": -0.8, "type": "mcq", "q": "当 `assert` 后面的条件为假时，会发生什么？", "o": ["什么都不会发生", "测试被悄悄跳过", "Python 替你修正这个值", "测试失败并告诉你"], "a": 3, "why": "`assert` 的意思是：这必须为真。如果为真，什么都不会发生。如果为假，测试失败并告诉你。", "at": "catch_your_own_bugs.html#step1"},
  {"id": "catch_your_own_bugs_02", "unit": "catch_your_own_bugs", "d": -0.5, "type": "fill", "q": "pytest 只会找到名称以相同的几个字符开头的文件和函数。这些名称必须以什么开头？请输入。", "answers": ["test_", "test"], "why": "pytest 会找到以 `test_` 开头的文件和以 `test_` 开头的函数。名称不对，它就什么也找不到。", "at": "catch_your_own_bugs.html#step0"},
  {"id": "catch_your_own_bugs_03", "unit": "catch_your_own_bugs", "d": -0.3, "type": "mcq", "q": "这个 pytest fixture 使用了 `tmp_path`。每个要用到数据库的测试都接收 `db`。为什么？", "code": "@pytest.fixture\ndef db(tmp_path, monkeypatch):\n    monkeypatch.setattr(homework, \"DB_PATH\", tmp_path / \"test.db\")\n    conn = homework.open_db()\n    yield conn            # the test runs here\n    conn.close()", "o": ["为了得到一个全新的数据库，而不是真正的那个", "为了对列表里的每个名字都把同一个测试运行一次", "为了捕获程序打印出来的所有内容", "为了在第一次失败时就停止整个测试运行"], "a": 0, "why": "`tmp_path` 是 pytest 创建的一个新的空文件夹，用完之后就丢掉。所以每个测试都从一个空数据库开始，绝不会碰到真正的那个。", "at": "catch_your_own_bugs.html#step2"},
  {"id": "catch_your_own_bugs_04", "unit": "catch_your_own_bugs", "d": 0.2, "type": "mcq", "q": "在这个 pytest 测试里，`@pytest.mark.parametrize` 是做什么的？", "code": "@pytest.mark.parametrize(\"name\", [\n    \"Ploy\",\n    \"O'Brien\",\n    \"\",\n])\ndef test_any_name_survives(db, name):\n    homework.add(db, Args(student=name, title=\"Read page 40\", due=None))\n    saved = db.execute(\"SELECT student FROM jobs\").fetchone()[0]\n    assert saved == name", "o": ["只为一个测试把应用指向一个新的文件夹", "对列表里的每个名字都把同一个测试运行一次", "在测试结束之后再次运行准备代码", "只挑出名字里带有某个词的那些测试"], "a": 1, "why": "`parametrize` 对列表里的每个值都把同一个测试运行一次，并用输入给每次运行命名。", "at": "catch_your_own_bugs.html#step3"},
  {"id": "catch_your_own_bugs_05", "unit": "catch_your_own_bugs", "d": 0.5, "type": "mcq", "q": "你的 pytest 测试全部通过了。为什么故意把程序弄坏、再运行一遍测试是个好主意？", "o": ["坏掉的应用会让测试运行得更快", "pytest 需要一次失败才能找到文件", "你从没见过它失败的测试，可能什么也没测", "fixture 只在失败之后才做清理"], "a": 2, "why": "一个你从没见过它失败的测试不算测试。它可能什么都没有检查，而你却不知道。", "at": "catch_your_own_bugs.html#step4"},
  {"id": "catch_your_own_bugs_06", "unit": "catch_your_own_bugs", "d": 0.8, "type": "mcq", "q": "一个函数向 SQLite 数据库添加一行，但有人删掉了它的 `conn.commit()`。程序运行时一切看起来都正常。测试必须怎么做才能抓住这个问题？", "o": ["捕获程序打印的内容，在里面找那个名字", "添加一个带撇号的名字，再把它读回来", "在同一个打开的连接上数一数行数", "关闭连接，重新打开文件，然后再数"], "a": 3, "why": "在内存里一切看起来都完美。只有通过一个新的连接从磁盘上把数据读回来时，缺少的 `commit()` 才会显露出来。", "at": "catch_your_own_bugs.html#step6"},
  {"id": "python_data_classes_01", "unit": "python_libraries", "d": -0.7, "type": "mcq", "q": "像 Pydantic 这样的 Python 库可以对一个值做强制转换（coerce）。对一个值做强制转换是什么意思？", "o": ["检查它，如果不对就停止", "把它变成正确的类型", "把它锁住，让它以后不能改变", "从原始的 JSON 文本里读出它"], "a": 1, "why": "验证（validate）是检查。强制转换（coerce）是改变，例如文本 “28” 变成数字 28。", "at": "python_data_classes.html#s1"},
  {"id": "python_data_classes_02", "unit": "python_data_classes", "d": -0.5, "type": "mcq", "q": "下面哪一个是 Python 自带的，什么都不用安装？", "o": ["attrs", "Pydantic", "pydantic-settings", "数据类"], "a": 3, "why": "数据类是 Python 免费自带的。attrs 和 Pydantic 是需要你安装的库。", "at": "python_data_classes.html#s0"},
  {"id": "python_data_classes_03", "unit": "python_data_classes", "d": -0.3, "type": "mcq", "q": "一个 Python 类 `Booking` 是用 `@dataclass(frozen=True, slots=True)` 创建的，`b` 是一个 `Booking` 对象。当代码运行 `b.people = 5` 时，会发生什么？", "o": ["抛出一个 `FrozenInstanceError`", "这个字段悄悄地变成 5", "返回一个新的 `Booking` 对象", "这个值被强制转换成文本"], "a": 0, "why": "`frozen=True` 在你创建对象之后就把它锁住。Python 抛出 `FrozenInstanceError: cannot assign to field 'people'`。", "at": "python_data_classes.html#s5"},
  {"id": "python_data_classes_04", "unit": "python_libraries", "d": 0.1, "type": "mcq", "q": "在 attrs 中，怎样让文本 `\"28\"` 变成数字 28？", "o": ["给字段加上 `v.instance_of(int)`", "写上类型提示 `age: int`", "给字段加上 `converter=int`", "给字段加上 `v.gt(0)`"], "a": 2, "why": "attrs 从不会凭空自动做强制转换。只有在你写了 `converter=` 的地方才会发生强制转换。", "at": "python_data_classes.html#s6"},
  {"id": "python_data_classes_05", "unit": "python_libraries", "d": 0.3, "type": "mcq", "q": "当一个 Pydantic 类带有 `model_config = {\"strict\": True}` 时，有什么变化？", "o": ["文本 `\"28\"` 原样保存为文本", "文本 `\"28\"` 会被 `int` 字段拒绝", "对象创建之后，字段就被锁住", "关闭检查，好让它运行得更快"], "a": 1, "why": "默认情况下，Pydantic 会把 \"28\" 修复成 28。严格模式关闭了修复，所以 \"28\" 也会失败。", "at": "python_data_classes.html#s4"},
  {"id": "python_data_classes_06", "unit": "python_libraries", "d": 0.5, "type": "mcq", "q": "一个健康的 Python 程序常常把 Pydantic 和普通的数据类放在一起用。怎么用？", "o": ["数据类在边界上做检查；Pydantic 在内部保存数据", "attrs 检查每一个类；Pydantic 只在测试里使用", "每个类都用上 Pydantic；数据类一个也不用", "Pydantic 在边界上做检查；数据类在内部保存数据"], "a": 3, "why": "用 Pydantic 在边界上检查一次。在内部，一切都已经是干净的，所以普通的数据类就够了，而且它们很快。", "at": "python_data_classes.html#s10"},
  {"id": "python_data_classes_07", "unit": "python_libraries", "d": 0.7, "type": "mcq", "q": "一个 Pydantic 设置对象 `settings` 有一个类型为 `SecretStr` 的字段 `app_password`，所以打印这些设置时只显示星号。代码登录时怎样拿到真正的密码？", "o": ["它调用 `settings.model_dump()`", "它打印 `settings` 再读出来", "它对这个字段调用 `.get_secret_value()`", "它对这个字段调用 `.model_dump_json()`"], "a": 2, "why": "`print`、`model_dump()` 和 `model_dump_json()` 都会让 `SecretStr` 保持遮住的状态。只有用 `.get_secret_value()` 才能取出真正的值。", "at": "python_data_classes.html#s11"},
  {"id": "python_data_classes_08", "unit": "python_data_classes", "d": -0.4, "type": "mcq", "q": "打印这个数据类会显示什么？", "code": "from dataclasses import dataclass\n\n@dataclass\nclass Student:\n    name: str\n    age: int\n    city: str = \"Chiang Mai\"\n\nprint(Student(\"Ploy\", 28))", "o": ["<__main__.Student object at 0x10f3a2b50>", "Student(\"Ploy\", 28)", "出错：没有给 city", "Student(name='Ploy', age=28, city='Chiang Mai')"], "a": 3, "why": "`@dataclass` 会帮你写好 `__init__`、`__repr__` 和 `__eq__`。`__repr__` 显示每个字段，`city` 使用它的默认值。", "at": "python_data_classes.html#s2"},
  {"id": "python_data_classes_09", "unit": "python_data_classes", "d": 0.2, "type": "mcq", "q": "一个数据类有字段 `guests: list = []`。会发生什么？", "o": ["Python 用 `ValueError` 拒绝它", "每个对象都得到自己的新列表", "这个列表被锁住，不能改变", "这个字段悄悄变成 `None`"], "a": 0, "why": "一个列表会被所有对象共用，所以 Python 拒绝它。写 `field(default_factory=list)`，每次都得到一个新列表。", "at": "python_data_classes.html#s5"},
  {"id": "python_data_classes_10", "unit": "python_data_classes", "d": 0.0, "type": "mcq", "q": "一个数据类有字段 `age: int`。`Student(name=\"Ploy\", age=\"28\")` 会怎样？", "o": ["把 \"28\" 变成数字 28", "把文字 \"28\" 原样保存", "因 `TypeError` 停止", "把年龄保存为 `None`"], "a": 1, "why": "数据类什么都不检查：它的类型提示只是说明。文字 \"28\" 会作为文字保存。", "at": "python_data_classes.html#s3"},
  {"id": "send_email_01", "unit": "send_email", "d": -0.7, "type": "mcq", "q": "Gmail 和 Yahoo 让你为替你发邮件的程序创建一个应用专用密码。为什么用它，而不用你平常的密码？", "o": ["它是一个单独的密码，你可以只把它作废。", "它更短，所以在终端里输入起来更容易。", "它让程序不需要邮件服务器就能发邮件。", "它是唯一能在 465 端口上使用的密码。"], "a": 0, "why": "应用专用密码是一个单独的密码，只给程序用。你可以单独把它作废，而你平常的密码不会出现在程序里。", "at": "send_email.html#need"},
  {"id": "send_email_02", "unit": "send_email", "d": -0.5, "type": "mcq", "q": "文件 `shopping.txt` 里已经有四行。用 `\"w\"` 打开它会怎样？", "o": ["它把新文本加在这四行后面。", "它在写入之前先清空文件。", "它以只读方式打开文件。", "它给这个文件再做一个副本。"], "a": 1, "why": "`\"w\"` 会先清空文件。`\"a\"` 这个字母才是加到末尾，并保留原有内容。", "at": "send_email.html#open-mode"},
  {"id": "send_email_03", "unit": "send_email", "d": -0.3, "type": "fill", "q": "用 Python 的 `imaplib` 时，`box.search(None, \"ALL\")` 会找出文件夹里的每一封邮件。用哪个 IMAP 搜索词代替 `ALL`，就只得到未读邮件？请输入这个英文单词。", "answers": ["unseen", "\"unseen\"", "'unseen'"], "why": "`UNSEEN` 是表示未读邮件的 IMAP 搜索词。`SEEN` 得到的正好相反。", "at": "send_email.html#search"},
  {"id": "send_email_04", "unit": "send_email", "d": 0.0, "type": "mcq", "q": "这行 Python `imaplib` 代码用 `BODY.PEEK[...]` 取回一封邮件的三个邮件头。为什么 `PEEK` 很重要？", "code": "status, raw = box.fetch(message_id, \"(BODY.PEEK[HEADER.FIELDS (FROM DATE SUBJECT)])\")", "o": ["没有它，服务器只发送主题。", "没有它，取回时会下载每一个附件。", "没有它，读取就会把邮件标为已读。", "没有它，最新的邮件会排在最后返回。"], "a": 2, "why": "在 IMAP 服务器上，取回一封邮件就会把它标为已读。`PEEK` 取回邮件，并让它保持未读。", "at": "send_email.html#list-found"},
  {"id": "send_email_05", "unit": "send_email", "d": -0.2, "type": "mcq", "q": "这个文件保存为 `mailer.py`。当你单独运行 `python mailer.py` 时，会发生什么？", "code": "import smtplib\nfrom email.message import EmailMessage\n\nSMTP_HOST = \"smtp.gmail.com\"\n\ndef send_email(sender, password, to, subject, body, host=SMTP_HOST):\n    message = EmailMessage()\n    message[\"From\"] = sender\n    message[\"To\"] = to\n    message[\"Subject\"] = subject\n    message.set_content(body)\n    with smtplib.SMTP_SSL(host, 465) as server:\n        server.login(sender, password)\n        server.send_message(message)\n    return to\n\nif __name__ == \"__main__\":\n    print(\"mailer.py holds send_email().\")", "o": ["它询问一个地址，并发送一封测试邮件。", "它发送一封主题和正文都为空的邮件。", "它出错并停止，因为没有东西导入它。", "它打印一行文字，不发送任何邮件。"], "a": 3, "why": "运行这个文件会定义 `send_email()`，然后只运行 `if __name__ == \"__main__\":` 下面的那个 `print`。没有东西调用这个函数，所以不会发送邮件。", "at": "send_email.html#mailer-tool"},
  {"id": "send_email_06", "unit": "send_email", "d": 0.2, "type": "mcq", "q": "一个程序把一份带编号的搜索结果列表交给语言模型。它要求模型用好结果的编号来回答，而不是用它们的网址。为什么用编号？", "o": ["模型会把很长的地址抄错。", "模型根本读不了网址。", "编号让模型回答得快得多。", "提示词里不允许出现网址。"], "a": 0, "why": "模型常常把很长的网址抄错，但一个短短的编号它能应付。然后程序按编号查出对应的地址。", "at": "send_email.html#pick-links"},
  {"id": "send_email_07", "unit": "send_email", "d": 0.4, "type": "mcq", "q": "一个程序按名称向 NVIDIA 的模型 API（`/chat/completions`）请求一个模型，得到的 HTTP 状态码是 410。这是什么意思？", "o": ["你的密钥不对，或者密钥的种类不对。", "这个模型已经对所有人停用了。", "这个模型在列表里，但不对你的密钥开放。", "这个模型回答了，所以你可以用它。"], "a": 1, "why": "410 表示这个模型已经对所有人停用了。404 是另一个问题：在列表里，但不对你的密钥开放。", "at": "send_email.html#nvidia-models"},
  {"id": "send_email_08", "unit": "send_email", "d": 0.1, "type": "mcq", "q": "使用工具调用时（例如在 Ollama 的聊天 API 里），语言模型决定使用一个名叫 `check_pantry` 的工具。模型发回的是什么？", "o": ["一个文件的文本，由模型自己打开。", "一个新的 Python 函数，你的代码必须把它保存下来。", "函数的名称和它的参数。", "最终的答案，是一句完整的话。"], "a": 2, "why": "模型从不运行你的代码。它只发送一个名称和一些参数。由你的代码运行这个函数，再把结果发回去。", "at": "send_email.html#tools"},
  {"id": "send_email_09", "unit": "send_email", "d": 0.6, "type": "mcq", "q": "一个程序为别人发给你的邮件起草回复。为什么把这件事交给本地模型（例如在 Ollama 里），而不交给更快的云端模型 API？", "o": ["云端模型写不出一封简短、礼貌的邮件。", "本地模型在回复里从不编造事实。", "免费的云端密钥不能用于 IMAP 邮件。", "这样邮件就永远不会离开你自己的电脑。"], "a": 3, "why": "发给云端 API 的提示词里的所有内容，都会被运营它的公司看到。一封邮件属于写它的人，而本地模型让它留在你的电脑上。", "at": "send_email.html#reply-draft"},
  {"id": "send_email_10", "unit": "send_email", "d": 0.4, "type": "mcq", "q": "这段代码使用文字转语音库 Piper，把列表里的每一行都读出来。为什么 `PiperVoice.load(ENGLISH)` 这一行在循环外面？", "code": "voice = PiperVoice.load(ENGLISH)\nfor number, line in enumerate(LESSON, start=1):\n    with wave.open(\"lesson\" + str(number) + \".wav\", \"wb\") as wav:\n        voice.synthesize_wav(line, wav)", "o": ["加载很慢，所以只做一次。", "一个声音只能被加载一次。", "循环看不到在它里面创建的名字。", "Piper 每次加载都需要互联网。"], "a": 0, "why": "加载一个声音很慢，而说话很快。如果放在循环里面，每一行都要再付出一次加载的代价。", "at": "send_email.html#say-fast"},
  {"id": "python_six_libraries_01", "unit": "python_six_libraries", "d": -0.4, "type": "mcq", "q": "使用 Biopython 时，`Entrez.efetch` 从 NCBI 基因库下载一个基因。在发出请求之前，你必须给 NCBI 什么？", "o": ["每次请求一个付费的密钥", "一个账号名和一个密码", "一个电子邮件地址，不用密码", "一封你自己学校开的信"], "a": 2, "why": "NCBI 允许任何人索取它的数据。你把 `Entrez.email` 设为你的电子邮件地址，这就是全部的登录手续。", "at": "python_six_libraries.html"},
  {"id": "python_six_libraries_02", "unit": "python_six_libraries", "d": -0.4, "type": "mcq", "q": "下面哪个 Python 库完全不需要互联网，并且能生成一张手机可以读取的图片？", "o": ["`qrcode`", "`wbgapi`", "`skyfield`", "`osmnx`"], "a": 0, "why": "`qrcode` 不需要互联网。两行代码就生成一张 PNG 图片，手机扫描它就会打开链接。", "at": "python_six_libraries.html"},
  {"id": "python_six_libraries_03", "unit": "python_six_libraries", "d": 0.0, "type": "mcq", "q": "这段 Skyfield 代码打印出从地球上某个地点看到的空间站在天空中的高度，单位是度。高度大于 0 是什么意思？", "code": "from skyfield.api import load, wgs84\n\nurl  = (\"https://celestrak.org/NORAD/elements/\"\n        \"gp.php?GROUP=stations&FORMAT=tle\")\nsats = load.tle_file(url)\niss  = {s.name: s for s in sats}[\"ISS (ZARYA)\"]\nhere = wgs84.latlon(18.79, 98.98)\nalt, az, dist = (iss - here).at(load.timescale().now()).altaz()\nprint(\"height\", alt.degrees)", "o": ["空间站现在在地平线以下", "空间站正好在你的头顶上", "必须重新加载卫星列表", "空间站升起来了，在地平线以上"], "a": 3, "why": "`alt` 是高出地平线的角度。大于 0 度时，空间站此刻就在天上，所以你可以到外面去找它。", "at": "python_six_libraries.html"},
  {"id": "python_six_libraries_04", "unit": "python_six_libraries", "d": 0.2, "type": "mcq", "q": "OSMnx 和 folium 是两个处理地图的 Python 库。它们之间有什么区别？", "o": ["folium 下载街道；OSMnx 画地图", "OSMnx 下载街道；folium 画地图", "OSMnx 需要账号；folium 不需要", "OSMnx 不用互联网就能用；folium 需要 wifi"], "a": 1, "why": "OSMnx 把 OpenStreetMap 的街道下载到 Python 里，这样你就可以针对它们提问。folium 画地图。", "at": "python_six_libraries.html"},
  {"id": "python_six_libraries_05", "unit": "python_six_libraries", "d": 0.3, "type": "mcq", "q": "这段代码向世界银行索取什么？", "code": "import wbgapi as wb\n\n# SE.XPD.TOTL.GD.ZS = money spent on school, as a share of GDP\ntable = wb.data.DataFrame(\"SE.XPD.TOTL.GD.ZS\",\n                          [\"THA\", \"VNM\", \"MYS\"], mrnev=5)\nprint(table)", "o": ["三个国家的教育支出占 GDP 的比例", "泰国三个府的学校数量", "世界上最大的三个经济体的 GDP", "过去五年里学校课本的价格"], "a": 0, "why": "第一个参数是世界银行的一个代码，表示花在教育上的钱占 GDP 的比例。列表里写了三个国家：泰国、越南和马来西亚。", "at": "python_six_libraries.html"},
  {"id": "python_six_libraries_06", "unit": "python_six_libraries", "d": 0.6, "type": "mcq", "q": "这段 ObsPy 代码向一个数据中心索取某台地震仪记录下来的内容，但没有数据返回。你应该先检查什么？", "code": "from obspy import UTCDateTime\nfrom obspy.clients.fdsn import Client\n\nstart = UTCDateTime(\"2025-03-28T06:20:00\")\nstream = Client(\"IRIS\").get_waveforms(\n    \"IU\", \"CHTO\", \"00\", \"BHZ\",\n    start, start + 900)\nprint(stream)", "o": ["你的账号和你的密钥", "你的电脑的速度", "台站代码和时间", "绘图窗口的大小"], "a": 2, "why": "台站代码写错就拿不到数据，而且有些台站会连续几个月没有数据。所以先检查台站代码和时间。不需要账号或密钥。", "at": "python_six_libraries.html"},
  {"id": "mailbot_01", "unit": "mailbot", "d": -0.8, "type": "mcq", "q": "一个 Python 机器人每小时读取一次邮箱，并自己回复其中一些邮件。它必须防范的主要危险是什么？", "o": ["从服务器读取邮件太慢", "写出的回复太短，没法读", "一遍又一遍地回复它自己的回复", "每小时都用应用专用密码登录"], "a": 2, "why": "对机器人邮件的回复，看起来可能像一个新的请求。一个会回答自己的回答的机器人会造成邮件循环，把收件箱塞满。", "at": "mailbot.html"},
  {"id": "mailbot_02", "unit": "mailbot", "d": -0.5, "type": "mcq", "q": "用 Python 的 `imaplib` 时，一个程序用 `box.select(\"INBOX\", readonly=True)` 打开邮箱。`readonly=True` 是做什么用的？", "o": ["这样就不会有邮件被意外标为已读", "这样服务器会更快地发送邮件", "这样程序不需要密码就能登录", "这样只能搜索带有标记的邮件"], "a": 0, "why": "以只读方式打开的邮箱不能被修改。程序可以查看邮件，而且不会有任何邮件被意外标为已读。", "at": "mailbot.html#step1"},
  {"id": "mailbot_03", "unit": "mailbot", "d": -0.3, "type": "mcq", "q": "这个 Python 函数在一个 JSON 文件里为每一天保存一段文本。`load_saved()` 把这个文件作为字典返回。今天的日期已经是其中的一个键。这个函数会做什么？", "code": "def for_today():\n    day = time.strftime(\"%Y-%m-%d\")\n    saved = load_saved()\n    if day in saved:\n        return day, saved[day], \"from the file\"\n    written = ask_ollama(ASK.format(day=day))\n    saved[day] = written\n    SAVED.write_text(json.dumps(saved, ensure_ascii=False, indent=2),\n                     encoding=\"utf-8\")\n    return day, written, \"written just now\"", "o": ["它让模型写一段新的文本。", "它删除旧的文本，再写一段新的。", "它一直等到午夜，等一个新的键。", "它返回已保存的文本，不调用模型。"], "a": 3, "why": "这个文件是一个缓存。找到这个键时，函数立刻返回保存的值，根本不会运行到调用模型的那一行。", "at": "mailbot.html#step2"},
  {"id": "mailbot_04", "unit": "mailbot", "d": 0.1, "type": "mcq", "q": "一个邮件机器人在它发出的每封邮件上加一个像 `[my-bot]` 这样的标记，这样它以后就能认出自己的邮件。为什么主题行是放这个标记的好地方？", "o": ["邮件服务器会删掉正文里的标记。", "每一封回复都会把主题引用回来。", "主题对读者是隐藏的。", "垃圾邮件过滤器不读主题。"], "a": 1, "why": "任何对一封邮件作出回应的东西都会把它的主题引用回来：回复、转发，甚至是外出自动回复。所以这个标记会自己回来。", "at": "mailbot.html#step3"},
  {"id": "mailbot_05", "unit": "mailbot", "d": 0.3, "type": "mcq", "q": "一个邮件机器人每一轮都在 IMAP 邮箱里搜索 `UNSEEN` 的邮件。它找到一封自己发的邮件并跳过了它，但仍然把那封邮件标为已读。为什么？", "o": ["这样发件人就知道机器人已经读过它。", "这样这封邮件就会移到垃圾邮件文件夹。", "这样下一轮就不会再找到它。", "这样主题行里就会加上一个标记。"], "a": 2, "why": "搜索只要未读邮件。一封被跳过但仍然未读的邮件，每一轮都会再被找到。", "at": "mailbot.html#step4"},
  {"id": "mailbot_06", "unit": "mailbot", "d": 0.4, "type": "mcq", "q": "这个 Python 循环运行一个邮件机器人。`one_pass` 登录邮件服务器，读取邮箱并发送回复。`try` 和 `except Exception` 是做什么用的？", "code": "EVERY = 3600\n\nwhile True:\n    clock = time.strftime(\"%H:%M:%S\")\n    try:\n        sent = one_pass(user, password)\n        print(clock, \"done.\", sent, \"sent. Sleeping\", EVERY, \"seconds.\")\n    except Exception as problem:\n        print(clock, \"that pass failed:\", problem)\n        print(clock, \"trying again in\", EVERY, \"seconds.\")\n    time.sleep(EVERY)", "o": ["一轮失败不能让循环停下来。", "它让循环休眠一个小时。", "它阻止机器人回复自己。", "它把错误用邮件发给你。"], "a": 0, "why": "一个要运行好几个星期的程序总会遇到连接中断。`except` 把问题打印出来，然后循环休眠，再试一次。", "at": "mailbot.html#step5"},
  {"id": "mailbot_07", "unit": "mailbot", "d": 0.6, "type": "mcq", "q": "你让一个小的本地模型用 JSON 来回答：一个是或否，再加一个从 0 到 1 的 `confidence` 数字。每个“是”都带着 0.80 回来，每个“否”都带着 0.20。你的程序应该怎样使用这个回答？", "o": ["只有当这个数字大于 0.5 时才回复。", "问模型两次，然后取平均值。", "调高 temperature，得到真实的数字。", "只用是或否，忽略那个数字。"], "a": 3, "why": "模型并没有用那个数字在测量任何东西。只用是或否，不要在那个数字上设阈值。", "at": "mailbot.html#step6"},
  {"id": "mailbot_08", "unit": "mailbot", "d": 0.8, "type": "mcq", "q": "一个邮件机器人查看你收件箱里今天所有的未读邮件。它问一个模型，每封邮件是不是给机器人的请求。对其中一封邮件，模型说不是。为什么机器人应该让那封邮件保持未读？", "o": ["模型可能错了，所以机器人之后再问一次。", "它不是给机器人的邮件；标为已读会把它藏起来。", "服务器不允许给今天的邮件加标志。", "未读邮件在下一轮搜索起来更快。"], "a": 1, "why": "一个读取你整个收件箱的机器人，会碰到本来是给你的邮件。如果它把这样的邮件标为已读，你可能永远看不到它。", "at": "mailbot.html#step7"},
  {"id": "apis_01", "unit": "apis", "d": -0.8, "type": "mcq", "q": "API 是做什么的？", "o": ["它在服务器内部做繁重的工作", "它保护你的密钥的安全", "它在两个程序之间传递消息", "它把你的文件存在你自己的电脑上"], "a": 2, "why": "API 是一个信使。它把你的请求带给服务器，再把响应带回来。", "at": "apis.html"},
  {"id": "apis_02", "unit": "apis", "d": -0.4, "type": "fill", "q": "哪个状态码表示请求成功了？请输入这个数字。", "answers": ["200"], "why": "200 表示 OK：成功。401、404 和 500 都表示出了问题。", "at": "apis.html"},
  {"id": "apis_03", "unit": "apis", "d": -0.2, "type": "mcq", "q": "请求的哪一部分告诉 API“你是谁”？", "o": ["密钥", "请求体", "方法", "地址"], "a": 0, "why": "密钥是你的秘密口令，API 靠它知道真的是你。地址说的是去哪里，方法说的是做什么，请求体是你点的单。", "at": "apis.html"},
  {"id": "apis_04", "unit": "apis", "d": 0.1, "type": "mcq", "q": "API 回答的状态码是 401。哪里出了问题？", "o": ["地址不对", "服务器上有东西坏了", "请求体太长了", "你的密钥缺失或不对"], "a": 3, "why": "401 的意思是“你是谁？”：你的密钥缺失或不对。地址不对是 404，服务器坏了是 500。", "at": "apis.html"},
  {"id": "apis_05", "unit": "apis", "d": 0.4, "type": "mcq", "q": "为什么绝不能把真正的密钥写在公开网页的 JavaScript 里？", "o": ["JavaScript 根本不能发送 POST 请求", "每个访问的人都可能看到并偷走它", "密钥在浏览器里就不能用了", "API 只接受从 Python 发来的密钥"], "a": 1, "why": "在网页里，每个访问的人都可能偷看并偷走密钥。真正的网站会把密钥藏在服务器上。", "at": "apis.html"},
  {"id": "apis_06", "unit": "apis", "d": 0.6, "type": "mcq", "q": "这段 Python 代码使用 `requests` 库。`response` 是一个图片 API 的回答。为什么 `if not response.ok:` 这个检查放在保存文件之前？", "code": "if not response.ok:\n    print(f\"API rejected the request ({response.status_code}): {response.text}\")\n    raise SystemExit(1)\n\nwith open(\"robot.jpg\", \"wb\") as file:\n    file.write(response.content)", "o": ["这样错误就不会被写进 `robot.jpg`", "这样 API 密钥会被再检查一次", "这样图片会被再发送一次", "这样每次运行后 `robot.jpg` 都会被删除"], "a": 0, "why": "如果 API 拒绝了，响应里装的就是一条错误信息，而不是图片。这个检查会显示原因并停止，所以错误不会被保存成图片文件。", "at": "apis.html"},
  {"id": "html_intro_01", "unit": "html_intro", "d": -0.8, "type": "mcq", "q": "一个网页是用 HTML、CSS 和 JavaScript 做成的。HTML 的任务是什么？", "o": ["它装饰网页：颜色和字体。", "它让网页对你的点击作出反应。", "它描述网页的结构。", "它给网页一个互联网上的地址。"], "a": 2, "why": "HTML 用标签描述网页的结构。CSS 是装饰，JavaScript 让网页具有功能。", "at": "html_intro.html#overview"},
  {"id": "html_intro_02", "unit": "html_intro", "d": -0.4, "type": "mcq", "q": "你在文本编辑器里写了一个网页，把它保存为 `index.html`。为什么名字必须以 `.html` 结尾？", "o": ["它让文件保存时更小。", "它让记事本能再次打开这个文件。", "它把文件放到互联网上。", "它告诉电脑这是一个网页。"], "a": 3, "why": "`.html` 扩展名告诉你的电脑这个文件是一个网页，所以它会在浏览器里打开。", "at": "html_intro.html#create-file"},
  {"id": "html_intro_03", "unit": "html_intro", "d": 0.3, "type": "mcq", "q": "一个网页把一份目标列表放在 `<ol>` 里，把一份兴趣列表放在 `<ul>` 里。这两个标签有什么区别？", "o": ["`<ol>` 是有序列表；`<ul>` 是无序列表。", "`<ol>` 是网页链接的列表；`<ul>` 是纯文本的列表。", "`<ol>` 会显示在浏览器里；`<ul>` 保持隐藏。", "`<ol>` 只能放一项；`<ul>` 可以放很多项。"], "a": 0, "why": "`<ul>` 是无序列表（带圆点），`<ol>` 是有序列表。", "at": "html_intro.html#first-project"},
  {"id": "html_intro_04", "unit": "html_intro", "d": -0.3, "type": "mcq", "q": "你在文本编辑器里修改了 `index.html`。怎样在浏览器里看到这个改动？", "o": ["关闭浏览器，然后重新启动电脑。", "保存文件，然后在浏览器里按 F5。", "给文件改名，然后把它拖到编辑器里。", "把新的文字输入到浏览器的地址栏里。"], "a": 1, "why": "编辑之后，保存文件，然后在浏览器里按 F5（或 Ctrl+R）就能看到你的改动。", "at": "html_intro.html#view-browser"},
  {"id": "html_intro_05", "unit": "html_intro", "d": 0.1, "type": "mcq", "q": "你在浏览器里打开一个本地 HTML 文件。地址不是以 `http://` 或 `https://` 开头的。它以什么开头？", "o": ["`file://`", "`local://`", "`html://`", "`disk://`"], "a": 0, "why": "查看本地文件时，URL 以 `file://` 开头，例如 `file:///C:/Users/YourName/Desktop/index.html`。", "at": "html_intro.html#view-browser"},
  {"id": "html_intro_06", "unit": "html_intro", "d": 0.5, "type": "mcq", "q": "一个 HTML 文件在它的 `<head>` 里有 `<title>My First Web Page</title>`，在它的 `<body>` 里有 `<h1>Hello World!</h1>`。你会在哪里看到 `My First Web Page` 这几个词？", "o": ["作为浏览器里的大标题", "作为标题下面的一个段落", "在浏览器窗口的标签页上", "哪里都看不到，因为它是注释"], "a": 2, "why": "`<title>` 是显示在浏览器标签页上的标题。`<h1>` 的文字是你在网页正文里看到的标题。", "at": "html_intro.html#create-file"},
  {"id": "javascript_lab_01", "unit": "javascript_lab", "d": -0.7, "type": "mcq", "q": "在 JavaScript 中，`let` 这个词是做什么的？", "o": ["把一个新值放进一个旧盒子", "把一个值打印到控制台上", "问两个值是不是一样", "做一个有名字的新盒子"], "a": 3, "why": "变量就是一个有名字的盒子。`let` 做出一个新盒子，而且只在第一次时才写它。", "at": "javascript_lab.html#L-var"},
  {"id": "javascript_lab_02", "unit": "javascript_lab", "d": -0.4, "type": "mcq", "q": "`\"5\" + \"5\"` 的结果是什么？", "o": ["`10`", "`\"55\"`", "`\"10\"`", "`25`"], "a": 1, "why": "加了引号就成了文字，所以加号把两个字符串连在一起。它不会把数字相加。", "at": "javascript_lab.html#L-str"},
  {"id": "javascript_lab_03", "unit": "javascript_lab", "d": 0.0, "type": "mcq", "q": "`=` 和 `==` 有什么区别？", "o": ["`=` 把一个值放进盒子；`==` 问两个值是不是一样", "`=` 问两个值是不是一样；`==` 把一个值放进盒子", "`=` 用于数字；`==` 只用于字符串", "`=` 做一个新盒子；`==` 修改一个已有的盒子"], "a": 0, "why": "一个 `=` 把一个值放进盒子。两个 `==` 是在问“这两个一样吗？”。把它们弄混是初学者非常常见的 bug。", "at": "javascript_lab.html#L-if"},
  {"id": "javascript_lab_04", "unit": "javascript_lab", "d": 0.4, "type": "mcq", "q": "这几行运行之后，`places.length` 是多少？", "code": "let places = [\"Tha Phae Gate\", \"Warorot Market\"];\nplaces.push(\"Wat Don Chan\");\nplaces.pop();", "o": ["0", "1", "2", "3"], "a": 2, "why": "`push` 在末尾加上一项，这样就有三项。`pop` 把最后一项拿走，所以还剩两项。", "at": "javascript_lab.html#L-arr"},
  {"id": "javascript_lab_05", "unit": "javascript_lab", "d": 0.2, "type": "fill", "q": "`points(4)` 的结果是什么？请输入这个数字。", "code": "function points(hits) {\n  return hits * 50;\n}", "answers": ["200"], "why": "这个函数接收 `hits`，`return` 交回 `hits * 50`。4 乘以 50 是 200。", "at": "javascript_lab.html#L-fn"},
  {"id": "javascript_lab_06", "unit": "javascript_lab", "d": 0.7, "type": "mcq", "q": "这段 JavaScript 代码让一张图片在画布上移动。你删掉了 `ctx.clearRect(...)` 这一行。你会看到什么？", "code": "let x = 0;\nfunction frame() {\n  ctx.clearRect(0, 0, 520, 280);\n  x = x + 3;\n  ctx.drawImage(alien, x, 100);\n  requestAnimationFrame(frame);\n}\nframe();", "o": ["图片停在左边不动", "图片在身后留下一长条拖影", "图片移动的速度是原来的两倍", "图片只画一次，然后就停了"], "a": 1, "why": "动画就是擦掉、移动、绘制，再来一遍。`clearRect` 就是擦掉这一步。没有它，旧的图片会留下来，所以画布上会出现一长条拖影。", "at": "javascript_lab.html#L-anim"},
  {"id": "it__ai__ai_code_01", "unit": "it__ai__ai_code", "d": -0.8, "type": "mcq", "q": "使用 AI 代码生成时，你要给 AI 什么？", "o": ["你想让程序做的事情", "一个写好的、待编译的程序", "一份列出你代码里所有错误的清单", "一门新的编程语言"], "a": 0, "why": "你告诉 AI 你想让程序做什么，AI 就替你写代码。", "at": "it/ai/ai_code.html"},
  {"id": "it__ai__ai_code_02", "unit": "it__ai__ai_code", "d": -0.4, "type": "mcq", "q": "重构（refactor）代码是什么意思？", "o": ["找出并修正其中的错误", "把它变成一个能运行的程序", "把同一个动作重复很多很多次", "改进它的结构，而不改变它做的事"], "a": 3, "why": "重构（refactor）是指改进代码的结构，而不改变它做的事。找出并修正错误叫调试（debug）。", "at": "it/ai/ai_code.html"},
  {"id": "it__ai__ai_code_03", "unit": "it__ai__ai_code", "d": -0.1, "type": "mcq", "q": "哪个词指的是用一门编程语言写代码的规则，就像代码的文法？", "o": ["运行时（Runtime）", "语法（Syntax）", "代码片段（Snippet）", "算法（Algorithm）"], "a": 1, "why": "语法（Syntax）是用一门编程语言写代码的规则。如果语法错了，代码就无法编译。", "at": "it/ai/ai_code.html"},
  {"id": "it__ai__ai_code_04", "unit": "it__ai__ai_code", "d": 0.2, "type": "mcq", "q": "AI 刚刚按你的提示词写好了代码。接下来最好做什么？", "o": ["马上把它粘贴到你的项目里", "让 AI 写一个更长的版本", "读一遍，并弄懂每一行", "把它删掉，自己写代码"], "a": 2, "why": "不要只是复制代码。先读一遍，试着弄懂每一行是做什么的。", "at": "it/ai/ai_code.html"},
  {"id": "it__ai__ai_code_05", "unit": "it__ai__ai_code", "d": 0.4, "type": "mcq", "q": "你读了 AI 为你写的代码，但没有看懂。最好怎么做？", "o": ["让 AI 解释每一行", "让 AI 再写一个程序", "多运行几遍，直到弄清楚", "换成另一种语言"], "a": 0, "why": "如果你看不懂代码，就问 AI：“Please explain each line of this code in simple English.”", "at": "it/ai/ai_code.html"},
  {"id": "it__ai__ai_code_06", "unit": "it__ai__ai_code", "d": 0.4, "type": "mcq", "q": "你让 AI 写一个计算器程序。你的提示词里有这一行：“If the user enters zero for division, the program will show an error.” 为什么提示词里有这一行是好的？", "o": ["它让 AI 更快地写出代码", "它说明了在特殊情况下该怎么做", "它阻止 AI 使用任何循环", "它让 AI 解释每一行"], "a": 1, "why": "一条清楚的提示词会准确地告诉 AI 程序必须做什么，包括像除以零这样的特殊情况。这样 AI 就会为这种情况写代码。", "at": "it/ai/ai_code.html"},
  {"id": "prompt_lab_01", "unit": "prompt_lab", "d": -0.7, "type": "mcq", "q": "你给一个 AI 聊天模型写提示词，却没有给它角色。通常会发生什么？", "o": ["AI 拒绝回答这条提示词。", "AI 用错误的语言回答。", "AI 给出泛泛的回答。", "AI 给出长得多的回答。"], "a": 2, "why": "没有角色，AI 就给出泛泛的回答。`Act as a [role]` 会重塑整个回答。", "at": "prompt_lab.html#anatomy"},
  {"id": "prompt_lab_02", "unit": "prompt_lab", "d": -0.4, "type": "mcq", "q": "给 AI 模型的一条写得好的提示词最多可以有四个部分。哪个列表说出了它们？", "o": ["角色、背景、任务、限制条件", "问题、答案、示例、分数", "英语、泰语、中文、语法", "标题、引言、正文、结论"], "a": 0, "why": "一条提示词可以由角色、背景、任务以及长度、风格和格式这样的限制条件组成。", "at": "prompt_lab.html#builder"},
  {"id": "prompt_lab_03", "unit": "prompt_lab", "d": 0.0, "type": "mcq", "q": "给 AI 模型的提示词可以有角色、背景、任务和限制条件。一条好的提示词至少应该有什么？", "o": ["长度和格式", "背景和风格", "用三种语言写的任务", "角色和任务"], "a": 3, "why": "角色告诉 AI 要当谁，任务告诉它要做什么。一条好的提示词至少有这两样。", "at": "prompt_lab.html#builder"},
  {"id": "prompt_lab_04", "unit": "prompt_lab", "d": 0.4, "type": "mcq", "q": "在提示词里，你要求 AI 的回答符合某个长度、某种风格和某种格式。这些属于提示词的哪个部分？", "o": ["角色", "限制条件", "背景", "任务"], "a": 1, "why": "一条提示词有角色、背景、任务和限制条件。长度、风格和格式限制了回答的样子，所以它们是限制条件。", "at": "prompt_lab.html#builder"},
  {"id": "prompt_lab_05", "unit": "prompt_lab", "d": 0.6, "type": "mcq", "q": "AI 用英语和中文给出了同样的回答。为什么中文的那个在屏幕上看起来更短？", "o": ["AI 在中文里漏掉了限制条件。", "AI 对中文提示词理解得没那么好。", "AI 用中文回答时想法更少。", "一个汉字可以表达一整个词。"], "a": 3, "why": "泰语没有空格，又有简短的助词，所以更紧凑；中文还要更紧凑：一个汉字可以表达一整个英语单词。", "at": "prompt_lab.html#length"},
  {"id": "it__git_basics_01", "unit": "it__git_basics", "d": -0.8, "type": "mcq", "q": "Git 里的提交是什么？", "o": ["对你的文件的一份保存记录，外加一条说明", "为某个功能单独开出的一条开发线", "别人的仓库在你名下的一份副本", "把你的代码合并进某个项目的请求"], "a": 0, "why": "提交是 Git 的保存动作。它记录你的文件在那一刻的状态，并附上一条说明，写明你改了什么。", "at": "it/git_basics.html"},
  {"id": "it__git_basics_02", "unit": "it__git_basics", "d": -0.5, "type": "mcq", "q": "`git status` 是做什么的？", "o": ["显示完整的提交历史", "显示有哪些改动", "把提交发送到远程仓库", "保存文件的一份快照"], "a": 1, "why": "`git status` 检查你的文件有哪些改动。在暂存或提交之前，先用这条命令是个好习惯。", "at": "it/git_basics.html"},
  {"id": "it__git_basics_03", "unit": "it__git_basics", "d": -0.3, "type": "fill", "q": "`git ____` 会在当前文件夹里创建一个全新的仓库。请输入缺少的那个英文单词。", "answers": ["init", "git init"], "why": "`git init` 在当前文件夹里创建一个新仓库。`git clone` 从服务器上复制一个已有的仓库。", "at": "it/git_basics.html"},
  {"id": "it__git_basics_04", "unit": "it__git_basics", "d": 0.1, "type": "mcq", "q": "工作流程是暂存、提交、推送。`git add` 做什么？", "o": ["把本地的提交上传到远程仓库", "创建一个新分支并切换过去", "把文件的改动放进暂存区", "从 GitHub 下载最新的改动"], "a": 2, "why": "暂存区在你提交之前存放文件的改动。文件是用 `git add` 暂存的。", "at": "it/git_basics.html"},
  {"id": "it__git_basics_05", "unit": "it__git_basics", "d": 0.4, "type": "mcq", "q": "`git checkout -b feature/my-new-feature` 做什么？", "o": ["把这个分支合并回 `main`", "按名字列出所有本地分支", "只是切换到一个已有的分支", "创建这个分支并切换过去"], "a": 3, "why": "`git branch` 创建一个分支，`git checkout` 切换到一个分支。`git checkout -b` 一步完成这两件事。", "at": "it/git_basics.html"},
  {"id": "it__git_basics_06", "unit": "it__git_basics", "d": 0.7, "type": "mcq", "q": "你在 GitHub 上 fork 了一个仓库，并克隆了你的 fork。之后怎样拿到原仓库的更新？", "o": ["把它添加为 `upstream`，然后 `git pull upstream main`", "在你的克隆里运行 `git push -u origin main`", "在克隆下来的文件夹里再运行一次 `git init`", "向你自己的 fork 发起一个 pull request"], "a": 0, "why": "`git remote add upstream <address>` 给原仓库起名，`git pull upstream main` 下载它的更新。", "at": "it/git_basics.html"},
  {"id": "it__github_pages_lab_01", "unit": "it__github_pages_lab", "d": -0.8, "type": "mcq", "q": "Git 和 GitHub 有什么区别？", "o": ["Git 是一个网站；GitHub 是你电脑上的一个工具", "Git 是你电脑上的一个工具；GitHub 是一个网站", "Git 和 GitHub 是同一个网站的两个名字", "Git 是给团队用的；GitHub 是给单独工作的人用的"], "a": 1, "why": "Git 是在你电脑上跟踪改动的工具。GitHub 是在网上存放 Git 仓库的网站。", "at": "it/github_pages_lab.html#github"},
  {"id": "it__github_pages_lab_02", "unit": "it__github_pages_lab", "d": -0.4, "type": "mcq", "q": "`git add index.html` 做了什么？", "o": ["它把你的提交上传到 GitHub", "它把暂存的改动保存到历史记录里", "它把别人的提交拉取到你这里", "它把这处改动放进暂存区"], "a": 3, "why": "`git add` 把一处改动放进暂存区。只有暂存了的内容才会进入下一次提交。", "at": "it/github_pages_lab.html#git"},
  {"id": "it__github_pages_lab_03", "unit": "it__github_pages_lab", "d": -0.2, "type": "mcq", "q": "在 GitHub Pages 上，名字恰好是 `username.github.io`（用你自己的用户名）的仓库很特别。为什么这个名字很重要？", "o": ["GitHub 会把这个确切的名字变成你的主网站", "Git 无法克隆任何用了其他名字的仓库", "只有这个名字才能让你创建公开仓库", "这个名字会让仓库得到一个免费的 README 文件"], "a": 0, "why": "GitHub 会把名字恰好如此的仓库变成你的主网站，地址是 `https://username.github.io`。如果名字拼错了，Pages 就找不到它。", "at": "it/github_pages_lab.html#mission-a"},
  {"id": "it__github_pages_lab_04", "unit": "it__github_pages_lab", "d": 0.1, "type": "mcq", "q": "为什么要在每台电脑上运行一次 `git config --global user.name \"Your Name\"`？", "o": ["它让你登录 GitHub，好做第一次推送", "它选定你上线网站的地址", "每次提交都会署上这个名字和邮箱", "它把你的仓库设为公开，供 GitHub Pages 使用"], "a": 2, "why": "每次提交都会署上这个名字和邮箱。历史记录就是这样说明谁改了什么。", "at": "it/github_pages_lab.html#mission-b"},
  {"id": "it__github_pages_lab_05", "unit": "it__github_pages_lab", "d": 0.5, "type": "mcq", "q": "你修改了 `index.html`，运行了 `git add` 和 `git commit`，但是忘了 `git push`。你上线的网站显示什么？", "o": ["新页面，不过要等一两分钟", "旧页面：GitHub 上没有这次提交", "一个错误页面，直到你推送这次提交", "新页面，但只在你自己的浏览器里"], "a": 1, "why": "`git commit` 把改动保存在你电脑上的仓库里。只有 `git push` 才把你的提交送上 GitHub，而网站就在那里。", "at": "it/github_pages_lab.html#mission-b"},
  {"id": "it__github_pages_lab_06", "unit": "it__github_pages_lab", "d": 0.3, "type": "mcq", "q": "在 Git 里，哪一条命令能新建一个名为 `add-about` 的分支并切换到它上面？", "o": ["`git branch add-about`", "`git checkout add-about`", "`git switch -c add-about`", "`git switch add-about`"], "a": 2, "why": "`git switch -c add-about` 新建一个分支并切换到它上面。之后 `git branch` 会在 `add-about` 旁边显示一个星号。旧一些的 `git checkout -b add-about` 效果相同。", "at": "it/github_pages_lab.html#mission-c"},
  {"id": "it__github_pages_lab_07", "unit": "it__github_pages_lab", "d": 0.7, "type": "mcq", "q": "你在 GitHub 上合并了一个 pull request，从你的分支合并到 `main`。然后你在自己的电脑上运行 `git switch main` 和 `git pull`。为什么？", "o": ["为了删除 GitHub 上已合并的分支", "为了再开一个 pull request 供人审查", "为了把你的分支再上传到 GitHub 一次", "为了把这次合并拉取到你自己的 `main`"], "a": 3, "why": "合并发生在 GitHub 上。`git pull` 把那些提交拉取下来，这样你电脑上的 `main` 就与之一致了。", "at": "it/github_pages_lab.html#mission-c"},
  {"id": "javascript_space_game_01", "unit": "javascript_space_game", "d": -0.7, "type": "mcq", "q": "在 HTML 文件里，人们为什么把 `script` 放在 `body` 的底部？", "o": ["这样在代码需要 HTML 元素之前，它们就已经存在", "这样浏览器可以先把每一张图片都加载完", "这样代码运行时控制台就已经打开了", "这样标题会画在其他所有内容的上方"], "a": 0, "why": "浏览器从上到下读取文件。把脚本放在底部，代码去查找元素时（例如用 `document.getElementById`），这些元素就已经存在了。", "at": "javascript_space_game.html"},
  {"id": "javascript_space_game_02", "unit": "javascript_space_game", "d": -0.4, "type": "mcq", "q": "这个 JavaScript 函数在 canvas 上绘图。`pics[name]` 是一个还没有加载完的 `Image`。这个函数会做什么？", "code": "function drawPic(name, x, y, size, colour) {\n  const img = pics[name];\n  if (img && img.complete && img.naturalWidth > 0) {\n    ctx.drawImage(img, x, y, size, size);\n  } else {\n    ctx.fillStyle = colour;\n    ctx.fillRect(x, y, size, size);\n  }\n}", "o": ["它一直等到图片到达为止", "它在这一帧里什么都不画", "它改为画一个带颜色的方块", "它在控制台里打印一条错误"], "a": 2, "why": "图片需要时间才能到达。在 `img.complete` 变为 true 之前，运行的是 `else` 部分，它用传入的颜色填充一个方块。", "at": "javascript_space_game.html"},
  {"id": "javascript_space_game_03", "unit": "javascript_space_game", "d": 0.0, "type": "mcq", "q": "一个 JavaScript canvas 游戏有两个函数：`update` 修改数值，`draw` 把它们显示出来。把两者分开，能让这个循环做到什么？", "code": "function loop() {\n  if (!over) update();\n  draw();\n  requestAnimationFrame(loop);\n}", "o": ["不擦除 canvas 就画出每一帧", "让浏览器把整个游戏运行得更快", "同时使用不止一个 canvas", "不调用 `update` 就能暂停游戏"], "a": 3, "why": "`update` 修改数值，`draw` 把它们显示出来。当 `over` 为 true 时，循环不再调用 `update`，但继续绘制，所以画面保留着，什么都不动。", "at": "javascript_space_game.html"},
  {"id": "javascript_space_game_04", "unit": "javascript_space_game", "d": 0.1, "type": "mcq", "q": "在这个 JavaScript 游戏里，子弹沿着 canvas 向上飞。如果删掉最后一行，会出什么问题？", "code": "let bullets = [];\n\nfunction shoot() {\n  bullets.push({ x: ship.x + 18, y: ship.y, w: 4, h: 12 });\n}\n\n// inside update(), which runs on every frame:\nbullets.forEach(b => { b.y -= 8; });\nbullets = bullets.filter(b => b.y > -20);", "o": ["子弹到了屏幕顶部就不再移动", "旧子弹留着不走，游戏越来越慢", "一次只能发射一颗子弹", "空格键会让窗口向下滚动"], "a": 1, "why": "`filter` 生成一个新数组，里面只有仍在屏幕附近的子弹。没有它，旧子弹会永远向上飞，数组不断变大，游戏慢慢变得越来越慢。", "at": "javascript_space_game.html"},
  {"id": "javascript_space_game_05", "unit": "javascript_space_game", "d": 0.3, "type": "mcq", "q": "这个 `draw` 函数在 canvas 游戏的每一帧都会运行，飞船在移动。你把它的前两行去掉。你会看到什么？", "code": "function draw() {\n  ctx.fillStyle = \"#05070f\";\n  ctx.fillRect(0, 0, canvas.width, canvas.height);\n  drawPic(\"player\", ship.x, ship.y, ship.w, \"#ffd83d\");\n}", "o": ["飞船立刻停止移动", "canvas 永远是空的", "飞船在身后留下拖影", "飞船被画成黑色"], "a": 2, "why": "这两行在每一帧擦除 canvas。不擦除的话，以前画的每一样东西都留着，所以凡是移动的东西都会留下拖影。", "at": "javascript_space_game.html#sec-play"},
  {"id": "javascript_space_game_06", "unit": "javascript_space_game", "d": 0.5, "type": "mcq", "q": "在这段 JavaScript 游戏代码里，一排排外星人横着走。代码设置了一个标志 `atEdge`，而不是让每个外星人碰到墙时各自转向。为什么？", "code": "let atEdge = false;\naliens.forEach(a => {\n  a.x += dir * 0.9;\n  if (a.x < 6 || a.x > canvas.width - a.w - 6) atEdge = true;\n});\nif (atEdge) {\n  dir = -dir;\n  aliens.forEach(a => { a.y += 18; });\n}", "o": ["这样只有第一排会向下走一步", "这样整支军队会一起转向", "这样外星人在墙边移动得更快", "这样死掉的外星人会离开列表"], "a": 1, "why": "如果每个外星人各自转向，队伍就会散开。代码把它们全部检查一遍，设置一个标志，然后让整支军队一起转向。", "at": "javascript_space_game.html"},
  {"id": "javascript_space_game_07", "unit": "javascript_space_game", "d": 0.7, "type": "mcq", "q": "这段 JavaScript 在游戏的每一帧都会运行。为什么 `if` 在加分之前要先检查 `!a.dead`？", "code": "bullets.forEach(b => {\n  aliens.forEach(a => {\n    if (!a.dead && hit(b, a)) { a.dead = true; b.dead = true; score += 50; }\n  });\n});\nbullets = bullets.filter(b => !b.dead);\naliens  = aliens.filter(a => !a.dead);", "o": ["这样死掉的外星人会被画成灰色", "这样循环不会跳过下一颗子弹", "这样下一波会立刻开始", "这样两颗子弹不能在同一个外星人身上得分"], "a": 3, "why": "两颗很快的子弹可能在同一帧都位于同一个外星人体内。外星人只是被标记为死亡，在 `filter` 运行之前仍留在数组里，所以这个检查能防止它再次被击中。", "at": "javascript_space_game.html"},
  {"id": "github_01", "unit": "github", "d": -0.8, "type": "mcq", "q": "什么是 Git？", "o": ["一个在线存放你的项目的网站", "一种用 `#` 和 `-` 给文本排版的简单方法", "一个跟踪代码改动的版本控制系统", "一项替你托管网站的免费服务"], "a": 2, "why": "Git 是一个版本控制系统：它跟踪你代码里的改动，并让你回到以前的版本。GitHub 才是那个网站。", "at": "github.html#overview"},
  {"id": "github_02", "unit": "github", "d": -0.5, "type": "mcq", "q": "“push” 是什么意思？", "o": ["把你的改动发送到 GitHub", "从 GitHub 获取改动", "把两个分支合并成一个", "把一个仓库复制到你的电脑"], "a": 0, "why": "push 把你的改动发送到 GitHub。pull 把改动从 GitHub 获取到你的电脑。", "at": "github.html#vocabulary"},
  {"id": "github_03", "unit": "github", "d": 0.2, "type": "mcq", "q": "哪个词表示把别人的仓库复制到你自己的 GitHub 账号？", "o": ["Clone", "Merge", "Pull", "Fork"], "a": 3, "why": "fork 把别人的仓库复制到你的 GitHub 账号。clone 把一个仓库从 GitHub 复制到你的电脑。", "at": "github.html#vocabulary"},
  {"id": "github_04", "unit": "github", "d": 0.1, "type": "mcq", "q": "下面哪一个完全照原样写，就可以作为 GitHub 仓库的名字？", "o": ["`my first repo`", "`hello_world2`", "`My Website`", "`my repo!`"], "a": 1, "why": "仓库名可以包含字母、数字、连字符和下划线。它不能包含空格，也不能包含 `!` 这样的符号。", "at": "github.html#create-repo"},
  {"id": "github_05", "unit": "github", "d": 0.4, "type": "mcq", "q": "为什么 `update` 是一条不好的提交信息？", "o": ["它太短，GitHub 不会接受", "它没有说明你改了什么、为什么改", "它没有以大写字母开头", "它没有像 v1.0 这样的版本号"], "a": 1, "why": "提交信息应该清楚、具体，并说明你改了什么、为什么改。“Fix typo in introduction” 就是一条好的提交信息。", "at": "github.html#edit-files"},
  {"id": "github_06", "unit": "github", "d": -0.3, "type": "mcq", "q": "在 Markdown 里，一行开头的哪个字符会生成标题？", "o": ["`*`", "`#`", "`>`", "`-`"], "a": 1, "why": "在 Markdown 里，`#` 生成标题，`-` 生成列表项，`**` 生成粗体文字。", "at": "github.html#edit-files"},
  {"id": "github_07", "unit": "github", "d": 0.6, "type": "mcq", "q": "你的仓库名为 `my-first-repo`。里面有 `index.html`，并且你打开了 GitHub Pages。你的网站在哪里？", "o": ["`https://yourusername.github.io/my-first-repo/`", "`https://github.com/yourusername/index.html`", "`https://my-first-repo.github.com/yourusername/`", "`https://yourusername.github.com/pages/main/`"], "a": 0, "why": "GitHub Pages 网站的地址是 `https://yourusername.github.io/repository-name/`。", "at": "github.html#github-pages"},
  {"id": "free_local_ai_01", "unit": "free_local_ai", "d": -0.7, "type": "mcq", "q": "你用 Ollama 和 Open WebUI 在自己的电脑上运行 AI 模型。人们说这是免费的。哪两样东西并不免费？", "o": ["电费和你的时间", "Ollama 和 Open WebUI 软件", "Ollama 和电费", "Open WebUI 和你的时间"], "a": 0, "why": "软件（Ollama、Open WebUI）是免费的。电费，以及你用来安装和维护它的时间，并不免费。", "at": "free_local_ai.html"},
  {"id": "free_local_ai_02", "unit": "free_local_ai", "d": -0.4, "type": "fill", "q": "你在运行 `ollama run qwen2.5:7b` 之后和模型聊天。要退出，你该输入什么？", "answers": ["/bye"], "why": "在用 `ollama run` 开始的聊天里，输入 `/bye` 就可以结束聊天，回到终端。", "at": "free_local_ai.html#fit"},
  {"id": "free_local_ai_03", "unit": "free_local_ai", "d": 0.0, "type": "mcq", "q": "Open WebUI 给你什么？", "o": ["一个适合小笔记本电脑的更快的模型", "一种不用 Docker 运行 Ollama 的方法", "一个可以分享的 ChatGPT 风格的聊天页面", "一个给学校用的免费 Gemini 账号"], "a": 2, "why": "Open WebUI 给你一个 ChatGPT 风格的聊天页面。同一个 Wi-Fi 上的其他人也能用它，而且它需要先装好 Docker。", "at": "free_local_ai.html#fit"},
  {"id": "free_local_ai_04", "unit": "free_local_ai", "d": 0.4, "type": "mcq", "q": "一个模型放不进显卡自己的内存，溢出到了普通 RAM 里。会发生什么？", "o": ["Ollama 拒绝加载这个模型。", "模型会变慢很多。", "回答会变得不那么正确。", "模型会被重新下载。"], "a": 1, "why": "在显卡上，模型必须能放进显卡自己的内存。如果它溢出到普通 RAM 里，就会变慢很多。", "at": "free_local_ai.html#fit"},
  {"id": "free_local_ai_05", "unit": "free_local_ai", "d": 0.7, "type": "mcq", "q": "很多人同时使用一台机器。Ollama 和 vLLM 有什么不同？", "o": ["Ollama 是免费的，而 vLLM 按每个用户收费。", "Ollama 共用一个模型；vLLM 给每个人加载一个。", "Ollama 同时服务所有人；vLLM 一次服务一个人。", "Ollama 一次服务一个人；vLLM 同时服务所有人。"], "a": 3, "why": "Ollama 一次回答一个人。vLLM 只读取一次模型，同时回答所有人，但它并不是没有上限：当非常多的人同时提问时，它会变慢。", "at": "free_local_ai.html#fit"},
  {"id": "it__ai__ai_ollama_01", "unit": "it__ai__ai_ollama", "d": -0.7, "type": "mcq", "q": "Ollama 是做什么的？", "o": ["它在云端训练新的 AI 模型", "它在你自己的电脑上运行 AI 模型", "它按 token 出售模型的使用权", "它把网页应用变成 Docker 容器"], "a": 1, "why": "Ollama 让你完全在自己的电脑上运行大型语言模型。它不训练模型；它让模型变得容易运行。", "at": "it/ai/ai_ollama.html"},
  {"id": "it__ai__ai_ollama_02", "unit": "it__ai__ai_ollama", "d": -0.3, "type": "fill", "q": "Ollama 的 REST API 监听哪个端口？请输入这个数字。", "answers": ["11434"], "why": "Ollama 在端口 11434 上有一个 REST API，任何应用都可以调用，例如 `http://localhost:11434/api/chat`。", "at": "it/ai/ai_ollama.html"},
  {"id": "it__ai__ai_ollama_03", "unit": "it__ai__ai_ollama", "d": 0.2, "type": "mcq", "q": "Ollama 在第一次请求时把模型加载进 RAM。之后模型会怎样？", "o": ["它留在 RAM 里，保持就绪，等后面的请求", "它会从磁盘上删除，好省出空间", "它会被发送到云端，以便接受检查", "每来一个新请求，它都会重新下载一次"], "a": 0, "why": "模型只在第一次时加载进 RAM。之后它一直保持就绪。", "at": "it/ai/ai_ollama.html"},
  {"id": "it__ai__ai_ollama_04", "unit": "it__ai__ai_ollama", "d": 0.5, "type": "mcq", "q": "在 Ollama 里，估算一个模型需要多少 RAM 的经验法则是什么？", "o": ["大约是模型文件大小的一半", "不管模型多大，总是 48 GB", "大约是模型文件的大小，再加约 30%", "只和提示词的长度一样多"], "a": 2, "why": "你需要的 RAM 大致和模型文件一样大，再加大约 30% 的余量。", "at": "it/ai/ai_ollama.html"},
  {"id": "it__ai__ai_ollama_05", "unit": "it__ai__ai_ollama", "d": 0.6, "type": "mcq", "q": "Ollama 以流式方式发送回复。这对用户意味着什么？", "o": ["整个回答要等全部完成后才会到达", "提示词会同时发送给好几个模型", "模型文件是从磁盘上一块一块加载的", "每个词元一生成出来就马上发回来"], "a": 3, "why": "Ollama 一次生成一个词元，每生成一个就以流式方式发回来，所以用户很快就能看到开头的几个词。", "at": "it/ai/ai_ollama.html"},
  {"id": "make_an_ai_tutor_01", "unit": "make_an_ai_tutor", "d": -0.7, "type": "mcq", "q": "Ollama 的工作是什么？", "o": ["它在你自己的机器上运行 AI", "它把你的话发送给一家大公司", "它是那个负责聊天的 Python 库", "它修复 Windows 上的泰文字母"], "a": 0, "why": "Ollama 在你自己的机器上运行 AI 模型。之后，LlamaIndex 这样的 Python 库就可以和 Ollama 对话。", "at": "make_an_ai_tutor.html#step0"},
  {"id": "make_an_ai_tutor_02", "unit": "make_an_ai_tutor", "d": -0.4, "type": "mcq", "q": "在 Modelfile 中，`SYSTEM` 这一块说的是什么？", "o": ["从哪个大脑开始", "选词有多大胆", "模型必须做什么工作", "哪台电脑运行模型"], "a": 2, "why": "`FROM` 说的是从哪个大脑开始。`SYSTEM` 说的是要做什么工作。", "at": "make_an_ai_tutor.html#step4"},
  {"id": "make_an_ai_tutor_03", "unit": "make_an_ai_tutor", "d": -0.2, "type": "fill", "q": "在 Ollama 的 Modelfile 里，`PARAMETER temperature` 取哪个值会让模型总是选最有可能的词，使同一个问题每次都得到相同的回答？请输入这个数字。", "answers": ["0", "0.0", "zero"], "why": "temperature 为 0 时，模型总是选最有可能的词。于是同一个问题每次都得到相同的回答。", "at": "make_an_ai_tutor.html#step6"},
  {"id": "make_an_ai_tutor_04", "unit": "make_an_ai_tutor", "d": 0.1, "type": "mcq", "q": "你修改了 Ollama 的 Modelfile，然后再次运行程序，但模型的表现和之前一样。你忘了什么？", "o": ["用 `ollama pull` 重新下载", "用 `ollama create` 重新构建", "用 `ollama serve` 重新启动", "用 `ollama list` 重新检查"], "a": 1, "why": "Ollama 只在构建模型时读取 Modelfile。每次修改之后，你都必须用 `ollama create <name> -f Modelfile` 重新构建。", "at": "make_an_ai_tutor.html#step5"},
  {"id": "make_an_ai_tutor_05", "unit": "make_an_ai_tutor", "d": 0.3, "type": "mcq", "q": "一个 Python 聊天程序和在 Ollama 里运行的模型对话。程序怎样才能让模型记住你之前说过的话？", "o": ["它把每个回答保存在 Modelfile 里", "它设置 `num_ctx`，让 Ollama 保存对话", "它让 Ollama 打开自己的记忆功能", "它每次都把整段对话重新发送一遍"], "a": 3, "why": "模型没有记忆。程序保存着一个包含每条消息的列表，每次都把整段对话重新发送一遍。", "at": "make_an_ai_tutor.html#step8"},
  {"id": "make_an_ai_tutor_06", "unit": "make_an_ai_tutor", "d": 0.6, "type": "mcq", "q": "一个模型的 `SYSTEM` 块已经三次告诉它不要写 Markdown 星号，它却还是不停地写，`**like this**`。哪种修复方法每次都有效？", "o": ["在你自己的代码里把星号去掉", "在 `SYSTEM` 里把这条规则写第四遍", "换一个更大、会遵守规则的模型", "把 temperature 设为 0，让星号消失"], "a": 0, "why": "像 `.replace(\"*\", \"\")` 这样的一行代码，每一次都能去掉星号。一条能用代码强制执行的规则，胜过一条只能指望模型遵守的规则。", "at": "make_an_ai_tutor.html#step5"},
  {"id": "make_an_ai_tutor_07", "unit": "make_an_ai_tutor", "d": 0.7, "type": "mcq", "q": "你向四个本地 AI 模型问同一个简单的问题，而你知道答案。其中三个答错了，而且各有各的错法。最好的结论是什么？", "o": ["Modelfile 里有什么地方坏了", "小模型回答简单问题是可靠的", "模型在猜，所以要先测试你的模型", "这个问题对任何模型来说都太难"], "a": 2, "why": "当所有模型都以同样的方式出错时，是有什么地方坏了。当它们以不同的方式出错时，它们是在猜，所以在信任一个模型之前要先测试它。", "at": "make_an_ai_tutor.html#step7"},
  {"id": "tkinter_chat_01", "unit": "tkinter_chat", "d": -0.8, "type": "mcq", "q": "在 tkinter 程序里，`root.mainloop()` 做了什么？", "o": ["它把窗口里的文字保存到一个文件里。", "它绘制窗口，并等待鼠标点击。", "它为耗时的工作启动第二个线程。", "它在工作完成后把窗口关闭。"], "a": 1, "why": "`mainloop` 绘制窗口，并等待按键和点击，一遍又一遍，直到窗口关闭。", "at": "tkinter_chat.html#step1"},
  {"id": "tkinter_chat_02", "unit": "tkinter_chat", "d": -0.3, "type": "mcq", "q": "在 tkinter 里，你用 `tk.Button(row, text=\"Send\", command=self.send())` 创建一个按钮，`send` 后面带了括号。会发生什么？", "o": ["这个按钮之后会把每条消息发送两次。", "点击时 Python 会报错并停止。", "窗口会打开，但里面不显示按钮。", "`send` 现在就运行一次；点击没有任何反应。"], "a": 3, "why": "带了括号，Python 现在就调用 `send` 一次，并把它的返回值交给按钮。`command` 需要的是函数本身，不带括号。", "at": "tkinter_chat.html#step1"},
  {"id": "tkinter_chat_03", "unit": "tkinter_chat", "d": -0.1, "type": "mcq", "q": "一个小的 tkinter 窗口，上面是一个 `ScrolledText`，下面是一个单行输入框。创建 `ScrolledText` 时没有给 `height` 选项。可能出什么问题？", "o": ["输入框会掉到窗口底部之外。", "`ScrolledText` 再也无法滚动。", "按下 Enter 时窗口会关闭。", "长的行会在单词中间被截断。"], "a": 0, "why": "`ScrolledText` 默认要求 24 行。这可能比窗口还高，于是输入框被挤到底部之外。把 `height` 设小一些就能解决。", "at": "tkinter_chat.html#step1"},
  {"id": "tkinter_chat_04", "unit": "tkinter_chat", "d": 0.2, "type": "mcq", "q": "一个 tkinter 程序在工作线程里请求一个很慢的 web API。这个线程把回答的每一段放进一个 `queue.Queue`，主线程再把这些段取出来。为什么要这样设计？", "o": ["队列能让 API 回答得更快。", "队列能把这些词保留到第二天。", "工作线程不可以碰窗口。", "队列能把这些词排成正确的顺序。"], "a": 2, "why": "在 tkinter 里，不是创建窗口的那个线程不可以碰窗口。所以工作线程只往队列里放东西，由主线程把队列里的内容取出来放进窗口。", "at": "tkinter_chat.html#step2"},
  {"id": "tkinter_chat_05", "unit": "tkinter_chat", "d": 0.5, "type": "mcq", "q": "在这个 tkinter 方法里，`self.inbox` 是一个队列，由工作线程往里面放词。如果漏掉最后一行，会发生什么？", "code": "def drain(self):\n    try:\n        while True:\n            kind, text = self.inbox.get_nowait()\n            if kind == \"word\":\n                self.write(text)\n    except queue.Empty:\n        pass\n    self.root.after(50, self.drain)", "o": ["工作线程运行时窗口会卡住。", "队列恰好只被清空一次。", "程序因线程错误而停止。", "回答作为一整块文字到达。"], "a": 1, "why": "`after` 预约的是一次调用，而不是一个重复的定时器。没有这一行，`drain` 就再也不会运行，所以队列恰好只被清空一次。", "at": "tkinter_chat.html#step2"},
  {"id": "tkinter_chat_06", "unit": "tkinter_chat", "d": -0.5, "type": "mcq", "q": "一个聊天程序通过 Ollama 的 `/api/chat` 和模型对话。模型似乎记得你之前说过的话。这份记忆保存在哪里？", "o": ["在你程序里的一个列表中，每次都发送", "在 Ollama 加载到 RAM 里的模型内部", "在聊天窗口的文本框里", "在 Ollama 服务器上，每个聊天一个文件"], "a": 0, "why": "Ollama 在两次提问之间什么都不保留。记忆就是你程序里的一个消息列表，你在每次请求时都把它发送过去。", "at": "tkinter_chat.html#step3"},
  {"id": "tkinter_chat_07", "unit": "tkinter_chat", "d": 0.6, "type": "fill", "q": "一个 tkinter 程序用 `self.next_drain = self.root.after(50, self.drain)` 预约了一次调用。哪个 Tk 方法能在窗口被销毁之前取消这次预约的调用？请输入它的名字。", "answers": ["after_cancel", "after_cancel()", "root.after_cancel", "root.after_cancel()", "self.root.after_cancel", "self.root.after_cancel()"], "why": "如果窗口消失时还有预约着的调用，Tk 就会去调用一个已经不存在的命令。`after_cancel` 接收 `after` 返回的值，并取消这次调用。", "at": "tkinter_chat.html#step5"},
  {"id": "tkinter_chat_08", "unit": "tkinter_chat", "d": 0.8, "type": "mcq", "q": "一个聊天程序把对话保存在一个列表里，每次提问都把这个列表发送给模型。有一次请求失败了。为什么程序应该把那个问题从列表里取出来？", "o": ["这样历史文件在磁盘上保持很小。", "这样用户必须把问题再输入一遍。", "这样状态栏显示的是正确的模型。", "这样下一个回答不会去回复它。"], "a": 3, "why": "失败的问题不能留下任何痕迹。如果它留在列表里，模型下次会看到它，并去回复一个用户以为从没问过的问题。", "at": "tkinter_chat.html#step5"},
  {"id": "it__ai__ai_windows_stack_01", "unit": "it__ai__ai_windows_stack", "d": -0.7, "type": "mcq", "q": "Ollama 是什么？", "o": ["一个在你的 PC 上运行 AI 模型的免费程序", "一个为你自己的笔记建立索引的 Python 库", "一个写着模型岗位说明的文本文件", "一个经过训练、会挑选并调用工具的模型"], "a": 0, "why": "Ollama 是引擎：一个下载 AI 模型并在你自己的 PC 上运行它们的免费程序。", "at": "it/ai/ai_windows_stack.html"},
  {"id": "it__ai__ai_windows_stack_02", "unit": "it__ai__ai_windows_stack", "d": -0.5, "type": "mcq", "q": "一个名为 `Modelfile` 的文件里写着给 Ollama 的 `SYSTEM` 提示词。`ollama create my-tutor -f Modelfile` 做了什么？", "o": ["它从云端下载基础模型", "它为你的文件建立搜索索引", "它用这段提示词生成一个自定义模型", "它和基础模型开始一个聊天循环"], "a": 2, "why": "Modelfile 里写着系统提示词。`ollama create` 把它固化到一个自定义模型里，这里命名为 `my-tutor`，这样你就再也不用输入这段提示词了。", "at": "it/ai/ai_windows_stack.html#quickstart"},
  {"id": "it__ai__ai_windows_stack_03", "unit": "it__ai__ai_windows_stack", "d": -0.3, "type": "mcq", "q": "你想让一个本地模型根据你的课堂笔记来回答，而笔记经常改动。哪种技术最合适？", "o": ["在 Modelfile 里写更长的 `SYSTEM` 块", "用 LlamaIndex 对笔记做 RAG", "一个拥有更多工具的智能体模型", "用新的模型权重进行微调"], "a": 1, "why": "模型知道什么，是 RAG 该做的事。LlamaIndex 在提问时去查找笔记，所以你只需要编辑一个文件，然后再运行一次。", "at": "it/ai/ai_windows_stack.html"},
  {"id": "it__ai__ai_windows_stack_04", "unit": "it__ai__ai_windows_stack", "d": 0.1, "type": "mcq", "q": "`.venv` 文件夹是做什么用的？", "o": ["它存放 Ollama 下载的模型", "它保存索引，让下一次运行更快", "它让 PowerShell 能运行平时被阻止的脚本", "它把 AI 软件包和电脑上的其他部分隔开"], "a": 3, "why": "虚拟环境把 AI 软件包单独隔开。如果出了问题，你可以把这个文件夹直接扔掉。", "at": "it/ai/ai_windows_stack.html"},
  {"id": "it__ai__ai_windows_stack_05", "unit": "it__ai__ai_windows_stack", "d": 0.4, "type": "mcq", "q": "一个 LlamaIndex RAG 脚本使用 Ollama 里原版的 `llama3.2`。它把一些博客文章的索引保存在一个文件夹里。你删除了那个文件夹。现在哪句话是对的？", "o": ["模型就是原版的 `llama3.2`，不知道任何一篇文章", "模型仍然知道它们；它已经被微调过了", "Ollama 必须把聊天模型再下载一次", "嵌入模型也从磁盘上被删除了"], "a": 0, "why": "这是 RAG，不是微调。索引是图书馆，模型只是图书管理员，所以没有了索引，它就是原版的 `llama3.2`。", "at": "it/ai/ai_windows_stack.html"},
  {"id": "it__ai__ai_windows_stack_06", "unit": "it__ai__ai_windows_stack", "d": 0.5, "type": "mcq", "q": "一个 LlamaIndex 的 `ReActAgent` 把普通的 Python 函数当作工具，每个函数都用 `FunctionTool.from_defaults()` 包装。它经常调用错误的工具。你应该先检查哪里？", "o": ["模型的 `request_timeout`", "脚本最外层的聊天循环", "每个工具函数的文档字符串", "智能体的 `verbose=True` 设置"], "a": 2, "why": "模型读取每个工具的文档字符串来决定是否调用它。文档字符串写得不好，是智能体选错工具的主要原因。", "at": "it/ai/ai_windows_stack.html#walkthroughs"},
  {"id": "it__ai__ai_windows_stack_07", "unit": "it__ai__ai_windows_stack", "d": 0.6, "type": "mcq", "q": "在这段 LlamaIndex 代码里，`similarity_top_k=3` 做了什么？", "code": "index = VectorStoreIndex.from_documents(docs)\nengine = index.as_query_engine(similarity_top_k=3)\nresponse = engine.query(question)", "o": ["引擎只读取文件夹里的前 3 个文件", "引擎为一个问题找出最接近的 3 个文本块", "模型最多用 3 个短句来回答", "引擎把每个问题最多尝试 3 次"], "a": 1, "why": "查询引擎把问题转换成嵌入向量，并找出最接近的 3 个已保存向量。数字越大，上下文越多，但速度越慢。", "at": "it/ai/ai_windows_stack.html#walkthroughs"},
  {"id": "it__ai__ai_windows_stack_08", "unit": "it__ai__ai_windows_stack", "d": 0.2, "type": "mcq", "q": "你刚刚用 `winget` 在 Windows 上安装了 Ollama。在同一个 PowerShell 窗口里你输入 `ollama`，PowerShell 显示 `'ollama' is not recognized as a cmdlet`。该怎么解决？", "o": ["用 `Bypass` 运行一次 `Set-ExecutionPolicy`", "拉取一个更小的模型，例如 `llama3.2:1b`", "删除 `.venv` 文件夹，再重新创建它", "打开 Ollama 应用，再开一个新的 PowerShell"], "a": 3, "why": "Ollama 已经安装好了，但它的文件夹还不在 PATH 里。从开始菜单打开 Ollama 应用，然后关闭 PowerShell，再打开一个新窗口。", "at": "it/ai/ai_windows_stack.html#downloads"},
  {"id": "it__ai__ai_mac_stack_01", "unit": "it__ai__ai_mac_stack", "d": -0.7, "type": "mcq", "q": "Ollama 是什么？", "o": ["一个为你的文件建立索引的 Python 库", "一个经过训练、会挑选并调用工具的模型", "一个在本地运行 AI 模型的程序", "一个写着模型岗位说明的文本文件"], "a": 2, "why": "Ollama 是一个免费程序，它下载 AI 模型并在你自己的电脑上运行它们，不需要云端。", "at": "it/ai/ai_mac_stack.html#agent-video"},
  {"id": "it__ai__ai_mac_stack_02", "unit": "it__ai__ai_mac_stack", "d": -0.4, "type": "mcq", "q": "命令 `ollama create kru-ai -f Modelfile` 做了什么？", "o": ["它根据 Modelfile 生成一个自定义模型", "它从互联网下载模型 `kru-ai`", "它为文件夹 `kru-ai` 建立搜索索引", "它和模型 `kru-ai` 开始一次聊天"], "a": 0, "why": "`ollama create` 根据 Modelfile 构建一个新的自定义模型，这里命名为 `kru-ai`。那个文件里的 `SYSTEM` 提示词会被固化到模型里。", "at": "it/ai/ai_mac_stack.html#quickstart"},
  {"id": "it__ai__ai_mac_stack_03", "unit": "it__ai__ai_mac_stack", "d": -0.3, "type": "mcq", "q": "你想让一个本地模型用你自己笔记里的事实来回答，而你经常编辑这些笔记。哪种技术最合适？", "o": ["Modelfile 里的 `SYSTEM` 块", "对笔记做 RAG（LlamaIndex）", "会调用工具的模型（Hermes 3）", "对模型进行微调"], "a": 1, "why": "RAG 在提问时去查找笔记，所以它适合少量经常改动的文件：编辑文件，再运行一次，就完成了。`SYSTEM` 块和微调都需要重新构建才能改动。", "at": "it/ai/ai_mac_stack.html"},
  {"id": "it__ai__ai_mac_stack_04", "unit": "it__ai__ai_mac_stack", "d": 0.2, "type": "mcq", "q": "一个用 LlamaIndex 构建的 RAG 程序在每个回答之后打印一行，例如 `sources: about_me.md`。为什么这一行很重要？", "o": ["它表明是哪个模型写了这个回答", "它表明这个文件已保存到索引里", "它表明智能体调用了哪个工具", "它表明回答来自一个真实的文件"], "a": 3, "why": "这一行写出了检索到的文字来自哪个文件。模型没有猜，你可以打开那个文件来核对回答。", "at": "it/ai/ai_mac_stack.html"},
  {"id": "it__ai__ai_mac_stack_05", "unit": "it__ai__ai_mac_stack", "d": 0.4, "type": "mcq", "q": "在这段 LlamaIndex 智能体代码里，普通的 Python 函数就是工具。模型读取什么来决定调用哪个工具？", "code": "def multiply(a: float, b: float) -> float:\n    \"\"\"Multiply two numbers and return the product.\"\"\"\n    return a * b\n\ndef days_until(iso_date: str) -> str:\n    \"\"\"How many days between today and a YYYY-MM-DD date.\"\"\"\n    target = date.fromisoformat(iso_date)\n    delta = (target - date.today()).days\n    return f\"{delta} day(s) from now\"\n\nTOOLS = [\n    FunctionTool.from_defaults(multiply),\n    FunctionTool.from_defaults(days_until),\n]\nllm = Ollama(model=\"hermes3:8b\", request_timeout=300)\nagent = ReActAgent(tools=TOOLS, llm=llm, verbose=True)", "o": ["`TOOLS` 列表的顺序", "每个函数的文档字符串", "运行每个工具得到的结果", "每个函数的 `return` 那一行"], "a": 1, "why": "`FunctionTool.from_defaults()` 把每个函数的文档字符串变成工具的描述。模型读取这句话来决定是否调用这个函数，所以文档字符串写得不好，就会选错工具。", "at": "it/ai/ai_mac_stack.html#walkthroughs"},
  {"id": "it__ai__ai_mac_stack_06", "unit": "it__ai__ai_mac_stack", "d": 0.5, "type": "mcq", "q": "一个 RAG 程序回答关于某位作者的博客的问题。它使用原版的 `llama3.2` 和一个由 LlamaIndex 保存的索引文件夹。你删除了索引文件夹和博客文件。现在模型对这位作者知道些什么？", "o": ["只有原版 `llama3.2` 本来就知道的内容", "全部内容，因为 RAG 训练了这个模型", "只有它最后查找到的那些文本块", "只有那些博客文章的标题"], "a": 0, "why": "RAG 不是微调，所以模型本身从不改变。它只是在索引里查找内容，没有了索引，它就又是原版的基础模型了。", "at": "it/ai/ai_mac_stack.html"},
  {"id": "it__ai__ai_mac_stack_07", "unit": "it__ai__ai_mac_stack", "d": 0.7, "type": "mcq", "q": "在 Ollama 上做一个使用工具的智能体，为什么选 `hermes3:8b` 而不选原版的 `llama3.2`？", "o": ["`llama3.2` 需要联网才能运行", "`llama3.2` 需要的内存太多", "`llama3.2` 不能用英语回答", "`llama3.2` 不擅长使用工具"], "a": 3, "why": "原版的 `llama3.2` 会编造参数，或者调用错误的工具。Hermes 3 经过了微调，能可靠地选择工具。", "at": "it/ai/ai_mac_stack.html#walkthroughs"},
  {"id": "decider_01", "unit": "decider", "d": -0.8, "type": "mcq", "q": "你给 Decider 一段文本、一个问题和几个选项。它返回什么？", "o": ["一句解释它的选择的话", "每个选项各自的概率", "一份可以再试的新选项列表", "一个你可以运行的 Python 文件"], "a": 1, "why": "Decider 不写文本。它给每个选项一个概率，这些概率加起来等于 1。", "at": "decider.html"},
  {"id": "decider_02", "unit": "decider", "d": -0.5, "type": "mcq", "q": "Decider 的 `decide()` 从三个选项里选出一个，置信度是 0.4887。这个数字告诉你什么？", "o": ["模型对自己的选择没有把握", "模型几乎确信自己是对的", "模型没能读取这条消息", "模型需要一个 Hugging Face 访问令牌"], "a": 0, "why": "在 `decide()` 里，置信度就是最高的那个概率。0.49 不到一半，所以模型没有把握。要看这个数字，而不是只看选择。", "at": "decider.html#step1"},
  {"id": "decider_03", "unit": "decider", "d": 0.1, "type": "mcq", "q": "Decider 模型读取的文字把选项列为 `(A)`、`(B)`、`(C)`，并以 `Answer: (` 结尾。为什么它停在那里？", "o": ["这样模型接下来会写一个完整的句子", "这样模型就不再往下读这些选项", "这样下一个词元就会是一个字母", "这样分词器可以统计 token 的数量"], "a": 2, "why": "下一个词元会是某个选项的字母。所以 A、B 和 C 的 logits 就是答案，不会生成任何文字。", "at": "decider.html#step2"},
  {"id": "decider_04", "unit": "decider", "d": 0.0, "type": "mcq", "q": "一个 Decider 选择题的选项只是光秃秃的名字。它选错了选项，而且模型没有把握。你应该先尝试什么？", "o": ["把模型再加载一遍", "给每个选项写上含义", "把文字大幅缩短", "把 temperature 调低"], "a": 1, "why": "在 `criteria` 里，每个选项可以是一个名字加上它的含义。含义告诉模型这个名字涵盖什么。temperature 永远不会改变哪个选项最高。", "at": "decider.html#step3"},
  {"id": "decider_05", "unit": "decider", "d": -0.4, "type": "fill", "q": "在 Decider 的 `system_one()` 里，是非题有自己的类型。它的答案是一个数字：“是”的概率。请输入这个类型的名字。", "answers": ["noul"], "why": "`noul` 是是非题类型的名字。接近 0 表示“否”，接近 1 表示“是”。", "at": "decider.html#step4"},
  {"id": "decider_06", "unit": "decider", "d": 0.3, "type": "mcq", "q": "一个 Decider 模型创建时带有 `abstain_below=0.7`，并用 `decide_batch()` 提问。对其中一段文字，最好的选项概率是 0.42。这段文字的选择是什么？", "o": ["和平常一样，是 0.42 的那个选项", "这段文字会被再问一遍", "这次调用因错误而停止", "选择是 `None`：没有把握"], "a": 3, "why": "当最好的选项低于 `abstain_below` 时，选择就是 `None`。这样就可以由人来读那段文字。", "at": "decider.html#step9"},
  {"id": "decider_07", "unit": "decider", "d": 0.5, "type": "mcq", "q": "一个 Decider 是非题问的是一个数字是否比另一个大。每段文字里只有这两个数字。在 12 段文字上，模型对了 6 次，而其中 6 个正确答案是“否”。最有可能发生了什么？", "o": ["它把一半的数字对比较对了", "它只读了这些文字的前六段", "它每次都答“否”：它不会比较", "数字有两位小数时它就出错"], "a": 2, "why": "Decider 只读一遍，不做推理，所以它无法比较两个数字。它每次都答“否”，而有六个正确答案是“否”。让你的程序去比较，再用文字把结果告诉模型。", "at": "decider.html#step10"},
  {"id": "decider_08", "unit": "decider", "d": 0.7, "type": "mcq", "q": "Decider 的 `decide()` 收到一段很长的文字：先是 150 条规则，然后才是问题所针对的那条消息。一共 2,969 个 token。它以 0.99 给出回答，而这个回答是错的。为什么？", "o": ["那条消息在被读到之前就被截掉了", "那些规则告诉了模型该选哪个选项", "各个选项的含义全都是 `None`", "模型给每个问题单独读了一遍"], "a": 0, "why": "`decide()` 只读取文字的前 1,536 个 token，而且不给任何警告。那条消息排在这之后，所以模型从没看到它。", "at": "decider.html#step12"},
  {"id": "decider_09", "unit": "decider", "d": -0.6, "type": "mcq", "q": "Decider 被称为 System One 模型。那是一种什么样的模型？", "o": ["一种一次写一个词来写出回答的语言模型", "一种不生成文字的语言模型", "一种只对你问的第一个问题起作用的模型", "一种需要一块 GPU、而且不能多于一块的模型"], "a": 1, "why": "它的开发者称它为“一种不生成文字的语言模型”。System One 是这类模型的名字：它只读一遍，并给每个选项一个概率。", "at": "decider.html"},
  {"id": "decider_10", "unit": "decider", "d": 0.0, "type": "mcq", "q": "人们这样评价 Decider 这类 System One 模型：“它很快。它不思考。”这是什么意思？", "o": ["它还没读完整段文字就给出回答", "它对每个问题都给出同样的回答，所以很快", "它读一遍就回答。它不会一步一步地推理", "它保存着以前的回答，并再把它们查出来"], "a": 2, "why": "Decider 读一遍就回答。它不会一步一步地推理，这就是它无法比较两个数字的原因。", "at": "decider.html"},
  {"id": "decider_11", "unit": "decider", "d": 0.3, "type": "mcq", "q": "一个程序向聊天模型和 Decider 问同一件事。对每一个回复，程序分别要做什么？", "o": ["对两者，它都要读一个句子，并从中找出决定", "对两者，它都读取字典里的一个键，得到一个概率", "对 Decider，它要读一个句子；对聊天模型，它读取一个键", "对聊天模型，它要读一个句子；对 Decider，它读取一个键"], "a": 3, "why": "聊天模型返回一个句子，你的程序得去读它。Decider 给每个选项返回一个概率，你的程序读取字典里的一个键。", "at": "decider.html"},
  {"id": "decider_12", "unit": "decider", "d": 0.6, "type": "mcq", "q": "这四项工作中，哪一项适合聊天模型，而不适合 Decider 这样的 System One 模型？", "o": ["给客人的消息写一段简短、礼貌的回复", "从三个团队中挑出应该接收某条消息的一个", "回答是或否：这条消息紧急吗？", "说出它有多确定某条消息是在要求退款"], "a": 0, "why": "Decider 不写文本，它只能用你给的选项之一来回答。聊天模型可以用一个句子回答任何问题。挑选、是或否、有多确定，才是 Decider 的用途。", "at": "decider.html"},
  {"id": "jev_01", "unit": "jev", "d": -0.7, "type": "mcq", "q": "聊天模型写出一句话。决策模型返回什么？", "o": ["一个你必须解析的长句子", "一份文档的简短摘要", "每个选项对应的一个数字", "一份接下来要问的问题列表"], "a": 2, "why": "对于你问的每个问题的每个选项，决策模型都返回一个 0 到 1 之间的数字。", "at": "jev.html"},
  {"id": "jev_02", "unit": "jev", "d": -0.4, "type": "mcq", "q": "这段代码向 Kev（决策模型 Jev 的小型开放版本）问一个关于客户消息的是非题。`kev.noul()` 把文字和问题发送给模型。代码没有直接传入原始消息：它先把消息放进 `SITUATION` 里。为什么？", "code": "SITUATION = \"A support desk is reading a customer message:\\n{ticket}\"\nASK = \"Is this customer asking for a refund?\"\n\nsure = kev.noul(SITUATION.format(ticket=ticket), ASK)", "o": ["没有背景时模型回答得更差", "模型拒绝没有第一行的文字", "它让模型的日志看不到这条消息", "它让模型用一个句子来回答"], "a": 0, "why": "把背景告诉决策模型，而不只是告诉它问题，这是你能控制的最重要的一件事。在一次运行中，一条关于发票的消息原本是 0.082，去掉背景后变成了大约 0.5。", "at": "jev.html#step1"},
  {"id": "jev_03", "unit": "jev", "d": -0.3, "type": "mcq", "q": "决策模型 Jev 和它的开放版本 Kev 接受三种类型的问题。哪种类型把文字放在一个有顺序的刻度上？", "o": ["`choice`", "`score`", "`noul`", "`state`"], "a": 1, "why": "`choice` 选出一个选项，`score` 把它放在一个刻度上，`noul` 回答是或否。", "at": "jev.html#step2"},
  {"id": "jev_04", "unit": "jev", "d": 0.3, "type": "mcq", "q": "有人要求一个聊天模型在每个回答里报告它自己的置信度。在一次运行中，十五条消息里有十一条它都说是 0.80，容易的和难的都一样。十五个回答全都正确。为什么这个置信度仍然是个问题？", "o": ["它表明聊天模型的回答是猜出来的", "你无法在这个数字上设置阈值", "它让聊天模型回答得更慢", "解析 JSON 时这个数字会丢失"], "a": 1, "why": "置信度只有在模型把握较小时会变动，才有用处。一个几乎不动的数字，让你没有可以据以行动的阈值。", "at": "jev.html#step3"},
  {"id": "jev_05", "unit": "jev", "d": 0.5, "type": "mcq", "q": "一个程序只在决策模型最好的选项概率高于某个阈值时，才自行分发一条消息。其余的交给人处理。你把阈值从 0.60 提高到 0.90。会发生什么？", "o": ["分发的消息更多，错误也更多", "分发的消息更多，错误却更少", "分发的消息更少，错误也更少", "分发的消息更少，错误却更多"], "a": 2, "why": "阈值越高，不经人手处理的消息越少，放过去的错误也越少。在一次运行中，0.60 分发了 15 条中的 13 条，其中 11 条正确；0.90 分发了 5 条，5 条全部正确。", "at": "jev.html#step4"},
  {"id": "jev_06", "unit": "jev", "d": 0.4, "type": "mcq", "q": "有人问一个小型决策模型“Is this customer asking for a refund?”。消息“What is your refund policy, out of interest?”得到了 0.979。这说明这个小模型有什么特点？", "o": ["它盯的是那个词，而不是请求本身", "它读不懂末尾的问号", "它给长消息打的分太高", "它处理难的消息需要密钥"], "a": 0, "why": "小模型学到的是：refund 这个词是一个很强的信号。它分不清是在谈论一件事，还是在请求这件事。", "at": "jev.html#step5"},
  {"id": "jev_07", "unit": "jev", "d": 0.1, "type": "fill", "q": "发给决策模型 Jev 的请求就是普通的 OpenAI 聊天格式。其中有一个字段用来放问题。它和向 OpenAI 的聊天 API 请求结构化输出时所用的字段同名。请输入它的名字。", "answers": ["response_format"], "why": "`response_format` 用来放问题。每个问题都有名字、有类型，并给出了它的选项。", "at": "jev.html#step6"},
  {"id": "jev_08", "unit": "jev", "d": 0.8, "type": "mcq", "q": "一个 0.6B 的决策模型在某些消息上把一个是非题答错了。你给问题加了一条例外：“Asking about the policy does not count.”在一次运行中，这让得分差了很多。对这样小的模型，规则是什么？", "o": ["总是把每一条例外都加进问题里", "把每个问题问三遍，再取平均值", "用一个聊天模型来改写这个问题", "简短、宽泛的问题效果更好"], "a": 3, "why": "0.6B 的模型装不下一条例外。多出来的从句会让问题变模糊，所以简短宽泛胜过冗长精确。", "at": "jev.html#step7"},
  {"id": "jev_09", "unit": "jev", "d": 0.2, "type": "mcq", "q": "关于同一张工单，你有三个问题。两类模型各自怎样接收这些问题？", "o": ["两者都需要三次调用，每个问题调用一次", "决策模型读一遍就全部处理。聊天模型需要三次调用，或者一个需要拆解的回答", "聊天模型读一遍就全部处理。决策模型需要三次调用，或者一个需要拆解的回答", "两者都不能接收三个：一张工单只能有一个问题"], "a": 1, "why": "决策模型把文字读一遍，就回答了所有问题。聊天模型需要三次调用，或者一次调用再加一个需要拆解的回答。", "at": "jev.html"},
  {"id": "jev_10", "unit": "jev", "d": 0.6, "type": "mcq", "q": "在一次测试中，一个小型开放决策模型答对了 15 条消息中的 12 条，一个聊天模型 15 条全部答对。那么，决策模型给你的是什么？", "o": ["在每一类消息上都比聊天模型更好的回答", "和聊天模型一样的回答，不用密钥，也不花钱", "一个你可以据以行动的数字，而不是更好的回答", "一个更短、因而更容易读的句子"], "a": 2, "why": "决策模型给你一个概率，你的程序可以据此行动。它给出的回答并不比聊天模型更好：好的是接口，小模型并不聪明。", "at": "jev.html"},
  {"id": "a1_01", "unit": "agno_agents", "d": -0.5, "type": "mcq", "q": "在 Agno 里，`agent.run(...)` 向一个智能体发送一条消息。在这段代码里，这次调用返回的是什么？", "code": "MODEL = \"qwen2.5:3b\"\n\nagent = Agent(model=Ollama(id=MODEL), instructions=\"Answer in one short sentence.\")\nrun = agent.run(\"What is the capital of Thailand?\")", "o": ["只是一段文字，也就是模型的回答，除此之外什么都没有", "一次运行：回答在 `run.content` 里，还有一份消耗记录，例如 token 数", "一个列表，列出 Ollama 在这台电脑上有的模型，以及每个模型的大小", "什么都没有。Agno 自己在终端里打印回答，之后 `run` 总是 `None`"], "a": 1, "why": "`run.metrics.input_tokens` 和 `run.metrics.output_tokens` 就是这份记录的一部分。", "at": "agno_agents.html#step2"},
  {"id": "a1_02", "unit": "agno_agents", "d": 0.0, "type": "mcq", "q": "`pip install agno` 成功了。后来，像 `from agno.db.sqlite import SqliteDb` 这样的一行因错误而停止，错误里写着一个没有安装的库的名字。为什么？", "o": ["Agno 的安装损坏了，必须把它卸载后再重新安装一遍", "这个库是 Agno 的付费部分，需要有密钥才能加载它", "Agno 只安装它的核心。其他每个库，都在某个部分需要时才要求安装", "Python 版本对 Agno 来说太旧，较新部分的每个导入都会这样失败"], "a": 2, "why": "Agno 让自己的安装保持很小。错误里写出了缺少的库的名字，所以用 pip 安装那个库，然后再运行一次。", "at": "agno_agents.html#step1"},
  {"id": "a1_03", "unit": "agno_agents", "d": 0.0, "type": "mcq", "q": "这个 Agno 智能体收到一份订单，例如 `2 khao soi, 1 water`。是谁算出了总价？", "code": "PRICES = {\"khao soi\": 60, \"pad thai\": 60, \"water\": 10}\n\ndef bill(order: str) -> str:\n    \"\"\"Add up an order: the dishes and how many of each, such as \"2 khao soi, 1 water\".\"\"\"\n    ...   # looks up each dish in PRICES and returns the bill as text\n\ncashier = Agent(\n    model=Ollama(id=MODEL, options={\"temperature\": 0}),\n    tools=[bill],\n    instructions=\"Use the bill tool for every order. Never work out or guess a price yourself.\",\n)", "o": ["模型，根据它的指令里给它看的那些价格来算", "Ollama，它为自己运行的每个模型都自带一个计算器", "Agno，它把在模型回答里找到的每个数字都加起来", "Python 函数 `bill`，模型把它作为工具请求调用"], "a": 3, "why": "模型读取消息，并请求使用这个工具。Agno 运行这个函数，所以求和是普通的 Python 运算，结果是精确的。", "at": "agno_agents.html#step3"},
  {"id": "a1_04", "unit": "agno_agents", "d": -0.4, "type": "mcq", "q": "在 Agno 里，是什么让 `bill` 这样的 Python 函数成为智能体的工具？", "o": ["在 `tools=[bill]` 里把它交给智能体", "一个以 `tool_` 开头的名字，Agno 会去找这样的名字", "文件顶部的 `import bill` 这一行", "代码旁边一个名为 `tools.json`、列出了它的文件"], "a": 0, "why": "这样模型就可以请求运行它。Agno 运行它，并把它的返回值交给模型。", "at": "agno_agents.html#step3"},
  {"id": "a1_05", "unit": "agno_agents", "d": 0.3, "type": "mcq", "q": "一个 Agno 智能体把一个 Python 函数当作工具。模型从来看不到这个函数的代码。它怎么知道这个工具是做什么用的？", "o": ["它根据用户的消息，以及它之前给出的那些回答来猜", "Agno 给它看函数的名字、文档字符串，以及参数和参数的类型", "它打开磁盘上的 Python 文件，读取函数上方的那些注释", "它正是用这个函数训练的，所以早就知道这个工具返回什么"], "a": 1, "why": "所以文档字符串不只是写给程序员看的备注。它是模型读取的指令。", "at": "agno_agents.html#step3"},
  {"id": "a1_06", "unit": "agno_agents", "d": 0.6, "type": "mcq", "q": "一个 Agno 工具被写成了 `def bill(order):`，没有类型提示，而本意是 `def bill(order: str):`。可能出什么问题？", "o": ["不会出问题：Agno 会读取文档字符串，自己补上缺少的提示", "Agno 会拒绝这个函数，智能体启动时就完全没有任何工具", "模型没被告知参数是文本，所以它可能改为发送一个 dict", "模型再也看不到工具的名字，所以它从不请求使用这个工具"], "a": 2, "why": "类型提示是 Agno 给模型看的内容的一部分。在一次运行中，函数期望的是字符串，模型却发送了 `{'khao soi': 2, 'thai iced tea': 1}`。", "at": "agno_agents.html#step3"},
  {"id": "a1_07", "unit": "agno_agents", "d": 0.1, "type": "mcq", "q": "在 Agno 里，一个 Ollama 模型是用 `Ollama(id=\"qwen2.5:3b\", options={\"temperature\": 0})` 创建的。`temperature` 为 0 有什么作用？", "o": ["它让模型回答得更快，回复也更短", "它防止电脑在模型运行时变热", "它把工具关掉，于是模型只能对话", "模型每次都给出它最有可能的回答"], "a": 3, "why": "这样，同一个问题在同一台电脑上运行两次，结果是一致的。", "at": "agno_agents.html#step3"},
  {"id": "a2_01", "unit": "agno_agents", "d": 0.2, "type": "mcq", "q": "这个文件 `shop.py` 里有 Agno 智能体。另一个文件运行 `from shop import books, cashier`。在导入的那一刻，创建了哪些对象？", "code": "DB_FILE = \"shop.db\"\n\ndef books():\n    return SqliteDb(db_file=DB_FILE)\n\ndef cashier(db):\n    return Agent(name=\"Cashier\", model=local_model(WORKER), tools=[bill], db=db)", "o": ["没有智能体，也没有数据库。每个函数在被调用时才构建它的那部分", "智能体和它的数据库，这样两者都为第一条消息做好了准备", "只有数据库，因为智能体需要先有它才能被创建出来", "智能体的模型，Ollama 随即马上把它加载到内存里"], "a": 0, "why": "`def` 只是定义一个函数。在 Agno 里，一组智能体可以是一个 Python 模块，导入它不会启动任何模型。", "at": "agno_agents.html#step4"},
  {"id": "a2_02", "unit": "agno_agents", "d": 0.1, "type": "mcq", "q": "一个 Agno 智能体创建时带有 `db=SqliteDb(db_file=\"shop.db\")`。这给了智能体什么？", "o": ["一个查找事实的地方：模型可以用 SQL 向这个文件提问", "一份记录：Agno 把智能体的对话和运行保存在这个文件里", "一个缓存：Agno 把模型的权重放在那里，让它加载得更快", "一个工具列表：Agno 从中读取智能体可以使用的函数"], "a": 1, "why": "每个拿到同一个数据库的智能体，都把自己的运行写在那里。这个文件是一个普通的 SQLite 数据库。", "at": "agno_agents.html#step4"},
  {"id": "a2_03", "unit": "agno_agents", "d": 0.4, "type": "mcq", "q": "在 Agno 里，一个工具函数被标记为 `@tool(stop_after_tool_call=True)`。这改变了什么？", "o": ["这个工具在一次对话里只能被调用一次", "智能体会停下来，等一个人说同意", "函数返回的内容就是智能体的回答", "这个工具运行太久时会被停止"], "a": 2, "why": "工具和它的参数仍然由模型选择。模型没有机会改写结果，还省下了对模型的又一次提问。", "at": "agno_agents.html#step4"},
  {"id": "a2_04", "unit": "agno_agents", "d": 0.5, "type": "mcq", "q": "一个 Agno 智能体调用了一个工具，工具返回 `I do not know the price of pizza.`。然后由模型自己来写给用户的回复。风险是什么？", "o": ["没有风险：模型在回复里总是一字不差地重复工具的文字", "回复是空的，因为模型读不了工具返回的那些文字", "Agno 会一遍又一遍地运行同一个工具，直到它返回一个数字", "模型可能把它改写错：在一次运行中，它反过来询问价格"], "a": 3, "why": "工具可能是对的，而包在它外面的句子却是错的。`stop_after_tool_call=True` 把工具自己的原话作为回答发出去。", "at": "agno_agents.html#step5"},
  {"id": "a2_05", "unit": "agno_agents", "d": 0.0, "type": "mcq", "q": "在这段 Agno 代码里，是什么把三次运行连成了一次对话？", "code": "TABLE = \"table-4\"\nMESSAGES = [\"2 khao soi, 1 thai iced tea\",\n            \"1 pad thai, 2 pizza\",\n            \"Sorry, no pizza. Make that 1 pad thai, 2 water.\"]\n\nfor message in MESSAGES:\n    run = agent.run(message, session_id=TABLE)", "o": ["每次运行都拿到同一个 `session_id`", "它们是从同一个 `for` 循环里发出的", "它们都在同一个列表 `MESSAGES` 里", "同一个变量 `run` 保存了它们每一个"], "a": 0, "why": "Agno 把一次对话称为一个 session（会话）。`session_id` 相同的运行属于同一个 session。", "at": "agno_agents.html#step5"},
  {"id": "a2_06", "unit": "agno_agents", "d": 0.3, "type": "mcq", "q": "`add_history_to_context=True` 对一个 Agno 智能体有什么作用？", "o": ["它把每次对话写进一个文本文件，供人以后阅读", "智能体能看到同一次对话里之前说过的内容", "它在每个回答里加上消息的日期和时间", "它让智能体也能读取其他每个会话的对话"], "a": 1, "why": "这样，那种只有接在前一条消息后面才讲得通的消息，例如一次更正，就能被理解了。", "at": "agno_agents.html#step4"},
  {"id": "a2_07", "unit": "agno_agents", "d": 0.5, "type": "mcq", "q": "一个 Agno 智能体有一个工具 `bill(order: str)`，它期望的是像 `2 khao soi, 1 water` 这样的文本。在一次已保存的对话里，用户写道：“Sorry, no pizza. Make that 1 pad thai, 2 water.”工具被调用时拿到的是 `'1 pad thai, 2 water'`。是谁把这条消息变成了工具要求的格式？", "o": ["函数 `bill`，用它自己的代码", "用户，他就是按那种格式输入的", "模型，它读取了这次对话", "数据库，它保存了第一条消息"], "a": 2, "why": "读懂一个杂乱的句子并选定工具的参数，是模型的分工。随后的求和是 Python 的分工。", "at": "agno_agents.html#step5"},
  {"id": "a3_01", "unit": "agno_agents", "d": 0.0, "type": "mcq", "q": "一个 Agno `Team` 创建时带有 `mode=TeamMode.route`。团队负责人的工作是什么？", "o": ["独自完成主要的工作，并把小问题交给其中一名成员", "检查每名成员的回答，并用它自己的话把回答重写一遍", "询问每一名成员，并把他们的回答合并成它自己的一个回答", "读取消息并挑选一名成员。那名成员的回答就是最终的回答"], "a": 3, "why": "在 route 模式下，负责人的模型只做选择。被选中成员的回答原封不动地作为团队的回答返回。", "at": "agno_agents.html#step6"},
  {"id": "a3_02", "unit": "agno_agents", "d": 0.4, "type": "mcq", "q": "在路由模式（route mode）的 Agno 团队里，团队负责人只读消息并挑选一名成员。在一次运行中，一个 3b 模型把成员的工作做得很好，但当它做团队负责人时，6 条消息中有 0 条发给了成员。一个 14b 模型则 6 条都发了。这说明什么？", "o": ["挑选成员可能是更难的工作，所以团队负责人可能需要更大的模型", "只有每个成员都和团队负责人用同一个模型思考，团队才能工作", "小模型太慢，带不了团队，所以它的六次运行每次都超时了", "路由模式不能配合 Ollama 使用，所以团队在那里得换一种模式"], "a": 0, "why": "小模型可能把指令当作文字写出来，而不去执行它。要用真实的消息来测试团队负责人的模型。", "at": "agno_agents.html#step6"},
  {"id": "a3_03", "unit": "agno_agents", "d": 0.2, "type": "mcq", "q": "一个程序向 Ollama 里的模型问了两个问题。第一个回答用了 72.6 秒，第二个用了 16.5 秒。第一个回答慢，通常是什么原因？", "o": ["对模型来说，第一个问题总是比第二个问题更难", "Ollama 必须先把模型加载到内存里，然后才能回答", "每次有程序启动时，Ollama 都会重新下载一遍模型", "数据库是空的，必须先把它所有的表都建出来"], "a": 1, "why": "把模型加载到内存的代价只付一次。之后的问题就快了。", "at": "agno_agents.html#step6"},
  {"id": "a3_04", "unit": "agno_agents", "d": 0.5, "type": "mcq", "q": "`determine_input_for_members=False` 在 Agno 的 `Team` 里起什么作用？", "o": ["团队负责人不能挑选成员，所以每个成员都会收到消息", "成员看不到用户的消息，只能看到团队负责人的回答", "成员收到的是用户的原话，而不是团队负责人转述的版本", "成员自己选择工具，团队负责人不能更改这些工具"], "a": 2, "why": "经团队负责人转述的消息，到成员手里时可能已经变了，比如其中一些部分被丢掉了。", "at": "agno_agents.html#step4"},
  {"id": "a3_05", "unit": "agno_agents", "d": 0.1, "type": "mcq", "q": "在 Agno 里，一个工具被标记为 `@tool(requires_confirmation=True)`。一个智能体请求使用它。Agno 会怎么做？", "o": ["它运行这个工具，事后再告诉人", "它拒绝，并以状态 `ERROR` 结束运行", "它再问模型一次，以便确认", "它不运行这个工具。运行以 `PAUSED` 停下"], "a": 3, "why": "暂停的运行带着一份它正在等待的事项列表。由人说“是”或“否”来决定工具是否运行。", "at": "agno_agents.html#step7"},
  {"id": "a3_06", "unit": "agno_agents", "d": 0.4, "type": "mcq", "q": "一次 Agno 运行处于 `PAUSED` 状态，因为某个工具需要人来确认。哪些调用可以答复它并让它继续？", "o": ["对它等待的每一项调用 `confirm()` 或 `reject()`，然后调用 `continue_run()`", "再调用一次 `agent.run()`，把单词 yes 或单词 no 作为消息", "智能体请求工具时调用 `pause()`，人同意时调用 `resume()`", "都不行：暂停的运行就丢失了，整个对话必须重新开始"], "a": 0, "why": "`run.active_requirements` 里的每一项都要得到 `confirm()` 或 `reject()`。`reject` 之后，运行同样以 `COMPLETED` 结束，智能体会说答案是“否”。", "at": "agno_agents.html#step7"},
  {"id": "a3_07", "unit": "agno_agents", "d": 0.6, "type": "mcq", "q": "一次 Agno 运行暂停下来等人说“是”，而这个“是”可能几小时后才来。为什么 `continue_run()` 仍然能找到这次暂停的运行？", "o": ["模型记得它做过的每一次运行", "暂停的运行保存在智能体的数据库里", "终端把它一直留在内存里，直到那时", "Agno 把它发送到自己的一个网络服务"], "a": 1, "why": "`continue_run()` 通过 `run_id` 和 `session_id` 找到这次运行。没有数据库的智能体会回答说不存在这个编号的运行。", "at": "agno_agents.html#step7"},
  {"id": "a4_01", "unit": "agno_agents", "d": -0.2, "type": "mcq", "q": "为什么 token 的预算就是钱的预算？", "o": ["不管智能体用哪个模型，付费的还是免费的，每个 token 都要花一泰铢", "token 保存在磁盘上，而真正花钱的地方是更大的磁盘", "一个 token 是一个词的一小块，付费模型就是按这个单位计费的", "并不是：token 只衡量速度，和费用没有任何关系"], "a": 2, "why": "自己电脑上的模型，每个 token 都不花钱。里面用了付费模型的智能体，花起钱来可以很快。", "at": "agno_agents.html#step8"},
  {"id": "a4_02", "unit": "agno_agents", "d": 0.4, "type": "mcq", "q": "这个 Agno 钩子会在一段对话用掉 800 个 token 时拒绝运行。`spent(agent)` 返回到目前为止用掉的 token 数。在一次运行中，总数是 527，下一条消息使它达到 899，却没有被拒绝。为什么？", "code": "BUDGET = 800\n\ndef check_budget(agent):\n    \"\"\"Agno calls this before every run, and hands it the agent.\"\"\"\n    if spent(agent) >= BUDGET:\n        raise InputCheckError(f\"The budget of {BUDGET} tokens is spent.\")\n\nagent.pre_hooks = [check_budget]", "o": ["这个钩子只统计最新一次运行的 token，而那是 372", "前置钩子在每次运行之后才被调用，所以它总是晚一条消息", "Agno 允许每个预算超出 100 个 token，然后才停止运行", "前置钩子在运行之前检查，无法知道这次运行会花多少"], "a": 3, "why": "在那次运行之前，总数是 527，低于 800。在运行之前做的检查，只能拒绝超出预算的那次运行之后的下一次运行。", "at": "agno_agents.html#step8"},
  {"id": "a4_03", "unit": "agno_agents", "d": 0.6, "type": "mcq", "q": "在 Agno 里，前置钩子必须停止一次运行，比如预算用完的时候。它必须抛出哪个错误？", "o": ["`InputCheckError`：在钩子能抛出的错误中，这一种会停止运行", "任何错误都行：只要有一个钩子失败，Agno 就会立刻停止运行", "`StopAgentRun`，因为它的名字说明它就是专门为此而设的", "`KeyError`，就是字典缺少某个键时 Python 抛出的那个错误"], "a": 0, "why": "在一次用 `StopAgentRun` 的测试中，Agno 打印了 `Pre-hook #1 execution failed`，但还是照样完成了运行。要通过把预算花光来测试预算。", "at": "agno_agents.html#step8"},
  {"id": "a4_04", "unit": "agno_agents", "d": 0.3, "type": "mcq", "q": "一个 Agno 智能体设置了 `add_history_to_context=True`。同一个会话里的三条短消息依次花了 235、292、372 个 token。为什么每一条都比前一条花得多？", "o": ["后面的消息更长，而每个词花的 token 是一样的", "智能体会看到到目前为止的对话，并为重读它付费", "对话越长，模型回答时用的词就越来越多", "每个钩子在 Agno 每次调用它时，都会用掉自己的 token"], "a": 1, "why": "对话越长，每条新消息花得越多，因为每次都要把之前的消息重读一遍。", "at": "agno_agents.html#step8"},
  {"id": "a4_05", "unit": "agno_agents", "d": 0.1, "type": "mcq", "q": "Agno 智能体设置了 `db=SqliteDb(db_file=\"shop.db\")`。之后，一个脚本要列出每一段对话，以及每次运行的状态、token 和工具。它需要什么？", "o": ["运行着的 Ollama，因为要由模型来说出它在每次运行中做了什么", "同样的智能体，重新构建出来，并让它们依次重复每一条消息", "只需要这个文件：`db.get_sessions(...)` 读取它，不启动任何模型", "一个付费的 Agno 账号，因为记录保存在 Agno 自己的服务器上"], "a": 2, "why": "这个文件是一个普通的 SQLite 数据库。被拒绝的运行也在里面，状态为 `ERROR`，token 数为 0。", "at": "agno_agents.html#step9"},
  {"id": "a4_06", "unit": "agno_agents", "d": 0.2, "type": "mcq", "q": "Agno 的 AgentOS 把智能体作为一个 FastAPI 应用提供出来。一个不从 Agno 导入任何东西的程序，或者用另一种语言写的程序，怎样使用这些智能体？", "o": ["做不到。只有导入了 Agno 的 Python 代码才能访问智能体", "它打开 SQLite 文件，把自己的消息写进里面的一张表", "它在 Ollama 里用同样的指令启动一个自己的模型", "它发送普通的网络请求，比如向 `/agents/<id>/runs` 发一个 POST"], "a": 3, "why": "AgentOS 把智能体和团队放在网络地址后面，比如 `/health`、`/agents` 和 `/sessions`。", "at": "agno_agents.html#step10"},
  {"id": "a4_07", "unit": "agno_agents", "d": 0.5, "type": "mcq", "q": "一个 Agno AgentOS 服务器第一次启动。它马上就能列出同一批智能体早先在其他脚本里进行过的对话。这是怎么做到的？", "o": ["它拿到的数据库文件和它们用的是同一个", "它启动时把所有旧脚本又运行了一遍", "客户端在请求里把旧对话一起发了过来", "Ollama 记得它进行过的每一段对话"], "a": 0, "why": "智能体是 Python 对象，记录是一个文件，而服务器是通往这两者的又一个入口。", "at": "agno_agents.html#step10"},
  {"id": "a4_08", "unit": "agno_agents", "d": 0.4, "type": "mcq", "q": "创建 Agno 的 `AgentOS` 时用了 `telemetry=False`。这是做什么用的？", "o": ["它关掉所有工具，这样服务器上的智能体就只能对话", "除非设为 `False`，Agno 会把匿名的使用数据发给它的开发者", "它把服务器隐藏起来，同一网络上的其他电脑看不到它", "它阻止服务器把自己的对话保存到数据库里"], "a": 1, "why": "让服务器只在这台电脑上可用是另一个设置：`uvicorn.run(...)` 里的 `host=\"127.0.0.1\"`。", "at": "agno_agents.html#step10"},
  {"id": "a5_01", "unit": "agno_agents", "d": -0.2, "type": "mcq", "q": "这个 pydantic 类被作为 `output_schema` 交给一个 Agno 智能体。`Field(ge=1, le=100)` 对 `score` 规定了什么？", "code": "class Review(BaseModel):\n    score: int = Field(ge=1, le=100, description=\"the quality of the paper, from 1 to 100\")\n    fixes: list[str] = Field(min_length=3, max_length=3,\n                             description=\"three sentences, each naming a thing to fix\")", "o": ["它一开始是 1，可以增长到 100", "它要被计算 1 到 100 次", "它必须是 1 到 100 之间的数", "它必须有 1 到 100 位数字"], "a": 2, "why": "`ge` 表示大于或等于，`le` 表示小于或等于。`score: int` 使它成为整数。", "at": "agno_agents.html#step11"},
  {"id": "a5_02", "unit": "agno_agents", "d": 0.3, "type": "mcq", "q": "创建一个 Agno 智能体时用了 `output_schema=Review`，其中 `Review` 是一个 pydantic 类，带有字段 `score: int`。这有什么作用？", "o": ["它把每个回答保存为一个单独的文件，以类 `Review` 的名字命名", "它给智能体一个工具，智能体可以用它在网上搜索一个分数", "它把每个回答删减到一百个词，这样读起来更快", "Agno 要求模型严格按这个格式回答，并返回一个 Python 对象"], "a": 3, "why": "这样 `run.content` 就是一个 `Review`，所以 `review.score` 是一个代码可以比较的数，比如 `if review.score >= TARGET`。", "at": "agno_agents.html#step11"},
  {"id": "a5_03", "unit": "agno_agents", "d": 0.0, "type": "mcq", "q": "在这个循环里，一个 Agno 智能体给一篇论文打分，然后其他智能体改进这篇论文。循环什么时候停止？", "code": "TARGET = 85\nROUNDS = 3\n\nfor turn in range(1, ROUNDS + 1):\n    review = reviewer.run(paper, session_id=session).content\n    if review.score >= TARGET or turn == ROUNDS:\n        break\n    ...   # search for facts, then write the paper again", "o": ["当分数达到 85 时，或者在第三次评审之后", "只有当分数达到 85 时，不管要花多久", "85 秒之后，或者在网上搜索三次之后", "只有当第三次评审给出 85 分或更高时"], "a": 0, "why": "这一行是 `if review.score >= TARGET or turn == ROUNDS: break`：够好了，或者轮数用完了。", "at": "agno_agents.html#step11"},
  {"id": "a5_04", "unit": "agno_agents", "d": 0.5, "type": "mcq", "q": "在这个循环里，一个评审智能体给论文打分，一个写作智能体把它重写一遍。`TARGET` 是 85，`ROUNDS` 是 3。三次的分数是 45、25 和 30。打印出来的是哪一篇论文？", "code": "best_score = 0\nfor turn in range(1, ROUNDS + 1):\n    review = reviewer.run(paper, session_id=session).content\n    if review.score > best_score:\n        best_score = review.score\n        best_paper = paper\n    if review.score >= TARGET or turn == ROUNDS:\n        break\n    paper = writer.run(paper, session_id=session).content\nprint(best_paper)", "o": ["第三篇，因为它是最新的", "第一篇，也就是任何重写之前的样子", "第二篇，因为它是第一次重写", "都不打印，因为没有达到目标"], "a": 1, "why": "`best_paper` 只在某个分数超过 `best_score` 时才改变。重写的可能比被它替换的更差，而这条规则保留最好的一篇。", "at": "agno_agents.html#step11"},
  {"id": "a5_05", "unit": "agno_agents", "d": 0.6, "type": "mcq", "q": "一个 Agno 的 `output_schema` 里有 `fixes: list[str] = Field(min_length=3, max_length=3)`，描述为三个句子。在一次运行中，一个小模型返回了三项：半个句子、一个空白项，以及句子的剩余部分。这说明什么？", "o": ["Agno 没有把 schema 交给模型，所以模型按自己的方式回答了", "小模型数不到三，所以 schema 应该只要求一项", "schema 检查的是形状，不是内容：有三项，但不是三个句子", "这个 schema 太严格了：换成要求四项的就能正常工作"], "a": 2, "why": "schema 能让分数成为 1 到 100 之间的整数。它不能让分数是正确的那个，所以仍然需要人来读结果。", "at": "agno_agents.html#step11"},
  {"id": "a5_06", "unit": "agno_agents", "d": 0.3, "type": "mcq", "q": "在这段代码里，三个 Agno 智能体处理同一篇论文。由什么决定轮到谁，以及工作何时停止？", "code": "for turn in range(1, ROUNDS + 1):\n    review = reviewer.run(paper, session_id=session).content\n    if review.score >= TARGET or turn == ROUNDS:\n        break\n    # both: the paper and the review, as one text\n    notes = researcher.run(both, session_id=session).content\n    paper = writer.run(both + notes, session_id=session).content", "o": ["评审者，因为它是给出分数的那个智能体", "第四个智能体，Agno 会给每一组智能体都加上它", "Ollama，它自己让这些智能体一个接一个地运行", "`for` 循环：这里的管理者是普通的 Python 代码"], "a": 3, "why": "每个 `run()` 都是一次普通的调用。循环把论文在智能体之间传一圈，它的 `if` 决定何时停止。", "at": "agno_agents.html#research"},
  {"id": "a5_07", "unit": "agno_agents", "d": 0.1, "type": "mcq", "q": "Python 库 `ddgs` 在能搜索网络之前需要什么？", "o": ["不需要账号，也不需要密钥", "一个付费账号和一个密钥", "一个来自 Ollama 的免费密钥", "一个打开着的浏览器"], "a": 0, "why": "它向公开的搜索引擎提出问题，并返回最前面的几条结果。不过每次搜索确实会把问题发到网上去。", "at": "agno_agents.html#step11"},
  {"id": "smolagents_01", "unit": "smolagents", "d": -0.8, "type": "mcq", "q": "智能体需要一个工具循环：提供工具，读取模型的选择，运行函数，把答案交回去。smolagents 怎样处理这个循环？", "o": ["它去掉了这个循环，所以不需要工具。", "它来写这个循环；你来提供工具。", "它用 Flask 把这个循环变成一个网页。", "它不用任何模型就能运行这个循环。"], "a": 1, "why": "smolagents 替你写好了工具循环。`agent.run()` 里包含了循环、重试和停止。", "at": "smolagents.html"},
  {"id": "smolagents_02", "unit": "smolagents", "d": -0.5, "type": "mcq", "q": "在 smolagents 里，函数上方的 `@tool` 装饰器起什么作用？", "o": ["它把这个函数运行一次来测试它。", "它把这个函数发送到 Ollama 服务器。", "它让智能体可以选择这个函数。", "它在这个函数运行之后停止智能体。"], "a": 2, "why": "`@tool` 把它下面的函数变成智能体可以选择的东西。smolagents 读取它的名字、文档字符串和类型提示来生成菜单。", "at": "smolagents.html#first-agent"},
  {"id": "smolagents_03", "unit": "smolagents", "d": -0.2, "type": "mcq", "q": "这个函数是一个 smolagents 工具。你从它的文档字符串里删掉了 `Args:` 这一块。会发生什么？", "code": "@tool\ndef check_pantry(item: str) -> str:\n    \"\"\"Check if one ingredient is in the kitchen right now.\n\n    Args:\n        item: the name of one ingredient, like \"coconut milk\"\n    \"\"\"\n    return \"no, there is no \" + item + \" in the kitchen\"", "o": ["smolagents 报错，拒绝这个工具。", "智能体能运行，但从不调用这个工具。", "智能体根据参数的名字来猜它。", "什么都不会发生：文档字符串只是好习惯。"], "a": 0, "why": "smolagents 在构建工具时检查文档字符串。没有 `Args:` 块就会得到 `DocstringParsingException`。", "at": "smolagents.html#first-agent"},
  {"id": "smolagents_04", "unit": "smolagents", "d": 0.1, "type": "mcq", "q": "在 smolagents 里，你可以把工具写成带 `@tool` 的函数，也可以写成继承 `Tool` 的类。什么时候用类更好？", "o": ["当工具必须搜索互联网时。", "当工具被两个智能体使用时。", "当工具有不止一个参数时。", "当工具需要自己的数据时。"], "a": 3, "why": "函数是快捷的那种。类适合需要记住某些东西的工具：它创建时就带着自己的数据，不去读全局变量。", "at": "smolagents.html#tools"},
  {"id": "smolagents_05", "unit": "smolagents", "d": 0.4, "type": "fill", "q": "在 smolagents 里，一个智能体可以管理其他智能体。`ToolCallingAgent` 的哪个参数接收工作智能体的列表？请输入这个参数名。", "answers": ["managed_agents", "managed_agents="], "why": "你给每个工作智能体一个 `name` 和一个 `description`，并通过 `managed_agents` 把这些工作智能体传给管理者。", "at": "smolagents.html#crew"},
  {"id": "smolagents_06", "unit": "smolagents", "d": 0.6, "type": "mcq", "q": "一个 smolagents 智能体有这个工具。`SHOP` 是一个文本文件的路径。在一次运行中，智能体用同一个物品调用了这个工具三次，所以文件里把这个物品记了三次。正确的修复方法是什么？", "code": "@tool\ndef add_to_shopping_list(item: str) -> str:\n    \"\"\"Add one thing to the shopping list.\n\n    Args:\n        item: the name of one ingredient\n    \"\"\"\n    with SHOP.open(\"a\", encoding=\"utf-8\") as shopping:\n        shopping.write(item + \"\\n\")\n    return \"added \" + item", "o": ["换一个更小的模型，这样智能体的调用会更少。", "让工具先读文件，跳过已经在里面的内容。", "给这个工具配一个自己的智能体，`max_steps` 设得低一些。", "调高 `max_steps`，让智能体有时间注意到它。"], "a": 1, "why": "智能体会重试，所以一个工具可能被用同样的输入调用两次。追加内容的工具必须先检查，这样调用两次也是安全的。", "at": "smolagents.html#tools"},
  {"id": "smolagents_07", "unit": "smolagents", "d": 0.8, "type": "mcq", "q": "一个 smolagents 管理者智能体有一个工具，把物品写入文件 `shopping.txt`（`SHOP` 是它的路径）。运行结束后，这段代码先打印智能体的回答，然后自己去读这个文件。为什么要读文件？", "code": "answer = manager.run(task)\nprint(\"\\nANSWER:\", answer)\nwritten = SHOP.read_text(encoding=\"utf-8\").split() if SHOP.exists() else []\nprint(\"shopping.txt holds:\", written)", "o": ["智能体可能说它添加了一个物品，其实并没有。", "在文件被读取之前，管理者给不出回答。", "工作智能体下一次调用时需要这个文件。", "读取文件可以减少调用模型的次数。"], "a": 0, "why": "智能体说的和做的可能不一样：它可能没调用工具就报告成功，或者写入一个错误的物品。要检查结果本身，而不是智能体的总结。", "at": "smolagents.html#crew"},
  {"id": "docker_lab_01", "unit": "docker_lab", "d": -0.8, "type": "mcq", "q": "镜像和容器有什么区别？", "o": ["镜像是免费的；容器要花钱。", "镜像放在磁盘上；容器在运行。", "镜像装的是代码；容器装的是数据。", "镜像在 Docker Hub 上；容器在本地。"], "a": 1, "why": "镜像是一个冷盒子，只是放在磁盘上。容器是一个在运行的热盒子，是用 `docker run` 从镜像做出来的。", "at": "docker_lab.html#L-img"},
  {"id": "docker_lab_02", "unit": "docker_lab", "d": -0.5, "type": "mcq", "q": "在 `docker run -d -p 8080:80 my-site` 里，左边的数字 `8080` 是什么？", "o": ["镜像的版本号", "容器内部的端口", "容器可以使用的内存", "你自己电脑上的端口"], "a": 3, "why": "选项 `-p` 打开一个端口。左边的数字是你电脑上的端口，右边的数字是容器内部的端口。", "at": "docker_lab.html#L-port"},
  {"id": "docker_lab_03", "unit": "docker_lab", "d": -0.3, "type": "mcq", "q": "`docker run hello-world` 显示“cannot connect to the Docker daemon”。这是什么意思？", "o": ["Docker Desktop 还没有运行。", "互联网连接断了。", "`hello-world` 镜像被删除了。", "你的 Docker Hub 密码错了。"], "a": 0, "why": "Docker Desktop 没有在运行。打开这个应用，等它变绿，然后再试一次。", "at": "docker_lab.html#sec-install"},
  {"id": "docker_lab_04", "unit": "docker_lab", "d": 0.6, "type": "mcq", "q": "这个 Dockerfile 先复制 `package*.json` 并运行 `npm install`，然后才复制其余的代码。为什么？", "code": "FROM node:20-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nRUN npm run build", "o": ["Docker 按字母顺序读取文件。", "在 Node 启动之前不能复制代码。", "这样改了代码也不会重新安装。", "这会让最终的镜像小得多。"], "a": 2, "why": "Docker 把每一行保存为一个缓存的层。改动过的一行会重建它的层以及之后的每一层。`npm install` 很慢，所以它放在经常变动的代码之前。", "at": "docker_lab.html#L-layer"},
  {"id": "docker_lab_05", "unit": "docker_lab", "d": 0.1, "type": "mcq", "q": "用 `tailscale funnel` 代替 `tailscale serve` 时，什么变了？", "o": ["网站换到了另一个端口。", "整个互联网都能打开这个网站。", "合上盖子后网站仍然继续运行。", "只有你自己的设备能打开这个网站。"], "a": 1, "why": "`serve` 只把网站分享给你自己的设备。`funnel` 把它分享给所有人：任何有链接的人都能打开它。", "at": "docker_lab.html#sec-tailscale"},
  {"id": "docker_lab_06", "unit": "docker_lab", "d": 0.4, "type": "fill", "q": "`docker ____ my-site:1.0 yourname/my-site:1.0` 给一个镜像起第二个名字，而不复制它。请输入缺少的那个英文单词。", "answers": ["tag", "docker tag"], "why": "`docker tag my-site:1.0 yourname/my-site:1.0` 不会复制。它只是给同一个镜像加上第二个名字。", "at": "docker_lab.html#sec-hub"},
  {"id": "docker_lab_07", "unit": "docker_lab", "d": 0.3, "type": "mcq", "q": "一个部署文件里写着 `replicas: 4`。有一个 pod 挂了。Kubernetes 会怎么做？", "o": ["它启动一个新的 pod，保持四个。", "它把这个数字改成三。", "它把另外三个 pod 也停掉。", "它等你来重新启动它。"], "a": 0, "why": "你说出想要的数量，Kubernetes 就让它一直成立。如果有一个副本挂了，它就启动一个新的。", "at": "docker_lab.html#L-k8s"},
  {"id": "it__ai__dockerfile_basics_01", "unit": "it__ai__dockerfile_basics", "d": -0.8, "type": "mcq", "q": "Dockerfile 是什么？", "o": ["一个现成的镜像，你下载后就可以运行", "一个文本文件，写着构建镜像的步骤", "一个正在你电脑上运行的容器", "一个带编号的端口，让你能访问容器"], "a": 1, "why": "Dockerfile 是一个写着指令的纯文本文件。`docker build` 按照它来制作镜像。", "at": "it/ai/dockerfile_basics.html#s2"},
  {"id": "it__ai__dockerfile_basics_02", "unit": "it__ai__dockerfile_basics", "d": -0.5, "type": "mcq", "q": "在 Dockerfile 里，`FROM python:3.12-slim` 这一行做什么？", "o": ["安装 `requirements.txt` 里列出的包", "把你的 Python 代码复制到 `/app` 文件夹", "从一个已经带有 Python 的现成镜像开始", "指定容器启动时运行哪条命令"], "a": 2, "why": "`FROM` 表示你的镜像从别人做好的镜像开始，这里是一个已经装好 Python 的小镜像。你写的各行在它上面添加层。", "at": "it/ai/dockerfile_basics.html#s3"},
  {"id": "it__ai__dockerfile_basics_03", "unit": "it__ai__dockerfile_basics", "d": -0.3, "type": "mcq", "q": "在 `docker build -t joke-machine .` 里，`-t joke-machine` 是做什么的？", "o": ["它给镜像起一个名字，以后可以按名字运行它", "它设定一个时间限制，构建太慢就会停下", "它在构建之前先运行文件夹里的测试", "它告诉 Docker 哪个文件夹里有 Dockerfile"], "a": 0, "why": "`-t` 是标签（tag）：镜像的名字。这里镜像被命名为 `joke-machine`。", "at": "it/ai/dockerfile_basics.html#s6"},
  {"id": "it__ai__dockerfile_basics_04", "unit": "it__ai__dockerfile_basics", "d": 0.1, "type": "mcq", "q": "一个镜像是用这个 Dockerfile 构建的。你只改了 `app.py`，然后再次运行 `docker build`。会发生什么？", "code": "FROM python:3.12-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\nCOPY app.py .\nEXPOSE 8000\nCMD [\"python\", \"app.py\"]", "o": ["每一行都重建，因为镜像是一个整体", "什么都不重建，因为 Dockerfile 没有变", "只有 `pip install` 重新运行；其余的是 CACHED", "第 1 到 4 行是 CACHED；第 5 行及以下重建"], "a": 3, "why": "Docker 会复用改动之上的每一层。第一个依赖于被改文件的行，以及它下面的每一行，都必须重建。这里就是第 5 行的 `COPY app.py .`。", "at": "it/ai/dockerfile_basics.html#s4"},
  {"id": "it__ai__dockerfile_basics_05", "unit": "it__ai__dockerfile_basics", "d": 0.5, "type": "mcq", "q": "为什么这个 Dockerfile 先复制 `requirements.txt` 并运行 `pip install`，然后才复制 `app.py`？", "code": "FROM python:3.12-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\nCOPY app.py .", "o": ["这样修改你的代码就不会重新运行安装", "这样 Docker 可以对照包列表检查代码", "这样镜像比只用一行 `COPY` 时更小", "这样 `pip install` 能在工作区里找到 `app.py`"], "a": 0, "why": "慢的部分是 `pip install`。你的代码经常变，而依赖列表几乎不变，所以分开写成几行能让安装保持在缓存里。", "at": "it/ai/dockerfile_basics.html#s4"},
  {"id": "it__ai__dockerfile_basics_06", "unit": "it__ai__dockerfile_basics", "d": 0.6, "type": "mcq", "q": "一个 Docker 容器在 `localhost:8000` 上提供一个网站。你改了代码并重建了镜像，但浏览器仍然显示旧网站。为什么？", "o": ["缓存复用了镜像里旧的代码层", "正在运行的容器仍在使用旧镜像", "`.dockerignore` 文件把你的代码排除了", "每次重建都必须更改镜像的名字"], "a": 1, "why": "正在运行的容器会一直运行旧代码。重建镜像不会影响它。你必须用 `docker rm -f` 删除它，再从新镜像启动一个新容器。", "at": "it/ai/dockerfile_basics.html#s8"},
  {"id": "it__ai__dockerfile_basics_07", "unit": "it__ai__dockerfile_basics", "d": 0.2, "type": "mcq", "q": "`.dockerignore` 文件是做什么用的？", "o": ["它列出 pip 不准安装的包", "它列出容器保持关闭的端口", "它列出不进入构建的文件", "它列出清理时要删除的镜像"], "a": 2, "why": "Docker 会把你的整个文件夹交给构建程序。`.dockerignore` 列出要排除的东西，比如视频和机密信息。", "at": "it/ai/dockerfile_basics.html#s9"},
  {"id": "linux_lab_01", "unit": "linux_lab", "d": -0.7, "type": "mcq", "q": "Ubuntu 用 `apt` 安装程序。Rocky Linux 用哪个命令做同样的事？", "o": ["`apt-get`", "`dnf`", "`dpkg`", "`getent`"], "a": 1, "why": "Ubuntu 使用 `apt` 和 deb 软件包。Rocky 使用 `dnf` 和 rpm 软件包。", "at": "linux_lab.html#L-pkg"},
  {"id": "linux_lab_02", "unit": "linux_lab", "d": -0.4, "type": "mcq", "q": "你用 `docker rm` 删除了一个容器。哪个文件会丢失？", "o": ["Docker 创建的卷里的一个文件", "从你的电脑共享的文件夹里的一个文件", "挂载在 `/data` 的卷里的一个文件", "容器自身文件系统里的一个文件"], "a": 3, "why": "容器自身的文件系统是草稿纸。卷里或共享文件夹里的文件，在容器被删除之后仍然还在。", "at": "linux_lab.html#L-store"},
  {"id": "linux_lab_03", "unit": "linux_lab", "d": 0.0, "type": "mcq", "q": "在一个全新的 Ubuntu 容器里，`apt-get install -y vim` 失败，提示 `Unable to locate package`。原因是什么？", "o": ["你没有先运行 `apt-get update`", "Ubuntu 上不存在这个软件包", "你必须在命令前面输入 `sudo`", "这个容器没有连接互联网"], "a": 0, "why": "`apt-get install` 要读一份商品清单，而全新的镜像里还没有这份清单。软件包是存在的；你必须先运行 `apt-get update`。", "at": "linux_lab.html#sec-two"},
  {"id": "linux_lab_04", "unit": "linux_lab", "d": -0.3, "type": "mcq", "q": "`echo \"again\" >> note.txt` 有什么作用？", "o": ["清空文件，然后写入这段文本", "把文件和这段文本一起打印出来", "把这段文本加到文件的末尾", "把这段文本送进下一个命令"], "a": 2, "why": "一个 `>` 会先清空文件。两个 `>>` 是加到末尾，这要安全得多。", "at": "linux_lab.html#sec-cli"},
  {"id": "linux_lab_05", "unit": "linux_lab", "d": 0.4, "type": "fill", "q": "读是 4，写是 2，执行是 1。要让只有所有者可以读写一个文件，其他任何人连读都不行，你该给 `chmod` 哪个数字？请输入这三位数字。", "answers": ["600", "chmod 600"], "why": "6 是所有者的读加写，0 表示不给用户组和其他所有人任何权限。所以命令是 `chmod 600` 加上文件名。", "at": "linux_lab.html#sec-cli"},
  {"id": "linux_lab_06", "unit": "linux_lab", "d": 0.4, "type": "mcq", "q": "两个容器运行在 Docker 默认给你的网络上。`getent hosts web` 什么也没打印。为什么？", "o": ["`web` 容器启动时没有加 `-p`", "名称在默认网络上不起作用", "Ubuntu 默认没有安装 `ping`", "你必须用 `localhost` 来代替名称"], "a": 1, "why": "只有在你自己用 `docker network create` 创建的网络上，容器才能通过名称找到彼此。在默认网络上，名称不起作用。", "at": "linux_lab.html#sec-net"},
  {"id": "linux_lab_07", "unit": "linux_lab", "d": 0.6, "type": "mcq", "q": "容器 `app` 里的一个程序在 `localhost` 上找数据库。数据库运行在另一个容器里。会发生什么？", "o": ["它永远找不到：`localhost` 就是 `app` 自己", "它能找到：`localhost` 就是你的电脑", "如果两者都用 `-p 8080:80` 运行，它就能找到", "它会一直失败，直到 `iproute2` 装进 `app` 里"], "a": 0, "why": "在容器内部，`localhost` 指的是容器自己。要访问数据库，请使用另一个容器的名称。", "at": "linux_lab.html#sec-net"},
  {"id": "linux_lab_08", "unit": "linux_lab", "d": 0.7, "type": "mcq", "q": "一个 Dockerfile 用 `CMD [\"/usr/sbin/sshd\", \"-D\", \"-e\"]` 启动 SSH 服务器。为什么需要 `-D`？", "o": ["这样 `sshd` 会把日志写进 `/run/sshd` 里的一个文件", "这样 `sshd` 每次启动时都会生成新的主机密钥", "这样 `sshd` 留在前台，容器就能一直活着", "这样 `sshd` 监听 2222 端口，而不是 22 端口"], "a": 2, "why": "`-D` 表示不要进入后台。容器只在它的主程序运行期间存活，所以进入后台的服务会让容器终止。", "at": "linux_lab.html#sec-ssh"},
  {"id": "b1_01", "unit": "bentoml", "d": -0.8, "type": "mcq", "q": "BentoML 把一个 Python 类变成网络服务。什么是网络服务？", "o": ["一个 Python 文件，把答案打印一次，然后就结束自己的工作", "一个一直开着的程序，等待并回答其他程序发给它的内容", "一个网页，显示某个函数的代码，这样别人就可以复制它", "一个文件夹，里面有一份单独的 Python 和某个项目的库"], "a": 1, "why": "函数只回答它自己所在的程序，而且只在那个程序运行期间。服务一直开着，回答任何能访问到它的程序。", "at": "bentoml.html#function"},
  {"id": "b1_02", "unit": "bentoml", "d": -0.5, "type": "mcq", "q": "命令 `python -m venv venv` 会创建什么？", "o": ["一个名叫 `venv` 的新网络服务，它在端口 3000 等待问题", "一个名叫 `venv.py` 的文件，里面有 BentoML 和 `httpx` 的代码", "一份单独的 Python，带有它自己的库，放在名叫 `venv` 的文件夹里", "一个名叫 `venv` 的 Bento，放在你主文件夹下名叫 `bentoml` 的文件夹里"], "a": 2, "why": "它创建一个虚拟环境。这样这个项目的库就和你其他项目的库分开了。", "at": "bentoml.html#step1"},
  {"id": "b1_03", "unit": "bentoml", "d": -0.4, "type": "fill", "q": "阅读 `price.py`。`price(\"dumplings\", 6)` 返回的数字是多少？", "code": "MENU = {\"beef noodles\": 60, \"fried rice\": 50, \"dumplings\": 40}\n\ndef price(dish, bowls):\n    return MENU[dish] * bowls", "answers": ["240"], "why": "`MENU[\"dumplings\"]` 是 40，40 乘以 6 等于 240。", "at": "bentoml.html#step2"},
  {"id": "b1_04", "unit": "bentoml", "d": 0.0, "type": "mcq", "q": "在 `bentoml serve service:Shop` 里，`service:Shop` 的两半分别是什么？", "o": ["服务的地址，然后是它等待请求的端口", "Bento 的名字，然后是 BentoML 自己起的版本号", "文件里的类，然后是这个类里面的方法", "文件 `service.py` 去掉 `.py`，然后是文件里的类"], "a": 3, "why": "冒号前面的词是文件，冒号后面的词是 BentoML 要提供服务的类。", "at": "bentoml.html#step4"},
  {"id": "b1_05", "unit": "bentoml", "d": 0.1, "type": "mcq", "q": "你运行 `bentoml serve service:Shop`。它打印出 `Service Shop initialized`，然后终端没有给你新的一行来输入。这是什么意思？", "o": ["服务正在运行，等待着问题", "服务因错误而停止，必须重新启动", "BentoML 还在安装它需要的库", "服务已经完成了工作，并自己关闭了"], "a": 0, "why": "服务不会结束。它等待着，每来一个请求就写一行。Ctrl+C 可以停止它。", "at": "bentoml.html#step4"},
  {"id": "b1_06", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "这个 BentoML 服务运行在 3000 端口。客户端把请求发到 `http://localhost:3000/price`。`/price` 是从哪里来的？", "code": "import bentoml\n\nMENU = {\"beef noodles\": 60, \"fried rice\": 50, \"dumplings\": 40}\n\n@bentoml.service\nclass Shop:\n    @bentoml.api\n    def price(self, dish: str, bowls: int) -> int:\n        return MENU[dish] * bowls", "o": ["来自 Python 文件的名字，去掉 `.py`", "来自标有 `@bentoml.api` 的方法的名字", "来自你在文件 `bentofile.yaml` 里写的一行", "来自客户端随请求发送的 JSON 里的一个键"], "a": 1, "why": "方法的名字成为地址的一部分。第二个带 `@bentoml.api` 的方法会产生第二个地址。", "at": "bentoml.html#step5"},
  {"id": "b1_07", "unit": "bentoml", "d": 0.5, "type": "mcq", "q": "一个 BentoML 服务正在运行。它是用 `bentoml serve service:Shop` 启动的。你修改了 `service.py` 里的代码并保存了文件。在服务使用新代码之前，你必须做什么？", "o": ["什么都不用做：每来一个请求它都会重新读文件", "给它发两个请求，这样服务会重新读文件", "用 Ctrl+C 停止服务，再重新启动它", "在虚拟环境里重新安装 BentoML"], "a": 2, "why": "正在运行的服务看不到文件里的改动，直到你重新启动它。", "at": "bentoml.html#step5"},
  {"id": "b1_08", "unit": "bentoml", "d": 0.3, "type": "mcq", "q": "这个文件是一个 BentoML 服务的客户端。文件里没有函数 `price`。你运行这个文件时，`price` 在哪里运行？", "code": "import bentoml\n\nshop = bentoml.SyncHTTPClient(\"http://localhost:3000\")\nprint(shop.price(dish=\"beef noodles\", bowls=2))", "o": ["在那个地址上的服务里：问题传到那里，答案再传回来", "在这个文件内部，因为 BentoML 启动时把函数复制进了客户端", "在网页浏览器里，它打开地址 `http://localhost:3000` 来运行它", "哪里都不运行：客户端记得上一次运行时得到的答案"], "a": 0, "why": "`bentoml.SyncHTTPClient` 创建一个客户端。它知道服务的地址，并把每次调用作为请求发给服务。", "at": "bentoml.html#step5"},
  {"id": "b2_01", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "BentoML 服务的一个方法标有 `@bentoml.api`，并带有类型提示 `bowls: int`。客户端向它发送 JSON `{\"dish\": \"dumplings\", \"bowls\": \"five\"}`。返回的是什么？", "o": ["状态码 200，答案是 0。BentoML 把一个单词算作零碗，方法运行了", "状态码 500。BentoML 放进了这个单词，随后服务自己在求和时失败了", "状态码 404。BentoML 找不到这份订单里的菜，方法没有运行", "状态码 400。BentoML 发现 “five” 不是 `int`，方法没有运行"], "a": 3, "why": "BentoML 在方法运行之前，按类型提示检查每个请求。回复里指出了出错的位置：`\"loc\":[\"bowls\"]`。", "at": "bentoml.html#step6"},
  {"id": "b2_02", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "这个 BentoML 服务收到一个请求，JSON 是 `{\"dish\": \"pizza\", \"bowls\": 2}`。两个类型都对，但 `MENU` 里没有键 `\"pizza\"`。会发生什么？", "code": "import bentoml\n\nMENU = {\"beef noodles\": 60, \"fried rice\": 50, \"dumplings\": 40}\n\n@bentoml.service\nclass Shop:\n    @bentoml.api\n    def price(self, dish: str, bowls: int) -> int:\n        return MENU[dish] * bowls", "o": ["状态码 400。BentoML 发现 `MENU` 没有这个键，方法没有运行", "状态码 500。方法运行了，`MENU[\"pizza\"]` 在服务里抛出了 `KeyError`", "状态码 200，答案是 0，因为字典里没有的键，它的值就是 0", "状态码 500。服务停止了，必须重新启动才能处理下一个请求"], "a": 1, "why": "类型提示检查的是值的种类。它无法知道字典有哪些键。出错之后，服务会继续运行。", "at": "bentoml.html#step6"},
  {"id": "b2_03", "unit": "bentoml", "d": -0.3, "type": "mcq", "q": "网络服务在每个回复的开头放一个状态码。状态码的第一位数字告诉你什么？", "o": ["4 表示服务失败了，5 表示请求有错", "4 表示答案是数字，5 表示答案是文本", "4 表示请求有错，5 表示服务自己失败了", "4 和 5 都表示请求顺利，2 表示请求失败了"], "a": 2, "why": "以 4 开头的状态码，比如 400 或 404，表示请求有错。以 5 开头的状态码，比如 500，表示服务失败了。200 表示请求顺利。", "at": "bentoml.html#step6"},
  {"id": "b2_04", "unit": "bentoml", "d": 0.1, "type": "mcq", "q": "这个 BentoML 服务启动一次，然后回答了 100 个请求。`__init__` 里的 `print` 运行多少次？", "code": "@bentoml.service\nclass Shop:\n    def __init__(self):\n        with open(\"menu.json\", encoding=\"utf-8\") as file:\n            self.menu = json.load(file)\n        print(\"The menu has\", len(self.menu), \"dishes.\")\n\n    @bentoml.api\n    def dishes(self) -> list[str]:\n        return list(self.menu)", "o": ["一次，在服务启动时", "100 次：每个请求一次", "JSON 文件里每个键一次", "一次也不：服务不能打印"], "a": 0, "why": "Python 在创建这个类的对象时运行 `__init__`，而 BentoML 只创建这个对象一次，就在服务启动时。", "at": "bentoml.html#step7"},
  {"id": "b2_05", "unit": "bentoml", "d": 0.5, "type": "mcq", "q": "这个方法是 BentoML 服务的一部分。`self.menu` 是一个没有键 `\"pizza\"` 的字典。请求菜品 `\"pizza\"` 会得到什么？", "code": "    @bentoml.api\n    def price(self, dish: str, bowls: int) -> int:\n        if dish not in self.menu:\n            raise bentoml.exceptions.NotFound(\"We do not sell \" + dish + \".\")\n        return self.menu[dish] * bowls", "o": ["状态码 500 和 `An unexpected error has occurred`", "状态码 400 和出错的位置", "状态码 200 和这道菜的价格 0", "状态码 404 和 `raise` 给出的消息"], "a": 3, "why": "`raise` 停止方法并报告错误。`bentoml.exceptions.NotFound` 给出状态码 404，回复里包含代码写下的消息。", "at": "bentoml.html#step7"},
  {"id": "b2_06", "unit": "bentoml", "d": 0.3, "type": "mcq", "q": "在这个 BentoML 服务里，`self.menu` 是做什么用的？", "code": "@bentoml.service\nclass Shop:\n    def __init__(self):\n        with open(\"menu.json\", encoding=\"utf-8\") as file:\n            self.menu = json.load(file)\n\n    @bentoml.api\n    def dishes(self) -> list[str]:\n        return list(self.menu)\n\n    @bentoml.api\n    def price(self, dish: str, bowls: int) -> int:\n        return self.menu[dish] * bowls", "o": ["它把菜单变成一个网络地址 `/menu`，其他程序可以向它请求菜品列表", "它把字典保存在对象里，这样类的每个方法都能读取它", "它把菜单发给每个客户端，这样客户端可以自己算出价格", "每当有程序询问价格时，它就把菜单存回 `menu.json`"], "a": 1, "why": "`self` 是一个类的各个方法共享数据的方式。`__init__` 加载字典，其他方法以 `self.menu` 的形式读取它。", "at": "bentoml.html#step7"},
  {"id": "b2_07", "unit": "bentoml", "d": 0.2, "type": "fill", "q": "`bentoml ____` 把服务打包成一个 Bento。请输入缺少的那个英文单词。", "answers": ["build", "bentoml build"], "why": "`bentoml build` 读取 `bentofile.yaml`，这个文件说明要打包哪个服务。", "at": "bentoml.html#step8"},
  {"id": "b2_08", "unit": "bentoml", "d": 0.6, "type": "mcq", "q": "你用 `bentoml build` 把文件 `shop.py` 里的服务打包成一个名为 `shop` 的 Bento。之后你用 `bentoml serve shop:latest` 启动服务。它读取的是哪个 `shop.py`？", "o": ["你文件夹里的那个，和往常一样。只有从那个文件夹启动时它才会回答", "电脑上任何位置最新的名为 `shop.py` 的文件，BentoML 通过搜索找到它", "Bento 里面的那份副本。即使从别的文件夹启动，它也会回答", "都不是：Bento 里没有 Python 文件，只有库的列表和 API 的列表"], "a": 2, "why": "Bento 里有你文件的一份副本、库的列表，以及对 API 的描述。`latest` 表示最新的版本。", "at": "bentoml.html#step8"},
  {"id": "b3_01", "unit": "bentoml", "d": 0.0, "type": "mcq", "q": "这个 BentoML 服务在 `__init__` 里加载 AI 模型，而不是在 `yes_or_no` 里面。为什么？", "code": "@bentoml.service(traffic={\"timeout\": 300})\nclass Judge:\n    def __init__(self):\n        self.model = Decider(\"Mapika/decider-2b\")\n\n    @bentoml.api\n    def yes_or_no(self, text: str, question: str) -> float:\n        questions = {\"answer\": {\"type\": \"noul\", \"instructions\": question}}\n        result = self.model.system_one(text, questions)\n        return result[\"answers\"][\"answer\"][\"noul\"]", "o": ["带 `@bentoml.api` 的方法不能使用模型，所以模型只能放在外面", "加载很慢。放在 `yes_or_no` 里面，每个请求都得等它加载", "`__init__` 是类里唯一可以打开并读取磁盘上文件的方法", "模型对一个方法来说太大了，而 Python 给 `__init__` 更多内存"], "a": 1, "why": "`__init__` 只运行一次，在服务启动时。模型在那里加载并保存在 `self` 里，每个请求只是使用它。", "at": "bentoml.html#judge"},
  {"id": "b3_02", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "`@bentoml.service(traffic={\"timeout\": 300})` 是做什么用的？", "o": ["它在服务回答了 300 个请求之后停止服务，让模型可以休息", "它让模型把每个回答控制在 300 个词以内，这样回答来得快", "它允许 300 个程序同时向服务提问，并让其他程序等待", "它允许一个请求花 300 秒。没有它，BentoML 在 60 秒后就放弃"], "a": 3, "why": "BentoML 在 60 秒后放弃一个请求。AI 模型的第一个回答可能比这更久，所以这个选项给请求更多时间。", "at": "bentoml.html#judge"},
  {"id": "b3_03", "unit": "bentoml", "d": 0.5, "type": "mcq", "q": "一个 BentoML 服务设置了 `traffic={\"timeout\": 300}`，它回答一次需要 50 秒。一个程序用 `bentoml.SyncHTTPClient(\"http://localhost:3000\", timeout=300)` 创建客户端。如果在客户端里去掉 `timeout=300`，会发生什么？", "o": ["客户端等待 30 秒，然后报错停止", "客户端永远等下去，从不自己停止", "客户端每隔 30 秒把同一个请求再发一次", "没有变化：服务自己的 300 的 `timeout` 就够了"], "a": 0, "why": "有两个计时器。BentoML 服务在 60 秒后放弃一个请求，而 BentoML 客户端在 30 秒后停止等待。慢的模型需要把两者都调高。", "at": "bentoml.html#judge"},
  {"id": "b3_04", "unit": "bentoml", "d": -0.3, "type": "mcq", "q": "模型 decider-2b 用一个数字来回答关于一段文本的是非问题。文本是 “Two bowls of beef noodles, please. No chilli.”，问题是 “Does the customer want it spicy?” 模型返回 `0.0085`。这是什么意思？", "o": ["是：这个数字不是零", "模型不知道", "否：这个数字接近 0", "模型在文本里发现了一个错误"], "a": 2, "why": "decider-2b 不写文字。它给出一个数字，表示它有多确定。接近 1 表示“是”，接近 0 表示“否”。", "at": "bentoml.html#judge"},
  {"id": "b3_05", "unit": "bentoml", "d": 0.2, "type": "mcq", "q": "这个 BentoML 服务从语言模型得到答案，但它的类没有 `__init__`。为什么？", "code": "import bentoml\nimport httpx\n\nOLLAMA = \"http://localhost:11434/api/generate\"\nMODEL = \"llama3.2\"\n\n@bentoml.service\nclass Talker:\n    @bentoml.api\n    def say(self, prompt: str) -> str:\n        request = {\"model\": MODEL, \"prompt\": prompt, \"stream\": False}\n        reply = httpx.post(OLLAMA, json=request, timeout=120)\n        return reply.json()[\"response\"]", "o": ["LLM 太大，无法保存在 `self` 里，所以每次都在方法 `say` 里重新加载", "它没有东西要加载：模型由 Ollama 保管，服务只把每个问题转交过去", "BentoML 会为每个没有 `__init__` 的类写一个，并在那里加载模型", "模型由客户端程序加载，客户端把它连同每条提示词一起发给服务"], "a": 1, "why": "Ollama 也是一个服务。这个服务向它提问，再把答案传回去。服务常常会去问别的服务。", "at": "bentoml.html#talker"},
  {"id": "b3_06", "unit": "bentoml", "d": 0.0, "type": "fill", "q": "Ollama 作为服务运行在你的电脑上。如果你没有改过，它在哪个端口等待？请输入这个数字。", "answers": ["11434"], "why": "Ollama 一直在后台开着，在 11434 端口等待。程序通过 `http://localhost:11434/api/generate` 这样的地址向它提问。", "at": "bentoml.html#talker"},
  {"id": "b3_07", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "一个 Python 程序向 Ollama 的 `/api/generate` 发送请求。请求里有 `\"stream\": False`。这是在要求什么？", "o": ["一个纯文本的回答，没有标点，也没有换行", "一个 Ollama 不会保存到磁盘任何地方的回答", "一个不需要互联网、在这台电脑上运行的模型", "一次给出整个回答，而不是一个词一个词地给"], "a": 3, "why": "整个回答放在一个回复里时，`reply.json()[\"response\"]` 里就是这段文本。", "at": "bentoml.html#talker"},
  {"id": "b3_08", "unit": "bentoml", "d": 0.5, "type": "mcq", "q": "一个 AI 模型返回 0 到 1 之间的数字。另一个 AI 模型返回一个句子。BentoML 服务可以提供其中任何一个。对于提问的程序来说，区别是什么？", "o": ["程序可以直接使用数字：`if answer > 0.5`。句子是给人读的", "BentoML 能提供返回数字的方法，但句子还需要第二个库", "句子总是比数字更正确，所以第二个模型是更好的那个", "数字可以作为 JSON 在网络上传输，但句子必须作为文件发送"], "a": 0, "why": "BentoML 以同样的方式提供这两者。不同的是谁能使用这个答案。", "at": "bentoml.html#talker"},
  {"id": "b4_01", "unit": "bentoml", "d": 0.6, "type": "mcq", "q": "在这个 BentoML 服务里，`FORM` 是一个带表单的网页的文本。为什么这个网页来自一个普通函数 `form_page`，用 `@bentoml.asgi_app` 挂上去，而不是来自一个带 `@bentoml.api` 的方法？", "code": "def form_page(request):\n    return HTMLResponse(FORM)\n\n@bentoml.service\n@bentoml.asgi_app(Starlette(routes=[Route(\"/\", form_page)]), path=\"/shop\")\nclass Shop:\n    @bentoml.api\n    def order(self, dish: str, bowls: int) -> HTML:\n        total = MENU[dish] * bowls\n        line = str(bowls) + \" bowls of \" + dish + \" cost \" + str(total) + \".\"\n        return \"<h1>\" + line + \"</h1><p><a href='/shop/'>Ask again</a></p>\"", "o": ["BentoML API 只能返回数字，而表单是一长段文本", "表单是作为 JSON 发送的，而浏览器不能把 JSON 显示成网页", "浏览器用 GET 打开页面，而 BentoML API 只回答 POST", "Starlette 是随 BentoML 一起来的，所以服务的每个页面都得用它来写"], "a": 2, "why": "浏览器用 GET 请求打开页面，而 BentoML API 只回答 POST 请求。表单本身是用 `method=\"post\"` 发送的，所以普通的 API 就能读取它。", "at": "bentoml.html#webshop"},
  {"id": "b4_02", "unit": "bentoml", "d": 0.3, "type": "mcq", "q": "网页表单把所有内容都作为文本发送，所以浏览器把 `\"3\"` 发给 BentoML API `order(self, dish: str, bowls: int)`。是什么把它变成数字 3？", "o": ["浏览器，在发送表单之前", "类型提示 `bowls: int`", "你在 `order` 里写的一行 `int(bowls)`", "没有：方法拿到的是文本 `\"3\"`"], "a": 1, "why": "BentoML 读取类型提示，并且用检查 JSON 的同样方式检查表单。在该填数字的地方填一个单词，会得到状态码 400。", "at": "bentoml.html#webshop"},
  {"id": "b4_03", "unit": "bentoml", "d": 0.2, "type": "mcq", "q": "BentoML 怎么知道表单的哪个字段对应 `order(self, dish: str, bowls: int)` 的哪个值？", "o": ["按表单里字段的顺序：第一个字段对第一个值", "按你在文件 `bentofile.yaml` 里写的一张表", "按类型：文本给 `dish`，数字给 `bowls`", "按名字：字段是 `name=\"dish\"` 和 `name=\"bowls\"`"], "a": 3, "why": "全部的接线就是这些。BentoML 把每个字段交给同名的那个值。", "at": "bentoml.html#webshop"},
  {"id": "b4_04", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "用 Python 库 `httpx`，一个程序调用 `httpx.post(address, json=order)`，另一个调用 `httpx.post(address, data=form)`。两者都发给同一个 BentoML API。区别是什么？", "o": ["`json=` 发送 JSON，`data=` 发送表单。API 两种都接受", "`data=` 用于数字，`json=` 用于文本。API 只接受一种", "`data=` 发出 GET 请求，`json=` 发出 POST 请求", "没有区别：它们是同一个东西的两个名字，都发送 JSON"], "a": 0, "why": "`data=` 做的事和你按下表单按钮时浏览器做的一样。BentoML API 读取表单的方式和读取 JSON 的方式相同。", "at": "bentoml.html#webshop"},
  {"id": "b4_05", "unit": "bentoml", "d": -0.3, "type": "mcq", "q": "一个 BentoML 服务运行在你电脑上的 `http://localhost:3000`。同一个 Wi-Fi 上的一部手机打开这个地址。为什么它访问不到服务？", "o": ["手机上没有 Python，而 BentoML 需要它", "每部手机的 3000 端口都被制造商关闭了", "在手机上，`localhost` 指的是手机自己", "BentoML 不支持手机，只支持电脑"], "a": 2, "why": "`localhost` 是每台设备对自己的称呼。另一台设备需要你的电脑在局域网上的地址，它看起来像 `192.168.1.20`。", "at": "bentoml.html#network"},
  {"id": "b4_06", "unit": "bentoml", "d": 0.4, "type": "mcq", "q": "在 `bentoml serve webshop:Shop --host 127.0.0.1` 里，`--host 127.0.0.1` 起什么作用？", "o": ["它把服务向整个互联网开放", "它让服务只在你自己的电脑上可用", "它把端口从 3000 改成 127", "它让服务回答得更快"], "a": 1, "why": "`127.0.0.1` 是用数字写的 `localhost`。没有这个选项，BentoML 会在电脑所连接的每个网络上监听。", "at": "bentoml.html#network"},
  {"id": "b4_07", "unit": "bentoml", "d": 0.2, "type": "mcq", "q": "一个服务运行在你的电脑上。这台电脑在你 Wi-Fi 上的地址是 `192.168.1.20`。另一栋房子里的朋友输入了这个地址。为什么不行？", "o": ["这个地址错了一个数字", "你的朋友需要 BentoML 才能打开它", "这个服务只接待一个访客", "局域网地址到你的路由器就止步了"], "a": 3, "why": "局域网就是同一个 Wi-Fi 上的那些设备。像 `192.168.1.20` 这样的地址在这个网络之外没有任何意义。", "at": "bentoml.html#network"},
  {"id": "b4_08", "unit": "bentoml", "d": 0.5, "type": "mcq", "q": "一个服务运行在你电脑的 3000 端口。Tailscale 命令 `tailscale funnel 3000` 给你什么？", "o": ["3000 端口上这个服务的一个公开地址", "一个以 100 开头的地址，只供你自己的设备使用", "这个服务在 Tailscale 的电脑上的一份副本", "这个服务的一个密码"], "a": 0, "why": "Tailscale Funnel 给服务一个公开地址，任何拿到它的人不用 Tailscale 就能打开。Ctrl+C 停止这条命令并关闭这个地址。", "at": "bentoml.html#tailscale"},
  {"id": "re_01", "unit": "reverse_engineer", "d": -0.8, "type": "mcq", "q": "什么是对一个程序做逆向工程？", "o": ["用一种更快的编程语言，把一个程序再写一遍", "把一个程序运行很多次，找出会让它停下来的输入", "从做好的程序出发，倒推出它是怎样构建的", "把一个程序变小，让它在浏览器里加载得更快"], "a": 2, "why": "作者是从设计走到做好的程序。逆向工程走的是相反的方向：从发布出来的东西倒推回它是怎样工作的。", "at": "reverse_engineer.html#overview"},
  {"id": "re_02", "unit": "reverse_engineer", "d": -0.5, "type": "mcq", "q": "一个工具对程序做静态分析。这是什么意思？", "o": ["它通过读程序的文件来研究程序，并不运行它", "它把程序运行一次，记下用到的每一行", "它修改程序的文件，让程序运行得更快", "它向程序的作者要设计图和注释"], "a": 0, "why": "静态的意思是什么都不动：工具只读文件。对你信不过的程序，这样做是安全的，但它看不到程序运行时才决定的东西。", "at": "reverse_engineer.html#overview"},
  {"id": "re_03", "unit": "reverse_engineer", "d": 0.2, "type": "mcq", "q": "一个工具不运行这段 JavaScript，只是读它。它能说出 `\"Sawasdee\"` 写在哪里。为什么它说不出 `greet` 返回的就是这个词？", "code": "const WORDS = { th: \"Sawasdee\", en: \"Hello\" };\n\nfunction greet(name, language) {\n  return WORDS[language] + \", \" + name + \"!\";\n}", "o": ["因为这个词在引号里，工具读不了引号里的内容", "因为 `WORDS` 是常量，工具没法跟踪常量", "因为这个函数有两个参数，这种工具只能跟踪一个", "因为取哪个词取决于 `language`，而它的值只有在运行时才有"], "a": 3, "why": "`WORDS[language]` 用一个只在程序运行时才存在的值来挑词。只读不运行的工具没法知道这个值，认真的工具会把这一点说出来。", "at": "reverse_engineer.html#overview"},
  {"id": "re_04", "unit": "reverse_engineer", "d": -0.1, "type": "mcq", "q": "一个逆向工程工具把它的一条发现标为“推断的”，而不是“看到的”。这说明什么？", "o": ["工具是在文件里它指明的某个位置看到这条发现的", "这条发现是工具根据看到的东西推出来的，可能是错的", "工具什么都没找到，完全没有给出回答", "工具运行了程序，亲眼看着这条发现发生"], "a": 1, "why": "“看到的”是指在文件里看见了。“推断的”是指根据看到的东西推出来的。推断出来的发现，最应该先去检查。", "at": "reverse_engineer.html#step2"},
  {"id": "re_07", "unit": "reverse_engineer", "d": 0.3, "type": "mcq", "q": "一个工具查看一个编译好的 Mac 程序，把 `_printf` 列为从 `libSystem` 导入的。编译好的程序的“导入”是什么？", "o": ["程序自己持有、并提供给别的程序使用的函数", "程序要用、但自己并不持有的函数：它在一个库里", "程序每次启动时都要从磁盘读取的一个文件", "程序运行时打印在屏幕上的一段文字"], "a": 1, "why": "程序调用了 `printf`，但 `printf` 的代码在库里。导入的列表显示了一个程序从它加载的每个库里拿了什么。", "at": "reverse_engineer.html#step7"},
  {"id": "re_08", "unit": "reverse_engineer", "d": 0.1, "type": "fill", "q": "Mac 应用看起来是一个文件，其实是一个名字以 `.app` 结尾的文件夹。这样的文件夹叫什么？输入一个英文单词。", "answers": ["bundle"], "why": "这个文件夹叫 bundle。编译好的程序是里面的一个文件，旁边还有应用的卡片、它的设计和别的文件。", "at": "reverse_engineer.html#step4"},
  {"id": "re_09", "unit": "reverse_engineer", "d": 0.2, "type": "mcq", "q": "一个 Mac 程序由它的开发者签了名。macOS 可以靠签名检查哪两件事？", "o": ["程序运行得有多快，以及程序需要多少内存", "程序是用哪种语言写的，以及它有多少个文件", "程序是免费的，而且它的源代码是公开的", "程序是谁做的，以及做好之后没有人改动过"], "a": 3, "why": "只有制作者才能加上签名。之后只要程序被改动一个字节，签名就对不上了。", "at": "reverse_engineer.html#step6"},
  {"id": "re_10", "unit": "reverse_engineer", "d": 0.5, "type": "mcq", "q": "在一个 Mac 应用里，菜单的设计为一个菜单项存着一个 action。这个 action 说的是什么？", "o": ["这一项被选中时要调用哪个方法，以及是哪个类的方法", "这个菜单项的文字用什么颜色、多大的字号", "应用启动以来，用户选过这一项多少次", "还有哪些应用被允许在屏幕上打开同一个菜单"], "a": 0, "why": "action 把用户的操作和代码连起来：一个方法的名字，以及它所属的类。要回答“这个菜单项做什么”，它是答案的前一半。", "at": "reverse_engineer.html#step8"},
  {"id": "re_11", "unit": "reverse_engineer", "d": 0.5, "type": "mcq", "q": "你用工具 B 检查工具 A 的答案，两者一致。后来你发现，工具 A 的答案就是靠运行工具 B 得来的。这种一致有多大价值？", "o": ["很大：两个工具一致，答案就一定是对的", "毫无价值：它证明两个工具都坏了", "很小：来自同一个来源的答案，即使错了也会一致", "很大：它证明工具 B 是工具 A 的制作者写的"], "a": 2, "why": "第二种意见只有来自不同的地方才算数。最好的第二意见是程序的源代码，如果你有的话。", "at": "reverse_engineer.html#step9"},
  {"id": "re_12", "unit": "reverse_engineer", "d": 0.0, "type": "mcq", "q": "你想对一个别人写的程序做逆向工程。你首先要查什么？", "o": ["程序有多少个文件，好知道这件事要花多久", "程序的许可证，看看你是否被允许这样做", "程序的价格，因为只有免费的程序才能读", "程序有多旧，因为旧程序的秘密更少"], "a": 1, "why": "很多许可证禁止逆向工程，而 MIT 这样的开源许可证允许这样做。动手之前先读许可证。", "at": "reverse_engineer.html#app"},
  {"id": "flask_01", "unit": "flask", "d": -0.8, "type": "mcq", "q": "`@app.route(\"/\")` 是什么意思？", "o": ["有人访问首页时，调用这个函数", "启动服务器，并让它一直运行在端口 5000 上", "每次你保存修改后，重新加载这个文件", "告诉 Flask 哪个文件夹存放你的 HTML 模板"], "a": 0, "why": "路由就是地址。单独一个斜杠表示首页，它下面的函数给出访问者看到的内容。", "at": "flask.html#hello"},
  {"id": "flask_02", "unit": "flask", "d": -0.4, "type": "fill", "q": "Flask 在一个文件夹里找 HTML 文件，而且文件夹的名字不能错。这个文件夹叫什么？请输入。", "answers": ["templates", "templates/"], "why": "Flask 把 HTML 放在一个叫 `templates` 的文件夹里。它只找这个名字，别的都不找。", "at": "flask.html#templates"},
  {"id": "flask_03", "unit": "flask", "d": -0.3, "type": "mcq", "q": "`@app.route(\"/hello/<name>\")` 里的尖括号起什么作用？", "o": ["它们标出一个只有指定访问者才能打开的页面", "它们截取地址的一部分，交给函数", "它们让 Flask 对任何没有列出的地址回复 404", "它们告诉模板它要填写哪一个空"], "a": 1, "why": "尖括号是地址里的一个空位。Flask 把它填上，并作为 `name` 交给函数。", "at": "flask.html#routes"},
  {"id": "flask_04", "unit": "flask", "d": 0.0, "type": "mcq", "q": "一个 Flask 路由显示一个带 `method=\"post\"` 的表单。这个路由有 `methods=[\"GET\", \"POST\"]`。你把这个列表去掉。访客发送表单时会发生什么？", "o": ["表单得到 404", "表单被发送两次", "表单得到 405", "输入框到达时是空的"], "a": 2, "why": "GET 显示表单，POST 处理表单，所以路由必须两者都接受。没有这个列表，Flask 不接受 POST，表单得到 405。", "at": "flask.html#form"},
  {"id": "flask_05", "unit": "flask", "d": 0.2, "type": "mcq", "q": "一个 Flask 网站显示访客输入的名字。为什么带 `{{ name }}` 的模板比把名字拼接进一个 HTML 字符串更安全？", "o": ["它让你的密码不出现在 Python 文件里", "它阻止访客打开其他的地址", "它给文本签名，这样访客无法修改它", "它把输入的标签变成无害的普通文字"], "a": 3, "why": "拼接的字符串会把输入的 `<img ...>` 作为真正的标签发回去，浏览器会执行它。模板把同样的文本变成 `&lt;img ...&gt;`，那只是文字。", "at": "flask.html#templates"},
  {"id": "flask_06", "unit": "flask", "d": 0.4, "type": "mcq", "q": "一个 Flask 应用用 Piper 语音朗读文本。`PiperVoice.load` 很慢，而每个请求都调用 `get_voice`。为什么这段代码把语音保存在字典 `loaded` 里？", "code": "loaded = {}\n\ndef get_voice(path):\n    if path not in loaded:\n        loaded[path] = PiperVoice.load(path)\n    return loaded[path]", "o": ["这样每个语音只加载一次，而不是每次访问都加载", "这样每个访客都有一份单独的对话列表", "这样浏览器不会重放旧的声音文件", "这样 Flask 不用你写路由就能提供声音"], "a": 0, "why": "加载语音很慢，朗读很快。字典在某个语音第一次被请求时加载它，然后为之后的每个请求保留它。", "at": "flask.html#speak"},
  {"id": "flask_07", "unit": "flask", "d": 0.6, "type": "mcq", "q": "这个 Python 循环是和 Ollama 里的模型的一段聊天。`ask` 把整个列表 `messages` 发给模型。你把最后一行注释掉。聊天中会发生什么？", "code": "while True:\n    said = input(\"you: \").strip()\n    messages.append({\"role\": \"user\", \"content\": said})\n    reply = ask(messages)\n    print(\"ai:\", reply)\n    messages.append({\"role\": \"assistant\", \"content\": reply})", "o": ["它忘记你的消息，但记得它自己的", "它忘记它自己的回答，但记得你的", "每到新的一轮，它就忘记之前说过的一切", "它崩溃，因为列表里没有 system 那一行"], "a": 1, "why": "模型没有记忆。它只知道你发送的列表里有什么，而现在列表里有你的消息，却没有它的回复。", "at": "flask.html#memory"},
  {"id": "flask_08", "unit": "flask", "d": 0.8, "type": "mcq", "q": "一个 Flask 聊天应用把每个访客的消息保存在 `session` 里。为什么这段代码只存最新的 12 条消息，而不是整个列表？", "code": "REMEMBER = 12          # how many past messages to keep\n\nmessages = session.get(\"messages\", [])\nmessages.append({\"role\": \"user\", \"content\": request.form[\"said\"]})\nmessages.append({\"role\": \"assistant\", \"content\": ask(messages)})\nsession[\"messages\"] = messages[-REMEMBER:]", "o": ["模型读不了超过十二条消息", "密钥只能给很短的一列数据签名", "浏览器会丢弃太大的 cookie", "Flask 在一个会话收到十二个请求后就删除它"], "a": 2, "why": "Flask 把整个会话打包进一个 cookie，而浏览器会悄悄丢弃超过大约 4000 字节的 cookie。", "at": "flask.html#session"},
  {"id": "gradio_01", "unit": "gradio", "d": -0.8, "type": "mcq", "q": "Gradio 是做什么的？", "o": ["把网页变成 Python 函数", "只在这台电脑上画一个窗口", "用你自己的数据训练 AI 模型", "把 Python 函数变成网页"], "a": 3, "why": "Gradio 把 Python 函数变成网页。你写函数，Gradio 画出它周围的一切。", "at": "gradio.html"},
  {"id": "gradio_02", "unit": "gradio", "d": -0.5, "type": "mcq", "q": "在 Gradio 里，`demo.launch()` 做什么？", "o": ["把网页作为 HTML 文件保存到磁盘上", "启动一个小型网络服务器，并打印它的地址", "打开一条通向互联网上所有人的隧道", "安装 Gradio 并打印它的版本号"], "a": 1, "why": "`launch()` 启动一个小型网络服务器并打印它的地址。你在浏览器里打开这个地址，Ctrl-C 可以停止它。", "at": "gradio.html#step1"},
  {"id": "gradio_03", "unit": "gradio", "d": -0.3, "type": "mcq", "q": "在 Gradio 里，`gr.Checkbox` 交给你的函数的是什么？", "o": ["`True` 或 `False`", "它的标签的文本", "一个 0 到 1 之间的数", "一个由选中的词组成的列表"], "a": 0, "why": "每个 Gradio 控件都成为函数的一个参数。`gr.Checkbox` 交出的是 `True` 或 `False`，不需要解析。", "at": "gradio.html#step5"},
  {"id": "gradio_04", "unit": "gradio", "d": 0.1, "type": "mcq", "q": "这个 `reply` 函数被交给 Gradio 的 `gr.ChatInterface`，然后 `talk` 被发给 Ollama。为什么 `reply` 要把每一轮的内容经过 `plain()` 处理？", "code": "def plain(content):\n    if isinstance(content, str):\n        return content\n    return \"\".join(part.get(\"text\", \"\") for part in content\n                   if part.get(\"type\") == \"text\")\n\ndef reply(message, history):\n    talk = [{\"role\": turn[\"role\"], \"content\": plain(turn[\"content\"])}\n            for turn in history]\n    talk.append({\"role\": \"user\", \"content\": message})\n    ...", "o": ["Gradio 给的是一个字符串，但 Ollama 要的是由若干部分组成的列表", "Ollama 读不了含有泰文字母的文本", "Gradio 给的是由若干部分组成的列表，但 Ollama 要的是一个字符串", "除非复制一份，否则 Gradio 会忘记历史记录"], "a": 2, "why": "Gradio 把每一轮的内容作为由若干部分组成的列表交出来，这样一轮里也可以有图片。Ollama 要的是字符串，而 `plain()` 把文字取出来。", "at": "gradio.html#step3"},
  {"id": "gradio_05", "unit": "gradio", "d": 0.4, "type": "mcq", "q": "这个函数被交给 Gradio 的 `gr.ChatInterface`，以流式方式输出模型的回答。你修改 `yield so_far`，让它只产出新的那一小段。你在浏览器里会看到什么？", "code": "def reply(message, history):\n    ...\n    so_far = \"\"\n    with urllib.request.urlopen(request, timeout=300) as response:\n        for line in response:\n            piece = json.loads(line)\n            so_far = so_far + piece[\"message\"][\"content\"]\n            yield so_far", "o": ["回答在屏幕上出现两次", "在整个回答准备好之前什么都不显示", "Gradio 显示一个错误，服务器停止", "每一小段一闪而过；什么都留不下来"], "a": 3, "why": "Gradio 用你产出的内容替换屏幕上已有的内容。它不会追加，所以你必须产出到目前为止的整个回答。", "at": "gradio.html#step4"},
  {"id": "gradio_06", "unit": "gradio", "d": 0.2, "type": "fill", "q": "`demo.launch()` 的哪个参数设为 `True` 时，会让 Gradio 打开一条隧道，并打印一个互联网上任何人都能打开的公开地址？请输入这个参数的名字。", "answers": ["share", "share=true", "share = true"], "why": "`share=True` 把你电脑上的程序放到互联网上。要和 `auth` 一起使用，而且绝不要让它一直开着。", "at": "gradio.html#step6"},
  {"id": "gradio_07", "unit": "gradio", "d": 0.6, "type": "mcq", "q": "一个 Gradio 测验应用把每个玩家的分数保存在 `gr.State` 里，而不是普通的全局变量里。为什么？", "o": ["全局变量存不了由数字组成的字典", "每个浏览器都有自己的一份分数副本", "分数必须显示在屏幕上的一个框里", "全局变量在每次点击按钮后都会被删除"], "a": 1, "why": "Gradio 应用在网络服务器上，两个人可以同时打开它。全局变量由他们所有人共享，而 `gr.State` 给每个浏览器一份自己的副本。", "at": "gradio.html#step7"},
  {"id": "gradio_08", "unit": "gradio", "d": 0.7, "type": "mcq", "q": "这个 smolagents 智能体被问到泰国的首都，模型试图调用一个名为 `web_search` 的工具。为什么它调用不了？", "code": "agent = ToolCallingAgent(tools=[coin_price], model=llm)\nanswer = agent.run(message, max_steps=6)", "o": ["`web_search` 不在 `tools` 列表里", "`max_steps=6` 会阻止对工具的每一次调用", "问题里没有任何币的名称", "Ollama 拦截互联网上的每一次搜索"], "a": 0, "why": "工具列表就是整个权限系统。不在列表里的工具无法被调用。", "at": "gradio.html#step7"},
  {"id": "rag_llm_01", "unit": "rag_llm", "d": -0.8, "type": "mcq", "q": "RAG 改变的是什么？", "o": ["模型自身内部的数字", "模型读到的提示词", "处理器的速度", "模型的许可证"], "a": 1, "why": "RAG 不改变模型的任何一个数字。它把找到的段落放进提示词，放在问题的前面。", "at": "rag_llm.html"},
  {"id": "rag_llm_02", "unit": "rag_llm", "d": -0.5, "type": "mcq", "q": "用户问 “Is there somewhere to park?” 一份文档写着 “There are four spaces for cars behind the building.” 为什么按问题里的词去搜索，在文档里什么都找不到？", "o": ["文档里根本没有关于汽车的段落", "这个问题太短，任何搜索都不行", "嵌入模型只能读泰文文本", "两者没有一个共同的关键词"], "a": 3, "why": "问题和这段话没有一个共同的关键词。这就是为什么 RAG 用嵌入来比较意思，而不是比较词语。", "at": "rag_llm.html#search"},
  {"id": "rag_llm_03", "unit": "rag_llm", "d": -0.3, "type": "mcq", "q": "两个嵌入向量的余弦相似度分数接近 0。这说明什么？", "o": ["这两段文字彼此没什么关系", "这两段文字的意思几乎一样", "搜索对自己的答案没有把握", "其中一段文字太长，模型读不了"], "a": 0, "why": "接近 1，两段文字的意思几乎一样。接近 0，它们彼此没什么关系。", "at": "rag_llm.html#step2"},
  {"id": "rag_llm_04", "unit": "rag_llm", "d": 0.1, "type": "fill", "q": "RAG 程序先把一份文档切成小段落，比如在每个空行处切开。这种切分叫什么？请输入这个英文单词。", "answers": ["chunking"], "why": "把文档切成小块叫做 chunking（分块）。它比别的任何东西都更能决定搜索的效果，因为一块讲两个主题的文字，得到的嵌入向量哪个主题都不像。", "at": "rag_llm.html#step3"},
  {"id": "rag_llm_05", "unit": "rag_llm", "d": 0.4, "type": "mcq", "q": "在 `transformers` 库里，你把 `documents` 传给 `apply_chat_template`，但模型的聊天模板里没有放文档的位置。会发生什么？", "o": ["Python 停下来，报一个关于聊天模板的错误", "这些段落会被放进系统指令里", "模型对每个问题都拒绝回答", "不报错，提示词里也没有任何段落"], "a": 3, "why": "这样的模板会丢掉文档，而且什么也不说。所以每换一个新模型，都要把提示词打印一次，检查段落是否在里面。", "at": "rag_llm.html#step4"},
  {"id": "rag_llm_06", "unit": "rag_llm", "d": 0.0, "type": "mcq", "q": "在这次对 `transformers` 库的调用中，`do_sample=False` 起什么作用？", "code": "output = model.generate(prompt_tokens, max_new_tokens=40, do_sample=False)", "o": ["同一个提示词每次运行都得到同样的回复", "模型写出 40 个新 token 之后就停下来", "模型不读任何文档就直接回答", "打印回复时不带其中的特殊词元"], "a": 0, "why": "`do_sample=False` 让模型每次都选概率最高的词元，所以同一个提示词每次运行都得到同样的回复。", "at": "rag_llm.html#step5"},
  {"id": "rag_llm_07", "unit": "rag_llm", "d": 0.6, "type": "mcq", "q": "一个 RAG 程序会打印搜索找到的段落的标题。对于问题“Can I pay by card?”，第一个标题是 `HOW TO PAY`，而这个段落说商店不收信用卡。模型却仍然回答“Yes, you can pay by card”。哪里出了问题？", "o": ["搜索根本没有找到正确的段落", "搜索找到了，是模型没读对", "文档里没有关于付款的段落", "嵌入模型给的分数太低了"], "a": 1, "why": "找到了不等于读懂了。打印出来的标题说明搜索完成了它的工作，所以错在模型。", "at": "rag_llm.html#step5"},
  {"id": "rag_llm_08", "unit": "rag_llm", "d": 0.3, "type": "mcq", "q": "一个 RAG 系统把搜索找到的 3 个段落交给模型。你把它改成每个提示词里都放进整份文档。什么一定会改变？", "o": ["模型必须用整份文档重新训练一次", "嵌入模型必须换成一个大得多的", "每个提示词的 token 更多，所以回答更慢", "模型没有答案可给时，现在会拒绝回答"], "a": 2, "why": "提示词越长，token 越多，读起来也越久。在一次运行中，整份文档的得分并不比找到的 3 个段落高，而每次回答花的时间大约是原来的三倍。", "at": "rag_llm.html#step6"},
  {"id": "rag_llm_09", "unit": "rag_llm", "d": 0.5, "type": "mcq", "q": "一个 RAG 系统的搜索几乎对每个问题都能找到正确的段落。但模型常常根据这个段落给出错误的答案。最值得尝试的是什么？", "o": ["用一个向量数据库来存放段落", "再加一个按词语查找的搜索", "每个提示词里放更多找到的段落", "换一个更大的语言模型来阅读"], "a": 3, "why": "段落找到了而答案还是错的，错就在阅读的模型，所以更好的搜索帮不上忙。在一次测试中，一个大小是原来 4.6 倍的阅读模型把每个找到的段落都读对了。", "at": "rag_llm.html#step7"},
  {"id": "rag_llm_10", "unit": "rag_llm", "d": -0.3, "type": "mcq", "q": "一个 RAG 程序每次启动时都读取一个存放事实的文本文件，并为它生成嵌入向量。有一条事实变了：一个价格涨了。你必须做什么？", "o": ["写新的示例，再把模型训练一次", "把嵌入模型重新下载一次", "修改文本文件；下一次回答就会用它", "删除虚拟环境，然后重新安装"], "a": 2, "why": "用 RAG 时，事实在文本文件里，不在模型里。你改掉那一行并保存，下一个问题就会得到新价格，不需要训练。", "at": "rag_llm.html#step8"},
  {"id": "keyestudio_smart_home_01", "unit": "keyestudio_smart_home", "d": -0.8, "type": "mcq", "q": "ESP32 这类 MicroPython 开发板会用到两个 Python 工具：`esptool` 和 `mpremote`。`mpremote` 是做什么用的？", "o": ["把 MicroPython 固件写进开发板的存储器", "让开发板连上你的 Wi-Fi 并打印它的地址", "读取传感器，并把结果作为字典返回", "把你的文件发送到开发板，并显示它的输出"], "a": 3, "why": "`esptool` 把 MicroPython 装到开发板上。`mpremote` 把你的文件发送到开发板，并显示开发板打印的内容。", "at": "keyestudio_smart_home.html#step1"},
  {"id": "keyestudio_smart_home_02", "unit": "keyestudio_smart_home", "d": -0.5, "type": "mcq", "q": "在这段 MicroPython 代码中，一盏灯接在开发板的 12 号引脚上。`led.value(1)` 起什么作用？", "code": "from machine import Pin\n\nled = Pin(12, Pin.OUT)\nled.value(1)", "o": ["接通电源，于是灯亮起来", "读取引脚，把 0 或 1 返回给你的代码", "让程序等待一秒钟", "把 1 号引脚作为输出交给你的代码"], "a": 0, "why": "`Pin.OUT` 把引脚设为输出。`value(1)` 接通它的电源，灯就亮了。`value(0)` 把它关掉。", "at": "keyestudio_smart_home.html#step2"},
  {"id": "keyestudio_smart_home_03", "unit": "keyestudio_smart_home", "d": -0.3, "type": "fill", "q": "在 MicroPython 中，一个按钮用 `Pin(16, Pin.IN, Pin.PULL_UP)` 来设置。按钮被按住时，`value()` 读到的是哪个数字？请输入这个数字。", "answers": ["0", "zero"], "why": "按钮没有动作时，`Pin.PULL_UP` 把引脚保持在 1。所以没人碰它时按钮读到 1，被按住时读到 0。", "at": "keyestudio_smart_home.html#step3"},
  {"id": "keyestudio_smart_home_04", "unit": "keyestudio_smart_home", "d": 0.1, "type": "mcq", "q": "舵机是一种转到某个角度并停在那里的电机。在 MicroPython 中，你的代码怎样告诉舵机要转到哪个角度？", "o": ["通过每秒钟发出的脉冲个数", "通过它发出的每个脉冲的长度", "通过发送 1 表示开、0 表示关", "通过给它两根线中的一根通电"], "a": 1, "why": "舵机每秒从一个 `PWM` 引脚收到 50 个脉冲，每个脉冲的长度用 `duty(...)` 设置，它告诉舵机角度。", "at": "keyestudio_smart_home.html#step4"},
  {"id": "keyestudio_smart_home_05", "unit": "keyestudio_smart_home", "d": 0.3, "type": "mcq", "q": "这段 MicroPython 代码是开发板上一个小型网络服务器的一部分。它先发回 `status`，再把 `reply` 作为 JSON 发回。这时来了一个路径为 `/garden` 的请求。服务器发回什么？", "code": "def do(path):\n    if path == \"/read\":\n        return read_house()\n    if path == \"/light/on\":\n        led.value(1)\n    elif path == \"/light/off\":\n        led.value(0)\n    else:\n        return None\n    return {\"done\": path}\n\nreply = do(path)\nstatus = \"200 OK\"\nif reply is None:\n    status = \"404 Not Found\"\n    reply = {\"error\": \"the house does not know \" + path}", "o": ["状态 200 和传感器的读数", "什么都不发；它关闭连接", "状态 404 和一条 JSON 错误信息", "状态 200 和 `{\"done\": \"/garden\"}`"], "a": 2, "why": "对于不认识的路径，`do` 返回 `None`，所以状态变成 `404 Not Found`，回复是一条 JSON 格式的错误。404 的意思是所请求的东西不存在。", "at": "keyestudio_smart_home.html#step6"},
  {"id": "keyestudio_smart_home_06", "unit": "keyestudio_smart_home", "d": 0.8, "type": "mcq", "q": "开发板上的一个 MicroPython 网络服务器对路径 `/light/on` 运行 `led.value(1)`，对 `/read` 运行这段代码。一个程序先请求 `/light/on`，再请求 `/read`。第二个回复显示 `'light': 1`。为什么？", "code": "led = Pin(12, Pin.OUT)\nmotion = Pin(14, Pin.IN)\n\ndef read_house():\n    return {\n        \"motion\": motion.value(),\n        \"light\": led.value(),\n    }", "o": ["第一个请求把灯打开了，之后它一直亮着", "光线传感器在那一刻测到房间很亮", "用 `Pin.PULL_UP` 设置的引脚在没有动作时读到 1", "`/read` 路径在读取之前会先把灯打开"], "a": 0, "why": "括号里什么都不写的 `led.value()` 是读取引脚。第一个请求把它设成了 1，在另一个请求把它设成 0 之前，它一直是 1。", "at": "keyestudio_smart_home.html#step7"},
  {"id": "keyestudio_smart_home_07", "unit": "keyestudio_smart_home", "d": 0.4, "type": "mcq", "q": "一块 MicroPython 开发板运行着一个网络服务器。它读取自己的传感器，并在收到请求时开关风扇。这个程序在你的电脑上运行，负责发送请求。你想让风扇在 35 度而不是 28 度时启动。你要改什么？", "code": "HOT = 28    # degrees Celsius\n\nreadings = ask(\"/read\")\nif readings[\"temperature\"] >= HOT:\n    ask(\"/fan/on\")\nelse:\n    ask(\"/fan/off\")", "o": ["改服务器文件，然后再把它发送到开发板", "改开发板联网时用的 Wi-Fi 设置", "改开发板启动时运行的 `main.py` 文件", "只改这个程序里的 `HOT`，就在你自己的电脑上"], "a": 3, "why": "规则在你电脑上的程序里，不在开发板上。你改的是那个程序，开发板继续运行同一个服务器文件。", "at": "keyestudio_smart_home.html#step8"},
  {"id": "thai_speech_to_text_01", "unit": "thai_speech_to_text", "d": -0.7, "type": "mcq", "q": "一个 Python 脚本用 `faster-whisper` 库把录音转成文字。录音是在哪里被转成文字的？", "o": ["在浏览器里的一个免费网站上", "上传之后，在 OpenAI 的服务器上", "在与电脑相连的一个手机应用上", "在你自己的电脑上，不用上传"], "a": 3, "why": "`faster-whisper` 在你自己的电脑上运行 Whisper 模型。什么都不上传。第一次运行会下载模型，只下载一次。", "at": "thai_speech_to_text.html"},
  {"id": "thai_speech_to_text_02", "unit": "thai_speech_to_text", "d": -0.3, "type": "mcq", "q": "这一行用了一个 `faster-whisper` 模型。`language=\"th\"` 起什么作用？", "code": "lines, info = model.transcribe(\"recording.wav\", language=\"th\", beam_size=5)", "o": ["它告诉模型不要去猜是哪种语言", "它把泰语语音翻译成英语文字", "它下载一个更小的、只懂泰语的模型", "它给每一行加上泰语的时间戳"], "a": 0, "why": "不写 `language` 时，Whisper 会根据音频来猜语言。`language=\"th\"` 告诉它语音是泰语，所以它不用猜。", "at": "thai_speech_to_text.html"},
  {"id": "thai_speech_to_text_03", "unit": "thai_speech_to_text", "d": 0.1, "type": "mcq", "q": "在 `faster-whisper` 中，`model.transcribe(...)` 里的 `beam_size=5` 让模型做什么？", "o": ["先把录音切成相等的五段", "权衡五个可能的句子，留下最好的", "把录音跑五遍，再把文字取平均", "每一秒钟打印五行文字"], "a": 1, "why": "用 `beam_size=5` 时，模型一边转写一边权衡五个可能的句子，留下最好的那个，而不是只取第一个。", "at": "thai_speech_to_text.html"},
  {"id": "thai_speech_to_text_04", "unit": "thai_speech_to_text", "d": 0.4, "type": "mcq", "q": "Whisper 有不同的大小。在泰语语音上，`large-v3` 模型和 `tiny` 模型相比怎么样？", "o": ["下载量更小，错的字符更多", "下载量相同，错误率也相同", "下载量更大，错的字符少得多", "下载量更大，错的字符多得多"], "a": 2, "why": "`tiny` 的下载量最小，错的字符最多。`large-v3` 的下载量最大，错的字符最少。", "at": "thai_speech_to_text.html"},
  {"id": "thai_speech_to_text_05", "unit": "thai_speech_to_text", "d": 0.6, "type": "mcq", "q": "一个检查程序读取一份语音转文字的文稿。它给每一行你应该再听一遍的内容打上标记。有一行没有被标记。这告诉你什么？", "o": ["这并不能证明这一行是对的", "这证明这一行是正确的泰语", "这说明这一行里没有语音", "这说明模型跳过了这一行"], "a": 0, "why": "这样的检查程序只告诉你哪里该再听一遍。没有标记的一行仍然可能是错的。", "at": "thai_speech_to_text.html#res"},
  {"id": "python_to_exe_01", "unit": "python_to_exe", "d": -0.8, "type": "mcq", "q": "PyInstaller 对你的脚本做什么？", "o": ["它用一种比 Python 更快的语言重写脚本。", "它把脚本和 Python 本身一起复制到一个应用里。", "它替你在对方的电脑上安装 Python。", "它给脚本签名，让 Windows 永远信任这个应用。"], "a": 1, "why": "PyInstaller 找出脚本需要的一切，并把它们全部复制到一个文件夹或一个文件里，Python 本身也包括在内。", "at": "python_to_exe.html"},
  {"id": "python_to_exe_02", "unit": "python_to_exe", "d": -0.5, "type": "mcq", "q": "你用 PyInstaller 构建 `hello.py`，不加任何额外的选项。在 `dist` 里你得到一个文件夹 `hello`，里面有 `hello.exe`。为什么必须把整个文件夹交给别人，而不能只给 `.exe`？", "o": ["Windows 会拦住每一个不带文件夹送来的 `.exe`。", "文件夹里有应用要读取的 `hello.spec` 配方文件。", "这个 `.exe` 只是指向脚本 `hello.py` 的快捷方式。", "没有 `_internal` 文件夹，这个 `.exe` 就无法运行。"], "a": 3, "why": "`.exe` 旁边的 `_internal` 文件夹装着程序需要的一切，所以这两样东西总是一起走。", "at": "python_to_exe.html#step3"},
  {"id": "python_to_exe_03", "unit": "python_to_exe", "d": -0.4, "type": "mcq", "q": "PyInstaller 把你的应用构建成了一个文件夹。为什么要把它做成 zip 文件再发给别人？", "o": ["zip 是签过名的，所以不会弹出警告框。", "zip 能让应用在 Mac 上启动得更快。", "文件夹不能作为附件添加到消息里。", "zip 能让应用在 Windows 和 Mac 上都运行。"], "a": 2, "why": "文件夹不能作为附件添加到消息里，而 zip 能把它所有的文件保持为一个整体。", "at": "python_to_exe.html#step11"},
  {"id": "python_to_exe_04", "unit": "python_to_exe", "d": 0.0, "type": "mcq", "q": "一个 Python 脚本用 `print` 打印一行文字。PyInstaller 用 `--windowed` 把它打包。应用运行时，这一行文字会怎样？", "o": ["应用关闭时，它显示在一个对话框里。", "它被保存到 `dist` 文件夹里的一个文件中。", "它哪儿也去不了，因为没有控制台。", "它出现在窗口的标题栏里。"], "a": 2, "why": "窗口式应用没有控制台来显示打印的文字，所以程序必须把消息放在它自己的窗口里。", "at": "python_to_exe.html#step5"},
  {"id": "python_to_exe_05", "unit": "python_to_exe", "d": 0.2, "type": "mcq", "q": "用 `--onefile` 构建的应用每次启动时做什么？", "o": ["它把自己解包到一个临时文件夹里。", "它从互联网上下载 Python。", "它在自己旁边建立 `_internal` 文件夹。", "它让 PyInstaller 把它再构建一次。"], "a": 0, "why": "单文件应用先把自己解包到一个临时文件夹里，从那里运行，等你关闭它时再删掉这个文件夹。这就是它启动更慢的原因。", "at": "python_to_exe.html#step5"},
  {"id": "python_to_exe_06", "unit": "python_to_exe", "d": 0.1, "type": "mcq", "q": "在 Windows 上，为什么输入 `python -m PyInstaller` 可能比输入短命令 `pyinstaller` 更保险？", "o": ["短命令构建出的应用里面不带 Python。", "短命令是一个新的 `.exe`，Windows 可能拒绝它。", "短命令只有在你给脚本签名之后才能用。", "短命令读不了 `--onefile` 这个选项。"], "a": 1, "why": "短命令是 pip 创建的一个小 `.exe`，而 Windows 可能拒绝运行一个全新的 `.exe`。`python -m PyInstaller` 启动的只有 Python。", "at": "python_to_exe.html#step2"},
  {"id": "python_to_exe_07", "unit": "python_to_exe", "d": 0.3, "type": "mcq", "q": "在 Windows 11 上，Smart App Control 拦住了你刚刚用 PyInstaller 构建的应用。怎样做是安全的？", "o": ["点击对话框里的“Run anyway”按钮。", "把 Smart App Control 关掉，再启动它。", "重新安装 Python，然后启动脚本。", "重新构建，或者等一等再试这个应用。"], "a": 3, "why": "新的构建是一个新文件，会得到一次新的判断，而被拦住的应用以后也可能被放行。对话框里没有“Run anyway”按钮，你也不需要把这项保护关掉。", "at": "python_to_exe.html#step3"},
  {"id": "python_to_exe_08", "unit": "python_to_exe", "d": 0.6, "type": "mcq", "q": "一个脚本在某个函数里有一行 `import`，导入一个很大的 AI 库，而这一行只是有时才运行。你在一个装了这个库的虚拟环境里用 PyInstaller 构建这个脚本。会发生什么？", "o": ["构建会停下来，因为这行导入不在文件开头。", "这个库先不打包，等那一行真的运行时再说。", "这个库被打包进去，应用变得大得多。", "应用第一次启动时会去下载这个库。"], "a": 2, "why": "只要它的 Python 能满足，PyInstaller 就会把每一个导入都打包，不管那一行是否真的会运行。在一次运行中，同一个脚本在普通环境里构建出来是 25 MB，在装了这个库的环境里是 563 MB。", "at": "python_to_exe.html#step8"},
  {"id": "python_to_exe_09", "unit": "python_to_exe", "d": 0.7, "type": "mcq", "q": "`llama_cpp` 库按名字加载 `.dll` 文件，而没有任何 import 行提到它们。哪个 PyInstaller 选项让它把这些文件带上？", "o": ["`--hidden-import llama_cpp`", "`--collect-binaries llama_cpp`", "`--onefile`", "`--windowed`"], "a": 1, "why": "PyInstaller 靠读 import 行来找库。`--collect-binaries llama_cpp` 让它把属于这个库的每个二进制文件都带上。", "at": "python_to_exe.html#step9"},
  {"id": "python_to_exe_10", "unit": "python_to_exe", "d": 0.8, "type": "mcq", "q": "一个用 PyInstaller 构建的窗口式应用在第二个线程里出了错。用户会看到什么？", "o": ["什么都没有：窗口一直开着，看起来一切正常。", "一个标题为“Unhandled exception in script”的对话框。", "一个黑色的控制台窗口，里面有错误信息。", "Smart App Control 的对话框，上面有一个按钮。"], "a": 0, "why": "主体部分出错会弹出一个对话框，但第二个线程里出错什么都不显示。所以第二个线程里的代码应该捕获自己的错误，并把它们显示在应用自己的窗口里。", "at": "python_to_exe.html#step7"},
  {"id": "fine_tune_llm_01", "unit": "fine_tune_llm", "d": -0.8, "type": "mcq", "q": "什么是语言模型的微调？", "o": ["用新的示例，对已经训练好的模型继续训练", "写一个更长的提示词，里面带几个做好的示例", "让模型在回答时到文档里查找事实", "用你自己的文本从头构建一个新模型"], "a": 0, "why": "微调就是用新的示例训练一个已经训练过的模型，教它做一件工作。模型保留它已经掌握的语言知识。", "at": "fine_tune_llm.html"},
  {"id": "fine_tune_llm_02", "unit": "fine_tune_llm", "d": -0.5, "type": "mcq", "q": "在 Python 中微调语言模型常常用到三个库：`torch`、`transformers` 和 `peft`。`peft` 增加了什么？", "o": ["改变那些数字的算术运算", "LoRA，它只训练很少的数字", "把文本变成词元，再变回来", "下载模型本身的文件"], "a": 1, "why": "`torch` 做算术运算，`transformers` 下载并运行模型，`peft` 增加 LoRA。它的名字是 parameter-efficient fine-tuning（参数高效微调）的缩写。", "at": "fine_tune_llm.html#step1"},
  {"id": "fine_tune_llm_03", "unit": "fine_tune_llm", "d": -0.3, "type": "mcq", "q": "为了某项工作去微调语言模型之前，明智的做法是先试一个又长又仔细、里面带几个做好的示例的提示词。为什么？", "o": ["模型在见过一个提示词之前是不能训练的", "这个提示词会被保存下来，用作第一个训练示例", "它便宜得多，而且如果管用，你就完成了", "长的提示词会让之后的训练跑得更快"], "a": 2, "why": "微调要花时间和心思。一条仔细的、带有做好的示例的指令常常就够了，那样就没有什么需要训练的。", "at": "fine_tune_llm.html#step3"},
  {"id": "fine_tune_llm_04", "unit": "fine_tune_llm", "d": 0.0, "type": "mcq", "q": "在聊天模型的每个训练示例中，答案都以模型的轮次结束词元结尾，例如 `<|im_end|>`。为什么要有它？", "o": ["它教模型停止往下写", "它标出用户消息在哪里结束", "它告诉 PyTorch 不要给这个词元评分", "它让答案成为程序可用的有效 JSON"], "a": 0, "why": "这个词元表示这一轮结束了。一个从没练习过写它的模型，永远学不会停下来。", "at": "fine_tune_llm.html#step5"},
  {"id": "fine_tune_llm_05", "unit": "fine_tune_llm", "d": 0.2, "type": "mcq", "q": "这段 PyTorch 代码来自一个语言模型的训练循环。`graded_tokens` 里的值 `-100` 起什么作用？", "code": "example_tokens = torch.tensor([question_tokens + answer_tokens])\ngraded_tokens = torch.tensor(\n    [[-100] * len(question_tokens) + answer_tokens])\nloss = model(input_ids=example_tokens, labels=graded_tokens).loss", "o": ["标记答案的词元，让它们被评分两次", "标记问题的词元，让它们被跳过", "设定一轮训练可以把每个数字移动多远", "告诉模型在那个位置停止往下写"], "a": 1, "why": "PyTorch 的损失计算会跳过标记为 -100 的位置，所以模型只在答案上被评分。", "at": "fine_tune_llm.html#step6"},
  {"id": "fine_tune_llm_06", "unit": "fine_tune_llm", "d": -0.4, "type": "fill", "q": "有一种微调会冻结模型的所有数字，只训练它们旁边的一小组新数字，这组数字叫做适配器。这种微调的名字由四个字母组成，是什么？请输入。", "answers": ["lora", "low-rank adaptation", "low rank adaptation"], "why": "LoRA 是 low-rank adaptation（低秩适配）的缩写。它让模型保持原样，只训练一个小的附加部分。全量微调则让训练改变每一个数字。", "at": "fine_tune_llm.html#step6"},
  {"id": "fine_tune_llm_07", "unit": "fine_tune_llm", "d": 0.1, "type": "mcq", "q": "你借助 `peft` 用 LoRA 微调一个语言模型，并用 `model.save_pretrained(\"tuned_lora\")` 保存结果。文件夹 `tuned_lora` 里有什么？", "o": ["一个完整的模型，大小是下载文件的两倍", "训练示例和用于测试的文件", "只有附加部分；它还需要原始模型", "每个数字都被改过的原始模型"], "a": 2, "why": "这个文件夹里没有模型。它只装着附加部分，旁边没有原始模型就毫无用处。这就是它这么小的原因。", "at": "fine_tune_llm.html#step8"},
  {"id": "fine_tune_llm_08", "unit": "fine_tune_llm", "d": 0.7, "type": "mcq", "q": "一个微调过的模型，在由生成它训练示例的同一个程序生成的测试消息上几乎完美。但对于人写的消息，其中有示例从没用过的词和拼写，它错了很多。怎样才能解决这些错误？", "o": ["再多训练很多轮", "用全量微调，不用 LoRA", "用更小的学习率", "用更好的训练示例"], "a": 3, "why": "模型能做到和它的示例一样好，不会更好。在示例从没出现过的词上出的错，要靠更好的示例来解决，而不是靠训练更久或改变每一个数字。", "at": "fine_tune_llm.html#step10"},
  {"id": "fine_tune_llm_09", "unit": "fine_tune_llm", "d": 0.8, "type": "mcq", "q": "一个模型用 400 个示例做了微调。每个示例都是一位顾客的点餐，订单写成 JSON。后来有人问它 `What time do you close today?`，它用一份 JSON 格式的点餐订单来回答。为什么？", "o": ["附加部分被合并进了模型", "每个训练示例都是一份订单", "回复在 50 个 token 处被截断了", "训练抹掉了它以前知道的东西"], "a": 1, "why": "模型学到的是：回复永远是一份订单。没有任何示例给它看过不是订单的消息。要处理这样的消息，训练示例里就必须有一些这样的消息。", "at": "fine_tune_llm.html#step11"},
  {"id": "fine_tune_llm_10", "unit": "fine_tune_llm", "d": 0.4, "type": "mcq", "q": "一家商店的价格和营业时间经常变，而语言模型必须回答关于它们的问题。对这样的事实，最好的做法是什么？", "o": ["每个星期把模型重新微调一次", "把它们放进 LoRA 的附加文件里", "手工把它们加到测试文件里", "把它们放在文档里，供模型查找"], "a": 3, "why": "在训练中学到事实的模型会一直重复旧的事实。把事实放在文档里，让模型去查找（检索，也就是 RAG）。", "at": "fine_tune_llm.html#when"},
  {"id": "m1_01", "unit": "python_to_mac_app", "d": -0.6, "type": "mcq", "q": "为什么一个 `.py` 文件本身不算一个程序？", "o": ["它太小了：macOS 只会启动大小超过一兆字节的文件", "它需要一台装有合适版本 Python 的电脑，还要有它导入的每一个库", "macOS 读不了文本文件，所以脚本必须先被转成一张图片", "它没有图标，而 Finder 拒绝打开任何没有自己图标的文件"], "a": 1, "why": "你想把它交给的大多数人，这些东西一样都没有。PyInstaller 把这一切，连同 Python，都复制进一个你可以交出去的东西里。", "at": "python_to_mac_app.html#first"},
  {"id": "m1_02", "unit": "python_to_mac_app", "d": 0.0, "type": "mcq", "q": "在 Mac 上，不加任何选项运行 `venv/bin/python -m PyInstaller hello.py`，你会得到什么？", "o": ["一个 Mac 应用 `dist/hello.app`，带图标，双击就能打开", "一个单独的文件 `dist/hello`，每次启动时都会自己解包", "一个文件夹 `dist/hello`，里面有程序和它的支持文件", "一个磁盘映像 `hello.dmg`，里面有应用和指向 Applications 的链接"], "a": 2, "why": "这是 PyInstaller 的默认方式 `--onedir`。对方必须知道要启动文件夹里的哪个文件。", "at": "python_to_mac_app.html#step2"},
  {"id": "m1_03", "unit": "python_to_mac_app", "d": 0.1, "type": "fill", "q": "PyInstaller 的哪个选项会生成一个双击就能打开的 Mac 应用 `dist/hello.app`？请连同两个短横线一起输入。", "answers": ["--windowed", "windowed", "-w", "--noconsole"], "why": "用了 `--windowed`，Finder 把应用显示成一个带图标的整体，而且它打开时不带 Terminal。", "at": "python_to_mac_app.html#step3"},
  {"id": "m1_04", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "一个脚本用 `print(...)` 打印一行文字，并打开一个 tkinter 窗口。PyInstaller 用 `--windowed` 把它构建成一个 Mac 应用，然后双击打开这个应用。打印的那一行去了哪里？", "o": ["进了窗口，显示在最后一个按钮下面的一行里，用户可以读到", "进了一个以应用名字命名的文件，PyInstaller 把它放在应用旁边", "进了 Terminal，macOS 会为每个应用在窗口后面打开它", "哪儿也没去。这样构建的程序必须把消息放在它自己的窗口里"], "a": 3, "why": "这个应用打开时没有 Terminal，所以 `print` 没有地方可写。", "at": "python_to_mac_app.html#step3"},
  {"id": "m1_05", "unit": "python_to_mac_app", "d": 0.4, "type": "mcq", "q": "用 PyInstaller 的 `--onefile` 构建的程序，每次启动时做什么？", "o": ["它把自己解包到一个临时文件夹，从那里运行，结束时再删掉这个文件夹", "它从互联网下载合适版本的 Python，并把它留到下一次启动时用", "它从脚本重新构建自己，这就是脚本必须留在这个文件旁边的原因", "它向用户要一个文件夹，把自己安装到那里，然后从那个文件夹启动"], "a": 0, "why": "这样的程序开着的时候，临时文件夹里会有一个名字以 `_MEI` 开头的文件夹。程序停止后它就没有了。", "at": "python_to_mac_app.html#step3"},
  {"id": "m1_06", "unit": "python_to_mac_app", "d": 0.3, "type": "mcq", "q": "PyInstaller 用两种方式构建同一个小脚本。`--onefile` 的构建是 9.7 MB，默认的文件夹构建是 26.4 MB。为什么单个文件更小？", "o": ["它不带 Python，而是用 Mac 上已有的 Python", "这个文件是压缩过的，代价在每次启动时付出", "它不带 tkinter，因为单个文件打不开窗口", "它只装着启动程序，其余部分在运行时再去取"], "a": 1, "why": "交给别人时更小，启动时更慢，因为它要先解包。", "at": "python_to_mac_app.html#step3"},
  {"id": "m1_07", "unit": "python_to_mac_app", "d": 0.6, "type": "mcq", "q": "你想要一个双击就能打开的 Mac 应用。PyInstaller 的哪些选项最合适？", "o": ["`--onefile` 和 `--windowed` 一起用", "用 `--onefile`，不用 `--windowed`", "用 `--windowed`，不用 `--onefile`", "什么选项都不加，用默认构建"], "a": 2, "why": "PyInstaller 的文档说，这两个选项在 Mac 上可以一起用，但不推荐，因为那样应用每次运行都要自己解包。", "at": "python_to_mac_app.html#step3"},
  {"id": "m2_01", "unit": "python_to_mac_app", "d": 0.0, "type": "mcq", "q": "PyInstaller 会把你的 Python 变成另一种语言吗？", "o": ["会，变成 C，然后再构建成一个适合这台 Mac 的芯片类型的小程序", "会，变成 Swift，也就是编写 Mac 应用所用的语言，然后再变成一个应用", "会，变成 Apple 芯片的机器码，这样应用里就不必放进一份 Python", "不会。它生成一个小的启动程序，并在旁边放上一份 Python 和你的代码"], "a": 3, "why": "它自己的日志就是这么说的，一个阶段一个阶段地写着。", "at": "python_to_mac_app.html#how"},
  {"id": "m2_02", "unit": "python_to_mac_app", "d": 0.3, "type": "mcq", "q": "`venv` 里装了一个库，但你的脚本从来没有导入它。它会进到应用里吗？", "o": ["不会。PyInstaller 打包的是你的代码导入的东西，而不是装了的所有东西", "会。`venv` 里的所有东西都会复制进应用，不管有没有用到", "只在 Intel 芯片的 Mac 上会，因为在那里 PyInstaller 读不出导入", "只在用 `--onefile` 时会，因为单个文件必须装下整个 `venv`"], "a": 0, "why": "PyInstaller 读你的脚本，并顺着它的导入一路找下去。", "at": "python_to_mac_app.html#step4"},
  {"id": "m2_03", "unit": "python_to_mac_app", "d": 0.6, "type": "mcq", "q": "有个库按一个名字来加载东西，而这个名字是程序运行时才算出来的。PyInstaller 在代码里读不到这个导入。哪个选项能帮上忙？", "o": ["`--windowed`", "`--hidden-import`", "`--onefile --windowed`", "`--exclude-module`"], "a": 1, "why": "PyInstaller 只能跟踪它在代码里读得到的导入。其余的需要你来帮忙。", "at": "python_to_mac_app.html#step4"},
  {"id": "m2_04", "unit": "python_to_mac_app", "d": 0.2, "type": "mcq", "q": "在 PyInstaller 的日志里，每一行都以一个数字开头，例如 `3054 INFO:`。这个数字是什么？", "o": ["到目前为止已经复制进应用的文件个数", "PyInstaller 此刻正在读取的你脚本里的行号", "从它开始运行算起的时间，单位是千分之一秒", "应用到目前为止的大小，单位是一千字节"], "a": 2, "why": "所以 `3054` 表示构建开始后三秒多一点。", "at": "python_to_mac_app.html#step4"},
  {"id": "m2_05", "unit": "python_to_mac_app", "d": -0.2, "type": "mcq", "q": "Mac 上的 `.app` 是什么？", "o": ["一个压缩文件，每次打开应用时 macOS 都会把它解包", "一个指向 Python 文件的链接，macOS 用自带的 Python 运行它", "一个磁盘映像，Finder 把它当成一个小磁盘那样打开", "一个文件夹。所有东西都在它里面一个名叫 `Contents` 的文件夹里"], "a": 3, "why": "按住 Control 键点按应用，选择“显示包内容”，就能看到里面。", "at": "python_to_mac_app.html#step5"},
  {"id": "m2_06", "unit": "python_to_mac_app", "d": 0.4, "type": "mcq", "q": "你双击一个由 PyInstaller 用 Python 脚本做成的 Mac 应用。macOS 首先做什么？", "o": ["它读取 `Info.plist`，并启动 `MacOS` 文件夹里的程序", "它用在 `Frameworks` 里找到的 Mac 自带的 Python 运行脚本", "它把应用解包到临时文件夹，并在那里启动它", "它从脚本重新构建应用，这要花几秒钟"], "a": 0, "why": "这个启动程序从 `Frameworks` 加载 Python 库，然后 Python 运行你的脚本。", "at": "python_to_mac_app.html#step5"},
  {"id": "m2_07", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "PyInstaller 把脚本 `hello.py` 打包成一个 Mac 应用。`hello.py` 是否作为一个你可以打开阅读的文件留在应用里？", "o": ["是。它在 `Contents/Resources` 里，就在应用的图标旁边", "否。它以编译后的 Python 形式放在启动程序内部的包里", "是，但只在用 `--windowed` 构建的应用里才这样", "否。它根本不在应用里，应用启动时才去取它"], "a": 1, "why": "应用里有你的代码，但不是你写下的那种文本形式。", "at": "python_to_mac_app.html#step5"},
  {"id": "m3_01", "unit": "python_to_mac_app", "d": 0.3, "type": "mcq", "q": "`codesign` 打印出 `Signature=adhoc` 和 `TeamIdentifier=not set`。这是什么意思？", "o": ["Apple 检查过这个应用，认为它安全", "这个应用完全没有任何一种签名", "这个应用的签名不属于任何特定的人", "这个应用是由 PyInstaller 的作者签名的"], "a": 2, "why": "Apple 芯片不会运行完全没有签名的程序，所以 PyInstaller 给了它这个空签名。", "at": "python_to_mac_app.html#step6"},
  {"id": "m3_02", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "对于你自己构建的一个 Mac 应用，`spctl --assess` 回答 `rejected`，可是这个应用在构建它的那台 Mac 上能打开。为什么？", "o": ["`spctl` 对你自己构建的应用判断有误，Gatekeeper 本身是会同意的", "Gatekeeper 只检查来自 App Store 的应用，其他应用一律放行", "这个应用是在 Terminal 里构建的，而 Gatekeeper 信任 Terminal 做出的一切", "只有带 `com.apple.quarantine` 的文件才会交给 Gatekeeper 检查，这是下载文件上的标签"], "a": 3, "why": "你自己构建的文件没有这个标签：`xattr` 对它什么也不打印。浏览器会给它下载的东西贴上这个标签。", "at": "python_to_mac_app.html#step6"},
  {"id": "m3_03", "unit": "python_to_mac_app", "d": 0.6, "type": "mcq", "q": "你用 PyInstaller 做了一个 Mac 应用。怎样才能让这个应用在别人的 Mac 上不需要额外步骤就能打开？", "o": ["用 Apple 付费开发者计划的证书给它签名，并让 Apple 检查它", "把它放进磁盘映像，因为 macOS 信任用户拖进 Applications 的应用", "用 `--onefile` 构建，因为单个文件不算应用，Gatekeeper 会放行", "用 `ditto` 压缩它，这会保留 PyInstaller 给它的签名，所以 macOS 会接受"], "a": 0, "why": "Apple 把这种检查叫做 notarization（公证）。PyInstaller 用于签名的选项是 `--codesign-identity`。", "at": "python_to_mac_app.html#step6"},
  {"id": "m3_04", "unit": "python_to_mac_app", "d": 0.3, "type": "mcq", "q": "Mac 应用最好用 macOS 自带的 `ditto` 来压缩，而不要用普通的 zip 程序。为什么？", "o": ["`ditto` 做出的文件比 zip 做出的更小", "`ditto` 把应用内部的链接仍然保留为链接", "`ditto` 会给应用签名，这样 Gatekeeper 就让它打开", "普通的 zip 程序装不了文件夹，只能装文件"], "a": 1, "why": "Mac 应用里有链接：就是指向应用内别处某个文件的名字。普通的 zip 程序可能把它们弄坏。", "at": "python_to_mac_app.html#step7"},
  {"id": "m3_05", "unit": "python_to_mac_app", "d": 0.2, "type": "mcq", "q": "一个用 PyInstaller 做的 Mac 应用，发布时的文件名类似 `hello-1.0-macos-arm64.zip`。为什么名字里有 `arm64`？", "o": ["它是应用里面 Python 的版本，用户的版本必须与它一致", "它是应用能够打开之前所需要的 macOS 版本", "应用只能在构建它的那种芯片上运行，所以名字里写明是哪种", "GitHub 要求这样做，并拒绝名字里不带芯片的发布文件"], "a": 2, "why": "`arm64` 是用 Apple 芯片的 Mac，`x86_64` 是用 Intel 芯片的 Mac。", "at": "python_to_mac_app.html#step7"},
  {"id": "m3_06", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "一个 Mac 应用只有 PyInstaller 给它的 ad-hoc 签名。把它放进磁盘映像（`.dmg`）或安装包（`.pkg`），能让它通过 Gatekeeper 吗？", "o": ["能。两者都是 macOS 自带的程序做出来的，所以它信任它们", "只有安装包可以，因为它会要求输入管理员密码", "只有磁盘映像可以，因为里面的应用不算下载的文件", "不能。macOS 出于同一个原因拒绝两者：没有可用的签名"], "a": 3, "why": "它们是把应用交给别人的方式，而不是绕过 Gatekeeper 的办法。", "at": "python_to_mac_app.html#step7"},
  {"id": "m3_07", "unit": "python_to_mac_app", "d": 0.0, "type": "mcq", "q": "GitHub 上的发布，在下载文件旁边常常有一个名叫 `SHA256SUMS.txt` 的文件。它是做什么用的？", "o": ["它保存着每个文件的指纹，这样就能检查下载的文件是否完整到达", "它是许可证，说明一个人可以用自己下载的这些文件做什么", "它告诉 macOS 应用是谁做的，这样 Gatekeeper 就不加询问地让它打开", "它列出应用里面的库，这样别人就能再构建出同样的应用"], "a": 0, "why": "指纹是 64 个字母和数字，文件只要有一个字节变了，它就会变。", "at": "python_to_mac_app.html#step8"},
  {"id": "m3_08", "unit": "python_to_mac_app", "d": 0.4, "type": "fill", "q": "每台 Mac 上都有的哪个程序，可以对照 `SHA256SUMS.txt` 检查下载的文件？请输入它的名字。", "answers": ["shasum", "shasum -a 256 -c sha256sums.txt", "shasum -a 256"], "why": "完整的命令是 `shasum -a 256 -c SHA256SUMS.txt`。每个完整到达的文件，它都打印 `OK`。", "at": "python_to_mac_app.html#step8"},
  {"id": "m4_01", "unit": "python_to_mac_app", "d": 0.0, "type": "mcq", "q": "一个带 Agno 智能体的脚本会向 Ollama 里的一个模型提问，它被 PyInstaller 打包成一个文件。文件里有什么，没有什么？", "o": ["模型在里面，Agno 不在里面", "Agno 在里面，模型不在里面", "Agno 和模型都在里面", "只有脚本在里面，不带任何库"], "a": 1, "why": "PyInstaller 打包脚本导入的库。模型留在 Ollama 里，程序通过同一台电脑的 11434 端口向它提问。", "at": "python_to_mac_app.html#step9"},
  {"id": "m4_02", "unit": "python_to_mac_app", "d": 0.3, "type": "mcq", "q": "一个 Python 脚本没有窗口：它在 Terminal 里打印答案。为什么要用 PyInstaller 的 `--onefile` 来构建它，而不加 `--windowed`？", "o": ["`--windowed` 会把脚本导入的库漏掉", "`--windowed` 做出的文件更大，构建时间也长得多", "加了 `--windowed`，程序打印的内容就哪儿也去不了", "`--windowed` 是给 Windows 用的，在 Mac 上完全不起作用"], "a": 2, "why": "用 `--windowed` 做出的应用打开时没有 Terminal，所以 `print` 没有地方可写。", "at": "python_to_mac_app.html#step9"},
  {"id": "m4_03", "unit": "python_to_mac_app", "d": 0.4, "type": "mcq", "q": "一个脚本导入了 `agno`，这是一个 Python 不自带的库。在 PyInstaller 打包这个脚本之前，要先把 `agno` 安装到运行 PyInstaller 的那个 Python 里。为什么？", "o": ["PyInstaller 是 Agno 的一部分，会随它一起装进来", "这样打包出来的文件会比不装时更小", "这样会下载智能体将要提问的那个模型", "PyInstaller 只打包负责构建的 Python 里有的库"], "a": 3, "why": "PyInstaller 用运行它的那个 Python 来构建应用。如果那个 Python 缺少某个库，PyInstaller 就找不到它，也就无法打包。", "at": "python_to_mac_app.html#step9"},
  {"id": "m4_04", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "一个用 PyInstaller 打包的程序停了下来，它的最后一行是 `Failed to execute script 'cashier' due to unhandled exception!`。这一行是从哪里来的？", "o": ["来自 PyInstaller 放在你脚本前面的启动程序", "来自 Python 本身，它在显示每个 traceback 之后都会打印这一行", "来自 Gatekeeper，它会拦下没有 Apple 签名的程序", "来自 Terminal，它为以代码 1 结束的程序打印这一行"], "a": 0, "why": "打包后的程序就是这样报告脚本没有捕获的任何错误的。真正的原因在它上面的几行里。", "at": "python_to_mac_app.html#step9"},
  {"id": "m4_05", "unit": "python_to_mac_app", "d": 0.3, "type": "mcq", "q": "一个向 Ollama 里的模型提问的程序，用 PyInstaller 的 `--onefile` 打包。它的第一个问题花了 76 秒。同一个问题再问一次花 20 秒，普通的 Python 脚本也是 20 秒。是什么让第一次这么慢？", "o": ["第一次时，那个单文件还在给自己解包", "第一次时，Ollama 必须把模型加载进内存", "第二个答案是从第一次保存下来的，所以没有运行模型", "Mac 在睡眠中，必须为第一个问题醒过来"], "a": 1, "why": "加载模型的代价只付一次。单文件程序每次启动都要解包，所以解包解释不了为什么只有第一个问题慢。", "at": "python_to_mac_app.html#step9"},
  {"id": "m4_06", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "一个带智能体的脚本向 Ollama 里的模型提问。你用 PyInstaller 把这个脚本打包。回答一次所花的时间会怎样？", "o": ["大幅下降，因为打包后的文件是压缩过的", "大幅上升，因为模型在每次启动时都要解包", "几乎不变：大部分时间是模型在回答", "减少一半，因为 PyInstaller 把 Python 变成了 C"], "a": 2, "why": "模型在 Ollama 里运行，在打包的程序之外。在一次测试中，打包的脚本和普通的脚本回答同一个问题都花了大约 20 秒。", "at": "python_to_mac_app.html#step9"},
  {"id": "m4_07", "unit": "python_to_mac_app", "d": 0.2, "type": "mcq", "q": "你没有 Mac，却想用 PyInstaller 构建一个 Mac 应用。哪种办法行得通？", "o": ["在 Windows 上运行 PyInstaller，加上选项 `--target-architecture arm64`", "构建一个 Windows 的 `.exe`，再把文件改名，让它以 `.app` 结尾", "在 Linux 上构建，因为 macOS 能原样运行每一个 Linux 程序", "通过 GitHub Actions 在一台 Mac 上运行构建，用 `runs-on: macos-15`"], "a": 3, "why": "PyInstaller 不是交叉编译器：它只为运行它的那种电脑构建。GitHub Actions 可以在 GitHub 的一台 Mac 上运行你的命令。", "at": "python_to_mac_app.html#nomac"},
  {"id": "m4_08", "unit": "python_to_mac_app", "d": 0.5, "type": "mcq", "q": "一个 GitHub Actions 工作流在 GitHub 的一台 Mac 上构建 Mac 应用（`runs-on: macos-15`）。在 `on: push: paths:` 下面它只列出了 Python 文件，所以只改动其他文件的推送不会启动它。这样做有什么好理由？", "o": ["GitHub 的 Mac 上一分钟的费用，大约是它最小的 Linux 机器上一分钟的十倍", "GitHub 的 Mac 只能读取 Python 文件，所以改动任何其他文件都会让运行报错停止", "工作流在 Mac 上一天只能运行一次，所以每一次运行都得留给代码的改动", "没有 `paths`，工作流根本不会启动，因为 GitHub 不知道该监视哪些文件"], "a": 0, "why": "有一份价目表给出的价格是：Mac 每分钟 $0.062，Linux 机器每分钟 $0.006。`paths` 让运行只发生在要紧的改动上。", "at": "python_to_mac_app.html#nomac"},
  {"id": "space_invaders_01", "unit": "space_invaders", "d": -0.7, "type": "mcq", "q": "你用 `pip3 install pygame-ce` 安装了这个库。在代码里要输入什么才能使用它？", "o": ["`import pygame_ce`", "`import pygame-ce`", "`from ce import pygame`", "`import pygame`"], "a": 3, "why": "在代码里你仍然输入 `import pygame`。只有安装时用的名字不一样。", "at": "space_invaders.html#step0"},
  {"id": "space_invaders_02", "unit": "space_invaders", "d": -0.4, "type": "mcq", "q": "`if __name__ == \"__main__\":` 这一行写在 `main()` 前面，它是什么意思？", "o": ["运行 `main()`，但只在我自己启动这个文件时。", "运行 `main()`，每次有别的文件导入它时都运行。", "运行 `main()`，每秒 60 次，一直不停。", "运行 `main()`，但只在文件名叫 `main` 时。"], "a": 0, "why": "它的意思是：只有我自己启动这个文件时，才运行 `main()`。如果别的文件导入这个文件，`main()` 不会运行。", "at": "space_invaders.html#step1"},
  {"id": "space_invaders_03", "unit": "space_invaders", "d": -0.3, "type": "mcq", "q": "一个 pygame 游戏设置了 `FPS = 60`，并创建了 `clock = pygame.time.Clock()`。游戏循环的最后一行是 `clock.tick(FPS)`。这一行起什么作用？", "o": ["它为每一帧计算得分。", "它让循环保持在每秒 60 帧。", "它在 60 秒之后关闭窗口。", "它让飞船每次移动 60 个像素。"], "a": 1, "why": "没有这个时钟，循环会以电脑能达到的最快速度运行。时钟让它保持在每秒 60 帧。", "at": "space_invaders.html#step3"},
  {"id": "space_invaders_04", "unit": "space_invaders", "d": 0.0, "type": "mcq", "q": "这个 pygame 类用来移动子弹。玩家的子弹创建时速度是 `-10`。敌人的子弹创建时速度是 `5`。它们在屏幕上往哪个方向走？", "code": "class Bullet:\n    def __init__(self, x, y, speed, colour):\n        self.x = x\n        self.y = y\n        self.speed = speed\n        self.colour = colour\n\n    def update(self):\n        self.y += self.speed", "o": ["两个都向上，只是速度不同。", "两个都向下，只是速度不同。", "玩家的向上，敌人的向下。", "玩家的向下，敌人的向上。"], "a": 2, "why": "在 pygame 窗口里，`y` 在顶部是 0，越往屏幕下方越大。所以负的速度让子弹向上移动，正的速度让它向下移动。", "at": "space_invaders.html#step6"},
  {"id": "space_invaders_05", "unit": "space_invaders", "d": 0.2, "type": "mcq", "q": "在一个 Python 游戏里，一个敌人又慢又耐打。下一个又快又脆弱。两个都是由同一个类 `Creep` 创建的。是什么让它们不一样？", "o": ["为每种敌人各写一个新的子类。", "把 `update` 方法复制一份，稍作修改。", "不同的游戏循环，每个敌人一个。", "传给 `__init__` 的数字不同。"], "a": 3, "why": "一个类只写一次。每个对象在 `__init__` 里得到自己的数字：同一个类，不同的行为。", "at": "space_invaders.html#step8"},
  {"id": "space_invaders_06", "unit": "space_invaders", "d": 0.4, "type": "mcq", "q": "看这个 pygame 函数。文件夹里缺少那个图片文件。这个函数会做什么？", "code": "def load_image(name, size, colour):\n    path = IMAGE_DIR / name\n    if path.exists():\n        picture = pygame.image.load(str(path)).convert_alpha()\n        return pygame.transform.smoothscale(picture, size)\n    box = pygame.Surface(size, pygame.SRCALPHA)\n    box.fill(colour)\n    return box", "o": ["它返回一个彩色方块来代替。", "它报错并让游戏停下来。", "它把图片重新下载一遍。", "它为那张图片返回 `None`。"], "a": 0, "why": "当 `path.exists()` 为假时，函数跳过加载。它创建一个 `Surface`，用那种颜色填满，并返回这个方块，所以游戏仍然能运行。", "at": "space_invaders.html#step12"},
  {"id": "space_invaders_07", "unit": "space_invaders", "d": 0.5, "type": "mcq", "q": "这段 Python 代码遍历的是 `list(my_bullets)`，而不是 `my_bullets`。为什么？", "code": "for bullet in list(my_bullets):\n    for creep in list(creeps):\n        if bullet.rect().colliderect(creep.rect()):\n            my_bullets.remove(bullet)\n            creep.hp -= 1\n            if creep.hp <= 0:\n                creeps.remove(creep)\n            break", "o": ["遍历列表的副本比遍历列表本身更快。", "循环会删除子弹，所以它遍历一个副本。", "在调用 `list` 之前，`my_bullets` 不是列表。", "副本把子弹排好顺序，最早的排在前面。"], "a": 1, "why": "`list(...)` 创建一个副本。在遍历一个列表的过程中，不能从这个列表里删除东西。", "at": "space_invaders.html#step10"},
  {"id": "chiang_mai_map_01", "unit": "chiang_mai_map", "d": -0.8, "type": "fill", "q": "这段 Python 代码生成一张可以拖动和缩放的地图，并把它保存为 HTML 文件。`____` 处缺的是哪个库？请输入它的名字。", "code": "import ____\n\nthe_map = ____.Map(location=(18.7883, 98.9853), zoom_start=13, tiles=\"OpenStreetMap\")\nthe_map.save(\"map.html\")", "answers": ["folium"], "why": "`folium` 用 `folium.Map(...)` 生成一张真正的地图，并把它保存为 HTML 文件。你在浏览器里打开这个文件。", "at": "chiang_mai_map.html#step0"},
  {"id": "chiang_mai_map_02", "unit": "chiang_mai_map", "d": -0.5, "type": "mcq", "q": "在 folium 中，`location` 的两个数字要按什么顺序写？", "o": ["较小的数字在前", "先经度，后纬度", "较大的数字在前", "先纬度，后经度"], "a": 3, "why": "永远是纬度在前，经度在后。如果把它们写反了，地图会显示世界上相差很远的另一个地方。", "at": "chiang_mai_map.html#step2"},
  {"id": "chiang_mai_map_03", "unit": "chiang_mai_map", "d": -0.3, "type": "mcq", "q": "一个地方的经度是 -105.07。这个负号是什么意思？", "o": ["它在零度线以西", "它在赤道线以南", "它离北极很远", "它在海平面以下"], "a": 0, "why": "对经度来说，负号表示西边。美洲的每个地方经度都是负的。", "at": "chiang_mai_map.html#step2"},
  {"id": "chiang_mai_map_04", "unit": "chiang_mai_map", "d": 0.0, "type": "mcq", "q": "在 folium 的 `Marker` 上，`tooltip` 和 `popup` 有什么区别？", "o": ["`tooltip` 是颜色；`popup` 是图标", "`tooltip` 是英语；`popup` 是泰语", "`tooltip` 在悬停时显示；`popup` 在点击时显示", "`tooltip` 在点击时显示；`popup` 在悬停时显示"], "a": 2, "why": "在 folium 的 `Marker` 上，`tooltip` 在鼠标悬停时显示，`popup` 在点击时显示。", "at": "chiang_mai_map.html#step4"},
  {"id": "chiang_mai_map_05", "unit": "chiang_mai_map", "d": 0.2, "type": "mcq", "q": "一张 folium 地图需要十四个图钉。为什么用一个表示地点的类，加上一个含十四个对象的列表，比逐个写出十四段 `Marker` 代码更好？", "o": ["用类能让地图加载得更快", "逐个写出来，就没法给它们排序或计数", "folium 只接受由类生成的图钉", "类会从 OpenStreetMap 获取坐标"], "a": 1, "why": "一个一个写出来的图钉要很多行代码，而且程序没法给它们排序或计数。列表里的对象可以在一个循环里排序、计数和添加。", "at": "chiang_mai_map.html#step5"},
  {"id": "chiang_mai_map_06", "unit": "chiang_mai_map", "d": 0.6, "type": "mcq", "q": "haversine 公式说，山上的一座寺庙离一座城门 7.7 km。为什么开车的路程要长得多？", "o": ["这个公式忘了地球是个球", "坐标只是接近，并不精确", "这个公式按英里算，不是 km", "它给的是直线；而路是弯的"], "a": 3, "why": "haversine 公式给出的是直线距离，也就是鸟飞的路线。公路要绕着山弯来弯去，所以开车的路程更长。", "at": "chiang_mai_map.html#step8"},
  {"id": "chiang_mai_map_07", "unit": "chiang_mai_map", "d": 0.4, "type": "mcq", "q": "一个 folium 的 `Circle` 设了 `radius=5000`。把地图缩小时，它会怎样？", "o": ["它会变大，所以看起来总是一样大", "它在地面上保持 5 km 宽", "它会消失，直到你再次放大", "它在圆心处变成一个图钉"], "a": 1, "why": "`radius=5000` 表示 5000 米。这个圆是地面上的真实距离，在任何缩放级别下都保持 5 km 宽。", "at": "chiang_mai_map.html#step9"},
  {"id": "grokking_algorithms_01", "unit": "grokking_algorithms", "d": -0.8, "type": "mcq", "q": "Big-O 记法描述的是什么？", "o": ["代码在你自己的笔记本电脑上要跑多少秒", "输入变大时，步骤数怎样增长", "这个算法需要多少行代码", "运行之前输入必须排好序到什么程度"], "a": 1, "why": "Big-O 不是在量秒数。它量的是：你给算法更多输入时，步骤数怎样增长。", "at": "grokking_algorithms.html#foundations"},
  {"id": "grokking_algorithms_02", "unit": "grokking_algorithms", "d": -0.5, "type": "mcq", "q": "一个递归函数没有基线条件。调用它时会发生什么？", "o": ["它返回 `1` 并立刻停止", "它运行一次并返回 `None`", "Python 替你把它变成一个 `for` 循环", "它不断调用自己，直到 Python 崩溃"], "a": 3, "why": "没有基线条件，函数就永远不会停止调用自己。Python 最后以 `RecursionError: maximum recursion depth exceeded` 结束。", "at": "grokking_algorithms.html#foundations"},
  {"id": "grokking_algorithms_03", "unit": "grokking_algorithms", "d": -0.4, "type": "mcq", "q": "对一个列表使用二分查找之前，它必须满足什么条件？", "o": ["列表必须是排好序的", "列表必须很短", "列表里必须只有数字", "列表里必须没有重复项"], "a": 0, "why": "二分查找每一步都扔掉列表的一半。只有列表排好了序，这样做才行得通。", "at": "grokking_algorithms.html#searching"},
  {"id": "grokking_algorithms_04", "unit": "grokking_algorithms", "d": 0.0, "type": "fill", "q": "这段 Python 代码打印什么？请输入。", "code": "def binary_search(arr, target):\n    lo, hi = 0, len(arr) - 1\n    while lo <= hi:\n        mid = (lo + hi) // 2\n        if arr[mid] == target:\n            return mid\n        if arr[mid] < target:\n            lo = mid + 1\n        else:\n            hi = mid - 1\n    return None\n\nprint(binary_search([1, 3, 5, 7, 9, 11, 13, 15], 10))", "answers": ["none"], "why": "10 不在列表里。搜索找遍了所有能找的位置，`while` 循环结束，函数返回 `None`。", "at": "grokking_algorithms.html#searching"},
  {"id": "grokking_algorithms_05", "unit": "grokking_algorithms", "d": 0.5, "type": "mcq", "q": "一个快速排序总是用第一个元素作为基准。哪种输入会让它出现很慢的 O(n²) 最坏情况？", "o": ["只有一个元素的列表", "顺序随机的列表", "已经排好序的列表", "元素个数为偶数的列表"], "a": 2, "why": "在排好序的列表上，第一个元素把其余的元素分成一堆空的和一大堆。所以递归有 n 层，而不是 log n 层。", "at": "grokking_algorithms.html#sorting"},
  {"id": "grokking_algorithms_06", "unit": "grokking_algorithms", "d": -0.1, "type": "mcq", "q": "在哈希表中，什么是冲突？", "o": ["一个键被存进了两个不同的槽", "两个键被哈希到同一个槽", "表里已经没有空的槽了", "一次查找要找一个不存在的键"], "a": 1, "why": "冲突就是两个不同的键被哈希到同一个槽。一种解决办法是分离链接法：每个槽保存一个小列表，里面是它的键和值。", "at": "grokking_algorithms.html#data-structures"},
  {"id": "grokking_algorithms_07", "unit": "grokking_algorithms", "d": 0.3, "type": "mcq", "q": "这段 Python 代码是广度优先搜索。`queue` 是一个 `deque`，`graph` 把每个节点映射到它的邻居。你把 `queue.popleft()` 改成 `queue.pop()`。你会得到什么？", "code": "def bfs(start, target):\n    queue = deque([start])\n    seen  = {start}\n    while queue:\n        node = queue.popleft()\n        if node == target:\n            return True\n        for neighbour in graph[node]:\n            if neighbour not in seen:\n                seen.add(neighbour)\n                queue.append(neighbour)\n    return False", "o": ["一个更快的广度优先搜索", "带权重的 Dijkstra 算法", "一个错误，因为队列是空的", "深度优先搜索，而不是 BFS"], "a": 3, "why": "`popleft()` 从前端取，所以 `deque` 当作队列用，而队列得到的是 BFS。`pop()` 从末端取，所以它当作栈用，而栈得到的是深度优先搜索。", "at": "grokking_algorithms.html#graphs"},
  {"id": "grokking_algorithms_08", "unit": "grokking_algorithms", "d": 0.5, "type": "mcq", "q": "一个图里有一些边的权重是负数。Dijkstra 算法在这个图上会怎样？", "o": ["它不声不响地给出错误答案", "它停下来，给出明确的错误信息", "它能用，但需要更多内存", "它变得和 BFS 一样"], "a": 0, "why": "Dijkstra 算法只有在权重非负时才正确。遇到负权重，它会给出错误的答案，而且不报错。对这样的图要用 Bellman-Ford 算法。", "at": "grokking_algorithms.html#graphs"},
  {"id": "grokking_algorithms_09", "unit": "grokking_algorithms", "d": 0.1, "type": "mcq", "q": "为什么带记忆化的 Fibonacci 比普通递归的 Fibonacci 快很多？", "o": ["它用循环，从不调用自己", "它在相加之前先把数字排序", "它保存每个答案并重复使用", "它猜一个答案，然后检查一次"], "a": 2, "why": "普通递归把同样的数一遍又一遍地重算。记忆化在第一次算出每个答案时就记住它，之后直接查。", "at": "grokking_algorithms.html#paradigms"},
  {"id": "grokking_algorithms_10", "unit": "grokking_algorithms", "d": 0.7, "type": "mcq", "q": "在 k-NN 中，重量的范围是 100 到 1000，直径的范围是 0.05 到 0.10。你没有对它们做缩放。会发生什么？", "o": ["两个特征在距离中的分量相同", "重量占主导，直径几乎被忽略", "代码报一个距离错误并停下来", "直径占主导，因为它的数字小"], "a": 1, "why": "欧几里得距离对每个特征一视同仁，所以数字大的特征占主导。使用 k-NN 之前，要把每个特征都归一化。", "at": "grokking_algorithms.html#ml"},
  {"id": "make_a_flashcard_app_01", "unit": "make_a_flashcard_app", "d": -0.8, "type": "mcq", "q": "在 tkinter 程序里，为什么 `root.mainloop()` 必须是最后一行？", "o": ["它会删除在它之前创建的每个控件。", "每个文件里它只能被调用一次。", "它后面的代码要等到窗口关闭才运行。", "它需要所有字体在它之前都加载好。"], "a": 2, "why": "`mainloop()` 是 tkinter 的循环，它等着人来操作。在窗口关闭之前，它后面的任何代码都不会运行。", "at": "make_a_flashcard_app.html#step1"},
  {"id": "make_a_flashcard_app_02", "unit": "make_a_flashcard_app", "d": -0.5, "type": "mcq", "q": "在 tkinter 中，给 `Button` 写 `command=flip()` 有什么问题？`flip` 是你自己写的函数。", "o": ["`flip` 立刻运行，按钮就失效了。", "Python 找不到 `flip` 这个函数。", "每点击一次，按钮调用 `flip` 两次。", "点击按钮时，窗口会关闭。"], "a": 0, "why": "括号的意思是“现在就运行它”。Python 立刻把 `flip` 运行一次，之后按钮什么也不做。要写成 `command=flip`。", "at": "make_a_flashcard_app.html#step3"},
  {"id": "make_a_flashcard_app_03", "unit": "make_a_flashcard_app", "d": -0.1, "type": "mcq", "q": "你向 tkinter 要一种电脑上没有的字体。会发生什么？", "o": ["程序报一个字体错误并停下来。", "你不声不响地得到另一种字体。", "标签用空方框来显示文字。", "tkinter 替你下载这种字体。"], "a": 1, "why": "要一种不存在的字体不会让程序崩溃。你只是不声不响地得到另一种字体。`tkinter.font.families()` 会告诉你电脑上到底有哪些字体。", "at": "make_a_flashcard_app.html#step2"},
  {"id": "make_a_flashcard_app_04", "unit": "make_a_flashcard_app", "d": 0.3, "type": "mcq", "q": "在 tkinter 程序里，使用 `pack()` 和 `grid()` 的规则是什么？", "o": ["按钮用 `pack()`，标签用 `grid()`。", "先调用 `pack()`，然后再调用 `grid()`。", "每个控件两个都要用，一个接一个。", "不要在同一个容器里混用这两个。"], "a": 3, "why": "选一个，然后一直用它。在同一个容器里混用 `pack()` 和 `grid()` 会让 tkinter 报错并停下。", "at": "make_a_flashcard_app.html#step5"},
  {"id": "make_a_flashcard_app_05", "unit": "make_a_flashcard_app", "d": 0.4, "type": "fill", "q": "`json.dumps` 的哪个参数设为 `False` 时，会让 Python 把泰语写成人能在文件里读懂的样子？请输入这个参数的名字。", "answers": ["ensure_ascii", "ensure_ascii=false", "ensure_ascii = false"], "why": "没有 `ensure_ascii=False`，文件照样能用，但没有人能手动读懂或修改里面的泰语。", "at": "make_a_flashcard_app.html#step6"},
  {"id": "make_a_flashcard_app_06", "unit": "make_a_flashcard_app", "d": 0.7, "type": "mcq", "q": "在 `self.words.sort(key=lambda w: -w[\"misses\"])` 里，负号起什么作用？", "o": ["它去掉你从来没有答错过的单词。", "它把答错次数最多的单词排在最前面。", "它把每个单词的答错次数减掉一次。", "它把容易的单词排在难的单词前面。"], "a": 1, "why": "负号让排序从大到小，所以答错次数最多的单词最先显示。", "at": "make_a_flashcard_app.html#step7"},
  {"id": "make_a_homework_app_01", "unit": "make_a_homework_app", "d": -0.8, "type": "mcq", "q": "在 Python 里，`sqlite3.connect(\"homework.db\")` 运行之后，文件夹里出现了一个新文件 `homework.db`。这个文件是什么？", "o": ["Python 程序本身的一份副本。", "你输入过的命令的一个列表。", "整个数据库，就在一个文件里。", "Python 不得不安装的一个库。"], "a": 2, "why": "SQLite 就是你磁盘上的一个文件。这个文件就是你的整个数据库。", "at": "make_a_homework_app.html#step1"},
  {"id": "make_a_homework_app_02", "unit": "make_a_homework_app", "d": -0.5, "type": "mcq", "q": "`IF NOT EXISTS` 在 `CREATE TABLE IF NOT EXISTS jobs` 里是做什么的？", "o": ["它只在第一次创建这张表。", "它在每次运行前删掉旧表。", "它拦住学生名字为空的行。", "它不用 `commit()` 就保存改动。"], "a": 0, "why": "它在第一次创建这张表，之后每次都什么也不做，所以你可以放心地把程序运行两次。", "at": "make_a_homework_app.html#step2"},
  {"id": "make_a_homework_app_03", "unit": "make_a_homework_app", "d": -0.1, "type": "mcq", "q": "这行 Python `sqlite3` 代码在 SQL 里写 `?`，把值另外单独传入。为什么不自己把值拼进 SQL 文本里？", "code": "conn.execute(\"INSERT INTO jobs (student, title) VALUES (?, ?)\", (name, title))", "o": ["这样数据库保存行的速度更快。", "这样一行代码能一次插入很多行。", "这样不用 `commit()` 就能保存这一行。", "像 O'Brien 这样的名字会弄坏拼接的文本。"], "a": 3, "why": "如果你自己把名字拼进去，O'Brien 里的 `'` 会让文本提前结束。用 `?` 的话，不管名字是什么，Python 都会安全地把值放进去。", "at": "make_a_homework_app.html#step3"},
  {"id": "make_a_homework_app_04", "unit": "make_a_homework_app", "d": 0.2, "type": "fill", "q": "你用 Python 的 `sqlite3` 添加了几行，但下一次运行时它们不见了。你忘了调用连接的哪个方法？请输入它的名字。", "answers": ["commit", "commit()", "conn.commit()", "conn.commit"], "why": "SQLite 会把你的更改先放在一边，直到你说“好，保存吧”。`conn.commit()` 就是你在说“好”。", "at": "make_a_homework_app.html#step4"},
  {"id": "make_a_homework_app_05", "unit": "make_a_homework_app", "d": 0.5, "type": "mcq", "q": "这行 Python `sqlite3` 代码运行之后，`result.rowcount` 是 0。这说明了什么？", "code": "result = conn.execute(\"UPDATE jobs SET done = 1 WHERE id = ?\", (args.job_id,))", "o": ["这项作业早已被标记为完成。", "没有哪项作业是这个 id。", "表里的每一行都被改动了。", "这次更改还没有被保存。"], "a": 1, "why": "`rowcount` 说的是有多少行被改动了。0 表示没有哪一行是这个 id。", "at": "make_a_homework_app.html#step6"},
  {"id": "make_a_homework_app_06", "unit": "make_a_homework_app", "d": 0.7, "type": "mcq", "q": "一个 Python 程序用 `sqlite3.connect(\"homework.db\")` 打开它的数据库。你从另一个文件夹运行它，所有的行好像都不见了。为什么？怎么修复？", "o": ["`connect` 在你所在的位置新建了一个文件；把它绑定到程序上。", "表被 `IF NOT EXISTS` 删掉了；把这张表重新创建一遍。", "这些行从来没有提交过；在 `SELECT` 之后加上 `conn.commit()`。", "从别的文件夹看这个文件是只读的；用模式 `\"w\"` 打开它。"], "a": 0, "why": "`sqlite3.connect(\"homework.db\")` 在你所在的位置创建文件，而不是在程序所在的位置。`Path(__file__).with_name(\"homework.db\")` 把它绑定到程序上。", "at": "make_a_homework_app.html#step8"},
  {"id": "make_a_school_board_01", "unit": "make_a_school_board", "d": -0.7, "type": "mcq", "q": "在 Flask 里，函数上方的 `@app.route(\"/\")` 是什么意思？", "o": ["把这个函数保存到数据库里", "把这个函数发送到每一部手机", "访问首页时运行这个函数", "服务器停止时运行这个函数"], "a": 2, "why": "路由就是一个和函数连在一起的地址。有人请求首页时，Flask 运行这个函数，浏览器显示它返回的内容。", "at": "make_a_school_board.html#step1"},
  {"id": "make_a_school_board_02", "unit": "make_a_school_board", "d": -0.4, "type": "mcq", "q": "一个终端程序和一个 Flask 网页应用是两个 Python 文件。两者都用单纯的文件名 `homework.db` 打开一个 SQLite 数据库。为什么这两个文件应该放在同一个文件夹里，并在那里运行？", "o": ["这样两个程序打开的是同一个数据库文件", "这样 Flask 可以把自己安装到那个文件夹里", "这样手机也可以打开那个终端程序", "这样终端程序可以显示 HTML 页面"], "a": 0, "why": "单纯的文件名是在程序运行所在的文件夹里查找的。在同一个文件夹里，两个程序打开的是同一个 `homework.db`，所以数据只存在一个地方。", "at": "make_a_school_board.html#step0"},
  {"id": "make_a_school_board_03", "unit": "make_a_school_board", "d": 0.0, "type": "mcq", "q": "有人填写一个网页表单，它会往数据库里添加新的一行。浏览器应该用哪种 HTTP 方法？为什么？", "o": ["GET，因为它显示某些东西", "POST，因为它更改某些东西", "GET，因为它读取这个表单", "POST，因为它显示一个页面"], "a": 1, "why": "GET 的意思是“给我看点东西”。POST 的意思是“这里有一些数据，去更改点东西”。添加一行是一次更改，所以用 POST。", "at": "make_a_school_board.html#step3"},
  {"id": "make_a_school_board_04", "unit": "make_a_school_board", "d": 0.4, "type": "mcq", "q": "在一个 Flask 应用里，你用 POST 提交了一个表单，然后在浏览器里按了刷新。同一行又被添加了一次。解决办法是什么？", "o": ["从数据库里删掉第二行", "把表单从 POST 改成 GET", "在 HTML 里关掉刷新按钮", "POST 之后重定向到一次新的 GET"], "a": 3, "why": "刷新会重复浏览器做的最后一件事，也就是一次 POST。在 `return redirect(url_for(...))` 之后，最后一件事是一次 GET，所以刷新只是重新加载页面。", "at": "make_a_school_board.html#step4"},
  {"id": "make_a_school_board_05", "unit": "make_a_school_board", "d": 0.3, "type": "mcq", "q": "在 Flask 里，`app.run` 的 `host` 取哪个值，可以让同一个 Wi-Fi 上的任何人都能打开这个应用？", "o": ["`127.0.0.1`", "`localhost`", "`0.0.0.0`", "`255.255.255.0`"], "a": 2, "why": "`localhost` 指的是这台机器，别人都不行。`host=\"0.0.0.0\"` 的意思是监听整个网络，所以同一个 Wi-Fi 上的任何设备都能访问这个应用。", "at": "make_a_school_board.html#step6"},
  {"id": "make_a_school_board_06", "unit": "make_a_school_board", "d": 0.6, "type": "mcq", "q": "一个小型 Flask 应用用 `app.run(host=\"0.0.0.0\")` 运行在共享的 Wi-Fi 上。它没有登录功能，它的 Jinja 模板会显示人们输入的内容。真正的风险是什么？", "o": ["服务器停止时它会丢失所有数据", "它会把所有数据发送到互联网上", "这个 Wi-Fi 上的任何人都能改数据", "输入到表单里的 `<script>` 会运行"], "a": 2, "why": "没有登录功能，应用就分不清谁是谁，所以这个 Wi-Fi 上的任何人都可以输入任何名字、更改任何一行。这对成绩或任何私密的东西都不安全。模板把 `<script>` 当作文本显示，所以它不会运行。", "at": "make_a_school_board.html#step7"},
  {"id": "is_the_air_safe_01", "unit": "is_the_air_safe", "d": -0.8, "type": "mcq", "q": "PM2.5 这个数字统计的是什么？", "o": ["一天里有烟的小时数", "城市附近火点的数量", "城市上空空气的热度", "空气中微小的脏东西颗粒"], "a": 3, "why": "PM2.5 统计的是空气中微小的脏东西颗粒，它们小到可以深入你的肺里。", "at": "is_the_air_safe.html"},
  {"id": "is_the_air_safe_02", "unit": "is_the_air_safe", "d": -0.5, "type": "mcq", "q": "一个 Python 程序运行 `reply = requests.get(...)`，并打印 `reply.status_code`。你看到 `200`。这是什么意思？", "o": ["请求成功了，网站作出了回答。", "网站在那个地址上什么也没有。", "网站用了 200 秒才作出回答。", "网站发来了 200 条读数数据。"], "a": 0, "why": "`status_code` 是 200 表示请求成功了。404 表示“没有这个东西”。先看它，再去相信回答。", "at": "is_the_air_safe.html#step1"},
  {"id": "is_the_air_safe_03", "unit": "is_the_air_safe", "d": -0.1, "type": "fill", "q": "一个 Python 程序有一个时间的列表和一个数字的列表，两者并排对应。哪个 Python 函数可以同时顺着这两个列表往下走？请输入它的名字。", "answers": ["zip", "zip()"], "why": "`zip` 同时顺着两个列表往下走：`for when, value in zip(times, values)` 每一轮给出一对。", "at": "is_the_air_safe.html#step6"},
  {"id": "is_the_air_safe_04", "unit": "is_the_air_safe", "d": 0.1, "type": "mcq", "q": "一个 Python 程序用 `requests.get(API, timeout=20)` 向一个网站请求数据。为什么要设置 `timeout`？", "o": ["这样数据永远不会超过 20 分钟那么旧。", "这样一个没有反应的网站不会让程序卡住。", "这样程序会向这个网站请求 20 次。", "这样回复里只包含接下来的 20 个小时。"], "a": 1, "why": "没有超时设置的话，一个没有反应的网站会让你的程序永远卡住，而且没有任何提示。", "at": "is_the_air_safe.html#step7"},
  {"id": "is_the_air_safe_05", "unit": "is_the_air_safe", "d": 0.4, "type": "mcq", "q": "一个空气质量 API 接收一个纬度和一个经度。相距几公里的两个地方给出完全相同的数字，但远处的一座城市给出的数字不同。为什么？", "o": ["这个 API 给每个调用者的数据都一样。", "同一个国家里的空气总是一样的。", "两个地方落在数据的同一个网格方块里。", "这个 API 把每个数字四舍五入到整十。"], "a": 2, "why": "这类数据来自由方块组成的网格，比如每个方块大约 11 km 宽。同一个方块里的两个地方得到相同的数字。这些数据说的是一片区域，而不是一条街。", "at": "is_the_air_safe.html#step5"},
  {"id": "is_the_air_safe_06", "unit": "is_the_air_safe", "d": 0.5, "type": "mcq", "q": "看这段代码。`band_for(40)` 返回哪个词？", "code": "BANDS = [\n    (12.0,  \"GOOD\",      \"Go outside. The air is clean.\"),\n    (35.4,  \"OK\",        \"Most people are fine. Play outside.\"),\n    (55.4,  \"NOT GOOD\",  \"If you cough or have asthma, stay in.\"),\n    (150.4, \"BAD\",       \"Everyone should stay inside. Close the windows.\"),\n    (250.4, \"VERY BAD\",  \"Do not go outside. Wear an N95 mask if you must.\"),\n    (99999, \"DANGEROUS\", \"Stay inside. Keep the windows shut. Use a filter.\"),\n]\n\ndef band_for(pm25):\n    for limit, word, advice in BANDS:\n        if pm25 <= limit:\n            return word, advice", "o": ["`GOOD`", "`OK`", "`BAD`", "`NOT GOOD`"], "a": 3, "why": "这个函数沿着列表往下走，直到数字合适为止。40 大于 12.0 和 35.4，但不大于 55.4，所以这一行是 `NOT GOOD`。", "at": "is_the_air_safe.html#step3"},
  {"id": "teach_the_computer_to_speak_01", "unit": "teach_the_computer_to_speak", "d": -0.7, "type": "mcq", "q": "为什么使用 `edge-tts` 需要 wifi？", "o": ["这个库需要一个付费账号", "程序必须发送一个密码", "`.mp3` 文件是在线播放的", "声音是在一台服务器上生成的"], "a": 3, "why": "声音是在一台服务器上生成的，所以你需要 wifi。你从不需要密钥、账号或密码。", "at": "teach_the_computer_to_speak.html#step0"},
  {"id": "teach_the_computer_to_speak_02", "unit": "teach_the_computer_to_speak", "d": -0.4, "type": "mcq", "q": "这个 `edge-tts` 程序说的是英语。你把文本改成了泰语的词。还有什么必须改，泰语才能正确地读出来？", "code": "import asyncio\nimport edge_tts\n\nasync def speak(text, voice, out_path):\n    talker = edge_tts.Communicate(text, voice)\n    await talker.save(out_path)\n\nasyncio.run(speak(\"Good morning, teacher.\",\n                  \"en-GB-LibbyNeural\",\n                  \"hello.mp3\"))", "o": ["导入的那个库", "声音名称的字符串", "`speak` 的函数体", "对 `asyncio.run` 的调用"], "a": 1, "why": "只需要改声音名称，比如改成 `th-TH-PremwadeeNeural`。文字和声音必须匹配，不需要新的库或新的函数。", "at": "teach_the_computer_to_speak.html#step2"},
  {"id": "teach_the_computer_to_speak_03", "unit": "teach_the_computer_to_speak", "d": 0.1, "type": "mcq", "q": "一个文字转语音程序用这个 Python 函数来挑选声音。这个函数是怎样判断一段文本的语言的？", "code": "def detect_language(text):\n    thai = sum(1 for c in text if \"\\u0e00\" <= c <= \"\\u0e7f\")\n    han  = sum(1 for c in text if \"\\u4e00\" <= c <= \"\\u9fff\")\n    if thai > han and thai > 0:\n        return \"th\"\n    if han > 0:\n        return \"zh\"\n    return \"en\"", "o": ["它数一数泰文字符和中文字符的个数", "它问服务器这段文本是哪一种语言", "它只看这段文本的第一个字母", "它从文件的名字里读出语言"], "a": 0, "why": "每个字符背后都有一个编号，泰文和中文处在不同的范围里。这个函数数出每个范围里的字符个数，其余的都算英语。", "at": "teach_the_computer_to_speak.html#step4"},
  {"id": "teach_the_computer_to_speak_04", "unit": "teach_the_computer_to_speak", "d": 0.1, "type": "fill", "q": "`edge_tts.Communicate` 的哪个选项能为初学者把声音放慢？请输入它的名字。", "answers": ["rate", "rate=", "--rate"], "why": "`rate` 写成带加号或减号的百分数。`rate=\"-25%\"` 对初学者来说是个合适的速度。", "at": "teach_the_computer_to_speak.html#step5"},
  {"id": "teach_the_computer_to_speak_05", "unit": "teach_the_computer_to_speak", "d": 0.6, "type": "mcq", "q": "一个 Python 程序用 `edge-tts` 通过 `await talker.save(out_path)` 保存语音。然后它检查文件的大小是否大于 0。为什么？", "o": ["大文件无法在手机上播放", "服务器先发送大小，再发送声音", "文件可以存在，却仍然是空的", "这个文件必须比上一个文件小"], "a": 2, "why": "文件可以存在，却仍然是空的。除非你的程序去看文件大小，否则它不会发现文件是空的。", "at": "teach_the_computer_to_speak.html#step6"},
  {"id": "teach_the_computer_to_speak_06", "unit": "teach_the_computer_to_speak", "d": 0.4, "type": "mcq", "q": "一个程序用 `edge-tts` 生成语音，这是一个免费的在线语音服务。有时一个请求会失败，报 `NoAudioReceived`，而同样的请求两秒钟之后又成功了。程序应该怎么做？", "o": ["立刻停止，并删掉那个空文件", "再试一次，而且每次等得更久", "换成另一个声音，然后继续", "让用户把文本重新输入一遍"], "a": 1, "why": "免费的服务有时会很忙。程序应该多试几次，而且每次多等一会儿。", "at": "teach_the_computer_to_speak.html#step6"},
  {"id": "robot_python_01", "unit": "robot_python", "d": -0.8, "type": "mcq", "q": "Python 库 `bleak` 是做什么的？", "o": ["它在一块小的 LED 屏幕上画图", "它在你的电脑上安装 Python", "它替你的程序进行蓝牙通信", "它通过扬声器播放声音"], "a": 2, "why": "`bleak` 是 Python 的一个蓝牙库。它找到蓝牙设备，连接上它，并向它发送字节。", "at": "robot_python.html"},
  {"id": "robot_python_02", "unit": "robot_python", "d": -0.6, "type": "fill", "q": "在 Python 里，在一个 `async` 函数内部，哪个关键字的意思是“在这里等这件慢的事情做完，然后继续”？请输入这个英文单词。", "code": "async def main():\n    devices = ____ BleakScanner.discover(timeout=10)", "answers": ["await"], "why": "`async` 标记一个可以等待的函数。`await` 的意思是“在这里等，然后继续”。蓝牙很慢，所以程序必须等它。", "at": "robot_python.html"},
  {"id": "robot_python_03", "unit": "robot_python", "d": -0.3, "type": "mcq", "q": "一个 Python 程序向一个蓝牙设备发送 `b\"G#\"`。文本前面的 `b` 是什么意思？", "o": ["它是字节，不是字母", "它让文本变成粗体", "它把文本发送两次", "它把文本倒着读"], "a": 0, "why": "字母 `b` 生成的是一个字节值，而不是由字母组成的字符串。无线电发送的是字节，所以蓝牙写入需要字节。", "at": "robot_python.html"},
  {"id": "robot_python_04", "unit": "robot_python", "d": 0.0, "type": "mcq", "q": "这个 Python 辅助函数用 `bleak` 向一个小型蓝牙机器人发送一条命令。为什么它在每条命令之后要休眠 0.05 秒？", "code": "async def say(car, command):\n    await car.write_gatt_char(BOX, command.encode() + b\"#\")\n    await asyncio.sleep(0.05)", "o": ["让电池有时间休息一下", "让蓝牙每次都重新连接", "防止笔记本电脑变热", "防止发得太快的消息丢失"], "a": 3, "why": "小型设备一次只读一条命令。发得太快的消息会丢失，每条之后稍微歇一下就能解决这个问题。", "at": "robot_python.html"},
  {"id": "robot_python_05", "unit": "robot_python", "d": 0.3, "type": "mcq", "q": "这段 Python 代码驾驶一辆机器人小车。`say` 发送一条命令。为什么停止命令放在 `finally` 里面？", "code": "try:\n    await say(car, \"A\")      # forward\n    await asyncio.sleep(1)\nfinally:\n    await say(car, \"0\")      # stop", "o": ["`finally` 让停止命令运行得快很多", "`finally` 总会运行，即使代码出错", "`finally` 只在没有错误时才运行", "`finally` 在失败之后重新连接蓝牙"], "a": 1, "why": "`finally` 总会运行。所以即使 `try` 里的代码出错，小车也还是会停下。", "at": "robot_python.html"},
  {"id": "robot_python_06", "unit": "robot_python", "d": 0.2, "type": "mcq", "q": "一个小型蓝牙机器人开着，而且就在旁边。你用 `bleak` 的 Python 程序还是找不到它。你应该检查什么？", "o": ["程序是否调用了两次 `await`", "机器人的设备名称是否更长", "是否没有手机应用连着它", "机器人的灯是否已经关掉"], "a": 2, "why": "这样的小型蓝牙设备一次只和一个对象通信。如果一个手机应用占着它，Python 就找不到它。", "at": "robot_python.html#scripts"},
  {"id": "robot_python_07", "unit": "robot_python", "d": 0.5, "type": "mcq", "q": "这段 Python 代码通过蓝牙让一辆机器人小车停下。`say` 发送一条命令，`\"0\"` 表示停止。为什么它把停止命令发送三次，而不是一次？", "code": "finally:\n    for i in range(3):\n        await say(car, \"0\")\n        await asyncio.sleep(0.15)", "o": ["小车需要三条命令才能刹车", "每条命令停住其中一个轮子", "前两条只是把灯关掉而已", "丢一条消息，它就会一直开下去"], "a": 3, "why": "蓝牙消息可能会丢失。如果唯一的一条停止消息丢了，小车就会一直开下去，所以代码不止发送一次。", "at": "robot_python.html#scripts"},
  {"id": "robot_python_08", "unit": "robot_python", "d": 0.6, "type": "mcq", "q": "`SONG` 是 `\"1155665443322 1\"`，`say` 向机器人发送一条命令。当 `note` 是那个空格时，这个循环做什么？", "code": "for note in SONG:\n    if note == \" \":\n        await asyncio.sleep(0.4)\n        continue\n    await say(car, note)\n    await say(car, COLOUR[note])", "o": ["它等待，不发送任何命令", "它亮着红灯演奏音符 1", "它停止循环并结束这首歌", "它把所有的灯永久关掉"], "a": 0, "why": "遇到空格时，循环先休眠，然后 `continue` 跳到下一个字符，不调用 `say`。", "at": "robot_python.html"},
  {"id": "control_your_phone_01", "unit": "control_your_phone", "d": -0.7, "type": "mcq", "q": "哪个工具会在你的电脑上打开一个窗口，显示 Android 手机的屏幕？", "o": ["`adb`", "`scrcpy`", "`uiautomator2`", "`pip3`"], "a": 1, "why": "`adb` 是两台机器之间的通路，`scrcpy` 是显示手机的窗口，`uiautomator2` 是按按钮的 Python 库。", "at": "control_your_phone.html#step2"},
  {"id": "control_your_phone_02", "unit": "control_your_phone", "d": -0.5, "type": "mcq", "q": "怎样让 Android 手机上隐藏的开发者菜单出现？", "o": ["点按 Build number 七次", "按住电源键十秒钟", "在手机上安装 `adb`", "把 USB 线插上两次"], "a": 0, "why": "在 Settings 的 About phone 里，点按 Build number 七次。之后 Developer options 就在 Settings 的 System 里。", "at": "control_your_phone.html#step1"},
  {"id": "control_your_phone_03", "unit": "control_your_phone", "d": 0.2, "type": "mcq", "q": "`adb pair` 成功了，但 `adb connect` 显示 `failed to connect`。这里最常见的错误是什么？", "o": ["你输入六位数配对码时太慢了", "你在 `adb devices` 之前运行了 `scrcpy`", "你用了配对端口来连接", "你忘了安装 `uiautomator2`"], "a": 2, "why": "配对端口和连接端口不是同一个。要用 Wireless debugging 界面顶部的那个端口。", "at": "control_your_phone.html#step3"},
  {"id": "control_your_phone_04", "unit": "control_your_phone", "d": 0.0, "type": "mcq", "q": "`adb devices` 在你的手机旁边显示 `unauthorized` 这个词。你应该怎么做？", "o": ["在 Mac 上运行 `adb kill-server`", "打开手机自己的热点", "插上 USB 线，然后等待", "看一下手机，点接受"], "a": 3, "why": "手机在请求许可，却没有人回应。看一下手机，勾选 Always allow，然后接受。", "at": "control_your_phone.html#step3"},
  {"id": "control_your_phone_05", "unit": "control_your_phone", "d": 0.4, "type": "mcq", "q": "一个 `uiautomator2` 脚本用 `d(text=\"Wi-Fi\").click()` 点击 Android 手机上的一行，而不是用屏幕上的坐标。为什么用名称更好？", "o": ["坐标让手机找起来比较慢", "布局变了之后，名称仍然有效", "坐标需要一根 USB 线才能发送", "名称让脚本不用 `adb` 也能运行"], "a": 1, "why": "坐标今天有效，明天就失效：换成更大的屏幕，或者一次更新把这一行挪了位置。按名称来找则能一直有效。", "at": "control_your_phone.html#step6"},
  {"id": "control_your_phone_06", "unit": "control_your_phone", "d": 0.6, "type": "mcq", "q": "到一部 Android 手机的无线 `adb` 连接昨天还能用。今天用同样的地址和端口，`adb connect` 却失败了。为什么？", "o": ["无线调试选了一个新的端口", "配对码只有一天的有效期", "辅助应用在夜里被删除了", "电脑上有两份 `scrcpy`"], "a": 0, "why": "无线调试每次重启都会选一个新的端口。配对会被记住，端口不会，所以要读出新的端口，再连接一次。", "at": "control_your_phone.html#step8"},
  {"id": "control_your_phone_07", "unit": "control_your_phone", "d": 0.5, "type": "mcq", "q": "iPhone 没有 `adb`。Python 用脚本控制 iPhone 时，是在和什么通信？", "o": ["QuickTime Player", "iPhone Mirroring", "WebDriverAgent", "AirPlay Receiver"], "a": 2, "why": "WebDriverAgent 是一个小应用，你在 Xcode 里把它构建出来并留在手机上。之后 Python 就和这个应用通信。", "at": "control_your_phone.html#step9"},
  {"id": "crewai_autogen_01", "unit": "crewai_autogen", "d": -0.7, "type": "mcq", "q": "在 CrewAI 里，每个智能体都会得到什么？", "o": ["一个 `name`、一个 `model_client` 和一个 `system_message`", "一个 `role`、一个 `goal` 和一个 `backstory`", "一个 `@tool` 和一个 `final_answer()`", "一个 `prompt`、一个 `seed` 和一个 `temperature`"], "a": 1, "why": "CrewAI 描述智能体的方式，就像经理描述员工：一个角色（职位名称）、一个目标，还有一段决定它行事风格的背景故事。", "at": "crewai_autogen.html#crewai"},
  {"id": "crewai_autogen_02", "unit": "crewai_autogen", "d": -0.4, "type": "mcq", "q": "在 CrewAI 里，后面的一个 `Task` 必须看到前面两个任务的输出，它们名叫 `spec` 和 `build`。怎样把两者都交给它？", "o": ["用 `verbose=False` 创建它的智能体", "用 `tracing=False` 创建这个团队", "把两个输出保存在同一个文本文件里", "给这个任务加上 `context=[spec, build]`"], "a": 3, "why": "在 CrewAI 里，每个任务通过 `context=[...]` 接收前面的任务。一个同时列出 `spec` 和 `build` 的任务可以读到两者的输出。", "at": "crewai_autogen.html#crewai"},
  {"id": "crewai_autogen_03", "unit": "crewai_autogen", "d": 0.4, "type": "mcq", "q": "在 CrewAI 里，如果在 `Crew` 里漏掉 `tracing=False`，会发生什么？", "o": ["CrewAI 在最后停下来，询问有关追踪记录的事", "CrewAI 以随机的顺序来运行这些任务", "智能体再也看不到彼此的工作成果", "团队的结果被保存下来，但里面没有文本"], "a": 0, "why": "没有它，CrewAI 会在工作完成后停下来，在终端里问你是否想查看执行追踪记录。这看起来就像脚本卡住了。", "at": "crewai_autogen.html#crewai"},
  {"id": "crewai_autogen_04", "unit": "crewai_autogen", "d": 0.5, "type": "mcq", "q": "一个 CrewAI 团队里有一个程序员智能体，负责写一个 HTML 文件，还有一个测试员智能体，负责对照一份检查清单来检查它。为什么你还是应该自己在浏览器里打开这个文件？", "o": ["测试员智能体检查完之后会删掉这个文件", "程序员智能体从来看不到那份检查清单", "测试员只是把 HTML 当作文本重读一遍", "这个团队无法把任何文件保存到磁盘上"], "a": 2, "why": "测试员智能体不是一套测试。它对照检查清单重读 HTML，并不打开浏览器，所以没有任何东西能证明这个文件可以正常渲染。", "at": "crewai_autogen.html#crewai"},
  {"id": "crewai_autogen_05", "unit": "crewai_autogen", "d": -0.3, "type": "mcq", "q": "AutoGen 的 `RoundRobinGroupChat` 怎样决定由谁发言？", "o": ["每次由一个经理智能体挑选下一个发言者", "智能体轮流发言，然后列表从头再来", "所有智能体在同一时刻回答这个任务", "目前为止消息最少的智能体下一个发言"], "a": 1, "why": "这是最简单的团队：智能体轮流发言，列表走完之后就从头再来。", "at": "crewai_autogen.html#autogen"},
  {"id": "crewai_autogen_06", "unit": "crewai_autogen", "d": 0.5, "type": "mcq", "q": "在一个 AutoGen 群聊里，第一条消息包含一些数字，其中一个智能体在其他智能体发言之前不得使用它们。这个智能体在自己的第一轮就用了它们。用大写字母把“先不要使用它们”再写一遍，毫无作用。怎么修复？", "o": ["在后面的每条消息里重复这条指令", "让模型自己把这些数字编出来", "给这一个智能体用更低的温度", "只在它需要时才把数字交给它"], "a": 3, "why": "模型会使用摆在它面前的东西。所以解决办法不是更强硬的指令，而是更小的上下文：在这个智能体需要之前，不要让这些数字出现在聊天里。", "at": "crewai_autogen.html#autogen"},
  {"id": "crewai_autogen_07", "unit": "crewai_autogen", "d": 0.0, "type": "mcq", "q": "你有几项顺序固定的工作，每一项由一个带职位名称的智能体来完成。哪个 Python 智能体库是为此设计的？", "o": ["CrewAI", "AutoGen", "smolagents", "LangChain"], "a": 0, "why": "CrewAI 适合几项顺序固定的工作，每一项由一个带职位名称的智能体完成。顺序由你决定，而且写得明明白白。", "at": "crewai_autogen.html#pick"},
  {"id": "langchain_01", "unit": "langchain", "d": -0.8, "type": "mcq", "q": "在 LangChain 里，`prompt | model | StrOutputParser()` 中的管道符 `|` 起什么作用？", "o": ["它让这三个部件同时运行。", "它把一个部件的输出送给下一个。", "它从三个部件里挑出最好的回答。", "它把每个部件保存成你机器上的文件。"], "a": 1, "why": "你用 `|` 把提示词、模型和解析器连接起来。从一个里面出来的东西进入下一个，就像一条生产线。", "at": "langchain.html#s1"},
  {"id": "langchain_02", "unit": "langchain", "d": -0.4, "type": "mcq", "q": "在 LangChain 里，如果 `StrOutputParser()` 是一条链的最后一个部件，你从这条链得到什么？", "o": ["一个带有命名字段的 pydantic 对象。", "一个来自智能体的消息列表。", "一个来自模型的消息对象。", "纯文本，而不是消息对象。"], "a": 3, "why": "没有 `StrOutputParser()`，你得到的是一个消息对象。有了它，你得到的是纯文本。", "at": "langchain.html#s2"},
  {"id": "langchain_03", "unit": "langchain", "d": -0.1, "type": "mcq", "q": "这一行创建了一个在链里使用的 LangChain 模型。为什么 `reasoning=False` 在这里很重要？", "code": "model = ChatOllama(model=\"qwen3\", base_url=\"http://localhost:11434\",\n                   num_ctx=8192, reasoning=False)", "o": ["这样它的思考过程不会被传给下一步。", "这样模型不使用互联网就能作出回答。", "这样模型被允许自己去调用一个工具。", "这样模型每一次都给出相同的回答。"], "a": 0, "why": "qwen3 通常会在回答之前先写出它的思考过程，而链会把这些思考过程直接交给下一步。`reasoning=False` 会把思考关掉。", "at": "langchain.html#s1"},
  {"id": "langchain_04", "unit": "langchain", "d": 0.3, "type": "mcq", "q": "你用 `with_structured_output()` 和一个 pydantic 类，要求 LangChain 模型给出结构化输出。每个字段返回时都填好了，但有一个值是错的。这说明了什么？", "o": ["结构化输出只有付费模型才能用。", "这个类还需要再加一个字段才对。", "结构化输出只保证输出的形状。", "模型必须先思考才能填好字段。"], "a": 2, "why": "结构化输出保证的是形状，别的都不保证。值仍然可能是错的，所以要像对待其他任何回答一样检查它们。", "at": "langchain.html#s3"},
  {"id": "langchain_05", "unit": "langchain", "d": 0.5, "type": "mcq", "q": "在 LangChain 里，链和智能体有什么区别？", "o": ["链需要密钥；智能体在免费模型上运行。", "链给出对象；智能体给出纯文本。", "链运行相同的步骤；智能体自己决定。", "链使用一个提示词；智能体一个也不用。"], "a": 2, "why": "链总是运行相同的步骤。智能体在回答之前自己选择是否调用工具。", "at": "langchain.html#s5"},
  {"id": "langchain_06", "unit": "langchain", "d": 0.7, "type": "mcq", "q": "你的工作是一组固定的步骤：先解释，再翻译，再缩短。哪个库是围绕着用管道符 `|` 连接固定步骤而构建的？", "o": ["LangChain", "smolagents", "CrewAI", "AutoGen"], "a": 0, "why": "LangChain 用 `|` 把提示词、模型和解析器连接起来。固定的管道容易阅读和测试，因为它每次做的事情都一样。", "at": "langchain.html#s6"},
  {"id": "face_recognition_01", "unit": "face_recognition", "d": -0.8, "type": "mcq", "q": "人们说的“人脸识别”其实不止一项工作。下面哪一项工作对程序来说是难的？", "o": ["找出照片里人脸在什么位置", "说出照片里的人叫什么名字", "把照片从彩色变成灰色", "在每张人脸周围画一个框"], "a": 1, "why": "找出人脸是一项老技术，速度快，而且管用。说出人脸的名字要难得多：光线、角度，还有程序从没见过的人，都会造成错误。", "at": "face_recognition.html#step1"},
  {"id": "face_recognition_02", "unit": "face_recognition", "d": -0.5, "type": "mcq", "q": "在 Python 里用 OpenCV 4 做人脸识别，为什么要安装 contrib 版本 `opencv-contrib-python`？", "o": ["它是唯一能打开照片的版本", "它把照片发送到更快的服务器", "它会下载一个训练好的 AI 模型", "它还带有 `cv2.face` 模块"], "a": 3, "why": "contrib 版本带有人脸查找器，还带有 `cv2.face`，也就是给人脸说出名字的那部分（例如 `LBPHFaceRecognizer_create`）。", "at": "face_recognition.html#step2"},
  {"id": "face_recognition_03", "unit": "face_recognition", "d": -0.3, "type": "mcq", "q": "OpenCV 的 Haar 级联人脸检测器通过 `detectMultiScale` 调用。把它的 `minNeighbors` 值调高，会发生什么？", "o": ["它的误报会变少", "它能找到更多人脸", "它忽略所有小的人脸", "它让照片保持彩色"], "a": 0, "why": "`minNeighbors` 决定检测器有多挑剔。调高它，误报更少；调低它，能找到更多人脸。", "at": "face_recognition.html#step3"},
  {"id": "face_recognition_04", "unit": "face_recognition", "d": 0.0, "type": "mcq", "q": "OpenCV 的 Haar 级联人脸检测器有时会在一个圆形的印刷标志周围画一个框。为什么？", "o": ["它把每张真实的人脸数了两三次", "它是用印刷标志的照片训练出来的", "它只是在匹配明暗相间的图案", "它把每个颜色鲜亮的形状都框起来"], "a": 2, "why": "检测器拿一些简单的明暗图案在灰度图片上滑动。一个带有两个深色标记的圆形符合这些图案，所以它被报告成一张人脸。", "at": "face_recognition.html#step3"},
  {"id": "face_recognition_05", "unit": "face_recognition", "d": 0.2, "type": "mcq", "q": "这段 Python 代码使用 OpenCV 的 LBPH 人脸识别器。照片里找到了一张人脸，`model.predict` 给出的距离是 64.3。`who` 返回哪个名字？", "code": "THRESHOLD = 56\n\ndef who(model, names, path):\n    \"\"\"Name the face in one photo, or say it does not know.\"\"\"\n    face = face_from(path)\n    if face is None:\n        return \"no face found\", None\n    label, distance = model.predict(face)\n    if distance > THRESHOLD:\n        return \"I do not know\", distance\n    return names[label], distance", "o": ["`I do not know`", "`no face found`", "最接近的那个名字", "一条错误消息"], "a": 0, "why": "距离小表示匹配得近。这里 64.3 高于阈值 56，所以代码拒绝给出名字。", "at": "face_recognition.html#step4"},
  {"id": "face_recognition_06", "unit": "face_recognition", "d": 0.6, "type": "mcq", "q": "一个人脸识别器为每张照片给出一个距离，距离小表示匹配得近。在一次测试中，它认识的人得分从 34.6 到 77.8，陌生人得分从 52.1 到 88.7。为什么没有任何单一的阈值能把这两组分开？", "o": ["距离在每次运行时都会变", "训练它时用的人太多了", "这两个距离范围互相重叠", "陌生人的得分总是更低"], "a": 2, "why": "这两个范围互相重叠，所以任何阈值都会犯两种错误：把一些认识的人拒之门外，又把一些陌生人放进来。", "at": "face_recognition.html#step5"},
  {"id": "face_recognition_07", "unit": "face_recognition", "d": 0.7, "type": "mcq", "q": "一个 Python 程序用 OpenCV 在实时的摄像头画面里找人脸。它的作者本可以加一个识别器，给每张人脸说出名字，但没有加。一个好的理由是什么？", "o": ["摄像头画面太小，无法说出名字", "错误的名字比没有名字更糟糕", "说出名字需要一个来自互联网的模型", "OpenCV 无法给视频里的人脸说出名字"], "a": 1, "why": "说出名字是经常出错的那一半。一个悄悄给陌生人安上名字的程序，比一个什么都不说的程序更糟糕。", "at": "face_recognition.html#step6"},
  {"id": "physics_with_python_01", "unit": "physics_with_python", "d": -0.8, "type": "mcq", "q": "这个 Python 函数模拟一块从 `HEIGHT` 落下的石头。它没有使用公式 ½gt²。它用的是什么？", "code": "def simulate(dt, stop):\n    y, v, t = HEIGHT, 0.0, 0.0\n    while t < stop - 1e-12:\n        y = y - v * dt\n        v = v + G * dt\n        t = t + dt\n    return y", "o": ["石头在每一秒的高度", "石头落到地面所需要的时间", "石头的质量和空气的密度", "重力改变速度；速度改变位置"], "a": 3, "why": "这样的模拟只知道两件事，并以很小的时间步长不断重复：重力改变速度，速度改变位置。", "at": "physics_with_python.html#step2"},
  {"id": "physics_with_python_02", "unit": "physics_with_python", "d": -0.4, "type": "mcq", "q": "一个模拟石头下落的 Python 程序重复两行代码：先是 `y = y - v * dt`，然后是 `v = v + G * dt`。你把 `dt` 缩小到原来的十分之一。结果的误差会怎样？", "o": ["它缩小到原来的十分之一。", "它的大小保持完全不变。", "它增大到原来的十倍。", "它变成恰好为零。"], "a": 0, "why": "用这种简单的方法，误差随 `dt` 成比例增长。在一次运行中，0.1 s 给出的误差是 0.9810 m，0.01 s 给出的是 0.0981 m。模拟是近似的，而你可以有意地把误差缩小。", "at": "physics_with_python.html#step2"},
  {"id": "physics_with_python_03", "unit": "physics_with_python", "d": 0.5, "type": "mcq", "q": "一个石头下落的模拟里有一个符号写错了：本该是 `y - v*dt`，却写成了 `y + v*dt`。它运行时没有报错，而石头往上飞。要发现这类错误，可靠的办法是什么？", "o": ["等 Python 报告一个符号错误。", "用更小的 `dt` 再运行一次。", "拿它和纸上已知的答案对照。", "运行两次，比较两次的结果。"], "a": 2, "why": "一个模拟可以自己和自己一致，却仍然完全是错的。要拿它和不是用它算出来的东西对照，比如公式 ½gt²。", "at": "physics_with_python.html#step2"},
  {"id": "physics_with_python_04", "unit": "physics_with_python", "d": -0.3, "type": "mcq", "q": "下落的球什么时候达到终端速度？", "o": ["当它已经下落了十秒钟时", "当空气的推力等于它的重量时", "当空气变得太稀薄、推不动它时", "当重力不再把它往下拉时"], "a": 1, "why": "球越快，空气推得越用力。当推力等于重量时，球就不再加速。", "at": "physics_with_python.html#step3"},
  {"id": "physics_with_python_05", "unit": "physics_with_python", "d": 0.1, "type": "mcq", "q": "一个球在空气中下落。为什么要用 Python 模拟这次下落，而不是直接用一个公式？", "o": ["公式太慢，Python 运行不了。", "这个公式需要一个没有安装的库。", "计算最终速度的那个公式是错的。", "整个下落过程没有一个简洁的公式。"], "a": 3, "why": "最终速度有一个简洁的公式 √(mg/K)，但下落的其余部分没有。代数用到头了，就去模拟。", "at": "physics_with_python.html#step3"},
  {"id": "physics_with_python_06", "unit": "physics_with_python", "d": 0.0, "type": "mcq", "q": "没有空气时，以 45 度抛出的球飞得最远。在模拟里加上空气阻力之后，最佳角度会怎样？", "o": ["它变得低于 45 度。", "它变得高于 45 度。", "它保持在恰好 45 度。", "它变成恰好 90 度。"], "a": 0, "why": "45 度只是在没有空气的世界里才是正确答案。在一次有空气的、用力抛出网球的模拟中，最佳角度是 39 度。", "at": "physics_with_python.html#step4"},
  {"id": "physics_with_python_07", "unit": "physics_with_python", "d": 0.7, "type": "mcq", "q": "教科书上单摆周期的公式是 T = 2π√(L/g)。这个公式有多准？", "o": ["它在每个角度上都错得一样多。", "只有当摆长是一米时它才是对的。", "摆幅小时它很准，摆幅大时就差一些。", "它是精确的，只有模拟才有误差。"], "a": 2, "why": "这个公式里不含角度，但真实的周期随摆幅增大而变长。在一次模拟中，这个公式在 1 度时偏差 0.00%，在 90 度时偏差 18.04%。", "at": "physics_with_python.html#step5"},
  {"id": "telescope_data_01", "unit": "telescope_data", "d": -0.8, "type": "mcq", "q": "在空间望远镜的数据里，MAST 是什么？", "o": ["一个绘制星系图片的 Python 库。", "下载文件之前需要的一个密码。", "NASA 存放望远镜图片的档案库。", "2022 年开始工作的最新空间望远镜。"], "a": 2, "why": "NASA 把这些图片存放在一个名叫 MAST 的大型档案库里，你不需要账号就可以从 Python 向它提问。", "at": "telescope_data.html"},
  {"id": "telescope_data_02", "unit": "telescope_data", "d": -0.5, "type": "mcq", "q": "Python 库 `astroquery` 是做什么用的？", "o": ["它知道怎样向 MAST 这样的档案库提问。", "它把原始文件变成制作完成的图片。", "它把表格打印成整齐、对齐的列。", "它读取你在文件名后面输入的内容。"], "a": 0, "why": "`astropy` 了解天空，`astroquery` 知道怎样向档案库提问。MAST 是其中之一。", "at": "telescope_data.html#step1"},
  {"id": "telescope_data_03", "unit": "telescope_data", "d": -0.3, "type": "mcq", "q": "这一行用 `astroquery` 向 MAST 档案库提问。`radius=\"0.01 deg\"` 是什么意思？", "code": "obs = Observations.query_object(\"M51\", radius=\"0.01 deg\")", "o": ["档案库最多可以返回多少行。", "望远镜的快门打开了多长时间。", "允许下载的文件最大有多大。", "在目标周围搜索多宽的一个圆。"], "a": 3, "why": "半径说的是搜索多宽的一个圆。半径越大，结果越多。", "at": "telescope_data.html#step3"},
  {"id": "telescope_data_04", "unit": "telescope_data", "d": 0.0, "type": "mcq", "q": "这个 Python 循环统计每个名字有多少行。为什么它用 `tally.get(name, 0)` 而不用 `tally[name]`？", "code": "tally = {}\nfor row in obs:\n    name = str(row[\"obs_collection\"])\n    tally[name] = tally.get(name, 0) + 1", "o": ["`tally[name]` 会把这些名字按错误的顺序排序。", "名字是新的时，`tally[name]` 会因 `KeyError` 而停下。", "`tally[name]` 会在循环里把每个名字数两次。", "`tally[name]` 只能用在列表上，绝不能用在字典上。"], "a": 1, "why": "循环第一次遇到每个名字时，这个名字都还不存在。对一个不存在的键使用方括号会引发 `KeyError`，而 `.get(name, 0)` 会交回备用的 0。", "at": "telescope_data.html#step4"},
  {"id": "telescope_data_05", "unit": "telescope_data", "d": 0.4, "type": "mcq", "q": "`obs` 是一个 `astropy` 表格。为什么这个筛选用 `&` 而不用 `and` 来连接它的两个条件？", "code": "pics = obs[(obs[\"obs_collection\"] == \"HST\") &\n           (obs[\"dataproduct_type\"] == \"image\")]", "o": ["在大表格上，`and` 比 `&` 慢。", "`and` 还会改动表格 `obs`。", "每个条件对每一行都给出一个答案。", "只有一个条件为 True 时，`&` 也保留该行。"], "a": 2, "why": "在表格上，`==` 一次为每一行给出一个 True 或 False。这就是为什么表格需要 `&` 而不是 `and`，并且每个条件要放在各自的括号里。", "at": "telescope_data.html#step5"},
  {"id": "telescope_data_06", "unit": "telescope_data", "d": 0.1, "type": "fill", "q": "一个列表有五项。切片 `[:3]` 给你多少项？请输入这个数字。", "answers": ["3", "three"], "why": "计数从 0 开始，结尾的数字不包括在内，所以 `[:3]` 给出第 0、1 和 2 项。", "at": "telescope_data.html#step6"},
  {"id": "telescope_data_07", "unit": "telescope_data", "d": 0.6, "type": "mcq", "q": "这段 `astroquery` 代码从 MAST 档案库下载。它只保留 `PREVIEW` 文件，按大小给它们排序，然后取 `[:1]`。这是为了什么？", "code": "products = Observations.get_product_list(pics[:1])\nsmall = products[products[\"productType\"] == \"PREVIEW\"]\nsmall.sort(\"size\")\nresult = Observations.download_products(small[:1], download_dir=\"sky\")", "o": ["为了让档案库不向你索要密码。", "为了让下载的是一张制作完成的新闻图片。", "为了让文件保存在文件夹 `sky` 里。", "为了让你绝不会误抓一个巨大的文件。"], "a": 3, "why": "预览文件很小，`sort(\"size\")` 把最小的排在最前面，`[:1]` 取一个。原始科学数据文件可能有 100 MB。", "at": "telescope_data.html#step10"},
  {"id": "telescope_data_08", "unit": "telescope_data", "d": 0.7, "type": "mcq", "q": "使用 `astroquery` 时，`Observations.query_object(\"Saturn\", radius=\"0.01 deg\")` 给出一个 `ResolverError`。为什么？", "o": ["搜索圆的半径太小了。", "行星会移动；名称查找器只认识固定的东西。", "档案库里没有哪台望远镜观测过土星。", "这个名字必须用大写字母来写。"], "a": 1, "why": "名称查找器只认识固定的东西，而行星会移动。请使用恒星、星系或星云，或者用数字给出位置。", "at": "telescope_data.html#step11"},
  {"id": "cloakbrowser_01", "unit": "cloakbrowser", "d": -0.8, "type": "mcq", "q": "为什么一次普通的 `requests.get` 调用会从大型商业网站得到 `403 Forbidden`？", "o": ["这个网站只回答付过费的用户。", "`requests` 库无法打开 HTTPS 页面。", "机器人防护判定这个请求来自脚本。", "HTML 太大了，Python 下载不了。"], "a": 2, "why": "机器人防护服务会给请求看起来有多像人打分。一次裸的 `requests` 调用几乎通不过任何一项检查，所以它得到的是验证页面。", "at": "cloakbrowser.html"},
  {"id": "cloakbrowser_02", "unit": "cloakbrowser", "d": -0.5, "type": "mcq", "q": "在 CloakBrowser 里，和在 Playwright 里一样，`page.content()` 给你什么？", "o": ["网页当前的 HTML，是一个字符串", "网页的一张图片，是一个图像文件", "在网页上找到的每个链接的列表", "服务器发回来的那个状态码"], "a": 0, "why": "`page.content()` 把当前的 HTML 作为一个 Python 字符串返回，和人在浏览器里看到的 HTML 一样。", "at": "cloakbrowser.html"},
  {"id": "cloakbrowser_03", "unit": "cloakbrowser", "d": -0.6, "type": "fill", "q": "Python 包 `cloakbrowser` 的哪个函数启动隐身的 Chromium 浏览器？请输入它的名字。", "answers": ["launch", "launch()", "browser = launch()", "browser=launch()", "cloakbrowser.launch", "cloakbrowser.launch()"], "why": "`launch()` 启动浏览器进程，并交给你一个浏览器对象。", "at": "cloakbrowser.html"},
  {"id": "cloakbrowser_04", "unit": "cloakbrowser", "d": -0.1, "type": "mcq", "q": "你用 CloakBrowser 在一次运行中抓取好几个 URL。启动浏览器的代价很高，而在一个还在加载的页面里跳转到新的 URL 可能会失败。正确的做法是什么？", "o": ["为每一个 URL 都启动一个新的浏览器。", "所有的 URL 都重复使用同一个页面。", "在一个标签页里同时打开所有 URL。", "只启动一次，每个 URL 打开一个新页面。"], "a": 3, "why": "启动浏览器的代价很高，所以只启动一次。在多次跳转之间重复使用同一个页面会引起“navigation is changing”竞争问题，所以要为每个 URL 调用 `browser.new_page()`。", "at": "cloakbrowser.html"},
  {"id": "cloakbrowser_05", "unit": "cloakbrowser", "d": 0.2, "type": "mcq", "q": "为什么网页抓取程序应该在解析之前先把原始 HTML 保存到磁盘上？", "o": ["这样网站看不出是谁抓取了 HTML。", "这样你可以重新解析，而不必再抓取。", "这样浏览器运行时需要的内存更少。", "这样 HTML 文件在 Chrome 里加载更快。"], "a": 1, "why": "抓取是又慢又脆弱的部分，而解析要重做很多次。原始 HTML 存在磁盘上，你就只需抓取一次，之后重新解析不花任何代价。", "at": "cloakbrowser.html"},
  {"id": "cloakbrowser_06", "unit": "cloakbrowser", "d": 0.4, "type": "mcq", "q": "抓取程序应该先读取购物网站的 JSON-LD 块，只把对可见文本使用正则表达式作为后备办法。为什么？", "o": ["每一个网页上都能找到 JSON-LD。", "JSON-LD 不需要 JavaScript 就能加载。", "JSON-LD 比页面标记更稳定。", "机器人防护看不到 JSON-LD。"], "a": 2, "why": "网站嵌入 JSON-LD 是为了让 Google 能读取它。它比可见的页面标记稳定得多，后者总是在变。", "at": "cloakbrowser.html"},
  {"id": "cloakbrowser_07", "unit": "cloakbrowser", "d": 0.7, "type": "mcq", "q": "一个 AI 智能体搜索网络并给结果排名。最后，Python 程序从它自己保存的数据里打印出结果链接。它不让模型来写这些链接。为什么？", "o": ["模型抄写长地址时很容易出错。", "模型没有用来打印的工具。", "Python 搜索网络要快得多。", "搜索网站对模型隐藏链接。"], "a": 0, "why": "模型负责判断，Python 负责保管事实。模型抄写长地址时很容易出错，所以链接来自程序保存的数据。", "at": "cloakbrowser.html#agent"},
  {"id": "cloakbrowser_08", "unit": "cloakbrowser", "d": 0.5, "type": "mcq", "q": "为什么要把 CloakBrowser 抓取程序作为普通的 `.py` 脚本运行，而不是放在 Jupyter notebook 里运行？", "o": ["notebook 不能用 `pip` 安装软件包。", "notebook 不能把 HTML 文件保存到磁盘。", "notebook 会更快地被 Cloudflare 拦截。", "`launch()` 拒绝在 Jupyter 的事件循环里启动。"], "a": 3, "why": "`launch()` 是一个同步 API。它拒绝在 Jupyter 已经在运行的 asyncio 事件循环里启动；普通脚本没有正在运行的事件循环。", "at": "cloakbrowser.html#getit"},
  {"id": "rust_start_01", "unit": "rust_start", "d": -0.8, "type": "mcq", "q": "`cargo check` 是做什么的？", "o": ["它构建程序，然后运行它。", "它为你创建一个新的项目文件夹。", "它构建一个供实际使用的快速程序。", "它检查代码能不能构建。"], "a": 3, "why": "`cargo check` 问编译器“这能构建吗？”，但并不真的构建，所以它很快。", "at": "rust_start.html#s4"},
  {"id": "rust_start_02", "unit": "rust_start", "d": -0.6, "type": "fill", "q": "在 Rust 里，用 `let` 创建的变量不能改变。要让它可以改变，你在 `let` 后面加哪个词？请输入这个词。", "answers": ["mut", "let mut"], "why": "变量默认是不可变的。`let mut` 告诉编译器，也告诉读代码的人，这个值会改变。", "at": "rust_start.html#s5"},
  {"id": "rust_start_03", "unit": "rust_start", "d": -0.3, "type": "mcq", "q": "在这个函数里，如果在 `a + b` 后面加一个分号，会发生什么？", "code": "fn add(a: i32, b: i32) -> i32 {\n    a + b\n}", "o": ["这个函数会把和返回两次。", "没有任何变化；分号是可有可无的。", "它什么也不返回，所以无法编译。", "这个函数只返回 `a` 的值。"], "a": 2, "why": "代码块里最后那个不带分号的东西，就是返回的值。加了分号，代码块什么也不返回，编译器会拒绝这个函数，因为它承诺过返回一个 `i32`。这是初学者非常常见的错误。", "at": "rust_start.html#s7"},
  {"id": "rust_start_04", "unit": "rust_start", "d": 0.4, "type": "mcq", "q": "为什么 `main` 在最后一行还能使用 `name`？", "code": "fn shout(text: &String) -> String {\n    text.to_uppercase()\n}\n\nfn main() {\n    let name = String::from(\"Chiang Mai\");\n    println!(\"{}\", shout(&name));\n    println!(\"{} is still here\", name);\n}", "o": ["`shout` 会把它收到的每个 `String` 都复制一份。", "`&` 只是把 `name` 借出去，所以 `main` 还留着它。", "`name` 是用 `let mut` 创建的，所以它可以移动回来。", "`println!` 打印完 `name` 之后会把它还回来。"], "a": 1, "why": "传递一个 `String` 时不带 `&`，它就被移动走了，你不能再用它。带上 `&`，函数只是借用它，你还留着它。", "at": "rust_start.html#s9"},
  {"id": "rust_start_05", "unit": "rust_start", "d": 0.0, "type": "mcq", "q": "一个 Rust 函数返回 `Option<u32>`。这个类型是什么意思？", "o": ["也许是一个数，也许什么都没有：`Some` 或 `None`。", "它成功了或者失败了：`Ok` 或 `Err`。", "一个允许调用者去更改的数。", "一个可供调用者从中挑选的数字列表。"], "a": 0, "why": "Rust 没有 null。如果某个东西可能不存在，类型会说明这一点：`Option<u32>` 永远是 `Some(number)` 或 `None`。", "at": "rust_start.html#s10"},
  {"id": "rust_start_06", "unit": "rust_start", "d": 0.6, "type": "mcq", "q": "这个 Rust 程序运行时会发生什么？", "code": "fn to_number(text: &str) -> Result<i32, std::num::ParseIntError> {\n    let number: i32 = text.parse()?;\n    Ok(number * 2)\n}\n\nfn main() {\n    match to_number(\"cat\") {\n        Ok(n) => println!(\"double is {}\", n),\n        Err(e) => println!(\"bad number: {}\", e),\n    }\n}", "o": ["程序会 panic，并停在 `?` 那一行。", "函数返回 `Ok(0)`，并打印“double is 0”。", "代码无法构建，因为“cat”是文本。", "`?` 把错误传回去，由 `main` 打印它。"], "a": 3, "why": "如果成功了，`?` 给出那个值。如果失败了，它就停在那里，把错误传回给调用者，由那里 `match` 的 `Err` 分支来处理。", "at": "rust_start.html#s12"},
  {"id": "rust_start_07", "unit": "rust_start", "d": 0.7, "type": "mcq", "q": "在这个 Rust 循环里，`parse_line` 返回一个 `Result<Student, String>`。五行里有三行是坏的，所以它们给出 `Err`。程序怎样处理它们？", "code": "for line in lines {\n    match parse_line(line) {\n        Ok(student) => {\n            println!(\"ok      {} scored {}\", student.name, student.score);\n            good.push(student);\n        }\n        Err(problem) => println!(\"skipped {}\", problem),\n    }\n}", "o": ["它停在第一个坏行上，之后什么也不再打印。", "它把每个坏行都算作一个零分的成绩。", "它跳过每一个，说明原因，然后继续。", "它会 panic，因为 `match` 容不下 `Err`。"], "a": 2, "why": "`match` 处理 `Result` 的两种情况：每个 `Ok` 的行被保留，每个 `Err` 打印“skipped”和原因。什么都不会崩溃。", "at": "rust_start.html#s13"}
 ]
}
