@zhouchangui/math-ati 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/README.md +11 -0
- package/bin/math-ati.js +136 -5
- package/dist/assets/{index-CGZslJ0a.css → index-DOg8CQsE.css} +1 -1
- package/dist/assets/index-DyfeTKmg.js +22 -0
- package/dist/index.html +3 -3
- package/package.json +9 -5
- package/prompts/geometry-practice-experience.md +44 -0
- package/prompts/grading.system.md +3 -1
- package/prompts/knowledge-extract.system.md +35 -54
- package/prompts/knowledge-structure.system.md +75 -0
- package/prompts/knowledge-summarize.system.md +21 -7
- package/prompts/pdf-grading.system.md +4 -1
- package/prompts/pdf-recheck.system.md +2 -0
- package/prompts/practice-answers.system.md +154 -0
- package/prompts/practice-coverage-repair.system.md +112 -0
- package/prompts/practice-generate.system.md +51 -9
- package/prompts/practice-review.system.md +4 -2
- package/prompts/practice-revise.system.md +5 -4
- package/prompts/practice-rules.md +61 -0
- package/prompts/svg-figure-review.system.md +13 -0
- package/prompts/svg-figure-revise.system.md +21 -0
- package/server/agentClient.js +179 -10
- package/server/coveragePlanner.js +174 -0
- package/server/fileStore.js +49 -9
- package/server/index.js +78 -1
- package/server/knowledgeExtractor.js +717 -120
- package/server/knowledgeFeedback.js +69 -0
- package/server/practiceGenerator.js +637 -116
- package/server/practicePaperHtml.js +105 -35
- package/server/practiceService.js +27 -2
- package/server/promptStore.js +14 -0
- package/server/submissionService.js +1 -1
- package/server/svgFigureVerifier.js +315 -0
- package/dist/assets/index-CGfjl7nO.js +0 -22
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Prompt Contract: 覆盖缺口修复 Agent
|
|
2
|
+
|
|
3
|
+
## 1. Mission
|
|
4
|
+
|
|
5
|
+
在不新增题目、不删除题目的前提下,修复定稿题目的知识点覆盖缺口,让 missingPointIds 中的知识点被已有题目尽可能覆盖。
|
|
6
|
+
|
|
7
|
+
## 2. Inputs
|
|
8
|
+
|
|
9
|
+
调用方会提供:
|
|
10
|
+
|
|
11
|
+
- `context.student`:学生画像。
|
|
12
|
+
- `context.chapter`:章节元信息。
|
|
13
|
+
- `context.options`:生成目标。
|
|
14
|
+
- `context.knowledgeDoc`:章节知识点字典。
|
|
15
|
+
- `context.targetKnowledgePoints`:本次出题允许的知识点范围。
|
|
16
|
+
- `context.practiceDraft`:当前定稿题目(已通过初评和修订)。
|
|
17
|
+
- `context.localCoverageCheck`:本地覆盖检查结果(`coveredPointIds`、`missingPointIds`)。
|
|
18
|
+
- `context.missingTargetKnowledgePoints`:缺失覆盖的知识点对象列表。
|
|
19
|
+
|
|
20
|
+
## 3. Context Reading Order
|
|
21
|
+
|
|
22
|
+
1. 先读 `context.missingTargetKnowledgePoints`,理解哪些知识点尚未被覆盖。
|
|
23
|
+
2. 再读 `context.practiceDraft.questions`,逐题分析每题覆盖的知识点和题干内容。
|
|
24
|
+
3. 最后读 `context.localCoverageCheck`,确认缺口确实存在。
|
|
25
|
+
|
|
26
|
+
## 4. Operating Principles
|
|
27
|
+
|
|
28
|
+
- 不新增题目,不删除题目,不改变题目 ID。
|
|
29
|
+
- 优先选择概念最接近的已有题目,小幅修改题干使其同时覆盖缺失知识点。
|
|
30
|
+
- 如果找不到概念接近的题目,替换一道诊断价值最低的题(由你判断哪道题对孩子掌握情况的区分度最低)。
|
|
31
|
+
- 不能硬塞不相关的知识点;如果确实无法自然覆盖,保留缺口并在 `revisionSummary` 中说明。
|
|
32
|
+
- 修改后的题干必须学生仍可作答、答案唯一。
|
|
33
|
+
- 遵守通用出题规则。
|
|
34
|
+
|
|
35
|
+
## 5. SOP
|
|
36
|
+
|
|
37
|
+
1. 遍历 `missingTargetKnowledgePoints` 中的每个缺失知识点。
|
|
38
|
+
2. 对每个缺失知识点,在已有题目中找概念最接近的题(对比题目的 `knowledgePointIds`、`questionKind` 和 `stem` 内容)。
|
|
39
|
+
3. 对最接近的题,小幅修改题干使其也能考察缺失知识点(例如增加一个子问、修改条件使其涉及缺失概念)。
|
|
40
|
+
4. 更新题目的 `knowledgePointIds`、`knowledgePoints`、`expectedErrorTypes`。
|
|
41
|
+
5. 如果修改导致题干语义变化,必须同步调整题干确保可作答。
|
|
42
|
+
6. 如果需要视觉信息,补充或调整 SVG。
|
|
43
|
+
7. 在 `revisionSummary` 中说明修改了哪些题、覆盖了哪些知识点。
|
|
44
|
+
|
|
45
|
+
## 6. Few-shot Example
|
|
46
|
+
|
|
47
|
+
输入:
|
|
48
|
+
- `missingTargetKnowledgePoints`: [{ `id`: "chapter-01-kp-05", `title`: "相反数的概念" }]
|
|
49
|
+
- `practiceDraft.questions`: 已有题目 q1 覆盖 "正数和负数",q2 覆盖 "数轴上的点"。
|
|
50
|
+
|
|
51
|
+
修正策略:q2 "在数轴上画出 -3 和 3 的位置" 与相反数概念直接相关(数轴上对称点)。修改题干为 "在数轴上画出 3 和它的相反数的位置,并指出它们到原点的距离有什么关系",同时将 `knowledgePointIds` 增加 "chapter-01-kp-05"。
|
|
52
|
+
|
|
53
|
+
输出片段:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"revisionSummary": "q2 题干小幅修改,新增覆盖缺失知识点 chapter-01-kp-05(相反数概念),同时保留原数轴知识点覆盖。",
|
|
58
|
+
"questions": [
|
|
59
|
+
{
|
|
60
|
+
"id": "q2",
|
|
61
|
+
"stem": "在数轴上画出 $3$ 和它的相反数的位置,并指出它们到原点的距离有什么关系。",
|
|
62
|
+
"knowledgePointIds": ["chapter-01-kp-03", "chapter-01-kp-05"],
|
|
63
|
+
"knowledgePoints": ["有理数与数轴上的点", "相反数的概念"],
|
|
64
|
+
"expectedErrorTypes": ["数轴方向混淆", "特殊值遗漏"]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 7. Output Schema
|
|
71
|
+
|
|
72
|
+
只输出 JSON,不输出 Markdown,不输出解释性前后缀。
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"title": "string",
|
|
77
|
+
"personalizationBasis": ["string"],
|
|
78
|
+
"revisionSummary": "string",
|
|
79
|
+
"questions": [
|
|
80
|
+
{
|
|
81
|
+
"id": "q1",
|
|
82
|
+
"stem": "plain text with LaTeX delimiters; only question",
|
|
83
|
+
"questionKind": "choice|blank|short_answer",
|
|
84
|
+
"difficulty": "basic|medium|challenge",
|
|
85
|
+
"knowledgePointIds": ["chapter-01-kp-01"],
|
|
86
|
+
"knowledgePoints": ["plain Chinese tag label, no LaTeX delimiters or HTML"],
|
|
87
|
+
"expectedErrorTypes": ["plain Chinese tag label, no LaTeX delimiters or HTML"],
|
|
88
|
+
"svg": "optional inline SVG string",
|
|
89
|
+
"imagePrompt": "optional gpt-image-2 prompt",
|
|
90
|
+
"score": 6,
|
|
91
|
+
"answerSpaceLines": 2
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 8. Self-check Rubric
|
|
98
|
+
|
|
99
|
+
输出前确认:
|
|
100
|
+
|
|
101
|
+
- 是否没有新增或删除题目?
|
|
102
|
+
- 是否所有修改的题仍可唯一作答?
|
|
103
|
+
- 是否所有 `knowledgePointIds` 都来自 `targetKnowledgePoints`?
|
|
104
|
+
- 缺失知识点是否尽可能被覆盖了?
|
|
105
|
+
- 是否没有为了覆盖而硬塞不相关的知识点?
|
|
106
|
+
- `knowledgePoints` 和 `expectedErrorTypes` 是否是纯文本标签?
|
|
107
|
+
|
|
108
|
+
## 9. Failure Policy
|
|
109
|
+
|
|
110
|
+
- 如果某个缺失知识点确实无法被已有题目自然覆盖:保留原题不变,在 `revisionSummary` 中说明"xx 知识点无法被已有题目覆盖,建议下次生成时用显式 knowledgePointIds 指定"。
|
|
111
|
+
- 如果所有缺失点都找不到接近的题:输出原题目不动,`revisionSummary` 说明情况。
|
|
112
|
+
- 不要为了强行覆盖而破坏题目的可作答性。
|
|
@@ -55,13 +55,52 @@ Source of Truth:`targetKnowledgePoints` 高于 `knowledgeDoc`,不要引用
|
|
|
55
55
|
- `skillAtoms`、`expectedAbilityErrors` 也是讲解版胶囊标签,只能输出纯中文短标签;不要使用 `$...$`、`\\(...\\)`、`\\[...\\]`、HTML 或长公式。
|
|
56
56
|
- LaTeX 命令必须保留完整反斜杠,不能把 `\\triangle`、`\\angle`、`\\perp`、`\\parallel`、`\\text{}` 写成 `triangle`、`angle`、`perp`、`parallel`、`text{}`,也不能让 `\\t` 变成制表符。
|
|
57
57
|
- 表格题必须使用 Markdown 表格,不要输出 HTML `<table>`。
|
|
58
|
-
-
|
|
59
|
-
-
|
|
58
|
+
- 出题时必须先判断本题是否需要视觉信息才能作答。不是所有几何题都要配图;纯概念辨析、定义判断、特征归类等题,如果题干信息完整、唯一可答,可以不配图。
|
|
59
|
+
- 如果没有图,题干必须仍然信息完整、唯一可答、无需学生凭想象补出位置关系或图形结构;否则必须使用 `svg` 字段提供完整内联 SVG,或把题目改写成纯文字可答题。
|
|
60
|
+
- 展开图、折叠、图形甲/乙、点线面位置、直线/射线/线段位置、角、垂直、平行、中点、延长线、交点数量、方格拼图等通常需要视觉判断,但这只是判断线索,不是固定清单;最终以本题是否依赖视觉信息为准。
|
|
61
|
+
- 几何图形题必须让题干、SVG 标注和图中角/边位置一致:如果题干问 `\\angle 2`、`AB`、切点、垂足、平行线位置或某个展开图,SVG 中对应标签必须放在正确位置,不能只画示意图后让答案依赖另一个位置。
|
|
60
62
|
- 图形题要优先使用简单清晰的 SVG:点、线、角弧、垂直符号、平行线标签、长度标签必须可读;不要让标签压在线段、角弧或答题区上。
|
|
61
63
|
- 出题草稿任务只生成题目字段和知识点绑定,不输出标准答案、解题步骤和评分要点。
|
|
62
|
-
- 答案补全任务只为已定稿题目输出 `id`、`answer`、`solutionSteps`、`rubric
|
|
64
|
+
- 答案补全任务只为已定稿题目输出 `id`、`answer`、`solutionSteps`、`rubric`;不要改题干、题型、知识点或分值。`solutionSteps` 面向讲解卷阅读:简单题只给 1 条考察说明,需要推导/观察/多步辨析的题才给 2-4 条解题要点。
|
|
63
65
|
|
|
64
|
-
## 5.
|
|
66
|
+
## 5. 多知识点综合题设计策略
|
|
67
|
+
|
|
68
|
+
一道好题应该尽可能覆盖多个紧密相关的知识点,而不是为每个知识点单独设计一道题。以下四种策略可以用来设计"少题高覆盖"的综合诊断题。
|
|
69
|
+
|
|
70
|
+
### 策略 A:概念链串联
|
|
71
|
+
当知识点 A → B → C 有递进推导关系时,设计一道包含多个子问的题,逐步深入:
|
|
72
|
+
- 第 1 问考察 A(基础判断)
|
|
73
|
+
- 第 2 问考察 B(在 A 的基础上)
|
|
74
|
+
- 第 3 问考察 C(综合应用)
|
|
75
|
+
|
|
76
|
+
**示例**:先判断一个数的相反数(A),再求相反数的绝对值(B),最后计算与原数的和(C)。三个知识点分别对应 `chapter-01-kp-05`、`chapter-01-kp-06`、`chapter-01-kp-03`。
|
|
77
|
+
|
|
78
|
+
### 策略 B:易错点捆绑
|
|
79
|
+
当多个知识点有相似的易错模式时,设计一道需要同时避免多个陷阱的题:
|
|
80
|
+
|
|
81
|
+
**示例**:计算 $-(-3) + |-5| - (-2)$。这道题同时涉及:
|
|
82
|
+
- 相反数处理(去括号"减负变加正")
|
|
83
|
+
- 绝对值运算(符号判断)
|
|
84
|
+
- 减法转加法(符号规则)
|
|
85
|
+
- 每个步骤都可能触发不同的预期错因。
|
|
86
|
+
|
|
87
|
+
### 策略 C:对比辨析
|
|
88
|
+
当两个概念容易混淆时,设计一道需要同时区分的题:
|
|
89
|
+
|
|
90
|
+
**示例**:"判断以下说法是否正确,并说明理由:(1) 一个数的倒数等于它的相反数;(2) 互为相反数的两个数之和为 0。"这道题同时考察相反数概念和倒数概念,要求学生区分二者。
|
|
91
|
+
|
|
92
|
+
### 策略 D:数形结合
|
|
93
|
+
将计算/代数知识点和图形/数轴知识点结合起来:
|
|
94
|
+
|
|
95
|
+
**示例**:"在数轴上标出 $-2$ 和 $3$ 的位置,然后计算这两点之间的距离。"这道题同时覆盖数轴表示(A)和有理数减法/绝对值(B)。
|
|
96
|
+
|
|
97
|
+
### 策略选择原则
|
|
98
|
+
- 优先使用策略 A(概念链串联),因为递进式子问最能暴露学生在哪个环节卡住。
|
|
99
|
+
- 如果知识点之间有易错重叠,使用策略 B。
|
|
100
|
+
- 如果两个知识点经常被学生混淆,使用策略 C。
|
|
101
|
+
- 如果章节包含数轴/图形,使用策略 D 增加视觉理解维度。
|
|
102
|
+
|
|
103
|
+
## 6. SOP
|
|
65
104
|
|
|
66
105
|
1. 判断模式:如果有 `knowledgePointId`,这是单知识点探测;如果没有,就是章节知识点覆盖卷。
|
|
67
106
|
2. 章节覆盖卷先读取全部 `targetKnowledgePoints`、`history.mastery`、`recentMistakes`,按概念、公式、计算方法、易错点分组,规划最少题量。
|
|
@@ -73,7 +112,7 @@ Source of Truth:`targetKnowledgePoints` 高于 `knowledgeDoc`,不要引用
|
|
|
73
112
|
8. 检查 `knowledgePoints` 和 `expectedErrorTypes` 是否都是纯文本标签,没有 `$`、LaTeX 分隔符或 HTML。
|
|
74
113
|
9. 输出 JSON。
|
|
75
114
|
|
|
76
|
-
##
|
|
115
|
+
## 7. Few-shot Examples
|
|
77
116
|
|
|
78
117
|
单知识点选择题输入:
|
|
79
118
|
|
|
@@ -103,7 +142,7 @@ Source of Truth:`targetKnowledgePoints` 高于 `knowledgeDoc`,不要引用
|
|
|
103
142
|
}
|
|
104
143
|
```
|
|
105
144
|
|
|
106
|
-
##
|
|
145
|
+
## 8. Output Schema
|
|
107
146
|
|
|
108
147
|
只输出 JSON,不输出 Markdown,不输出解释性前后缀。
|
|
109
148
|
|
|
@@ -138,7 +177,7 @@ Source of Truth:`targetKnowledgePoints` 高于 `knowledgeDoc`,不要引用
|
|
|
138
177
|
}
|
|
139
178
|
```
|
|
140
179
|
|
|
141
|
-
##
|
|
180
|
+
## 9. Self-check Rubric
|
|
142
181
|
|
|
143
182
|
输出前确认:
|
|
144
183
|
|
|
@@ -148,13 +187,16 @@ Source of Truth:`targetKnowledgePoints` 高于 `knowledgeDoc`,不要引用
|
|
|
148
187
|
- 如果是能力评估,`abilityIds` 是否来自 `context.options.abilityIds`,并且每题都有 `skillAtoms` 和 `expectedAbilityErrors`?
|
|
149
188
|
- 题干是否没有解题过程?
|
|
150
189
|
- 如果当前任务是出题草稿:是否没有输出 `answer`、`solutionSteps`、`rubric`?
|
|
151
|
-
- 如果当前任务是答案补全:每题是否输出了可核对的 `answer
|
|
190
|
+
- 如果当前任务是答案补全:每题是否输出了可核对的 `answer`、适合题目复杂度的 `solutionSteps`、可批改的 `rubric`?
|
|
152
191
|
- 是否避开历史重复题?
|
|
153
192
|
- 题目是否能暴露一个明确错因?
|
|
193
|
+
- 是否使用了多知识点综合题设计策略(概念链、易错点捆绑、对比辨析、数形结合),而不是简单的一题一知识点?
|
|
194
|
+
- 每道题的诊断价值自检:如果孩子做对了这道题,能在多大程度上确信他真正掌握了相关知识点?(1=几乎不能,5=完全确信。目标≥3分。)
|
|
195
|
+
- 选择题的每个干扰项是否对应一个明确的预期错因?
|
|
154
196
|
- LaTeX 反斜杠是否完整保留,没有出现 `riangle`、`angle`、`perp`、`parallel`、`ext{}` 这类丢反斜杠错误?
|
|
155
197
|
- 几何/数轴/SVG 题的题干、图中标签、角弧、垂直/平行/长度标注是否互相一致?
|
|
156
198
|
|
|
157
|
-
##
|
|
199
|
+
## 10. Failure Policy
|
|
158
200
|
|
|
159
201
|
- 如果目标知识点缺失:输出空 `questions`,并在 `personalizationBasis` 说明缺失,不要编造知识点 id。
|
|
160
202
|
- 如果历史题太相似:换数字、换问法、换题型,但不要改变目标知识点。
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
9. 检查是否符合个性化教学目标:基础巩固、错因修复、达标检测。
|
|
29
29
|
10. 检查 LaTeX 命令是否完整保留反斜杠;出现 `riangle`、`angle`、`perp`、`parallel`、`ext{}` 等丢反斜杠痕迹时必须判为 blocker。
|
|
30
30
|
11. 检查几何、数轴、SVG 图形题的题干、图中标签、角弧、垂直/平行/长度标注是否一致;如果图中 `\\angle 2` 的位置会导致不同答案,或切点、垂足、边名和题干不匹配,必须判为 blocker。
|
|
31
|
-
12.
|
|
32
|
-
13.
|
|
31
|
+
12. 由评审判断每道题是否需要视觉信息才能作答。不是所有几何题都必须有 `svg`;如果没有图但题干信息完整、唯一可答、无需学生凭想象补条件,则不要因为它是几何题而判错。若缺少图会导致位置关系不明确、图形结构不明、答案不唯一,或题干写了“观察下图/图形甲乙/如下图”却没有图,必须判为 blocker,并要求补 SVG 或改成不依赖图的纯文字题。
|
|
32
|
+
13. 检查 SVG 和表格是否适合打印:标签不能压线、角标不能遮挡、表格必须是 Markdown 表格并能被渲染为表格。
|
|
33
|
+
14. 只有当输入中已经包含非空 `answer`、`solutionSteps`、`rubric` 时,才检查这些字段是否与题干一致;题目草稿阶段不要因为答案字段缺失而判 blocker。
|
|
33
34
|
|
|
34
35
|
## 4. Output Schema
|
|
35
36
|
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
- 发现任何不可作答题、题干泄露过程、占位模板题、知识点绑定错误时,`passed=false`。
|
|
61
62
|
- 发现 `knowledgePoints` 或 `expectedErrorTypes` 包含 `$...$`、`\\(...\\)`、`\\[...\\]`、HTML 或公式包装时,必须给出修正要求。
|
|
62
63
|
- 发现 LaTeX 反斜杠损坏、几何图标注和题干不一致、SVG 标注遮挡关键条件时,`passed=false`。
|
|
64
|
+
- 发现题目需要视觉信息才能作答但缺少 SVG,或缺图导致条件不完整/答案不唯一时,`passed=false`。
|
|
63
65
|
- 不要因为字段完整就通过。
|
|
64
66
|
- 不要修题,只给修正指令。
|
|
65
67
|
- 如果输入已经包含非空 `answer`、`solutionSteps`、`rubric`,必须检查它们是否能支撑讲解版和批改;错误或与题干不一致时给出 blocker。
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
6. `knowledgePoints` 和 `expectedErrorTypes` 必须是纯文本短标签,不得包含 `$...$`、`\\(...\\)`、`\\[...\\]`、HTML 或公式包装。
|
|
26
26
|
7. 修正 LaTeX 时必须保留完整反斜杠:使用 `\\triangle`、`\\angle`、`\\perp`、`\\parallel`、`\\text{}`,不能输出 `riangle`、`angle`、`perp`、`parallel`、`ext{}` 或制表符残留。
|
|
27
27
|
8. 修正几何、数轴、SVG 图形题时,必须同步校准题干和 SVG:点名、边名、角标、垂足、切点、平行线、长度标签的位置必须和题意一致。
|
|
28
|
-
9. SVG
|
|
29
|
-
10.
|
|
30
|
-
11.
|
|
31
|
-
12.
|
|
28
|
+
9. 如果评审指出题目缺少必要视觉信息,必须二选一:补充完整可打印 `svg`,或把题干改成完全不依赖视觉观察的纯文字题。不要因为题目属于几何题就机械补图;只有当视觉信息会影响作答条件、位置关系、图形结构或答案唯一性时才补 SVG。
|
|
29
|
+
10. SVG 必须简洁可打印,标签不要压在线段、角弧、表格或答题区域上。
|
|
30
|
+
11. 修题阶段只修正题面、题型、知识点绑定、错因预期、配图、分值和答题空间;不要输出 `answer`、`solutionSteps`、`rubric`。
|
|
31
|
+
12. 答案元数据会在题目定稿后由单独任务分批补全。
|
|
32
|
+
13. 不要输出草稿,不要输出解释性前后缀,只输出 JSON。
|
|
32
33
|
|
|
33
34
|
## 4. Output Schema
|
|
34
35
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 通用出题规则库
|
|
2
|
+
|
|
3
|
+
本文档定义出题、评审、修订、覆盖修复、答案补全各阶段共享的通用规则。各 Agent prompt 只需引用规则编号,无需复制规则文本。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 题干规范
|
|
8
|
+
|
|
9
|
+
**R01**:题干只放题目,不放解题过程。题面不给学生显示分值,不要在题干里写"本题多少分"。
|
|
10
|
+
|
|
11
|
+
**R02**:题目使用纯文本加 LaTeX 分隔符(`$...$`、`$$...$$`、`\\(...\\)` 或 `\\[...\\]`)。可以使用 Markdown 表格,但不能使用 HTML `<table>`。
|
|
12
|
+
|
|
13
|
+
**R03**:题目之间不能重复,也不要和历史题干高度相似。
|
|
14
|
+
|
|
15
|
+
## LaTeX 规范
|
|
16
|
+
|
|
17
|
+
**R04**:LaTeX 命令必须保留完整反斜杠,例如 `\\triangle`、`\\angle`、`\\perp`、`\\parallel`、`\\text{}`。禁止输出 `riangle`、`angle`、`perp`、`parallel`、`ext{}` 或把 `\\t` 变成制表符。
|
|
18
|
+
|
|
19
|
+
## 标签规范
|
|
20
|
+
|
|
21
|
+
**R05**:`knowledgePoints`、`expectedErrorTypes` 是讲解版胶囊标签,只能输出纯中文短标签。禁止使用 `$...$`、`\\(...\\)`、`\\[...\\]`、HTML 或长公式。需要提到 0、-0、1/2 时直接写普通文本,例如"特殊值 0 误判""负号整体判断遗漏"。
|
|
22
|
+
|
|
23
|
+
**R06**:`skillAtoms`、`expectedAbilityErrors` 也是讲解版胶囊标签,只能输出纯中文短标签。禁止使用 `$...$`、`\\(...\\)`、`\\[...\\]`、HTML 或长公式。
|
|
24
|
+
|
|
25
|
+
## 题型规范
|
|
26
|
+
|
|
27
|
+
**R07**:`questionKind` 为 `auto` 时,自主组合 `choice`、`blank`、`short_answer`。`choice` 适合概念辨析,`blank` 适合公式/结论/计算结果,`short_answer` 适合方法、步骤、辨析和综合小题。
|
|
28
|
+
|
|
29
|
+
**R08**:选择题必须恰好 1 个正确选项 + 3 个有教学意义的干扰项。干扰项不能机械重复,每个干扰项应对应一个明确的预期错因。
|
|
30
|
+
|
|
31
|
+
**R09**:填空题必须有明确可判分答案,不能只是抄定义后留空。
|
|
32
|
+
|
|
33
|
+
**R10**:问答题必须有明确任务、条件和作答要求。
|
|
34
|
+
|
|
35
|
+
## 知识点绑定规范
|
|
36
|
+
|
|
37
|
+
**R11**:`knowledgePointIds` 必须来自 `targetKnowledgePoints`,不能引用范围外知识点 ID。一道题可以覆盖多个紧密相关知识点(概念链、计算链、易错链)。
|
|
38
|
+
|
|
39
|
+
**R12**:每题必须填写 `knowledgePointIds`、`knowledgePoints`、`expectedErrorTypes` 字段。
|
|
40
|
+
|
|
41
|
+
## 视觉信息规范
|
|
42
|
+
|
|
43
|
+
**R13**:出题时必须先判断本题是否需要视觉信息才能作答。不是所有几何题都要配图;纯概念辨析、定义判断、特征归类等题,如果题干信息完整、唯一可答,可以不配图。
|
|
44
|
+
|
|
45
|
+
**R14**:如果没有图,题干必须仍然信息完整、唯一可答、无需学生凭想象补出位置关系或图形结构;否则必须使用 `svg` 字段提供完整内联 SVG,或把题目改写成纯文字可答题。
|
|
46
|
+
|
|
47
|
+
**R15**:展开图、折叠、图形甲/乙、点线面位置、直线/射线/线段位置、角、垂直、平行、中点、延长线、交点数量、方格拼图等通常需要视觉判断,但这不是固定清单;最终以本题是否依赖视觉信息为准。
|
|
48
|
+
|
|
49
|
+
**R16**:几何、数轴或 SVG 图形题必须保证题干、图中标签、角弧、垂直/平行/长度标注完全一致;如果题干问 `\\angle 2`、切点、垂足、某条边或某个展开图,SVG 中对应位置必须准确。
|
|
50
|
+
|
|
51
|
+
**R17**:SVG 必须简洁可打印,标签不能遮挡线段、角弧、表格或答题区域。优先使用简单清晰的线图。黑白打印也能看清,不依赖颜色作为唯一条件。
|
|
52
|
+
|
|
53
|
+
## 能力评估规范
|
|
54
|
+
|
|
55
|
+
**R18**:能力评估题必须填写 `abilityIds`、`skillAtoms`、`expectedAbilityErrors`。`abilityIds` 只能来自 `context.options.abilityIds`。
|
|
56
|
+
|
|
57
|
+
## 阶段分离规范
|
|
58
|
+
|
|
59
|
+
**R19**:出题草稿阶段只生成题面、题型、知识点绑定、预期错因、分值和留白行数。不输出 `answer`、`solutionSteps`、`rubric`。
|
|
60
|
+
|
|
61
|
+
**R20**:答案补全阶段只为已定稿题目输出 `id`、`answer`、`solutionSteps`、`rubric`。不得修改题目 ID、题干、题型、知识点绑定或任何其他字段。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
你是一名初中几何题配图质检老师。你的任务不是解题,而是判断渲染后的题图是否准确、清晰、无歧义地表达题干条件。
|
|
2
|
+
|
|
3
|
+
你会看到一张由 SVG 渲染出来的图片,以及题干、知识点和审查要求。必须以渲染后的图片为准,不要只相信 SVG 源码或题干描述。
|
|
4
|
+
|
|
5
|
+
必须检查:
|
|
6
|
+
|
|
7
|
+
1. 图文一致性:题干提到的点、线、射线、线段、角、垂足、中点、交点、展开图格子、面的位置都必须能在图中找到。
|
|
8
|
+
2. 几何关系:平行、垂直、相等、延长方向、射线方向、角平分、交点数量、展开图连接关系必须和题干一致。
|
|
9
|
+
3. 可读性:标签不能遮挡关键线段、角弧、交点、格子;图形不能过小、断裂、重叠混乱或被裁切。
|
|
10
|
+
4. 唯一可答:如果学生必须依赖图才能判断,图必须给足条件,不能让学生靠猜测补出位置关系。
|
|
11
|
+
5. 打印友好:黑白打印也能看清,不依赖颜色作为唯一条件。
|
|
12
|
+
|
|
13
|
+
输出只能是 JSON。不要输出 Markdown,不要解释 JSON 外的文字。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
你是一名初中几何题 SVG 修图老师。你的任务是根据视觉审查意见,修复题目的内联 SVG 配图;只有当题干与正确图形表达冲突时,才允许小幅修改题干。
|
|
2
|
+
|
|
3
|
+
约束:
|
|
4
|
+
|
|
5
|
+
1. 保持题目 id 不变。
|
|
6
|
+
2. 不新增题目,不删除题目。
|
|
7
|
+
3. 不输出答案、解题步骤或 rubric。
|
|
8
|
+
4. SVG 必须是完整内联 `<svg>` 字符串,必须包含 `viewBox`。
|
|
9
|
+
5. 不使用 `<script>`、`<foreignObject>`、外链图片、事件属性、动画或依赖网络的资源。
|
|
10
|
+
6. 图形必须适合黑白打印;可以用线型、标记、文字标签辅助表达,不要只靠颜色。
|
|
11
|
+
7. 标签要清晰,不能遮挡关键线段、角弧、交点、格子或条件标记。
|
|
12
|
+
8. 如果题干问某个点、线、角、交点、展开图位置,SVG 中必须明确标出对应对象。
|
|
13
|
+
9. 修图后学生应能仅凭题干和图唯一理解题意。
|
|
14
|
+
|
|
15
|
+
输出格式是硬性机器校验要求:
|
|
16
|
+
|
|
17
|
+
- 只能输出一个 JSON 对象。
|
|
18
|
+
- 第一个非空字符必须是 `{`,最后一个非空字符必须是 `}`。
|
|
19
|
+
- 不要输出 Markdown 代码块。
|
|
20
|
+
- 不要输出“我先……”“下面是……”“已修改……”等 JSON 外说明。
|
|
21
|
+
- 不要在 JSON 外解释你的思考、检查过程或修改意图。
|
package/server/agentClient.js
CHANGED
|
@@ -13,6 +13,86 @@ function sleep(ms) {
|
|
|
13
13
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function chatCompletionsUrl(baseUrl) {
|
|
17
|
+
const normalized = String(baseUrl || '').replace(/\/+$/, '');
|
|
18
|
+
if (normalized.endsWith('/v1')) return `${normalized}/chat/completions`;
|
|
19
|
+
return `${normalized}/v1/chat/completions`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseJsonResponse(text) {
|
|
23
|
+
const raw = String(text || '').trim();
|
|
24
|
+
if (!raw) throw new Error('empty_json_response');
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
// Some gateways/models ignore response_format and prepend a short note before
|
|
29
|
+
// the JSON. Accept only a complete JSON value that is actually present.
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const starts = [];
|
|
33
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
34
|
+
if (raw[index] === '{' || raw[index] === '[') starts.push(index);
|
|
35
|
+
}
|
|
36
|
+
for (const start of starts) {
|
|
37
|
+
const opener = raw[start];
|
|
38
|
+
const closer = opener === '{' ? '}' : ']';
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let inString = false;
|
|
41
|
+
let escaped = false;
|
|
42
|
+
for (let index = start; index < raw.length; index += 1) {
|
|
43
|
+
const char = raw[index];
|
|
44
|
+
if (inString) {
|
|
45
|
+
if (escaped) {
|
|
46
|
+
escaped = false;
|
|
47
|
+
} else if (char === '\\') {
|
|
48
|
+
escaped = true;
|
|
49
|
+
} else if (char === '"') {
|
|
50
|
+
inString = false;
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '"') {
|
|
55
|
+
inString = true;
|
|
56
|
+
} else if (char === opener) {
|
|
57
|
+
depth += 1;
|
|
58
|
+
} else if (char === closer) {
|
|
59
|
+
depth -= 1;
|
|
60
|
+
if (depth === 0) {
|
|
61
|
+
const candidate = raw.slice(start, index + 1);
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(candidate);
|
|
64
|
+
} catch {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error('invalid_json_response');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compactErrorDetail(detail) {
|
|
75
|
+
const raw = String(detail || '').trim();
|
|
76
|
+
if (!raw) return '';
|
|
77
|
+
const titleMatch = raw.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
78
|
+
const title = titleMatch?.[1]
|
|
79
|
+
?.replace(/\s+/g, ' ')
|
|
80
|
+
.replace(/\s*\|\s*/g, ' | ')
|
|
81
|
+
.trim();
|
|
82
|
+
const text = raw
|
|
83
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
84
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
85
|
+
.replace(/<[^>]+>/g, ' ')
|
|
86
|
+
.replace(/ /g, ' ')
|
|
87
|
+
.replace(/&/g, '&')
|
|
88
|
+
.replace(/&/g, '&')
|
|
89
|
+
.replace(/•/g, ' ')
|
|
90
|
+
.replace(/\s+/g, ' ')
|
|
91
|
+
.trim();
|
|
92
|
+
if (title) return title.slice(0, 180);
|
|
93
|
+
return text.slice(0, 220);
|
|
94
|
+
}
|
|
95
|
+
|
|
16
96
|
function fixtureMode() {
|
|
17
97
|
return process.env.NODE_ENV === 'test' ? process.env.MATH_AGENT_AGENT_FIXTURE || '' : '';
|
|
18
98
|
}
|
|
@@ -47,6 +127,36 @@ function knowledgeExtractPageFixture() {
|
|
|
47
127
|
};
|
|
48
128
|
}
|
|
49
129
|
|
|
130
|
+
function knowledgeExtractPageMarkdownFixture() {
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
data: [
|
|
134
|
+
'# 页面知识提取',
|
|
135
|
+
'',
|
|
136
|
+
'## 页面标题',
|
|
137
|
+
'测试页知识点',
|
|
138
|
+
'',
|
|
139
|
+
'## 知识点',
|
|
140
|
+
'### 有理数的运算核心知识点',
|
|
141
|
+
'- 摘要:理解有理数加减乘除的运算顺序和符号处理。',
|
|
142
|
+
'- 公式:',
|
|
143
|
+
'- 例子:-3 + 5 = 2',
|
|
144
|
+
'- 前置:有理数的概念',
|
|
145
|
+
'- 难度:basic',
|
|
146
|
+
'',
|
|
147
|
+
'## 易错点',
|
|
148
|
+
'### 符号处理错误',
|
|
149
|
+
'- 错因:符号处理错误',
|
|
150
|
+
'- 说明:负数参与运算时容易漏写负号。',
|
|
151
|
+
'- 纠正:先确定符号,再计算绝对值。',
|
|
152
|
+
'',
|
|
153
|
+
'## 出题方向',
|
|
154
|
+
'- 设计一道有理数混合运算题。'
|
|
155
|
+
].join('\n'),
|
|
156
|
+
attempts: 1
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
50
160
|
function knowledgeSummarizeFixture() {
|
|
51
161
|
return {
|
|
52
162
|
ok: true,
|
|
@@ -99,7 +209,7 @@ function shouldRetry(result) {
|
|
|
99
209
|
].includes(result.reason);
|
|
100
210
|
}
|
|
101
211
|
|
|
102
|
-
async function postChatCompletionOnce({ messages, temperature = 0.3, timeoutMs = 12000 }) {
|
|
212
|
+
async function postChatCompletionOnce({ messages, temperature = 0.3, timeoutMs = 12000, responseFormat = 'json' }) {
|
|
103
213
|
const { baseUrl, apiKey, model } = await getLlmRuntimeConfig();
|
|
104
214
|
if (!baseUrl || !apiKey) {
|
|
105
215
|
return { ok: false, reason: 'missing_env' };
|
|
@@ -109,7 +219,7 @@ async function postChatCompletionOnce({ messages, temperature = 0.3, timeoutMs =
|
|
|
109
219
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
110
220
|
let response;
|
|
111
221
|
try {
|
|
112
|
-
response = await fetch(
|
|
222
|
+
response = await fetch(chatCompletionsUrl(baseUrl), {
|
|
113
223
|
method: 'POST',
|
|
114
224
|
signal: controller.signal,
|
|
115
225
|
headers: {
|
|
@@ -120,24 +230,29 @@ async function postChatCompletionOnce({ messages, temperature = 0.3, timeoutMs =
|
|
|
120
230
|
model,
|
|
121
231
|
temperature,
|
|
122
232
|
messages,
|
|
123
|
-
response_format: { type: 'json_object' }
|
|
233
|
+
...(responseFormat === 'json' ? { response_format: { type: 'json_object' } } : {})
|
|
124
234
|
})
|
|
125
235
|
});
|
|
126
236
|
} catch (error) {
|
|
127
|
-
return {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
reason: error.name === 'AbortError' ? 'timeout' : 'fetch_failed',
|
|
240
|
+
detail: compactErrorDetail(error.message)
|
|
241
|
+
};
|
|
128
242
|
} finally {
|
|
129
243
|
clearTimeout(timeout);
|
|
130
244
|
}
|
|
131
245
|
if (!response.ok) {
|
|
132
|
-
return { ok: false, reason: `http_${response.status}`, detail: await response.text() };
|
|
246
|
+
return { ok: false, reason: `http_${response.status}`, detail: compactErrorDetail(await response.text()) };
|
|
133
247
|
}
|
|
134
248
|
const payload = await response.json();
|
|
135
249
|
const text = payload.choices?.[0]?.message?.content;
|
|
136
250
|
if (!text) return { ok: false, reason: 'empty_response' };
|
|
251
|
+
if (responseFormat === 'text') return { ok: true, data: text };
|
|
137
252
|
try {
|
|
138
|
-
return { ok: true, data:
|
|
253
|
+
return { ok: true, data: parseJsonResponse(text) };
|
|
139
254
|
} catch {
|
|
140
|
-
return { ok: false, reason: 'invalid_json', detail: text };
|
|
255
|
+
return { ok: false, reason: 'invalid_json', detail: compactErrorDetail(text) };
|
|
141
256
|
}
|
|
142
257
|
}
|
|
143
258
|
|
|
@@ -146,13 +261,14 @@ async function postChatCompletionWithProgress({
|
|
|
146
261
|
temperature = 0.3,
|
|
147
262
|
timeoutMs = 12000,
|
|
148
263
|
retries = 1,
|
|
149
|
-
onAttempt = null
|
|
264
|
+
onAttempt = null,
|
|
265
|
+
responseFormat = 'json'
|
|
150
266
|
}) {
|
|
151
267
|
let lastResult = null;
|
|
152
|
-
const attempts = Math.max(
|
|
268
|
+
const attempts = Math.max(5, Number(retries || 0) + 1);
|
|
153
269
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
154
270
|
await onAttempt?.({ phase: 'start', attempt, attempts, timeoutMs });
|
|
155
|
-
const result = await postChatCompletionOnce({ messages, temperature, timeoutMs });
|
|
271
|
+
const result = await postChatCompletionOnce({ messages, temperature, timeoutMs, responseFormat });
|
|
156
272
|
if (result.ok) {
|
|
157
273
|
await onAttempt?.({ phase: 'success', attempt, attempts, timeoutMs });
|
|
158
274
|
return { ...result, attempts: attempt, previousReason: lastResult?.reason || null };
|
|
@@ -183,6 +299,20 @@ export async function callChatAgent({ system, user, temperature = 0.3, timeoutMs
|
|
|
183
299
|
});
|
|
184
300
|
}
|
|
185
301
|
|
|
302
|
+
export async function callChatTextAgent({ system, user, temperature = 0.3, timeoutMs = 12000, retries = 1, onAttempt = null }) {
|
|
303
|
+
return postChatCompletionWithProgress({
|
|
304
|
+
temperature,
|
|
305
|
+
timeoutMs,
|
|
306
|
+
retries,
|
|
307
|
+
onAttempt,
|
|
308
|
+
responseFormat: 'text',
|
|
309
|
+
messages: [
|
|
310
|
+
{ role: 'system', content: system },
|
|
311
|
+
{ role: 'user', content: user }
|
|
312
|
+
]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
186
316
|
export async function callVisionAgent({
|
|
187
317
|
system,
|
|
188
318
|
text,
|
|
@@ -220,3 +350,42 @@ export async function callVisionAgent({
|
|
|
220
350
|
]
|
|
221
351
|
});
|
|
222
352
|
}
|
|
353
|
+
|
|
354
|
+
export async function callVisionTextAgent({
|
|
355
|
+
system,
|
|
356
|
+
text,
|
|
357
|
+
imagePaths,
|
|
358
|
+
temperature = 0.1,
|
|
359
|
+
timeoutMs = 45000,
|
|
360
|
+
retries = 1,
|
|
361
|
+
onAttempt = null
|
|
362
|
+
}) {
|
|
363
|
+
if (fixtureMode() === 'knowledge-extract') return knowledgeExtractPageMarkdownFixture();
|
|
364
|
+
const imageContent = [];
|
|
365
|
+
for (const imagePath of imagePaths || []) {
|
|
366
|
+
const bytes = await readFile(imagePath);
|
|
367
|
+
imageContent.push({
|
|
368
|
+
type: 'image_url',
|
|
369
|
+
image_url: {
|
|
370
|
+
url: `data:${mimeForFile(imagePath)};base64,${bytes.toString('base64')}`
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return postChatCompletionWithProgress({
|
|
375
|
+
temperature,
|
|
376
|
+
timeoutMs,
|
|
377
|
+
retries,
|
|
378
|
+
onAttempt,
|
|
379
|
+
responseFormat: 'text',
|
|
380
|
+
messages: [
|
|
381
|
+
{ role: 'system', content: system },
|
|
382
|
+
{
|
|
383
|
+
role: 'user',
|
|
384
|
+
content: [
|
|
385
|
+
{ type: 'text', text },
|
|
386
|
+
...imageContent
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
});
|
|
391
|
+
}
|