第 16 章 辅助编程实战:从零构建学习助手

🤖 本章特色:面向编程初学者,全程使用 OpenClaw 作为 AI 协作平台,配合大模型(LLM)从零生成代码。你会看到 OpenClaw 如何帮你完成需求分析、代码生成、文件读写、代码执行、测试部署等全流程操作,你负责提需求,OpenClaw 负责跑腿干活。

本章目录:

节 内容 你将学到
1 背景知识 项目需求、技术栈、数据结构与 API 规范
2 index.html 实现 三步递进式提示词策略、OpenClaw 自动生成并保存代码
3 upload.html 实现 接口规范前置、状态机、OpenClaw 一次生成完整页面
4 后端 Python 实现 架构约束前置、分层设计、OpenClaw 生成后端代码并运行
5 测试部署运行 curl 测试、systemd 服务、Nginx 代理
6 总结 核心心法、提示词模板复用、课后练习

1. 背景知识:需求、技术与规范设计

1.1 背景故事:从家里的小白板,到一个班级的共享工具

痛点:家里的那块小白板

很多家长都有这样的经历:

孩子放学回家,家长问:"今天留了什么作业?"
孩子拿起粉笔,在家里的小白板上一条条写。写完发现漏了两科。
第二天,擦掉重写。第三天,又漏了。
家长想跟踪孩子哪些作业已完成、哪些还没做,只能靠眼睛看、脑子记。

小白板的局限:

解决方案:用大模型构建"学习助手"Web 应用

我们要做的,是一个替代家里小白板的数字化工具:

  1. 跟踪老师布置的任务:拍张作业通知的照片,大模型自动识别并结构化
  2. 标注完成状态:每项作业可以打勾标记"已完成"或"未完成"
  3. 班级共享使用:一人录入,全班家长都能看到(部署后)
  4. 历史记录可追溯:每天作业按天归档,随时翻看之前的作业
  5. 个人进度独立跟踪:每个同学家长都能看到自己孩子的完成进度

1.2 OpenClaw 在开发中的角色

在整个开发过程中,OpenClaw 是你的全能协作伙伴:

开发阶段 OpenClaw 的能力 具体操作
需求分析 对话式梳理需求 直接在聊天中描述想法,OpenClaw 帮你整理
代码生成 自动调用大模型生成代码 接收你的提示词,生成代码并自动写入本地文件
文件读写 创建、编辑项目文件 用 write/edit 工具自动写入 app.py、index.html 等
运行测试 执行 shell 命令 用 exec 工具启动 Flask 服务器、运行 curl 测试
部署运维 生成配置、重启服务 帮你写 systemd 配置、Nginx 配置并部署上线
记忆管理 记住你的项目上下文 下次打开会话,OpenClaw 还记得项目状态

🔑 关键理解:OpenClaw 不只是聊天工具

传统的大模型对话(如 ChatGPT 网页版)只能生成代码并显示在聊天窗口中,你需要手动复制、粘贴到编辑器、保存文件。而 OpenClaw 能直接操作你的本地文件系统:

简单说:你在对话框里说需求,OpenClaw 自动完成"生成代码 → 保存文件 → 运行程序 → 检查结果"的完整闭环。

1.3 大模型编程基础知识

大模型(LLM)是什么?它能做什么?

大模型(Large Language Model)是经过海量文本训练的 AI 系统。在编程领域,它可以:

🔑 初学者必读:大模型不是"自动编程机",而是你的高级结对编程伙伴。它拥有海量的代码知识库,但缺乏对你具体业务的理解。所以你负责架构设计和逻辑把控,它负责代码生成和细节补全。

提示词工程(Prompt Engineering)黄金公式

向大模型提问时,遵循以下结构可获得最佳结果:
角色设定 + 背景上下文 + 具体任务 + 约束条件 + 输出格式

❌ 糟糕的提示词:
"帮我写一个上传文件的网页。"
(结果:大模型会给你一个极其简陋、没有样式、不符合你业务逻辑的通用代码。)

✅ 优秀的提示词:

[角色] 你是一个资深前端工程师。
[背景] 我正在开发一个小学生作业录入系统。
[任务] 请帮我写一个 upload.html 页面,包含图片拖拽上传和 JSON 预览编辑功能。
[约束] 使用原生 HTML/CSS/JS,不使用任何框架;UI 风格要现代、简洁,主色调为蓝色。
[输出] 请输出完整的单文件 HTML 代码。

大模型的三种角色模式

模式 适用场景 提示词关键词 示例
生成模式 从零创建代码 "请生成/编写" 新建一个 Flask 路由
修改模式 已有代码上迭代 "在现有基础上/请修订" 给 upload.html 加每日限传功能
Debug 模式 解决运行报错 "报错信息如下/请排查" 路由冲突导致 500

1.4 系统架构全景图

┌─────────────────────────────────────────────┐
│  班级家长(手机/电脑浏览器)                    │
│  ┌──────────┐  ┌──────────┐                  │
│  │ index    │  │ upload   │   ← 前端页面      │
│  │ .html    │  │ .html    │                  │
│  └────┬─────┘  └────┬─────┘                  │
└───────┼─────────────┼────────────────────────┘
        │             │
        ▼             ▼
