@zhouchangui/math-ati 0.1.2 → 0.1.3
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 +3 -1
- package/README.md +11 -0
- package/dist/assets/{index-CGZslJ0a.css → index--Um9OfFu.css} +1 -1
- package/dist/assets/index-CS-PgjYi.js +22 -0
- package/dist/index.html +3 -3
- package/package.json +3 -2
- package/prompts/geometry-practice-experience.md +44 -0
- package/prompts/knowledge-extract.system.md +35 -54
- package/prompts/knowledge-summarize.system.md +8 -6
- package/prompts/practice-generate.system.md +6 -4
- package/prompts/practice-review.system.md +4 -2
- package/prompts/practice-revise.system.md +5 -4
- 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 +40 -7
- package/server/index.js +30 -1
- package/server/knowledgeExtractor.js +553 -115
- package/server/practiceGenerator.js +610 -83
- package/server/practicePaperHtml.js +105 -35
- package/server/practiceService.js +27 -2
- package/server/submissionService.js +1 -1
- package/server/svgFigureVerifier.js +315 -0
- package/dist/assets/index-CGfjl7nO.js +0 -22
package/dist/index.html
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index
|
|
6
|
+
<title>学生数学提分 Agent</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CS-PgjYi.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index--Um9OfFu.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhouchangui/math-ati",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local ATI math learning loop for printable practice, PDF grading, and mastery tracking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node": ">=20"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"dev": "concurrently \"npm:dev:server\" \"npm:dev:client\"",
|
|
27
|
+
"dev": "sh -c 'WORK_DIR=\"${MATH_AGENT_WORK_DIR:-$HOME/math-ati-workspace}\"; MATH_AGENT_DATA_DIR=\"$WORK_DIR/data\" MATH_AGENT_ENV_FILE=\"$WORK_DIR/.env.local\" concurrently \"npm:dev:server\" \"npm:dev:client\"'",
|
|
28
28
|
"dev:client": "vite --host 127.0.0.1",
|
|
29
29
|
"dev:server": "node --watch server/index.js",
|
|
30
30
|
"build": "vite build",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"verify:ui": "node scripts/verify-ui-workflow.js",
|
|
49
49
|
"verify:knowledge-extract": "node scripts/verify-knowledge-extract-workflow.js",
|
|
50
50
|
"verify:knowledge-extract:acceptance": "node scripts/verify-knowledge-extract-workflow.js --fixture-server --acceptance",
|
|
51
|
+
"verify:coverage-planner": "node scripts/verify-coverage-planner.js",
|
|
51
52
|
"verify:coverage-state": "node scripts/verify-coverage-state.js",
|
|
52
53
|
"verify:mistake-lifecycle": "node scripts/verify-mistake-lifecycle.js",
|
|
53
54
|
"verify:ability-config": "node scripts/verify-ability-config.js",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Geometry Practice Experience: 几何题生成与校验经验库
|
|
2
|
+
|
|
3
|
+
本经验库用于几何章节出题、修题、复审和 SVG 渲染审查。目标是减少“题目能生成但不可作答、图文不一致、选择题答案不唯一、SVG 看似有图但表达错误”的问题。
|
|
4
|
+
|
|
5
|
+
## 1. 先判定题目是否需要图
|
|
6
|
+
|
|
7
|
+
- 不是所有几何题都需要 SVG。纯概念辨析、几何体特征归类、名称匹配、定义判断,只要文字条件完整且答案唯一,可以不配图。
|
|
8
|
+
- 需要学生观察位置关系、连接关系、展开图、折叠结果、角/线/点的相对位置、交点数量、延长方向、垂足/中点/切点时,通常需要 SVG。
|
|
9
|
+
- 如果题干写了“观察图”“如下图”“图中”“甲乙两图”,必须有图;否则应改成纯文字可答题。
|
|
10
|
+
- 可选 SVG 样本只是回归测试,不是题目成立的必要条件;若文字已唯一可答,SVG 修不好时应优先回退为文字题,而不是保留有歧义图。
|
|
11
|
+
|
|
12
|
+
## 2. 选择题必须唯一可判分
|
|
13
|
+
|
|
14
|
+
- 单选题必须恰好 1 个正确选项。若两个选项都可解释为正确,必须判 blocker。
|
|
15
|
+
- 禁止用“可能”“并非明显错误”“也许可以”“不一定能看出”等不确定表达作为错误选项的依据。
|
|
16
|
+
- “只要”“一定”“都”“只有”这类绝对词要特别检查;它们常用于设计干扰项,但也容易把正确命题写成错误选项或相反。
|
|
17
|
+
- 如果同一个选择题连续修订仍出现 `bad_options`,不要继续微调单个选项;必须重写整题和全部选项,或改成填空/简答,让答案唯一。
|
|
18
|
+
- 改错题中“有几句错误”必须和实际错误句数量一致。若题干说“两句错误”,评审必须逐句数出确有两句错误。
|
|
19
|
+
|
|
20
|
+
## 3. 正方体与棱柱/棱锥展开图经验
|
|
21
|
+
|
|
22
|
+
- “由 6 个全等正方形组成”不是正方体展开图的充分条件;还要看连接位置能否围成立方体。
|
|
23
|
+
- “1-4-1 型”本身是正方体展开图的一类,但具体连接位置仍要画清楚。不要把一个可折成正方体的 1-4-1 图当作错误选项。
|
|
24
|
+
- 对正方体展开图题,若要设计错误选项,优先使用明确错误的泛化说法,例如“只要 6 个全等正方形相连就一定能折成正方体”。
|
|
25
|
+
- 棱柱展开图应体现两个全等且对应的多边形底面,以及与边数一致的侧面带状结构。
|
|
26
|
+
- 棱锥侧面展开图只讨论侧面时,通常是若干三角形;完整展开图才包含底面。题干必须说清“侧面展开图”还是“完整展开图”。
|
|
27
|
+
- 圆柱需要两个圆形底面和一个侧面;圆锥需要一个圆形底面和一个扇形侧面;缺少底面个数条件时不能让学生猜。
|
|
28
|
+
|
|
29
|
+
## 4. SVG 作图规范
|
|
30
|
+
|
|
31
|
+
- SVG 必须表达题目条件,不只是装饰。图中的对象、标签、方向和题干引用必须一一对应。
|
|
32
|
+
- 展开图必须画清面数和连接边。面之间不能只靠靠近来暗示连接;共享边必须明确。
|
|
33
|
+
- 标签不能遮挡线段、格子、角弧、交点或关键连接边。长标签应放在图外空白处,用短标签标注图内对象。
|
|
34
|
+
- 不要让文字或图形被 viewBox 裁切。渲染后若标题、选项说明或标签被截断,必须修图。
|
|
35
|
+
- 黑白打印下仍应可读;不要用颜色作为唯一条件。
|
|
36
|
+
- 图中如果画了“示例图”“反例图”“图 1/图 2”,题干和选项必须明确对应每一幅图。
|
|
37
|
+
|
|
38
|
+
## 5. 复审与修订策略
|
|
39
|
+
|
|
40
|
+
- 复审要优先判断“答案是否唯一”和“图是否给足条件”,不要只看字段是否齐全。
|
|
41
|
+
- 发现 `bad_options`、`unanswerable`、`missing_svg`、`svg_mismatch`、`duplicate` 时,必须给出可执行修复指令。
|
|
42
|
+
- 终审修订不能只复述 blocker。修订后必须重新自查:选项唯一、错误句数量正确、图文一致、知识点绑定仍在范围内。
|
|
43
|
+
- 如果修订改变了题干或题型,原 SVG verified 缓存不能复用;必须重新渲染图片审查。
|
|
44
|
+
- 如果几何题反复因为图太复杂无法通过,应优先降低图形复杂度,改成更小、更清晰、条件更直接的题。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 1. Mission
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
从单页章节图片中提取可教学、可检测、可追溯的初中数学知识内容,输出紧凑 Markdown,供章节汇总 Agent 后续合并。
|
|
6
6
|
|
|
7
7
|
## 2. Inputs
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
参考文件位置:
|
|
17
17
|
|
|
18
18
|
- 原始章节图片:`output/images/初中数学_提分笔记_按章节/<chapter-folder>/*.png`
|
|
19
|
-
- 本 Agent 输出:`data/
|
|
19
|
+
- 本 Agent 输出:`data/chapters/<chapter-id>/knowledge/page_extracts/<page>.md`
|
|
20
20
|
|
|
21
21
|
## 3. Context Reading Order
|
|
22
22
|
|
|
@@ -53,59 +53,40 @@
|
|
|
53
53
|
|
|
54
54
|
正确输出片段:
|
|
55
55
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"correction": "$0$ 的相反数是 $0$。"
|
|
74
|
-
}
|
|
75
|
-
]
|
|
76
|
-
}
|
|
77
|
-
```
|
|
56
|
+
```markdown
|
|
57
|
+
# 页面知识提取
|
|
58
|
+
|
|
59
|
+
## 页面标题
|
|
60
|
+
相反数
|
|
61
|
+
|
|
62
|
+
## 原文结构
|
|
63
|
+
- 相反数:只有符号不同的两个数互为相反数
|
|
64
|
+
- $0$ 的相反数是 $0$
|
|
65
|
+
|
|
66
|
+
## 知识点
|
|
67
|
+
### 相反数的概念
|
|
68
|
+
- 摘要:只有符号不同的两个数互为相反数,$0$ 的相反数是 $0$。
|
|
69
|
+
- 公式:$a$ 的相反数是 $-a$
|
|
70
|
+
- 例子:$3$ 与 $-3$
|
|
71
|
+
- 前置:正数、负数和 0 的意义
|
|
72
|
+
- 难度:basic
|
|
78
73
|
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"knowledgePoints": [
|
|
88
|
-
{
|
|
89
|
-
"title": "string",
|
|
90
|
-
"summary": "string",
|
|
91
|
-
"formulas": ["string with LaTeX"],
|
|
92
|
-
"examples": ["short example or expression"],
|
|
93
|
-
"prerequisite": "string",
|
|
94
|
-
"difficulty": "basic|medium|challenge"
|
|
95
|
-
}
|
|
96
|
-
],
|
|
97
|
-
"easyMistakes": [
|
|
98
|
-
{
|
|
99
|
-
"title": "string",
|
|
100
|
-
"errorType": "string",
|
|
101
|
-
"description": "string",
|
|
102
|
-
"correction": "string"
|
|
103
|
-
}
|
|
104
|
-
],
|
|
105
|
-
"exerciseHints": ["string"]
|
|
106
|
-
}
|
|
74
|
+
## 易错点
|
|
75
|
+
### 漏掉 0 的特殊情况
|
|
76
|
+
- 错因:特殊值遗漏
|
|
77
|
+
- 说明:学生容易认为 $0$ 没有相反数。
|
|
78
|
+
- 纠正:$0$ 的相反数是 $0$。
|
|
79
|
+
|
|
80
|
+
## 出题方向
|
|
81
|
+
- 写出一个正数、一个负数和 $0$ 的相反数。
|
|
107
82
|
```
|
|
108
83
|
|
|
84
|
+
## 7. Output Format
|
|
85
|
+
|
|
86
|
+
只输出 Markdown,不输出 JSON,不输出解释性前后缀。
|
|
87
|
+
|
|
88
|
+
必须使用这些标题:`# 页面知识提取`、`## 页面标题`、`## 原文结构`、`## 知识点`、`## 易错点`、`## 出题方向`。
|
|
89
|
+
|
|
109
90
|
## 8. Self-check Rubric
|
|
110
91
|
|
|
111
92
|
输出前确认:
|
|
@@ -114,10 +95,10 @@
|
|
|
114
95
|
- 是否把例子误当成知识点?
|
|
115
96
|
- 是否把教材没有出现的知识扩写进来了?
|
|
116
97
|
- 是否保留了易错点和特殊值?
|
|
117
|
-
-
|
|
98
|
+
- Markdown 标题是否完整、简洁、可被后续汇总?
|
|
118
99
|
|
|
119
100
|
## 9. Failure Policy
|
|
120
101
|
|
|
121
102
|
- 如果图片局部看不清:在 `rawOutline` 写明不确定区域,能确定的内容照常提取。
|
|
122
|
-
-
|
|
103
|
+
- 如果整页无法识别:保留固定标题,并在 `## 原文结构` 说明原因。
|
|
123
104
|
- 不要编造看不清的文字。
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## 1. Mission
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
把同一章节的逐页 Markdown 提取结果合并、去重、压缩为可教学、可逐点检测、可归档追踪的章节知识点文档。
|
|
6
6
|
|
|
7
7
|
## 2. Inputs
|
|
8
8
|
|
|
9
9
|
调用方会提供:
|
|
10
10
|
|
|
11
11
|
- `context.chapter`:章节元信息。
|
|
12
|
-
-
|
|
13
|
-
- `context.
|
|
12
|
+
- 中间汇总请求会提供逐页 Markdown 或分组 Markdown。
|
|
13
|
+
- 最终汇总请求会提供 `context.chunkDocs` 或 `context.pageExtracts`,并要求输出最终 JSON。
|
|
14
|
+
- `context.localDraft`:本地程序从 Markdown 粗解析出的辅助草稿。
|
|
14
15
|
|
|
15
16
|
输出会写入:
|
|
16
17
|
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
|
|
21
22
|
## 3. Context Reading Order
|
|
22
23
|
|
|
23
|
-
1.
|
|
24
|
+
1. 先读逐页 Markdown 或分组 Markdown,它是主要来源。
|
|
24
25
|
2. 再读 `context.localDraft`,只作为辅助草稿。
|
|
25
26
|
3. 最后读 `context.chapter`,用于章节命名、主线和 id 前缀。
|
|
26
27
|
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
|
|
37
38
|
## 5. SOP
|
|
38
39
|
|
|
39
|
-
1.
|
|
40
|
+
1. 通读所有逐页/分组 Markdown,列出候选知识点。
|
|
40
41
|
2. 合并同义、过小、重复候选。
|
|
41
42
|
3. 将核心知识放入 `知识点覆盖` section。
|
|
42
43
|
4. 将常见误判、特殊值、限制条件放入 `易错题专项` section。
|
|
@@ -80,7 +81,8 @@
|
|
|
80
81
|
|
|
81
82
|
## 7. Output Schema
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
当用户要求“分组合并”时,只输出 Markdown。
|
|
85
|
+
当用户要求“最终章节知识文档”并提供 JSON schema 时,只输出 JSON,不输出 Markdown,不输出解释性前后缀。
|
|
84
86
|
|
|
85
87
|
```json
|
|
86
88
|
{
|
|
@@ -55,11 +55,13 @@ 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
66
|
## 5. SOP
|
|
65
67
|
|
|
@@ -148,7 +150,7 @@ Source of Truth:`targetKnowledgePoints` 高于 `knowledgeDoc`,不要引用
|
|
|
148
150
|
- 如果是能力评估,`abilityIds` 是否来自 `context.options.abilityIds`,并且每题都有 `skillAtoms` 和 `expectedAbilityErrors`?
|
|
149
151
|
- 题干是否没有解题过程?
|
|
150
152
|
- 如果当前任务是出题草稿:是否没有输出 `answer`、`solutionSteps`、`rubric`?
|
|
151
|
-
- 如果当前任务是答案补全:每题是否输出了可核对的 `answer
|
|
153
|
+
- 如果当前任务是答案补全:每题是否输出了可核对的 `answer`、适合题目复杂度的 `solutionSteps`、可批改的 `rubric`?
|
|
152
154
|
- 是否避开历史重复题?
|
|
153
155
|
- 题目是否能暴露一个明确错因?
|
|
154
156
|
- LaTeX 反斜杠是否完整保留,没有出现 `riangle`、`angle`、`perp`、`parallel`、`ext{}` 这类丢反斜杠错误?
|
|
@@ -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,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
|
+
}
|