feat: 新增认识时钟绘制

This commit is contained in:
R524809
2026-06-25 12:46:43 +08:00
parent b802709517
commit 783ebfb936
15 changed files with 1119 additions and 1 deletions
+171
View File
@@ -0,0 +1,171 @@
# AI 生成题目方案(对话式出题)
> 文档状态:方案讨论 / 待评审
> 创建日期:2026-06-16
> 背景:当前小程序与同类应用差异不明显,用户画像不够精准。探索引入 AI 能力——用户用自然语言描述需求,AI 将其翻译为对预置「技能(绘制方法)」的调用,并完成出题与绘制。
---
## 一、战略层面:先想清楚「为什么要做」
本方案在技术上完全可行,但要避免 AI 沦为「为差异化而加的功能」,而非「解决真实痛点的功能」。立项前需先回答:
- **用户的真实痛点是什么?** 是「找不到合适的题目」,还是「孩子不爱练」「不知道该练什么」?如果痛点不在出题,AI 出题器再酷也救不了留存。
- **家长真的会打字描述需求吗?** 「我想要培养数感的题目」这类话,家长未必说得出(多数人不知道「数感」是什么)。更真实的诉求往往是「我家娃 5 岁,给我今天该练的」。这意味着 AI 的价值可能不在「自然语言理解」,而在 **「帮不懂教育的家长做决策」**。
- **不用 AI 能否满足 80%?** 如果几个下拉框 + 模板就能满足,AI 的边际价值仅是「输入方式更自然」,通常撑不起差异化。
**结论 / 定位建议:** 把 AI 定位成 **「懂教育的助教」**(帮家长判断该练什么、循序渐进地推荐),而不是 **「自然语言转绘制指令的翻译器」**。前者是真差异,后者只是花哨的表单。
---
## 二、技术层面:经典的 Function Calling / Tool Use 架构
「把需求变成可调用的技能」在工程上的成熟范式叫 **工具调用(tool use / function calling**:已有的「绘制方法」即工具,LLM 负责把人话翻译成「调用哪个工具 + 什么参数」。
### 2.1 整体数据流(小程序云开发)
```
用户输入(自然语言)
→ 小程序前端 (聊天式 UI)
→ 云函数 ai-orchestrator
→ 调用 LLM(带 tools 定义)
├─ 缺参数 → 返回追问("孩子几岁?") ← 多轮
└─ 参数齐全 → 返回结构化调用意图
→ 确认环节(用户点"确认生成")
→ 云函数 / 前端调用内置绘制方法
→ 生成题目数据 + 渲染(canvas/图片)
→ 返回前端展示 / 保存 / 下载
```
### 2.2 关键模块
**1)技能注册表(最核心)**
每个绘制方法描述成一个 tool schema,让 LLM 知道有哪些能力、各需要什么参数:
```json
{
"name": "generate_number_sense",
"description": "生成培养数感的练习题(比大小、数的分解、数数等)",
"parameters": {
"type": "object",
"properties": {
"age": { "type": "integer", "description": "孩子年龄 3-8" },
"sub_type": { "type": "string", "enum": ["比大小", "数的分解", "按数取物"] },
"count": { "type": "integer", "description": "题目数量" },
"number_range": { "type": "string", "enum": ["1-10", "1-20", "1-100"] }
},
"required": ["age", "sub_type", "count"]
}
}
```
**2LLM 选型(国内合规很重要)**
云函数可发 HTTP 请求,能接任意 LLM。国内备案合规、且支持 OpenAI 兼容 function calling 的可选:**通义千问、豆包(火山方舟)、DeepSeek、文心、混元**。微信云开发本身也有「AI 能力 / 微信对话开放平台」。建议挑一个支持 `tools` 参数的,编排逻辑几乎无需自写。
**3)多轮对话 + 槽位填充(slot filling**
「缺年龄就追问年龄」的逻辑交给 LLM 判断,比手写 if-else 更强。需要做的只是:
- 每轮带上对话历史(存云数据库或前端回传);
- 在 system prompt 里要求:「参数不全时先友好追问,不要瞎猜」。
**4)「决策」与「生成」必须分离 ⚠️(最重要的工程原则)**
让 LLM 决定「生成什么」(结构化参数),但题目实际内容由确定性代码生成:
- ❌ 不要让 LLM 直接吐出 20 道算术题——会算错、会重复、不可控。
- ✅ 让 LLM 输出 `{age:5, sub_type:"比大小", count:20, range:"1-10"}`,再由绘制方法按规则生成 + 渲染。
兼得 AI 的「听得懂人话」与传统代码的「100% 正确可控」。
**5)确认环节**
真正调用绘制方法前,把解析出的参数回显给用户确认(「将生成 20 道 1-10 比大小,确认?」)。既防误解,也是好体验。
### 2.3 小程序云开发的几个坑
| 坑 | 说明 / 对策 |
| -------------- | ------------------------------------------------------------------------------------- |
| **云函数超时** | 默认 20s,LLM 调用可能慢。先做**非流式**(一次返回);想要打字机效果,后期再用 WebSocket 或 HTTP 触发器 + SSE,复杂度高,别一开始就做。 |
| **内容安全(必须)** | 微信强制要求。用户输入和 AI 输出都要过 `security.msgSecCheck`,否则可能被封。合规硬要求,非可选。 |
| **API Key 保护** | LLM 的 key 只能放云函数环境变量,**绝不能进前端**。 |
| **成本与频率** | 每次对话烧 token。加缓存(相同需求复用)、限频,防刷。 |
| **冷启动延迟** | 云函数冷启动 + LLM 延迟叠加,首次可能 3-5sUI 要有 loading 反馈。 |
---
## 三、要不要把绘制方法搬到服务端?
**短答:不需要把「绘制」搬到服务端。但要先把现有方法拆成两层——「出题数据」和「画图」——只有前者可能值得上服务端,后者留在小程序里。**
### 3.1 把「绘制方法」拆成三层
```
① 意图理解层 用户人话 → 结构化参数 ← 必须在服务端(云函数 + LLM)
② 出题数据层 参数 → 题目数据(JSON) ← 可服务端、可客户端
③ 渲染绘制层 题目数据 → canvas 画出来 ← 留在小程序前端
```
现有「绘制方法」大概率是 ②③ 混在一起(一个函数既算题目又调 `wx.canvas` 画图)。做 AI **真正要做的不是「搬到服务端」,而是「把 ② 和 ③ 解耦」**
### 3.2 为什么渲染层(③)不该搬服务端
- 小程序 canvas 是**客户端 API**,云函数(Node 环境)里没有 DOM、没有原生 canvas;要画图得引 `node-canvas` 之类,又重又易踩坑,得不偿失。
- 渲染留前端:性能好、可交互(手写、橡皮擦等)、省服务器成本。
→ ③ **保持现状,一行都不用动**
### 3.3 ② 出题数据层:搬不搬都行
**方案 A — 什么都不搬(最快上线,推荐起步)**
云函数只做 ①,返回结构化参数给前端:
```js
// 云函数返回
{ skill: "number_sense", age: 5, sub_type: "比大小", count: 20, range: "1-10" }
```
前端拿到参数,调已有的出题 + 绘制方法,照常跑。AI 是「加在前面的一层翻译」,老代码完全复用。
**方案 B — 把出题逻辑(②)搬服务端**
云函数直接算好题目数据返回:
```js
{ problems: [ {left:7, right:3, op:">"}, {left:2, right:8, op:"<"}, ... ] }
```
前端只负责画。
什么时候才值得选 B
- 出题逻辑要**保密/防作弊**(题库、难度算法不想暴露前端);
- **多端复用**(以后有 H5、APP,出题逻辑只维护一份);
- 出题需要**服务端资源**(查数据库题库、调别的接口)。
以上都没有,**先选 A**。
### 3.4 关键动作
唯一的关键动作:**确保出题逻辑是个「纯数据函数」(输入参数、输出 JSON、不碰 canvas)**。做到这点,搬不搬服务端都是后话,随时可切。
---
## 四、最小可行起步(MVP
别一上来做全套对话系统,先做最小闭环验证:
1. **单个技能**:只挑「数感题目」一个绘制方法接进来。
2. **单轮或两轮对话**:用户说需求 → AI 解析参数(缺了追问一次)→ 确认 → 生成。
3. **跑通链路**:「人话 → 结构化参数 → 已有绘制方法」。
跑通后回答关键问题:**家长真的会用自然语言输入吗?还是更想点几下就出题?** 用真实数据决定是否继续往「对话式」投入,而非凭感觉。
---
## 五、待办 / 下一步
- [ ] 盘点现有「绘制方法」清单及调用方式,判断 ②③ 缠绕程度。
- [ ] 确定 LLM 供应商(合规 + 支持 function calling)。
- [ ] 抽离一个出题逻辑为纯数据函数(以「数感题目」为试点)。
- [ ] 搭建 `ai-orchestrator` 云函数骨架 + 技能注册表。
- [ ] 接入内容安全 `msgSecCheck`
- [ ] MVP 灰度,观察家长真实输入行为与转化数据。
@@ -0,0 +1,183 @@
# 认识钟表 — 预览、出题与绘制设计
> 配套文档:[产品设计文档](../产品设计文档.md) | [技术架构设计文档](../技术架构设计文档.md)
> 版本:v1.0
> 最后更新:2026-06-25
---
## 一、目标与范围
为幼儿园中班至小学一年级(约 4–7 岁)提供 **「看模拟钟表,填写数字时间」** 的可打印练习纸。
- 每页 **4 行 × 3 列**,共 12 题
- 每组:上方模拟钟表 + 下方带冒号的数字填写框
- 小程序内预览、换题、下载打印
- 排版归类为技术架构文档 **模式 H:时钟/特殊图形型**
**实现路径**:独立绘制页 `mathPages/clockReading/`,不并入 `mathDraw` 聚合页(时钟绘制逻辑特殊,独立维护更清晰)。
---
## 二、页面结构
```
mathPages/clockReading/
├── clockReading.ts
├── clockReading.wxml
├── clockReading.less
├── clockReading.json
├── clockReading.config.ts
├── generators/
│ └── clock-generator.ts # 纯数据出题
└── draw/
├── clockReadingDraw.ts # 整页编排
└── drawAnalogClock.ts # 模拟钟表(可复用)
```
**交互区(预览卡片下方)**
| 区域 | 说明 |
|------|------|
| 预览卡片 | Canvas 实时预览 A4 效果,支持换一换 |
| 时刻类型 chips | 随机(默认)/ 整点 / 半点 / 刻钟 |
| 底部操作 | 分享、下载打印(复用 `pageMixin` |
---
## 三、练习纸版面
### 3.1 网格布局
```
┌──────────────────────────────────────────┐
│ [统一页眉] 认识钟表 │
│ 姓名:___ 日期:___ 得分:___ │
├──────────────────────────────────────────┤
│ [钟1] [钟2] [钟3] │
│ [__:__] [__:__] [__:__] │
│ [钟4] [钟5] [钟6] │
│ ... │
│ (4 行 × 3 列 = 12 题) │
└──────────────────────────────────────────┘
```
### 3.2 单题单元
1. **模拟钟表**:绿色外圈、1–12 数字、红色时针、黑色分针
2. **填写框**:绿色圆角描边,中间固定冒号 `:`,左右留白供手写
### 3.3 A4 尺寸参数(逻辑像素 595×842)
| 参数 | 值 |
|------|-----|
| 内容区起始 Y | 页眉后 ~110px |
| 左右边距 | 28px |
| 行数 / 列数 | 4 / 3 |
| 钟表半径 | ~58px |
| 钟表与填写框间距 | 10px |
| 填写框 | 宽 88px,高 30px,圆角 6px |
| 行高 | ~168px |
---
## 四、钟表视觉规范
| 元素 | 颜色 | 色值 |
|------|------|------|
| 外圈 | 绿色 | `#3D9E47` |
| 刻度 / 数字 | 黑色 | `#333333` |
| 时针 | 红色 | `#E53935` |
| 分针 / 中心点 | 黑色 | `#333333` |
| 填写框描边 | 绿色 | `#3D9E47` |
| 冒号 | 黑色 | `#333333` |
**指针角度**12 点方向为 0°,顺时针):
```ts
const minuteAngle = minute * 6;
const hourAngle = (hour % 12) * 30 + minute * 0.5;
// Canvas 绘制时减去 90° 偏移(0° 在 3 点方向)
```
---
## 五、时刻类型
| 模式 ID | 显示名称 | 分针位置 | 示例 | 难度 |
|---------|----------|----------|------|------|
| `random` | 随机 | 059 分均可 | 如 4:07、11:23 | ★★ |
| `whole-hour` | 整点 | 12 点(0 分) | 3:00, 9:00 | ★ |
| `half-hour` | 半点 | 6 点(30 分) | 6:30, 10:30 | ★★ |
| `quarter-hour` | 刻钟 | 3 或 9 点(15/45 分) | 2:15, 7:45 | ★★★ |
> **命名说明**:「刻钟」对应传统「一刻」「三刻」,比「一刻时」更符合小学钟表教学用语,且与「整点」「半点」形成清晰三元组。
**随机模式**:分钟在 0–59 之间均匀随机,不限制为整点/半点/刻钟。
---
## 六、出题规则(`clock-generator.ts`
```ts
interface ClockProblem {
hour: number; // 112
minute: number; // 随机模式 0–59;其他模式见时刻类型表
}
interface ClockReadingData {
problems: ClockProblem[]; // 固定 12 个
timeMode: ClockReadingTimeMode;
}
```
1. 每页固定 12 题
2. 同一页内 `(hour, minute)` 不重复
3. 小时 112 均匀随机
4. 按所选时刻类型约束 `minute`
5. 纯函数输出 JSON,不依赖 Canvas
---
## 七、绘制服务拆分
```
clockReadingDraw.draw(data)
├── prepareDraw() / drawHeaderAndDivider({ title: '认识钟表' })
└── drawGrid(problems)
└── for each:
├── drawAnalogClock(cx, cy, radius, hour, minute)
└── drawTimeInputBox(x, y, w, h)
```
`drawAnalogClock.ts` 独立封装,便于后续「画指针」题型复用。
---
## 八、配置与注册
- Worksheet ID`clock-reading`
- 分类:`math` / `clock-reading`
- 路径:`/mathPages/clockReading/clockReading?id=clock-reading`
- 注册:`app.json` 分包、`config/worksheets/clockReading.ts``category.data.ts`
---
## 九、验收标准
- [ ] 预览区 4×3 共 12 组「钟表 + 填写框」
- [ ] 绿圈、红时针、黑分针、1–12 数字
- [ ] 填写框中间有冒号
- [ ] 默认随机;切换整点/半点/刻钟后分针位置正确
- [ ] 换一换生成新题且不重复
- [ ] A4 下载打印清晰
- [ ] 分享、收藏、打印解锁链路正常
---
## 十、后续演进(非 MVP
1. 定制页眉(日期/姓名/用时 + 装饰边框,更贴近教辅纸)
2. 对答案页 / 画指针题型
3. 5 分钟间隔、逐分钟进阶模式
4. AI 出题 tool`generate_clock_reading({ timeMode, count: 12 })`