┌─────────────────────────────────────────────┐
│  Flask 后端(app.py + homework_ocr.py)        │
│  ┌──────────────────────────────────────┐    │
│  │ /api/homework/status   检查是否已上传 │    │
│  │ /api/homework/ocr      图片 → 大模型  │    │
│  │ /api/homework/<date>   保存/更新数据  │    │
│  │ homework/<date>.json  读取 JSON 文件 │    │
│  └──────────────────────────────────────┘    │
└───────────────────┬─────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────┐
│  文件系统(服务器本地)                         │
│  homework/2026-09-14.json  ← 全班共享数据      │
│  homework/2026-09-15.json                     │
│  homework/2026-09-16.json                     │
│  logs/homework_app.log     ← 运行日志          │
└─────────────────────────────────────────────┘

🔑 关键理解:

1.5 技术栈选择依据

技术选择 替代方案 为什么选它
Flask Django/FastAPI 轻量、路由直观、适合教学和初学者
纯原生 JS Vue/React 零构建步骤、零 CDN 依赖、内网可用
JSON 文件存储 SQLite/MySQL 按天归档天然隔离、无需连接池、零运维
视觉大模型(Qwen-VL) Tesseract OCR Tesseract 对中文识别率低;多模态大模型理解语义结构
localStorage 后端用户系统 无需注册登录,开箱即用,每人独立进度

1.6 项目目录结构

homewk/
├── app.py                   # Flask 入口:路由 + 配置
├── homework_ocr.py          # 纯业务逻辑:大模型调用 + JSON 保存
├── conf/
│   ├── homework.service     # systemd 服务配置(生产部署用)
│   └── nginx.conf           # Nginx 反向代理配置
├── homework/                # 数据存储:按天 JSON(全班共享)
│   └── 2026-09-14.json
├── logs/                    # 日志自动轮转
├── static/
│   ├── index.html           # ⭐ 每日作业列表页
│   └── upload.html          # ⭐ 作业录入页
├── test/
    ├── test.jpg             # 测试用图片
    └── test.sh              # curl 自动化测试脚本

1.7 规范先行:数据结构与 API 定义

💡 (规范先行):在让大模型写任何前后端代码之前,必须先定义好"数据长什么样"。这是前后端不扯皮、大模型不幻觉的基石。先定义数据结构和 API,再写代码

JSON 数据结构

这是2026年9月14日的真实项目数据:

{
  "date": "2026-09-14",
  "homework": [
    { "content": ["诊P18-21"], "subject": "物理" },
    { "content": ["课上学案"], "subject": "数学" },
    { "content": ["背单词", "诊P4-A"], "subject": "英语" },
    { "content": ["诊P5-6"], "subject": "化学" },
    { "content": ["完成第一课学探诊(自订自判)"], "subject": "历史" }
  ]
}

🔑 关键知识点:
1. homework 是数组而非字典,有多个科目的家庭作业。
2. content 也是数组:一科可能有多个任务。
3. 字段名 subject 和 content 必须在前后端、大模型 Prompt、JSON 存储中完全一致。

API 规范表

方法 路径 参数 成功响应 失败响应 说明
GET /api/homework/status date (query) {"uploaded_today": bool} — 检查当日是否已存在
POST /api/homework/ocr image (file), date (form) 200 + 结构化 JSON 409 {"error": "..."} 图片识别
PUT /api/homework/<date> JSON body 200 {"success": true} 409/400/500 保存/更新
GET homework/<date>.json 无 200 + JSON 404 直接读取文件

2. index.html 实现:用 OpenClaw 三步生成作业进度跟踪页

本节的重点是理解 OpenClaw 如何通过精心设计的提示词,分三步生成了 static/index.html 的完整实现。这是整个项目中前端代码量最大、算法逻辑最复杂的文件,但有了好的提示词策略,大模型可以一步步帮你搞定。

📄 参考提示词文件:每日作业打卡提示词-前端.txt(index.html 部分)

该文件包含三个递进式提示词,分别对应:① 基础功能与数据结构 → ② 双列布局与视觉优化 → ③ 布局算法精准调优。

2.1 提示词策略:三步递进式生成

在让 OpenClaw 生成 index.html 之前,我们先设计了一套三步递进式提示词。为什么不一次性把所有需求都塞进一个提示词?

  1. 需求迭代:我们并没有一次完全想清楚所有的事情。
  2. 大模型的注意力有限:一次性提出 10 个需求,它可能只做好其中 5 个
  3. 每步可验证:第一步生成基础页面后,你可以立即在浏览器中查看效果
  4. 迭代更精准:第二步说"在上一版基础上修订",大模型就知道不要重写已有功能

三步提示词概览:

步骤 核心目标 提示词关键词
第一步 基础功能与数据结构 "数据源为 JSON"、"localStorage"、"进度条"
第二步 双列布局与视觉优化 "左右两栏"、"蓝色主色调"、"移动端单列"
第三步 布局算法精准调优 "贪心算法"、"降序排列"、"高度差最小化"

🔑 提示词设计心法:每一步都以前一步的产物为基础,用"在上一版基础上"作为开头。这样大模型不会推翻已有代码,而是增量改进。

2.2 第一步:基础功能与数据结构

提示词原文

📄 摘自 每日作业打卡提示词-前端.txt 第一步:

请编写一个"每日作业打卡"单页应用(HTML+CSS+JS),要求如下:
1. 数据源为 /homework/YYYY-MM-DD.json,格式为 { "date": "...", "homework": [{ "subject": "数学", "content": ["任务1", "任务2"] }] };
2. 顶部显示当前日期,支持"前一天/后一天"按钮切换;
3. 显示完成进度条和分数(如 5/7);
4. 点击任务可切换完成状态,状态按日期独立存储在 localStorage;
5. 若 JSON 文件返回 404,显示"当天未留作业"提示,并提供"录入作业"链接;
6. 作业卡片按任务数量从多到少排序显示。

