跳转至

ZJUI-Learn 题目设置指引(独立版)

本文从docs/ZJUILEARN_0_TO_1_HANDBOOK_ZH.md中提取离“问题配置”相关内容,形成单独可用的建题手册。 适用于教师、助教、题目开发同学。


1. 题目文件结构(必须掌握)

每道题对应一个目录(QID):

questions/<QID>/
  info.json       # 题目元信息与策略开关
  question.html   # 题面与输入组件
  server.py       # 参数生成、解析、评分

2. 三核心文件配置详解

2.1 info.json:问题配置中心

最小模板:

{
  "uuid": "11111111-2222-3333-4444-555555555555",
  "type": "v3",
  "title": "欧姆定律计算",
  "topic": "电路基础",
  "tags": ["ohm", "intro"]
}

高频字段:

  • uuid:唯一ID,必须唯一
  • type:新题固定"v3"
  • title:题目标题
  • topic:主题主题分类
  • tags:筛选标签
  • singleVarianttrue 表示单变体(不随机)
  • partialCredit:是否允许部分得分
  • showCorrectAnswer:面板是否显示正确答案
  • gradingMethodInternal / External / Manual

常见坑:

  • 修改了info.json 后界面不刷新(本地开发需点击Load from disk
  • type 写错导致行为与预期不一致

2.2 question.html:题面与输入组件

模板示例:

<pl-question-panel>
  已知电压 $V={{params.v}}\\,V$,电阻 $R={{params.r}}\\,\\Omega$,求电流 $I=V/R$。
</pl-question-panel>

<pl-number-input answers-name="i" label="$I=$" suffix="A"></pl-number-input>

<pl-submission-panel> {{feedback.i}} </pl-submission-panel>

硬性规则:

  • {{params.xxx}} 必须在 server.pygenerate() 赋值中
  • 所有输入组件的 answers-name 必须唯一
  • 优先使用官方元素(如pl-number-inputpl-multiple-choice

常见坑:

  • answers-name重复,导致评分异常
  • 使用了 {{params.xxx}}server.py 未提供
  • 题面文案修改后,评分逻辑未同步

2.3 server.py:参数与评分核心

入门模板(生成 + 反馈):

import random
import math
import prairielearn as pl

def generate(data):
    v = random.randint(5, 20)
    r = random.choice([1, 2, 4, 5, 10])
    data["params"]["v"] = v
    data["params"]["r"] = r
    data["correct_answers"]["i"] = v / r

def grade(data):
    i_is_correct = math.isclose(data["partial_scores"]["i"]["score"], 1.0)
    if not i_is_correct:
        submitted = float(data["submitted_answers"]["i"])
        if submitted > 0:
            data["feedback"]["i"] = "方向正确,但请再检查除法计算。"

函数职责:

  • generate(data):生成参数与标准答案
  • parse(data):输入清洗/格式错误处理(任选)
  • grade(data):卡通评分与反馈(任选)
  • test(data):复杂题自动测试(可选)

评分建议:

  • 浮点比较避免==,优先math.isclose
  • 自定义 grade() 时覆盖避免组件已判对结果
  • 反馈要能指导学生下一步修改

3. 题目模板库(可直接复用)

每个模板都包含三个文件。复制后改uuid、题面与参数即可。

3.1 填空题(数字填空)

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a001",
  "type": "v3",
  "title": "一次函数求值",
  "topic": "代数",
  "tags": ["fill-blank", "number"]
}

question.html

<pl-question-panel>
  已知 $y=2x+3$,当 $x={{params.x}}$ 时,$y=$
  <pl-integer-input answers-name="y" label=""></pl-integer-input>
</pl-question-panel>

server.py

import random

def generate(data):
    x = random.randint(1, 10)
    data["params"]["x"] = x
    data["correct_answers"]["y"] = 2 * x + 3

3.2 填空题(文本,忽略大小写)

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a002",
  "type": "v3",
  "title": "术语拼写",
  "topic": "计算机基础",
  "tags": ["fill-blank", "string"]
}

question.html

<pl-question-panel>
  请补全短语:Prairie
  <pl-string-input
    answers-name="term"
    ignore-case="true"
    remove-leading-trailing="true"
  ></pl-string-input>
</pl-question-panel>

server.py

def generate(data):
    data["correct_answers"]["term"] = "Learn"

3.3 单选题

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a003",
  "type": "v3",
  "title": "复杂度判断",
  "topic": "算法",
  "tags": ["single-choice"]
}

question.html

<pl-question-panel> 对于归并排序,其平均时间复杂度是? </pl-question-panel>

<pl-multiple-choice answers-name="mc" order="fixed">
  <pl-answer correct="false">$O(n)$</pl-answer>
  <pl-answer correct="true">$O(n\log n)$</pl-answer>
  <pl-answer correct="false">$O(n^2)$</pl-answer>
  <pl-answer correct="false">$O(\log n)$</pl-answer>
