微信小程序+Flask+MNIST:AI模型服务化与接口联调实战
发布时间:2026/9/26 11:41:00来源:尧图网络
简介这是一份面向微信小程序开发者与人工智能初学者的实战示例代码包定位为可直接运行和二次改写的演示工程。项目用微信小程序承载人工智能应用场景包含关于、首页、我的、待办、消息等页面模块覆盖页面逻辑、样式、配置和组件封装适合希望把人工智能能力落地到小程序端、学习项目结构拆分的读者参考。资源包共 59 个文件以图片素材、JS 脚本、WXML/WXSS 页面代码、JSON 配置和说明文档为主整体仅 1.36MB轻量易下载便于快速查看目录并动手调试。目前已有 242 人浏览学习。通过这份实践包可以获得一套完整的小程序前端工程骨架与交互演示包括录音、播放、搜索等场景的视觉素材和工具函数还能借助说明文档快速理解各目录用途减少从零搭建的重复工作。1. 人工智能实战微信小程序这份 demo 到底能跑通什么事下载这份《人工智能实战微信小程序demo.zip》之前我先确认了一件事它到底是能直接跑的完整项目还是只有前端页面的空壳。解压之后可以明确这是一个典型的前后端分离小项目——微信小程序负责拍照和上传Python 后端负责跑 AI 模型推理再把结果返回页面。换句话说它补齐了人工智能大作业最常见的缺口模型训练好了却不知道怎么接进小程序。处理手写数字识别这类任务时常见做法是让 MNIST 模型跑在 Flask 服务里小程序端只做三件事取图、压缩、展示结果。这份 demo 的价值不在算法有多新而在于把「模型服务化」和「小程序对接」这两段最容易翻车的环节完整串了起来。如果你正在做课程设计或者想把一个训练好的模型接到微信小程序上又不想从零踩一遍接口联调的坑这份资源能帮你省下不少联调时间。后面几章我会按「架构 → 后端接口 → 小程序端 → 踩坑记录 → 改造方向」的顺序把这份 demo 完整拆开。代码里有注释的地方我会说明为什么这样写参数我会指出哪些能改、怎么改。适合人群有 Python 基础、正在写人工智能大作业或微信小程序项目实例的学生以及想快速把模型做成演示产品的开发者。2. 拆开 demo 看数据流小程序、Flask 与模型三层的调用链拿到一个 demo 压缩包第一件事不是看代码而是先搞清楚数据是怎么在页面、后端、模型之间流动的。这一层想清楚了后面改代码才不会像盲人摸象。2.1 一次识别请求的完整路径用户在页面点击「拍照识别」后完整路径是这样的小程序端调用wx.chooseImage拿到临时文件路径再用 canvas 把图片压缩成固定尺寸转成 base64 字符串接着wx.request发起 POST 请求到后端/predict接口Flask 收到请求后做 base64 解码用 PIL 打开成灰度图经过 resize、归一化等预处理变成张量模型执行一次前向推理得到 10 个类别的概率分布后端取出概率最高的类别和置信度用 JSON 返回小程序端拿到数据后setData渲染到页面上。这条链路里最容易被忽略的是「接口协议」——前后端对字段名、数据类型、错误码的约定。这份 demo 里前端传给后端的字段是imagebase64 字符串不带 dataURL 前缀后端返回的字段是class_id、label、confidence、cost_ms。如果换一个项目字段名可能是img或者result不先读协议直接套用后面必挂。另外注意一个细节小程序端在压缩图片时会先取图片的宽高按等比缩放后再drawImage而不是直接把原图塞进张量。因为原图可能是 3000×4000直接 resize 成 28×28 会丢失大量信息而且 base64 编码会让请求体膨胀约 33%传输和解析都慢。这个点后面第 4 章会展开讲。2.2 demo 目录结构哪些文件可以删哪些不能动解压后建议先按目录结构过一遍分清核心文件和可替换文件。下面这张表是我拆这份 demo 时整理出来的列了每个路径的职责以及改的时候能不能动。路径职责核心程度app.js/app.json小程序全局配置注册页面、设置窗口样式核心别删pages/index/index.wxml拍照按钮、结果展示区域核心pages/index/index.js页面逻辑取图、压缩、请求、渲染核心utils/request.jswx.request的 Promise 封装核心server/app.pyFlask 入口加载模型并处理推理核心server/model/预训练权重文件通常是一个.pt或.pth核心server/requirements.txt后端 Python 依赖清单建议保留项目根目录下还会有一个project.config.json这是微信开发者工具的项目配置文件里面记录了 appid、编译设置等信息。拿到压缩包后不要直接用文本编辑器改它正确做法是用微信开发者工具的「导入项目」功能选中解压后的目录工具会自动读取这个文件。如果导入后提示 appid 无效可以在详情里把 appid 改成测试号不影响本地开发调试。server/model/目录下通常只有一个权重文件几十到几百 KB。MNIST 这种任务模型很小整个 demo 解压后体积控制得很好不需要像目标检测项目那样准备几十 MB 的权重。这份资源的使用门槛低也体现在这里——一台普通笔记本的 CPU 就能把推理跑进几十毫秒。2.3 模型选型为什么手写数字识别更适合做课程 demo有人可能会问同样是「人工智能实战」为什么不用 YOLO 做目标检测或者用 BERT 做文本分类我的看法是demo 的核心目标不是证明模型多强而是把「AI 接口能跑通」这件事闭环。MNIST 在这条链路里有三个天然优势。第一数据公开且规模小。60000 张训练图片、10 个类别随便一台电脑几分钟就能训完不用为数据集发愁。第二类别少、任务直观。0 到 9 共 10 类识别结果看一眼就知道对不对演示效果好老师或用户不需要理解「这个框框代表什么」这种抽象概念。第三推理速度快。一个简单的 CNN 在 CPU 上推理一次只需要几十毫秒不会出现用户按了按钮要等半分钟才出结果的尴尬场面。对比一下如果换成 YOLOv8 做目标检测权重文件动辄几十 MB后端第一次加载要好几秒小程序端上传的图片还得先等比缩放到模型输入尺寸整个体验会差很多。BERT 就更不用说了模型体积按 GB 算普通课程设计跑不起来。所以这份 demo 选 MNIST 不是偷懒是成本、效果和可解释性权衡之后最稳的答案。3. 把模型变成接口Flask 推理服务的封装与自测后端是整条链路里最接近「黑匣子」的部分。模型文件放在那里不会自己工作必须有一个服务把它包装成 HTTP 接口小程序才能调用。这一章讲清楚这份 demo 的 Flask 服务是怎么写的以及为什么这样封装。3.1 为什么选 Flask依赖少、好解释、能落地这份 demo 的后端用的是 Flask不是 FastAPI也不是 Django。选择理由很实际课程设计和中小型 demo 的典型场景根本用不上 FastAPI 的异步特性和自动生成 OpenAPI 文档反而多引入一层学习成本。Flask 的优势在于足够简单——一个app.py文件就能把路由、请求解析、JSON 返回全部搞定依赖也就flask、torch、pillow、torchvision这几个。调试的时候直接python app.py起服务浏览器访问http://127.0.0.1:5000/predict就能看到响应心智负担很小。这里有一个实操建议给requirements.txt里的 Python 包标注版本比如torch2.1.0、flask3.0.0。我拆过不少 demo最常见的问题不是代码写错而是环境版本不匹配——模型用旧版本torch.load存出来的权重在新版本里加载报错。锁版本号能省掉很多这种莫名其妙的报错。3.2 推理接口代码加载、预处理、预测、返回下面是这份 demo 后端核心代码的简化版本去掉了日志和异常处理的装饰性代码保留主干逻辑。# server/app.py import base64 import io import time import torch import torchvision.transforms as transforms from flask import Flask, request, jsonify from PIL import Image from model import MnistCNN # MNIST 的 CNN 结构定义在 model.py app Flask(__name__) # 设备选择有 GPU 用 GPU没有就用 CPU device torch.device(cuda if torch.cuda.is_available() else cpu) # 模型只加载一次放在全局变量里避免每次请求都重新读取权重 model MnistCNN() model.load_state_dict(torch.load(model/mnist_cnn.pt, map_locationdevice)) model.to(device) model.eval() # 预处理必须和训练时保持一致灰度图、28x28、归一化 transform transforms.Compose([ transforms.Resize((28, 28)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) LABELS [str(i) for i in range(10)] # MNIST 类别就是 0-9 app.route(/predict, methods[POST]) def predict(): t0 time.time() data request.get_json(forceTrue) # 前端传来的是纯 base64不带 data:image/jpeg;base64, 前缀 img_bytes base64.b64decode(data[image]) img Image.open(io.BytesIO(img_bytes)).convert(L) # 转灰度 tensor transform(img).unsqueeze(0).to(device) # 增加 batch 维 with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1)[0] # 转成概率 cls_id int(torch.argmax(prob)) conf float(prob[cls_id]) return jsonify({ class_id: cls_id, label: LABELS[cls_id], confidence: round(conf, 4), cost_ms: int((time.time() - t0) * 1000) }) if __name__ __main__: # 0.0.0.0 让局域网内的手机也能访问方便真机调试 app.run(host0.0.0.0, port5000)代码逻辑分四段。第一段是模型初始化放在模块加载时执行这样整个服务启动后只读取一次权重后续请求直接复用内存里的模型。第二段是预处理Resize((28, 28))把任意尺寸的图片统一成 28×28ToTensor()把像素值从 0~255 归一化到 0~1Normalize用的是 MNIST 数据集的均值和标准差这三个参数必须和训练时的配置完全一致差一个数值识别率都会明显下降。第三段是推理torch.no_grad()关闭梯度计算减少内存占用softmax把原始 logits 转成概率分布。第四段是返回结构cost_ms这个字段对排查性能问题很有用前端可以把它打印到控制台。3.3 用 curl 先验证后端再谈联调前端联调前我一定会先用 curl 把后端接口打一遍。这一步能快速确认服务是否正常、返回格式是否符合预期避免前后端同时出问题时不知道先查哪边。curl -X POST http://127.0.0.1:5000/predict \ -H Content-Type: application/json \ -d {image: 这里粘贴一小段base64编码的图片数据}如果一切正常会看到类似下面的返回{class_id: 7, label: 7, confidence: 0.9954, cost_ms: 42}注意接口返回的label是字符串而不是数字前端渲染时不要直接拿它做算术。另外如果返回的不是 JSON 而是 HTML 错误页多半是后端抛了异常去终端看 traceback 定位如果是连接被拒绝先确认 Flask 服务有没有真的跑起来。提示后端先跑通再接前端联调。这样可以把问题范围缩小到「请求没到达」还是「返回没解析」效率高一倍。4. 小程序端请求封装与图片压缩从 wx.request 到结果渲染后端接口就绪后开始看小程序端。这部分是大多数初学者最头疼的——不是不会写页面而是不知道wx.request怎么封装、图片怎么处理、数据怎么渲染。4.1 请求封装统一 BASE_URL、超时与错误处理小程序端没有 axios 可用官方原生的wx.request用起来比较啰嗦所以 demo 里通常会在utils/request.js里做一层 Promise 封装。这样做的好处是每个页面不需要重复写header和错误处理改接口地址也只需改一个文件。// utils/request.js const BASE_URL http://127.0.0.1:5000; // 本地开发上线前改成正式域名 function request(path, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: POST, data, // 直接传对象不要手动 JSON.stringify header: { content-type: application/json }, timeout: 15000, // 后端 CPU 推理可能要 1~3 秒预留 15 秒 success(res) { if (res.statusCode 200 res.data) { resolve(res.data); } else { reject({ code: res.statusCode, msg: 接口非 200 }); } }, fail(err) { // err.errMsg 通常是 request:fail xxx 的形式 reject(err); } }); }); } module.exports { request, BASE_URL };这个封装有三个点值得注意。第一data直接传对象wx.request内部会自己做序列化如果先JSON.stringify再传会导致content-type为application/json时双引号被二次转义后端request.get_json解析失败。第二timeout默认是 60 秒但对用户来说超过 5 秒就有点焦虑了demo 里设 15 秒比较合理——既留足推理时间又不会让请求无限挂起。第三BASE_URL单独导出后面切换环境本地、测试、正式只需要改这一处。4.2 图片压缩与 base64别让原图撑爆请求体图片处理是整个前端最容易被低估的环节。手机相册里的原图动辄 3~10 MB直接转 base64 后请求体膨胀到 4~13 MB上传慢、解析慢还有可能触发微信的请求体大小限制。所以这份 demo 的处理思路是先用 canvas 等比压缩到适合模型输入的尺寸再转 base64 传给后端。// pages/index/index.js function compressImage(src, size 224, quality 0.8) { return new Promise((resolve) { wx.getImageInfo({ src, success(info) { // 等比缩放避免拉伸变形 const scale size / Math.max(info.width, info.height); const w Math.floor(info.width * scale); const h Math.floor(info.height * scale); const ctx wx.createCanvasContext(compressCanvas); ctx.drawImage(src, 0, 0, w, h); ctx.draw(false, () { // 等一帧再取数据canvas 绘制有时是异步的 setTimeout(() { wx.canvasToDataURL({ canvasId: compressCanvas, width: w, height: h, destWidth: w, destHeight: h, fileType: jpg, quality: quality, success(res) { // 去掉 dataURL 前缀只保留纯 base64 const b64 res.dataURL.replace(/^data:image\/\w;base64,/, ); resolve(b64); } }); }, 50); }); } }); }); }这里有两个细节我拆 demo 时特意验证过。一是scale按最长边计算保证图片不变形如果直接drawImage(src, 0, 0, 224, 224)竖屏照片会被压扁识别率直接崩。二是setTimeout 50的做法看起来有点「玄学」但在部分安卓机型上ctx.draw的回调不触发等一帧再取canvasToDataURL是最稳的兼容写法。画布本身要放在 WXML 里且不能加display: none否则某些机型取出来是空白。压缩后 224×224 的 JPEG 图片通常只有几十 KBbase64 编码后也就一百多 KB后端解析和处理都会很快。4.3 结果渲染与本地缓存识别记录怎么存拿到接口返回值后页面要做两件事展示当前识别结果以及把历史记录缓存到本地。展示部分在index.wxml里用{{ }}绑定result.label和result.confidence注意confidence是 0~1 的小数页面上显示百分比时要乘以 100 再toFixed(2)。缓存部分很多 demo 没有做但我建议加上因为微信小程序切后台会被销毁用户一离开页面识别结果就丢了。常见做法是用wx.setStorageSync存数组配合时间戳做 TTL 过期清理。这也就是微信小程序设置缓存时间的标准实现方式。// pages/index/index.js const CACHE_KEY history_records; const CACHE_TTL 24 * 60 * 60 * 1000; // 24 小时有效期 function saveRecord(record) { const list wx.getStorageSync(CACHE_KEY) || []; const now Date.now(); // 清理过期记录 const fresh list.filter((item) now - item.ts CACHE_TTL); fresh.unshift({ ...record, ts: now }); wx.setStorageSync(CACHE_KEY, fresh.slice(0, 20)); // 最多留 20 条 }TTL 的判断逻辑很简单每次读取时比较当前时间戳和记录里的ts字段超过有效期就丢弃避免缓存无限膨胀。20 条的限制也是必要的虽然单条数据很小但wx.setStorageSync的容量上限是 10 MB识别结果里如果带图片 base64存太多会顶到上限。如果不需要 history 功能这段可以直接不写。5. 微信小程序调 AI 接口的常见坑域名、超时与图片编码这一章集中写我在拆这种前后端联调项目时遇到的真实问题。每一条都按「现象 → 原因 → 解决」来写照着排查就行。5.1 开发者工具能调通真机一测就失败现象在微信开发者工具里点识别按钮结果正常返回换成手机预览请求直接失败Network 面板显示request:fail。原因这是最典型的开发环境与真机环境差异。开发者工具默认勾选了「不校验合法域名」所以访问http://127.0.0.1:5000没问题真机上没有这个豁免而且手机访问127.0.0.1指向的是手机自己根本不是你的电脑。另外Flask 如果只监听127.0.0.1局域网内其他设备也访问不到。解决开发调试阶段在开发者工具右上角「详情 → 本地设置」里勾选「不校验合法域名、web-view 域名、TLS 版本以及 HTTPS 证书」同时改后端启动参数把app.run(host0.0.0.0, port5000)中的 host 从127.0.0.1换成0.0.0.0让服务监听所有网卡手机和电脑连同一个 Wi-Fi把BASE_URL里的地址改成电脑的局域网 IP比如http://192.168.1.5:5000。5.2 图片传上去接口要 5 秒才返回现象接口能通但用户点完按钮后要等 5 秒以上才出结果体验很差。原因两类原因叠加。一是图片体积太大原图 4 MB 转成 base64 后接近 5.3 MB后端解码和 PIL 打开要花时间二是模型推理本身慢如果后端代码里每次请求都重新加载权重那第一次请求会额外多出好几秒。可以先在后端打印cost_ms看时间花在解码、预处理还是推理哪个环节。解决前端压缩到 224×224、quality 0.8图片体积能缩小到原来的几十分之一后端把模型加载移到全局变量只加载一次。如果cost_ms显示推理耗时超过 200 ms检查是否误开了 GPU 之外的 CPU 线程竞争或者模型结构里有没有多余的层。对于课程设计CPU 推理 40~80 ms 是正常范围。5.3 接口明明返回了数据页面却渲染不出来现象后端日志显示请求已处理并返回小程序控制台也能看到res.data有内容但页面上的识别结果一直是空的。原因前后端字段名不一致。比如后端返回的字段是label前端代码里却用了result.text或者后端返回的confidence是浮点数前端直接当字符串拼接导致渲染异常。还有一种情况后端报错时返回的是 HTML 错误页res.data里根本不是对象res.data.class_id取到undefined。解决统一的返回结构是{ class_id, label, confidence, cost_ms }不要混用别名。前端在success回调里先console.log(res.data)看原始结构再决定取哪个字段。后端接口用jsonify返回不用return str(张量)这种隐式转换——我见过有人直接return model(tensor)Flask 会把它当成响应体格式完全不是 JSON。5.4 安卓正常iOS 上传就报错现象同一个后端接口安卓真机和开发工具都正常iOS 一上传图片就报request:fail或者后端返回 400。原因iOS 的canvasToDataURL在部分系统版本上不遵循fileType: jpg的设置输出的是 PNG 格式PNG 对纯色背景图片压缩率低体积可能比 JPEG 大好几倍导致请求体超限。另外iOS 的wx.chooseImage返回的临时文件路径后缀可能是heiciPhone 默认格式PIL 在部分环境里打不开 HEIC 文件。解决前端指定fileType: jpg的同时后端解码逻辑里加一层容错用 PIL 打开失败时尝试把 base64 数据直接写入临时文件再读取。更稳的做法是后端不强依赖图片编码格式一律先转灰度再 resize——MNIST 本身只需要灰度信息。如果对图片质量要求不高可以在前端二次压缩把 quality 降到 0.6进一步缩小体积。5.5 第一次请求特别慢后面的请求快很多现象服务刚启动时第一次识别花了 3 秒后面再调用只需要 50 毫秒。原因模型权重懒加载。如果代码里把torch.load写在predict函数内部那每次第一次请求都要读磁盘、反序列化、重建模型结构就算写在全局PyTorch 的 CPU 数值库如 MKL-DNN在第一次推理时也会做初始化触发指令集选择和 kernel 编译。解决模型加载放全局变量只是第一步更彻底的做法是在服务启动后主动跑一次空推理预热。常见方式是加一个app.before_first_request装饰器Flask 2.3 后改为app.before_request加判断或者在__main__里起服务前先构造一个全零张量执行一次model(tensor)。预热之后第一次用户请求的耗时就会稳定在正常水平。6. 把 demo 改成自己的大作业换模型与上云改造这份 demo 直接跑通只是第一步大部分人会拿它改成自己的大作业或者作品集。最后一章讲两个最实用的改造方向。6.1 换模型时三处必须同步改如果不想用手写数字识别想换成自己的分类模型比如表情识别、猫狗分类这三个位置必须同步修改缺一个就会翻车。修改位置对应文件关键点图像预处理server/app.py的transform尺寸、归一化均值标准差必须与训练时完全一致类别标签server/app.py的LABELS顺序必须与训练数据的类别顺序一致输出解析server/app.py的softmax/argmax多标签任务要改成阈值判断不能再直接取顶最常见的坑是训练时用的输入尺寸是 32×32 或 64×64接口代码里还留着Resize((28, 28))识别率直接掉到随机水平还以为是模型训得不好。换模型后的第一件事不要接小程序先拿一张验证集图片用 curl 打接口看返回的class_id和label对不对再谈前端。6.2 从本地到线上HTTPS 域名与云函数如果要把 demo 部署到线上给别人演示只改BASE_URL是不够的。微信小程序正式环境要求所有请求域名必须是 HTTPS并且在小程序后台「开发管理 → 服务器域名」里配置 request 合法域名域名还必须有备案。本地开发时勾选「不校验合法域名」只是调试便利上线前不配域名真机上所有请求都会被拦截。如果你的后端跑在云服务器上需要先给域名配好 SSL 证书再把 Flask 服务用 gunicorn 或 uwsgi 跑起来前端BASE_URL改成https://你的域名/predict。如果只是想快速演示、不想折腾服务器可以改用微信云开发里的云函数把推理代码封装成云函数小程序端用wx.cloud.callFunction直接调用省掉域名和 SSL 配置这一整块工作。不过要注意云函数的内存和时间限制比较紧CPU 推理超过 5 秒的任务不建议走这条路。这次把 demo 完整跑通我最深的感觉是——这类项目的难点根本不在模型而在「接口约定」这个黑匣子。前端传什么字段、后端回什么结构、预处理尺寸和训练时是否一致任何一环漏了都要查半天。从那以后我每次接小程序后端都会先强制自己用 curl 打一遍接口再连前端两侧都打印完整请求响应日志确认cost_ms和返回体结构都在预期内才往下走。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网