提示词设计要点:数据结构前置(直接给出 JSON 格式)、行为描述而非实现描述、边界条件明确。

OpenClaw 执行过程

你将提示词复制到 OpenClaw 对话框后,OpenClaw 自动完成以下工作:

你(发送提示词)
    ↓
OpenClaw(调用大模型生成代码)
    ↓
OpenClaw(用 write 工具将代码直接保存为 static/index.html)
    ↓
OpenClaw(用 exec 工具启动 Flask 服务器)
    ↓
你(打开浏览器 http://localhost:5159 查看效果)

🔑 这就是 OpenClaw 的核心价值:大模型生成的代码不再需要你手动复制粘贴,OpenClaw 直接帮你保存到正确的位置,甚至可以帮你启动服务器让你立刻看到效果。

生成产物:ES6 Class 架构

大模型自动选择了 ES6 Class 模式组织代码:

class HomeworkApp {
    constructor() { ... }   // 初始化:日期、DOM 引用、事件绑定
    loadHomework() { ... }   // 加载数据:fetch JSON → 渲染
    render() { ... }         // 渲染引擎:贪心算法分配双列
    createCard() { ... }     // 卡片工厂:生成单个科目
    toggleTask() { ... }     // 交互处理:点击切换完成状态
    updateProgress() { ... } // 进度条更新
}

🔑 为什么用 Class 而不是散乱的函数?

生成产物:数据加载与 404 降级

async loadHomework() {
    const dateStr = this.formatDate(this.currentDate);

    try {
        const resp = await fetch(`homework/${dateStr}.json`);
        if (resp.status === 404) { this.renderEmpty(dateStr); return; }
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);

        const data = await resp.json();
        this.homeworkData = data.homework || [];

        // 按任务数量降序排列(贪心算法前置条件)
        this.homeworkData.sort((a, b) => {
            const lenA = Array.isArray(a.content) ? a.content.length : 1;
            const lenB = Array.isArray(b.content) ? b.content.length : 1;
            return lenB - lenA;
        });

        this.render();
    } catch (err) {
        // 错误状态渲染
    }
}

🔑 关键知识点:
1. 直接 fetch JSON 文件:不走 API 路由,直接从 homework/YYYY-MM-DD.json 读取
2. 404 优雅降级:文件不存在时显示"当天未留作业"并提供"录入作业"链接
3. 降序排序:任务多的科目排在前面,这是贪心分配算法的前置条件

生成产物:localStorage 状态管理

getStorageKey() { return `hw_status_${this.formatDate(this.currentDate)}`; }
getLocalStatus() {
    try { return JSON.parse(localStorage.getItem(this.getStorageKey())) || {}; }
    catch { return {}; }
}
saveLocalStatus(map) { localStorage.setItem(this.getStorageKey(), JSON.stringify(map)); }

🔑 为什么用 localStorage?

家长 A 家长 B
看到同样的作业内容 看到同样的作业内容
记录自己孩子的进度 记录自己孩子的进度
无需注册登录 无需注册登录
换个浏览器会丢失 换个浏览器会丢失

这种设计让系统开箱即用:不需要搭建用户系统、不需要数据库,但每个家庭都能独立跟踪进度。

存储格式:{"物理_0": true, "英语_1": true}(科目名_任务索引 → 是否完成)

生成产物:完成状态切换

toggleTask(el) {
    const key = el.dataset.key;
    const statusMap = this.getLocalStatus();
    statusMap[key] ? delete statusMap[key] : statusMap[key] = true;
    el.classList.toggle('done');
    this.saveLocalStatus(statusMap);

    const total = document.querySelectorAll('.task-item').length;
    const done = document.querySelectorAll('.task-item.done').length;
    this.updateProgress(done, total);
}

2.3 第二步:双列布局与视觉优化

提示词原文

📄 摘自 每日作业打卡提示词-前端.txt 第二步:

在上一版基础上修订布局:
1. 将作业列表改为左右两栏显示;
2. 内容必须保持原始排序顺序(不能打乱科目先后);
3. 两栏视觉高度需尽量均衡,避免一侧过长另一侧过短;
4. 移动端自动切换为单列布局;
5. 整体风格简洁清爽,使用蓝色主色调,完成项显示绿色对勾。

提示词设计要点:"在上一版基础上"告诉大模型不要重写已有功能;视觉约束具体化(不说"好看一点",而是说"蓝色主色调"、"绿色对勾")。

OpenClaw 执行过程

你(发送第二步提示词)
    ↓
OpenClaw(读取现有 static/index.html 的内容)
    ↓
OpenClaw(将现有代码 + 修改提示词一起发给大模型)
    ↓
OpenClaw(用 edit 工具精准替换需要修改的部分)

🔑 OpenClaw 的 edit 工具 vs write 工具:

生成产物:CSS 变量主题系统

:root {
    --primary-color: #4a90d9;    /* 主色调(蓝色) */
    --done-color: #52c41a;       /* 完成色(绿色) */
    --bg-color: #f5f7fa;         /* 背景色(浅灰) */
    --card-bg: #ffffff;          /* 卡片背景(白色) */
    --nav-height: 56px;          /* 导航栏高度 */
    --footer-height: 48px;       /* 页脚高度 */
}

生成产物:顶部导航栏

