OpenMAIC 智能课堂 Quiz 内容字段完整指南:scene.content 数据契约、patch_stage 原子写入与常见错误规避
【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC
本文是 OpenMAIC(Open Multi-Agent Interactive Classroom)舞台文档模型中Quiz 内容字段的实战参考:它完整讲解持久化在scene.content上的 Quiz 数据结构(内容根、题目、选项、答案与判分字段),说明如何通过read_stage/patch_stage/grep_stage三个通用工具安全读写 Quiz 场景,并逐条列出校验拒绝原因、语义级陷阱与五个可直接复用的原子补丁示例。读完本文,你将掌握在 OpenMAIC 课堂文档中"读源码 → 最小补丁 → 读回验证"的完整工作流,并能准确区分"结构上被接受"与"语义上正确"之间的边界。
1. Quiz 内容在 stage 文档模型中的位置
OpenMAIC 的持久化文档以stage为根,scenes[]按scene.order排序构成课堂页面(详见 skills/agent-runtime/stage-dsl/SKILL.md):
stage ├── outline └── scenes[] ordered by scene.order, shown as pages 1..N ├── id stable scene identity ├── order 1-based page position ├── type slide | quiz | interactive | pbl ├── content shape selected by scene.type │ └── quiz.questions[] └── actions[] ordered playback verbsscene.type与scene.content.type必须一致:quiz类型的场景必须携带QuizContent。这一绑定是真实不变量——消费者根据scene.type分支后直接按对应形态读取scene.content,契约在类型层面强制约束(见 packages/@openmaic/dsl/src/stage.ts 中Scene的分配条件类型实现)。
与幻灯片场景不同,页面元数据(如scene.title)不属于 Quiz 内容:scene.title在content之外,改标题用edit_deck,而不是patch_stage(后者仅允许以/content/...或/actions/...开头的场景根指针,见 lib/server/agent-runtime/course-edit/tools.ts 中edit_deck的定义)。本文所述 Quiz 字段契约由 packages/@openmaic/dsl/src/stage.ts 的QuizContent、QuizQuestion、QuizOption类型、Quiz 编辑器操作(components/edit/surfaces/quiz/quiz-edit-ops.ts)与文档写入校验器(lib/server/agent-runtime/dsl-tools.ts)共同确立,本文只陈述这些来源确立的行为。
2. 内容根节点(Root)
Quiz 场景的内容根是一个"封闭"对象,持久化形态如下:
{ "type": "quiz", "questions": [] }| 字段 | 类型 | 必填 | 合法取值 / 含义 |
|---|---|---|---|
type | string | 是 | 严格等于"quiz";必须与scene.type一致 |
questions | QuizQuestion[] | 是 | 有序的题目列表 |
封闭性约束:内容根对patch_stage是封闭的,除type和questions之外的任何字段都会被拒绝。这一约束在服务端校验中被显式执行——lib/server/agent-runtime/dsl-tools.ts 的validationError会把scene.content上不属于['type', 'questions']的键逐一筛出并返回/content: unknown field(s) ...错误。
3. QuizQuestion 字段契约
单个题目的标准形态:
{ "id": "q1", "type": "single", "question": "Which value is prime?", "options": [ { "label": "4", "value": "A" }, { "label": "5", "value": "B" } ], "answer": ["B"], "analysis": "5 has no positive divisors other than 1 and itself.", "points": 1 }| 字段 | 类型 | 必填 | 合法取值 / 语义 |
|---|---|---|---|
id | string | 是 | 稳定的题目标识,编辑器操作以此定位题目 |
type | string union | 是 | single/multiple/short_answer |
question | string | 是 | 面向学习者的题干 |
options | QuizOption[] | 否 | 选项行,通常用于 single / multiple 题型 |
answer | string[] | 否 | 正确选项 value 列表,或简答题接受的答案值列表 |
analysis | string | 否 | 答案解析,由暴露解析功能的 Quiz 展示面显示 |
commentPrompt | string | 否 | 可选提示字段;本文不规定其精确渲染时机 |
hasAnswer | boolean | 否 | 标识简答题是否已提供答案 |
points | number | 否 | 分值 / 权重 |
题目对象同样是封闭的:patch_stage校验会拒绝拼写错误(例如把analysis写成analaysis)而不是把它存下来。运行时契约按题型要求id、type、question三个字段,patch_stage的 Quiz 检查还会验证这些字段是字符串且type是上面三种取值之一(见 lib/server/agent-runtime/dsl-tools.ts)。
注意不要臆造约束:持久化类型没有规定字符串长度的最小/最大值;也没有为points规定"必须为正"或"必须为整数"的约束——points技术上可以是任意 number,但要与计分策略相匹配(见第 11 节的语义陷阱清单)。在 packages/@openmaic/dsl/src/stage.ts 的类型定义中,points?: number即是最完整的契约表达。
4. 三种题型:single / multiple / short_answer
4.1single(单选)
编辑器即使只有一个正确选项,也将答案建模为string[]。选择编辑器的toggleCorrect操作通过"选中某一行即唯一正确"来保持单选行为(radio 语义,见 components/edit/surfaces/quiz/quiz-edit-ops.ts)。但通用指针写入会绕过该菜单行为:直接写answer时,必须提供完整的目标数组。
"answer": ["B"]4.2multiple(多选)
answer中可以出现多个选项 value:
"answer": ["A", "C"]持久化类型不要求answer顺序与options顺序一致,但保持对齐会让 diff 与评审更容易。编辑器中toggleCorrect对多选按"逐项翻转"处理(checkbox 语义)。值得一提的实现细节是:选项编辑的底层通过OptionRow { label, correct }中间态往返——编辑作用于行,再由fromRows重新推导value = LETTERS[index]并重建answer,因此重排选项永远不需要手动重映射 answer(见 components/edit/surfaces/quiz/quiz-edit-ops.ts)。单个选择题最多 26 个选项(A–Z),对应源码中的MAX_OPTIONS。
4.3short_answer(简答)
options可选且通常缺席。判分代码把没有hasAnswer的简答题视为不可自动判分——这是已确立的消费者行为而非模式规则:isShortAnswer只按type === 'short_answer'分类,hasAnswer不会覆盖题型分类;未作答的选择题(空answer)仍是选择题,不会被转去 AI 判分(见 lib/quiz/grading.ts)。answer存在时仍是字符串数组:
"answer": ["2"]当把题目从选择型切换为简答时,编辑器会丢弃options与answer并保留hasAnswer种子字段;反向切换则播种两个空选项和空 answer(setQuestionType的结构性迁移,见 components/edit/surfaces/quiz/quiz-edit-ops.ts)。
5. QuizOption 字段契约
{ "label": "5", "value": "B" }| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
label | string | 是 | 面向学习者的选项文本 |
value | string | 是 | 存储在题目answer数组中的稳定值 |
选项对象封闭:未知字段与错误类型都会被拒绝。label与value是两个不同的东西:只改label、保持value不变不会破坏正确性;而改动value则必须在同一次原子批量中更新answer中每一个匹配条目。
6. 顺序与身份(Ordering and identity)
questions数组顺序即展示顺序。options数组顺序即展示的选择顺序。- 通用指针写入使用0 基数组下标。任何基于下标的编辑之前,都应立即重读源码——此前的插入或重排会改变下标。
patch_stage没有 Quiz 专属的"新增菜单",也不会铸造 Quiz 身份:新增题目或选项意味着写入完整的最终数组,包括由调用方提供的合法 id 与 option value。- 指针实现只接受规范的、已存在的数组下标。JSON Patch 传统的
/-追加标记不被支持,下标等于当前数组长度视为越界。正确姿势是:读取当前数组 → 内存中追加 →set数组字段为完整结果。 - 现有运行时校验器没有为题目 id 规定格式或全局唯一性规则。保留已有 id;对于新 id,遵循相邻文档的约定并自行保证唯一性。
7. Answer 耦合不变量(Answer coupling)
最重要的不变式是引用关系:
question.answer[] value -> one question.options[].valueTypeScript 类型允许answer中的值不指向任何选项;通用写入校验也不证明该关系。因此,一个结构上被接受的 Quiz 仍可能携带"无法判分"或"永远答错"的答案。三条联动规则:
- 删除选项时,在同一调用中把该 value 从
answer移除。 - 重赋值选项 value时,在同一调用中更新
answer。 - 变更题型时,把
options、answer、hasAnswer放在一起检查。
8. 可见文本投影(Visible text projection)
read_stage的detail:"text"视图包含:
question- 每个选项的
label - 存在时的
analysis - 存在时的
commentPrompt - 附加在场景上的可见动作文本(action text)
它不会把answer值当作面向学习者的文本。grep_stage scope:"text"搜索的是同一个投影。该投影在源码中的实现见 lib/server/agent-runtime/dsl-tools.ts 的textScene:它只add题干、options/*/label、analysis、commentPrompt四个来源的字符串。
当你要查找的是选项 value、题目 id、字段名或 answer 键时,请使用scope:"source"(搜索序列化后的场景 JSON,包含字段名与内部数据)。
9. patch_stage 的校验边界与常见拒绝原因
patch_stage的目标是单个场景路径/scenes/<order|sceneId>,每次调用携带人类可读的intent与一个或多个ops。操作是原子的:服务端对克隆应用全部 op、校验结果场景、一次性写入;op 2 失败则 op 1 也不会持久化(见 lib/server/agent-runtime/dsl-tools.ts)。Quiz 写入在既有文档校验器之外再套一层封闭的题目/选项检查(lib/server/agent-runtime/dsl-tools.ts),而 packages/@openmaic/dsl/test/validate.test.ts 则验证了"缺少 questions 数组的 quiz 场景会被拒绝"等基础约束。
常见的结构拒绝原因:
- 路径以
/questions/...开头,而不是/content/questions/...。 - 题目或选项下标已过期(stale)。
- 某个中间数组/对象不存在。
remove指向一个不存在的可选字段。- 移除了必填字段(
id、type、question、根questions)。 - 题目类型不是
single、multiple或short_answer。 - 选项缺少字符串类型的
label或value。 - 引入了题目 / 选项 / 内容根上的未知字段。
10. 校验不会拦截的语义错误清单
"被接受"只意味着当前持久化契约接受了该形态,不代表每个值在教学中都合理。以下错误在结构上可通过,但应主动避免:
- 正确答案值在
options中已不存在。 - 整数组写入后,
single题目出现多个答案值。 - 期望自动判分的
short_answer题目缺少hasAnswer。 - 重写数组时引入重复的题目 id。
- 选项 label 移动了,但 answer 值被意外重新生成。
points在技术上是个 number,但与计分策略不匹配。
判分侧的对应事实:选择型题目按答案键精确匹配判分,points缺省按 1 计,arraysEqual对双方排序后逐一比对(忽略顺序);short_answer不进入本地精确匹配,hasAnswer缺省即不视为可自动判分(见 lib/quiz/grading.ts)。
11. 五个实战补丁示例(可复制)
所有示例都遵循同一流程:读 source 定位 → 最小补丁 → 读回验证。涉及数组下标前务必先重读源码。
示例 1:修改一个选项的 label
先读源码:
read_stage({ "path": "/scenes/2", "detail": "source" })定位到精确选项:
/content/questions/0/options/1 { "label": "5", "value": "B" }只补丁它的 label:
patch_stage({ "target": "/scenes/2", "intent": "Clarify the second answer choice", "ops": [ { "op": "set", "path": "/content/questions/0/options/1/label", "value": "5(质数)" } ] })读回验证label已变,而value:"B"与answer:["B"]未变。
示例 2:修改选项 value 而不破坏答案键
读源码并定位:
/content/questions/0/options/1/value = "B" /content/questions/0/answer = ["B"]原子地写入两个耦合字段:
patch_stage({ "target": "/scenes/scene_quiz", "intent": "Rename the second option value while preserving correctness", "ops": [ { "op": "set", "path": "/content/questions/0/options/1/value", "value": "prime" }, { "op": "set", "path": "/content/questions/0/answer", "value": ["prime"] } ] })读回源码确认两次写入一起落地。
示例 3:移除可选的 analysis
先读源码证明analysis存在:
read_stage({ "path": "/scenes/2", "detail": "source" })移除叶子字段:
patch_stage({ "target": "/scenes/2", "intent": "Remove the outdated answer explanation", "ops": [ { "op": "remove", "path": "/content/questions/0/analysis" } ] })再次读源码,随后可选地运行:
grep_stage({ "query": "outdated phrase", "scope": "text" })源码必须不再含analysis,旧的可见短语在 text 投影中必须零命中。
示例 4:新增一道题目
以detail:"source"读取/scenes/2后,逐字保留每道既有题目,把包含新题目的完整对象数组写回数组字段本身:
patch_stage({ "target": "/scenes/2", "intent": "Add a second quiz question", "ops": [ { "op": "set", "path": "/content/questions", "value": [ { "id": "q1", "type": "single", "question": "Which value is prime?", "options": [ { "label": "4", "value": "A" }, { "label": "5", "value": "B" } ], "answer": ["B"] }, { "id": "q2", "type": "short_answer", "question": "Name the smallest prime number.", "answer": ["2"], "hasAnswer": true } ] } ] })不要使用/content/questions/-,它会被当作非规范数组下标而拒绝。
示例 5:新增一个选项
读取完整当前选项数组,追加一个新的{label, value}对,然后把/content/questions/0/options设为该完整结果数组;若新选项是正确的,则在同一原子批次中同步设置/content/questions/0/answer。永远不要写/content/questions/0/options/-。
12. 硬规则总结
- 读 source,绝不读 tree,以获得下标与完整邻接状态。
- 补丁最小叶子;除非耦合字段必须原子变更。
- 保留题目 id 与选项 value,除非意图明确要改变它们。
- 把
answer与选项value视为一个不变量。 - 新增 / 重排使用完整数组;通用指针不会铸造 Quiz id。
- 每次写入后都读回验证。
如需继续深入:Quiz 编辑操作(含 undo/redo 历史、行模型与题型迁移)可读 components/edit/surfaces/quiz/quiz-edit-ops.ts;编辑器操作的单元测试见 tests/edit/surfaces/quiz/quiz-edit-ops.test.ts 与 tests/edit/round-trip/quiz.test.ts;舞台文档的总览与工具词汇表见 skills/agent-runtime/stage-dsl/SKILL.md。
【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考