跳转至

互动题库的协议设计:Schema、答案隔离与幂等提交

把一道题显示在网页上很简单;把它做成可重试、可提示、可语音播报、不会提前泄露答案的互动教学系统,就需要一份稳定协议。

woke_tutor 的 choice-interaction-v1 目前只支持单选和多选。我刻意冻结这个范围,用完整协议跑通“内容 → 判分 → 反馈 → 语音 → 会话记录”,再考虑扩展题型。

内部题目与公开题目不是同一个对象

服务端内部题目包含完整内容:

{
  "content": {},
  "presentation": {},
  "response_spec": {},
  "grading_spec": {},
  "runtime_policy_id": "practice_v1",
  "narration": {}
}

浏览器初次加载时只得到题干、去除 is_correct 的选项、展示方式和当前可播放片段 ID。以下字段不能发送:

  • grading_spec 与正确选项;
  • 答案、解析和错误模式;
  • 未解锁提示;
  • 答案公布与解析的朗读文本;
  • 可猜测的受限音频直链。

如果只是前端“暂时不显示”答案,用户仍然可以从网络响应或源代码中找到它。安全边界必须在服务器序列化之前建立。

Schema 负责形状,业务校验负责语义

以单选题为例,JSON Schema 可以限制选项数组、字段类型和最少数量,但“恰好一个正确选项”通常由业务校验表达更清楚:

correct = [option for option in options if option.is_correct]
if question.type == "single_choice" and len(correct) != 1:
    raise ValidationError("单选题必须恰好有一个正确选项")

多选题则要求至少两个正确项。除此之外,还要检查选项 key 唯一、题干公式闭合、提示层级完整,以及每个错误选项是否有对应的错误路径。

这形成两层防线:Schema 阻止结构漂移,业务校验阻止结构合法但无法教学的内容进入发布包。

不在浏览器判分

浏览器提交:

{
  "contract_version": "choice-interaction-v1",
  "session_id": "session_001",
  "qid": "q_practice_t00001",
  "attempt_no": 1,
  "selected_keys": ["B"],
  "client_elapsed_ms": 8432
}

后端依次验证:会话是否存在、当前题是否匹配、尝试序号是否有效、选项 key 是否属于该题、单选数量是否正确。判分结果再由题库策略决定公开哪些内容。

practice_v1 可以即时返回对错和一层提示;assessment_v1 则返回 deferred,在会话结束前不公布答案。运行时不能解析自然语言提示来决定状态,只能依据冻结策略。

幂等是学习记录的一部分

用户双击提交、浏览器超时后重试、代理重复请求,都可能让同一次作答到达服务端多次。如果每次都新增记录,尝试次数和得分会被错误放大。

协议采用:

(session_id, qid, attempt_no) → 一次逻辑提交
  • 相同序号、相同选择:返回第一次结果;
  • 相同序号、不同选择:返回 409 idempotency_conflict
  • 下一个有效提交必须使用递增序号。

这比简单地在按钮点击后加 disabled 更可靠,因为网络层仍然可能重复,而服务端才掌握最终学习记录。

反馈状态机

选择题不只有“对 / 错”。多选题只选中部分正确项且没有错误项时,partial 可以触发不同反馈;诊断和测评场景需要 deferred

stateDiagram-v2
    READY --> NARRATING
    NARRATING --> AWAITING_ANSWER
    AWAITING_ANSWER --> EVALUATING
    EVALUATING --> COMPLETE: correct / deferred
    EVALUATING --> HINT_OR_RETRY: partial / incorrect
    HINT_OR_RETRY --> AWAITING_ANSWER: retry
    HINT_OR_RETRY --> COMPLETE: attempts exhausted

音频播放另有独立状态机。用户选择选项时可以继续播放,提交、切题或关闭声音时必须立即清空旧队列,避免上一题的讲解串到下一题。

受限音频也需要授权

答案文本没有发给浏览器,不代表答案音频天然安全。如果静态目录包含 answer_reveal.mp3,用户仍可猜 URL 访问。

项目使用 session_id + qid + segment_id 请求音频,服务端根据当前会话状态判断片段是否解锁。未解锁时返回 403。浏览器只知道当前可用片段 ID,不知道内部文件路径。

TTS API Key 同样只存在后端环境变量中,浏览器从不直连供应商。服务不可用时退化为文字,答题流程不受阻断。

协议版本何时升级

以下变化会改变消费者行为,需要新协议版本:

  • 增加填空、口语或主观题;
  • 改变用户答案结构;
  • 改变 partial 的判定语义;
  • 调整必需朗读片段或反馈解锁时机;
  • 允许选项随机打乱;
  • 增加用户语音回答。

更换音色、调整语速或增加内部日志字段则可以保持 V1。区分破坏性和兼容性变化,能避免生成器、后端和前端在不知情的情况下使用不同协议。

小结

一份可用的互动题库协议至少要同时定义:内容结构、公开结构、提交格式、判分语义、教学策略、幂等规则、语音授权和版本演进。只有这样,AI 生成的题目才不再是一段孤立 JSON,而是可以安全进入运行时的产品数据。