.top-nav {
    position: sticky; top: 0; z-index: 100;
    display: flex; align-items: center; justify-content: space-between;
}

生成产物:响应式设计

@media (max-width: 640px) {
    .homework-columns { flex-direction: column; }  /* 双列变单列 */
    .top-nav { padding: 0 16px; }
    .nav-links a { padding: 5px 10px; font-size: 0.8rem; }
}

2.4 第三步:布局算法精准调优

提示词原文

📄 摘自 每日作业打卡提示词-前端.txt 第三步:

当前双列布局在卡片任务量差异较大时仍不均衡,请修订分配逻辑:
- 采用贪心算法:先将科目按任务数降序排列,再依次将每个科目分配到当前已分配任务总数更少的一列;
- 确保两列高度差最小化,同时严格保持科目的原始相对顺序;
- 不要改变现有的样式结构和交互逻辑,仅修改 JS 中的渲染分配部分。

🔑 这是本项目中最精彩的提示词设计:先描述问题("不均衡"),再给出算法名称("贪心算法")和具体步骤("降序排列 → 分配到更短的列"),最后划定修改范围("仅修改 JS 渲染分配")。大模型对经典算法有精确理解,比模糊说"让两列看起来均衡"有效得多。

生成产物:贪心算法双列瀑布流

render() {
    let leftWeight = 0;
    let rightWeight = 0;

    this.homeworkData.forEach((subject) => {
        const taskCount = subject.content.length;

        // 贪心:哪列更短就放哪列
        if (leftWeight <= rightWeight) {
            leftCol.appendChild(card);
            leftWeight += taskCount;
        } else {
            rightCol.appendChild(card);
            rightWeight += taskCount;
        }
    });
}

算法原理:
1. 科目已按任务数降序排列(任务多的先分配)
2. 每次将科目放入当前总任务数更少的那一列
3. 用任务数近似列高度(假设每个任务卡片高度相近)

为什么不用简单的"交替分配"(左右轮流放一个)?

分配策略 场景举例 两列差
交替分配 物理(1项) 数学(1项) 英语(2项) 化学(1项) 历史(1项) 差2
贪心分配 任务多时差异显著缩小 最优

2.5 OpenClaw 三步生成的完整工作流

回顾整个过程,OpenClaw 在每一步中做了以下工作:

第1步:基础功能
    你 → 发送提示词 → OpenClaw → 大模型生成代码
    OpenClaw → write 工具保存 static/index.html
    OpenClaw → exec 工具启动服务器
    你 → 浏览器验证 ✅

第2步:双列布局
    你 → 发送修改提示词 → OpenClaw → 读取现有代码
    OpenClaw → 现有代码 + 修改提示词 → 大模型
    OpenClaw → edit 工具精准替换代码片段
    你 → 浏览器验证 ✅

第3步:贪心算法
    你 → "两列不均衡,请用贪心算法优化"
    OpenClaw → 修改提示词 + 现有代码 → 大模型
    OpenClaw → edit 工具替换渲染逻辑
    你 → 浏览器验证 ✅

💡 使用建议:


3. upload.html 实现:用 OpenClaw 一次生成作业录入页

upload.html 比 index.html 交互更复杂,涉及文件上传、异步请求、JSON 校验、状态机管理。本节聚焦于提示词如何驱动 OpenClaw 一次性生成这些复杂功能。

📄 参考提示词文件:每日作业打卡提示词-前端.txt(upload.html 部分)

该提示词采用了"角色与目标 + 核心接口定义 + 功能与交互要求 + 代码规范 + 输出要求"的五段式结构。

3.1 提示词设计解析:五段式结构

upload.html 的提示词与 index.html 不同,采用了一次性完整描述的方式(而非分步迭代),因为 upload.html 的核心复杂度在于状态管理和接口对接,这些功能紧密耦合,拆分反而增加联调成本。

📄 摘自 每日作业打卡提示词-前端.txt(upload.html 部分):

1. 角色与目标:你是一位资深前端工程师。请为 Flask 作业 OCR 系统编写一个单文件 upload.html 页面。该页面需与后端 POST /api/homework/ocr 接口无缝对接,提供流畅的图片上传、OCR 识别结果展示及错误反馈体验。不依赖任何前端框架,仅使用原生 HTML/CSS/JS。

2. 核心接口规范:
- 请求方式:POST /api/homework/ocr
- Content-Type:multipart/form-data
- 请求参数:字段名必须为 image(与后端 request.files['image'] 一致)
- 成功响应:HTTP 200,JSON 格式 {"text": "识别出的文字内容"}
- 失败响应:HTTP 4xx/5xx,JSON 格式 {"error": "具体错误描述"}

3. 功能与交互要求:支持点击上传和拖拽上传;使用 fetch API 异步提交;成功后渲染到可编辑文本框;失败时展示错误信息。

4. 代码规范:单文件结构(HTML/CSS/JS 内联);零外部依赖(不引入 CDN);防御性编程。

5. 输出要求:提供完整可直接运行的文件;附带本地测试说明。

🔑 提示词设计的核心思路,"接口规范前置":

在提示词的第二段就给出了完整的接口规范,包括请求方式、字段名、响应格式。为什么这样做?

3.2 OpenClaw 执行过程

你(发送 upload.html 提示词)
    ↓
OpenClaw(调用大模型生成代码)
    ↓
OpenClaw(用 write 工具直接保存为 static/upload.html)
    ↓