</pl-multiple-choice>

server.py

def generate(data):
    pass

3.4 多选题(部分得分)

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a004",
  "type": "v3",
  "title": "Python 语法判断",
  "topic": "编程基础",
  "tags": ["multi-choice", "checkbox"],
  "partialCredit": true
}

question.html

<pl-question-panel> 下面哪些是 Python 的合法数据类型?(可多选) </pl-question-panel>

<pl-checkbox answers-name="types" partial-credit="each-answer" order="fixed">
  <pl-answer correct="true">int</pl-answer>
  <pl-answer correct="true">dict</pl-answer>
  <pl-answer correct="false">char</pl-answer>
  <pl-answer correct="false">pointer</pl-answer>
</pl-checkbox>

server.py

def generate(data):
    pass

3.5 文字题(关键词自动评分)

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a005",
  "type": "v3",
  "title": "HTTP 状态码解释",
  "topic": "网络基础",
  "tags": ["short-answer", "text"]
}

question.html

<pl-question-panel> 请用一句话解释 HTTP 404 的含义。 </pl-question-panel>

<pl-string-input
  answers-name="desc"
  multiline="true"
  remove-leading-trailing="true"
  placeholder="请输入你的解释"
></pl-string-input>

<pl-submission-panel> {{feedback.desc}} </pl-submission-panel>

server.py

def generate(data):
    pass

def grade(data):
    text = str(data["submitted_answers"]["desc"]).lower()
    has_not_found = ("not found" in text) or ("不存在" in text)
    has_resource = ("resource" in text) or ("资源" in text)

    if has_not_found and has_resource:
        data["partial_scores"]["desc"]["score"] = 1.0
        data["feedback"]["desc"] = "解释完整。"
    elif has_not_found or has_resource:
        data["partial_scores"]["desc"]["score"] = 0.5
        data["feedback"]["desc"] = "方向正确,可补充“请求资源不存在”的完整语义。"
    else:
        data["partial_scores"]["desc"]["score"] = 0.0
        data["feedback"]["desc"] = "建议围绕“资源不存在 / Not Found”作答。"

3.6 文字题(人工评分)

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a006",
  "type": "v3",
  "title": "算法设计思路说明",
  "topic": "算法",
  "tags": ["essay", "manual"],
  "gradingMethod": "Manual"
}

question.html

<pl-question-panel>
  请描述你设计该算法的核心思路、时间复杂度与正确性依据(200 字以内)。
</pl-question-panel>

<pl-string-input
  answers-name="essay"
  multiline="true"
  remove-leading-trailing="true"
  placeholder="请输入简答内容"
></pl-string-input>

server.py

def generate(data):
    pass

3.7 一题多空

info.json

{
  "uuid": "2d66b1bf-a4ac-4f49-bf8b-1c2bbad1a007",
  "type": "v3",
  "title": "基础代数多空",
  "topic": "代数",
  "tags": ["multi-blank"]
}

question.html

<pl-question-panel> 已知 $a={{params.a}}, b={{params.b}}$,请填写: </pl-question-panel>

<p>$a+b=$ <pl-integer-input answers-name="sum"></pl-integer-input></p>
<p>$a-b=$ <pl-integer-input answers-name="diff"></pl-integer-input></p>

server.py

import random

def generate(data):
    a = random.randint(3, 10)
    b = random.randint(1, 2)
    data["params"]["a"] = a
    data["params"]["b"] = b
    data["correct_answers"]["sum"] = a + b
    data["correct_answers"]["diff"] = a - b

4.从建题到发布(最短SOP)

  1. Questions 新建问题(QID/标题)
  2. 完成三文件:
    • info.json
    • question.html
    • server.py
  3. preview页面反复生成变体验证
  4. 检查错误输入反馈是否合理
  5. 将题目加入评估并配置分值/尝试规则
  6. 发布前做一次学生视角走查

5. 发布前检查清单

  • info.json 字段完整(uuidtypetitletopic
  • question.html 所有 answers-name 唯一
  • server.py 生成了题面所需全部参数
  • 至少测试 2 个变体
  • 错误答案反馈可读、可执行
  • 评估配置(分值、时间、次数)已核对

6. 常见问题速查

  • 修改不生效:本地开发点击Load from disk(尤其是JSON元信息)
  • 页面渲染报错:先检查question.html标签和组件属性
  • 评分异常:检查answers-namecorrect_answers关键一致性
  • 仅改题面未改逻辑:对 server.py 的评分与反馈通路至关重要

7. 关联文档

  • 总手册:docs/ZJUILEARN_0_TO_1_HANDBOOK_ZH.md
  • 教师页面总表:docs/TEACHER_ADMIN_ALL_PAGES_MANUAL_ZH.md
  • 教师培训版:docs/TEACHER_TRAINING_PLAYBOOK_ZH.md