你(打开浏览器 http://localhost:5159/upload 查看效果)
    ↓
你(拖入一张作业照片测试)

🔑 与 index.html 的区别:index.html 用了三步迭代,upload.html 一步到位。选择哪种策略取决于功能复杂度的性质,UI 布局适合迭代(每步可独立验证),状态管理适合一步到位(功能间紧密耦合)。

3.3 生成产物:页面状态机

upload.html 本质上是一个状态机,有四种状态:

[初始状态] ──选择图片──▶ [上传中] ──成功──▶ [预览编辑]
    │                       │                  │
    │ 已上传                │ 失败              │ 保存成功
    ▼                       ▼                  ▼
[已锁定] ◀──────409─────────┘           [已保存]

🔑 初学者必读:什么是状态机?
状态机由状态集合、事件、转移规则组成:在某个状态下,特定事件会触发转移到另一个状态。简单来说,状态机就是在不同状态下,同样的操作会有不同结果。比如:

3.4 生成产物:当日上传限制逻辑

这是本项目最重要的业务规则:每天只能上传一次作业,防止重复录入。

let todayUploaded = false;

async function checkTodayStatus() {
    try {
        const res = await fetch(`/api/homework/status?date=${targetDate}`, { cache: 'no-store' });
        if (res.ok) {
            const data = await res.json();
            todayUploaded = !!data.uploaded_today;
            updateUploadUI();
        }
    } catch (e) {
        console.warn('上传状态预检失败,由后端兜底', e);
    }
}

🔑 关键知识点:
1. cache: 'no-store':强制不走浏览器缓存,每次获取最新状态
2. 预检失败静默降级:网络异常时不阻塞上传,由后端 409 兜底
3. !! 双重取反:将 undefined/null/false/true 统一转为布尔值

3.5 生成产物:UI 锁定与解锁

function updateUploadUI() {
    if (todayUploaded) {
        uploadArea.classList.add('disabled');
        fileInput.disabled = true;
        tip.style.display = 'block';  // 显示黄色警告条
    } else {
        uploadArea.classList.remove('disabled');
        fileInput.disabled = false;
        tip.style.display = 'none';
    }
}

3.6 生成产物:文件上传交互(拖拽 + 点击)

uploadArea.addEventListener('click', () => !todayUploaded && fileInput.click());
uploadArea.addEventListener('dragover', (e) => { 
    if (todayUploaded) return;
    e.preventDefault(); 
    uploadArea.classList.add('dragover'); 
});
uploadArea.addEventListener('drop', (e) => {
    e.preventDefault();
    if (todayUploaded) return;
    if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
});

🔑 关键知识点:
1. e.preventDefault():浏览器默认行为是打开文件,必须阻止
2. 每个事件都检查 todayUploaded:确保上传被禁用时所有入口都被拦截
3. dragover + dragleave:实现拖拽悬停高亮效果

3.7 生成产物:OCR 请求与响应处理

async function handleFile(file) {
    if (todayUploaded) {
        showStatus('今日已上传过作业,请勿重复操作', 'error');
        return;
    }
    if (!file.type.startsWith('image/')) return showStatus('请上传图片文件', 'error');
    if (file.size > 10 * 1024 * 1024) return showStatus('图片大小不能超过10MB', 'error');

    // 显示预览 + 加载状态
    previewImg.src = URL.createObjectURL(file);
    jsonEditor.value = '⏳ 正在识别中...';
    saveBtn.disabled = true;

    try {
        const formData = new FormData();
        formData.append('image', file);
        formData.append('date', targetDate);

        const res = await fetch('/api/homework/ocr', { method: 'POST', body: formData });

        // 409 冲突处理
        if (res.status === 409) {
            const errData = await res.json().catch(() => ({}));
            showStatus(errData.error || '今日已上传过作业', 'error');
            todayUploaded = true;
            updateUploadUI();
            return;
        }

        if (!res.ok) throw new Error(`识别失败: ${res.status}`);

        const data = await res.json();
        jsonEditor.value = JSON.stringify(data, null, 2);  // 格式化 JSON
        saveBtn.disabled = false;
        showStatus('✅ 识别完成,请核对内容后保存', 'success');
    } catch (err) {
        jsonEditor.value = '';
        showStatus(err.message, 'error');
    }
}

🔑 关键知识点:
1. FormData:浏览器自动构建 multipart/form-data 请求,字段名 image 必须与后端 request.files['image'] 一致
2. URL.createObjectURL(file):在浏览器本地生成图片预览 URL,无需先上传到服务器
3. JSON.stringify(data, null, 2):格式化 JSON(缩进 2 格),方便用户在 textarea 中阅读和编辑
4. 双重防御:前端先校验(文件类型、大小、今日限制),后端再校验(409 兜底)

3.8 生成产物:实时 JSON 校验

jsonEditor.addEventListener('input', () => {
    try {
        JSON.parse(jsonEditor.value);
        jsonEditor.classList.remove('invalid');
        saveBtn.disabled = false;
    } catch {
        jsonEditor.classList.add('invalid');
        saveBtn.disabled = true;
    }
});

3.9 生成产物:保存作业与自动跳转

saveBtn.addEventListener('click', async () => {
    const homeworkData = JSON.parse(jsonEditor.value);
    const res = await fetch(`/api/homework/${targetDate}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(homeworkData)
    });
    if (!res.ok) throw new Error(`保存失败: ${res.status}`);

    todayUploaded = true;
    updateUploadUI();
    setTimeout(() => window.location.href = `/homework?date=${targetDate}`, 1500);
});

4. 后端 Python 实现:用 OpenClaw 生成 Flask 服务

本节聚焦于OpenClaw 如何通过后端提示词生成完整的 Flask 后端代码,并自动安装依赖、配置环境、启动服务。与前端提示词不同,后端提示词更强调架构约束、安全规范和异常处理。

📄 参考提示词文件:每日作业提示词后端.md

该提示词采用了"角色与目标 + 核心技术约束 + 文件结构与职责划分 + 具体功能需求 + 输出要求"的五段式结构。

4.1 提示词设计解析:架构约束前置

后端提示词的核心挑战是架构约束,如何让大模型生成符合专业软件工程规范的代码?答案是:把架构规则写进提示词。

📄 摘自 每日作业提示词后端.md:

1. 角色与目标:你是一位资深 Python 后端工程师。请基于 Flask 框架开发一个"作业 OCR 识别与保存"模块。核心要求是:业务逻辑与 Web 框架解耦,且文件保存目录必须支持全局动态配置。

2. 核心技术约束:
- 配置优先级:保存目录需遵循 环境变量 > .env文件 > 代码默认值 三级策略。
- 依赖注入:homework_ocr.py 作为纯业务模块,禁止直接导入 app 实例或使用 current_app,所有外部配置必须通过函数参数显式传入。
- 安全性:文件名必须使用 werkzeug.utils.secure_filename 处理;启动时自动创建目录。
- 健壮性:OCR 接口需包含完整的异常捕获,区分内存错误、IO错误和业务错误,返回明确的 HTTP 状态码。

3. 文件结构与职责划分:
- app.py:配置定义与路由入口
- homework_ocr.py:纯业务逻辑层(不依赖 Flask)
- .env:本地环境配置

4. 具体功能需求:全局配置初始化、保存函数、API 接口、防御性编程。

5. 输出要求:完整可运行代码 + .env.example + Docker/生产环境配置说明。

🔑 提示词设计的核心思路,"架构约束前置":

在提示词的第二段就给出了四层技术约束(配置优先级、依赖注入、安全性、健壮性)。为什么这样做?

4.2 OpenClaw 执行后端开发的完整过程

这是 OpenClaw 最强大的能力展示,从提示词到运行中的服务,全程自动化:

你(发送后端提示词)
    ↓
OpenClaw(调用大模型生成代码)
    ↓ 返回 app.py + homework_ocr.py + .env.example
OpenClaw(用 write 工具分别创建三个文件)
    ↓ write("app.py", ...)
    ↓ write("homework_ocr.py", ...)
    ↓ write(".env.example", ...)
OpenClaw(用 exec 工具安装依赖)
    ↓ exec("pip install flask flask-cors openai python-dotenv")
你(告诉 OpenClaw "帮我把 API Key 配置到 .env 文件")
    ↓
OpenClaw(用 write 工具写入 .env)
    ↓
你(告诉 OpenClaw "启动服务器")
    ↓
OpenClaw(用 exec 工具启动 Flask)
    ↓ exec("python app.py") → 服务运行在 localhost:5159
你(打开浏览器测试,或告诉 OpenClaw "用 curl 测试")
    ↓
OpenClaw(用 exec 工具执行 curl 测试命令)
    ↓
你(告诉 OpenClaw "看看日志")
    ↓
OpenClaw(用 read 工具读取 logs/homework_app.log)

🔑 关键理解:

4.3 生成产物:分层架构设计

文件 职责 关键约束
app.py 路由定义 + Flask 配置 只处理 HTTP 请求/响应
homework_ocr.py 纯业务逻辑 不导入 Flask,配置通过参数传入

🔑 为什么要把业务逻辑从 Flask 路由中剥离?
- 可测试性:homework_ocr.py 可以在不启动 Flask 的情况下独立测试
- 可复用性:同样的逻辑可以迁移到 FastAPI、CLI 工具或其他框架
- 职责清晰:修改路由不影响业务逻辑,修改业务逻辑不影响路由

4.4 大模型客户端初始化

此处初始化的是兼容 OpenAI 协议的多模态大模型客户端。

from openai import OpenAI

client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY", "your-api-key-here"),
    base_url=os.getenv("LLM_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
)
MODEL_NAME = os.getenv("LLM_MODEL", "qwen3.8-flash")

🔑 配置三级优先级:

  1. 环境变量(最高):生产环境通过 systemd EnvironmentFile 注入
  2. .env 文件:本地开发时使用 python-dotenv 加载
  3. 代码默认值(最低):仅作为兜底,方便快速启动测试

本教程默认使用 qwen3.8-flash。qwen-vl-max 已于 2026 年 10 月 10 日下线,请勿使用。

qwen3.8-flash是一个可以识别图像,输出文本的大模型,并且费用比qwen3.8-max便宜很多,价格相差10倍左右。

4.5 生成产物:核心 Prompt 设计(大模型输出结构化 JSON)

prompt = f"""你是一个作业录入助手。请识别图片中的各科作业内容,并将其转换为严格的 JSON 格式。
要求:
1. 日期固定为:{target_date}
2. 提取所有科目(subject)和对应的具体任务列表(content)。
3. 如果某科没有具体任务,content 为空数组。
4. 仅输出合法的 JSON 字符串,不要包含任何 Markdown 标记(如 ```json)、解释性文字或多余字符。

期望的 JSON 结构如下:
{{
  "date": "{target_date}",
  "homework": [
    {{
      "subject": "数学",
      "content": ["完成练习册第10页", "订正试卷"]
    }}
  ]
}}
"""

🔑 视觉大模型 Prompt 设计技巧:

  1. 角色设定:"你是一个作业录入助手",限定模型的行为边界
  2. 强制 JSON 输出:"仅输出合法的 JSON 字符串",大模型默认喜欢加 Markdown 代码块,必须显式禁止
  3. 给出完整示例:用真实的 JSON 结构作为"期望输出",比文字描述精确 100 倍
  4. 边界条件说明:"如果某科没有任务,content 为空数组",防止模型输出 content: null 或省略字段
  5. 日期注入:用 f-string 将 target_date 直接写入 Prompt,确保模型不会编造日期
  6. temperature=0.1:降低随机性,保证输出格式稳定(结构化输出需要低温度)

4.6 生成产物:Base64 图片编码

def image_to_base64(image_path: str) -> str:
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")

为什么用 Base64? 用户上传的是临时文件,没有公网 URL。OpenAI 兼容协议支持 data:image/jpeg;base64,xxx 格式。对于作业照片(通常 < 5MB),Base64 增加约 33% 体积完全可接受。

4.7 生成产物:JSON 清理与异常分层

raw_text = response.choices[0].message.content.strip()

# 清理 Markdown 代码块标记
clean_text = re.sub(r'^```json\s*', '', raw_text, flags=re.IGNORECASE)
clean_text = re.sub(r'\s*```$', '', clean_text)

return json.loads(clean_text)

🔑 异常分层:

4.8 生成产物:Flask 路由实现

OCR 上传路由(含临时文件处理)

@app.route('/api/homework/ocr', methods=['POST'])
def ocr_upload():
    # 保存为临时文件
    with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as temp_file:
        file.save(temp_file.name)
        temp_path = temp_file.name

    if homework_json_exist(target_date):
        return jsonify({"error": "文件已存在"}), 409

    try:
        result_data = recognize_homework(temp_path, target_date)
        return jsonify(result_data), 200
    except Exception as e:
        logger.error(f"OCR 处理失败 | 日期: {target_date} | 错误: {e}")
        return jsonify({"error": str(e)}), 500
    finally:
        if os.path.exists(temp_path):
            os.remove(temp_path)

🔑 finally 块的重要性:无论成功还是异常都清理临时文件,防止磁盘被临时图片撑满。

保存作业路由(纵深防御)

@app.route('/api/homework/<date>', methods=['PUT'])
def save_homework(date):
    if not request.is_json:
        return jsonify({"error": "请求体必须是 JSON 格式"}), 400

    data = request.get_json()
    if "homework" not in data or not isinstance(data["homework"], list):
        return jsonify({"error": "数据缺少 homework 列表字段"}), 400

4.9 生成产物:数据一致性保障

def save_homework_json(data: dict, target_date: str) -> str:
    # 强制覆写 date 字段,防止前端传入的 date 与 URL 中的 date 不一致
    data["date"] = target_date 

    file_path = os.path.join(SAVE_DIR, f"{target_date}.json")

    with open(file_path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

🔑 关键知识点:

4.10 生成产物:生产级日志配置

from logging.handlers import RotatingFileHandler

file_handler = RotatingFileHandler(
    "logs/homework_app.log", maxBytes=5*1024*1024, backupCount=3, encoding="utf-8"
)

4.11 生成产物:路径遍历安全防护

if not all(c.isalnum() or c == '-' for c in date):
    abort(400, description="非法的日期格式")

恶意用户可能传入 ../../../etc/passwd,通过白名单校验(只允许字母、数字和连字符)完全阻止此类攻击。这也是提示词中必须要求大模型实现的安全约束。


5. 测试、部署与运行

代码生成完毕后,OpenClaw 同样能帮你完成测试和生产部署。

5.1 用 OpenClaw 执行接口测试

你可以直接在 OpenClaw 对话框中说"帮我测试一下 OCR 接口",OpenClaw 会用 exec 工具执行以下命令:

# 测试 OCR 上传
curl -X POST -F "image=@test.jpg" http://127.0.0.1:5159/api/homework/ocr

# 测试 409 冲突(重复上传)
curl -s -w "\nHTTP Status: %{http_code}\n" \
    -X POST -F "image=@test.jpg" -F "date=2026-09-16" \
    http://127.0.0.1:5159/api/homework/ocr

# 测试保存作业
curl -X PUT -H "Content-Type: application/json" \
    -d '{"date":"2026-09-16","homework":[{"subject":"测试","content":["test"]}]}' \
    http://127.0.0.1:5159/api/homework/2026-09-16

🔑 OpenClaw 的测试能力:

5.2 人工验证清单

5.3 为什么不能只用 flask run?

维度 flask run Gunicorn + Nginx
并发 单线程,同时只能处理 1 个请求 多 worker,可处理数十并发
稳定性 崩溃后不自动重启 systemd 自动重启(Restart=always)
性能 纯 Python HTTP 服务器 Gunicorn 用 C 优化,Nginx 处理静态文件
安全 直接暴露 Flask Nginx 反向代理,隐藏内部端口

5.4 用 OpenClaw 生成部署配置

你可以直接告诉 OpenClaw"帮我生成 systemd 和 Nginx 配置",它会自动生成以下文件并用 write 工具保存:

systemd 服务配置

[Unit]
Description=homework Analysis API Service
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/srv/app/homework/
EnvironmentFile=/etc/homework/env

ExecStart=/usr/bin/gunicorn -w 2 -b 127.0.0.1:5159 app:app
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

🔑 关键配置:
- -w 2:2 个 worker,一个处理 OCR(耗时较长),另一个响应 API
- -b 127.0.0.1:5159:只监听回环地址,不暴露公网(由 Nginx 代理)
- Restart=always:崩溃后 5 秒自动重启
- EnvironmentFile:API Key 等敏感信息从独立文件注入

Nginx 反向代理

location ^~ /api/homework/ {
   proxy_pass http://127.0.0.1:5159;
   proxy_set_header Host $host;
   proxy_read_timeout 90s;     # LLM 识别耗时较长
   proxy_connect_timeout 10s;  # 后端挂了快速失败
}

5.5 班级共享部署方案

部署完成后,全班家长通过浏览器访问同一个 URL:

http://your-server-ip:5159        # 内网直接访问
http://edu.bjbook.net/xxl/homework/ #部署在公网服务器上,Nginx代理

6. 总结

6.1 大模型编程的四个阶段

阶段 1:生成 —— "请帮我写一个 XXX"
         ↓
阶段 2:迭代 —— "在现有基础上,增加 XXX"
         ↓
阶段 3:调试 —— "运行报错了,错误信息如下..."
         ↓
阶段 4:重构 —— "请优化这段代码的结构"

一个完整项目通常会经历这四个阶段的多次循环。

6.2 提示词设计的核心心法

心法 一句话解释 本项目中的应用
接口规范优先 先定义数据结构,再写代码 先设计 JSON + API 规范表,再生成前后端
上下文为王 给大模型足够的背景 每次 Prompt 包含数据结构 + API 规范
循序渐进 拆分需求,逐步验证 index.html 分 3 步迭代,每步可独立验证
架构约束前置 先说规则,再说功能 后端提示词第二段就给出四层技术约束
安全规范显式化 不依赖大模型的"常识" 明确要求 secure_filename、白名单校验
纵深防御 多层校验 前端校验 + 路由校验 + 业务层校验
回传堆栈 完整堆栈给大模型 报错信息直接复制给 OpenClaw 让它 Debug

6.3 提示词模板的复用与改编

模板文件 用途 改编方法
每日作业打卡提示词-前端.txt 分三步生成 index.html + 一次生成 upload.html 改数据结构、导航项
完整提示词.md 一次性生成全项目 改业务规则、API 表
每日作业提示词后端.md 生成 app.py + homework_ocr.py 改存储方式、框架

改编原则:修改数据结构时前后端同步更新;修改业务规则时先改完整提示词再拆分。

6.4 OpenClaw 能力速查

你想做 对 OpenClaw 说什么 OpenClaw 怎么做
生成代码 发送提示词 调用大模型生成,write 保存到本地文件
修改代码 "把 XX 改成 YY" edit 工具精准替换指定代码片段
安装依赖 "安装 flask 和 openai" exec 执行 pip install
启动服务 "启动服务器" exec 执行 python app.py
测试接口 "用 curl 测试 OCR" exec 执行 curl 命令并分析结果
查看日志 "看看日志文件" read 工具读取日志内容
Debug 复制报错信息 发给大模型分析并自动修复代码
部署上线 "生成 systemd 配置" 生成配置并保存,执行部署命令
记住上下文 自动 记忆系统保存项目状态,下次继续

6.5 课后练习

基础练习

  1. 使用本教程中的提示词,在 OpenClaw 中从零生成"学习助手"项目
  2. 替换 API Key 和 base_url,用真实的视觉大模型识别一张作业照片
  3. 修改 index.html,增加"按科目筛选作业"的筛选功能

进阶挑战

  1. 课程表功能:参照本项目流程,用 OpenClaw 从零实现课程表模块
  2. 部署实战:让 OpenClaw 帮你部署到 Linux 服务器,配置 systemd + Nginx
  3. 作业录入支持密码验证: 可在 upload.html 增加密码输入框,前端将密码随 FormData 发送,后端在 /api/homework/ocr 中校验 request.form.get('password')。
  4. 周末作业在周六周日均可以完整显示

附录 A:环境依赖与安装

pip install flask flask-cors openai python-dotenv gunicorn

# 启动开发服务器
python app.py   # 访问 http://localhost:5159

# 生产部署
sudo cp conf/homework.service /etc/systemd/system/
sudo systemctl enable homework && sudo systemctl start homework

附录 B:常见错误速查表

错误现象 可能原因 解决方案
ModuleNotFoundError: No module named 'flask_cors' 未安装依赖 pip install flask-cors
AssertionError: View function mapping... 路由 endpoint 冲突 检查重复函数名
OCR 返回 JSONDecodeError 大模型返回了非 JSON 检查 Prompt + 加正则清理
前端报 CORS 错误 前后端跨域 确认 CORS(app) 已调用
Nginx 返回 504 LLM 识别超时 增加 proxy_read_timeout 到 90s
409 Conflict 但实际未上传 SAVE_DIR 路径错误 检查环境变量配置

💡 本章小结:你不需要成为编程专家才能构建一个实用的 Web 应用。有了大模型生成代码的能力,再加上 OpenClaw 的全流程协作,自动生成代码并保存到本地、自动安装依赖、自动启动服务、自动执行测试,任何人都可以从零做出一个全班共享的实用工具。关键是要学会"正确提问":这就是本章反复强调的 Prompt 工程。

本文为《OpenClaw 实战指南》系列第 16 篇。