1. 为什么是"本地AI":先把场景想清楚,再谈技术选型
错题辅导小程序这个产品,听起来似乎就是"拍照存错题 + 按知识点复习",门槛不高。但一旦把"本地AI"四个字加进去,整个技术路线就完全变了一个方向。这里说的本地AI,指的是在用户手机或平板上端侧完成图像识别、题目切分、知识点分类、推荐复习计划等推理工作,而不是把图片上传到云端服务器去处理。
我先说为什么我会坚定地走本地端侧路线,而不是常规的"小程序 + 云端API"方案。一个错题辅导工具,用户拍下的往往是自己的试卷、练习册、手写笔记,这些内容对隐私敏感度很高。如果每次拍题都要上传到云端做OCR和分类,用户心理上会有很强的抵触感,而且一旦网络状态不好,整个使用流程就直接卡死。错题整理这个动作,本来就该是随手拍、随时记、随时回看的轻量操作,网络依赖越重,产品的使用频率就越低。还有一个非常现实的问题是成本,云端OCR接口按次计费,一个活跃用户一天拍20道题,一个月就是600次调用,十万活跃用户带来的推理成本会迅速超出一个小团队的承受范围。把推理放到端侧之后,这部分成本几乎归零。
当然,端侧AI也不是没有代价。模型体积受限于安装包大小,推理速度受限于手机芯片算力,硬件兼容性必须自己一遍遍适配。本地AI意味着你在模型精度的选择上要做很多妥协,不可能像云端那样直接上几十亿参数的大模型。但错题识别这个场景,其实对模型能力的要求并没有想象中那么高——题目大多是印刷体,少数是手写体,需要识别的文字量不大,核心是版式理解、题目切分、知识点归类这三件事。用轻量级模型完全可以在不牺牲太多精度的情况下把这些事做扎实。
这篇内容主要面向两类读者:一类是想自己动手做一个错题类小程序或教育工具产品的开发者,另一类是正在做端侧AI落地、想了解从模型选型到推理链路怎么搭的算法工程师或全栈工程师。我会从架构决策讲起,一路讲到模型选型、框架对比、量化压缩、异常排查,把一个"本地AI错题辅导小程序"从零到落地的完整思路拆开讲。
2. 架构选型:端侧推理小程序的功能拆解与模块划分
2.1 核心功能流程:一拍、二认、三分类、四复习
在写第一行代码之前,务必要把用户操作路径和背后的技术链路对齐。错题辅导小程序如果只有"拍照存图"功能,那根本不需要AI参与,一个相册应用就能做到。AI的介入点在三个环节:拍完照之后要自动识别题目区域、自动切分每一道题、自动打上学科和知识点标签。后续的复习排程、薄弱点分析,则是基于这些标签做数据计算。
我把完整的功能流程拆成下面这条链路:
- 拍照或从相册导入试卷图片。
- 图像预处理:转灰度、去噪、透视校正、增强对比度。
- 版面分析:识别图片中的题目区域,区分题号和正文。
- 题目切分:把一整页的题目按题号切成单题图片。
- OCR文字识别:将每道题的题干和选项转为文字。
- 知识点分类:根据题干文本和题目类型,映射到学科知识图谱中的具体知识点。
- 错题标注:用户确认这道题是否做错,选择错误原因。
- 入库存储:按学科、知识点、错误类型、时间等维度写入本地数据库。
- 复习提醒:根据艾宾浩斯遗忘曲线或自定义计划,在合适时间推送复习任务。
这里第3到第6步就是端侧AI要干的活,其他步骤是传统的小程序逻辑。第6步虽然看起来像是个推荐系统问题,但在本地资源约束下,实际实现会用文本分类模型或者基于知识图谱的规则匹配来做,而不是上协同过滤。
有一个细节值得特别注意:题目切分这个环节,很多人会误以为直接做OCR然后用坐标切就行,实际完全不是这样。一份试卷的排版千变万化,有两栏混排、有题号错位、有手写批注穿插在印刷体之间、还有图片题和表格题占位。纯OCR输出的文字包围盒坐标,根本无法稳定还原题目的逻辑结构。所以正确的做法是先做版面分析,通过一个目标检测模型识别出每个题目区块,再做OCR。版面分析模型输出的是一组带坐标的矩形框,每个矩形框代表一道完整的题目,这样切分和后续的知识点分类才有稳定基础。
2.2 为什么选小程序形态:端侧推理的载体约束
"小程序"这个载体本身就决定了技术选型有边界。小程序不能像原生App那样直接调用系统级框架,比如iOS的Core ML或安卓的NNAPI,你只能通过小程序容器暴露的能力或插件机制来跑模型。这个限制非常关键。
目前主流的小程序平台都提供了某种形式的插件市场或原生扩展能力,允许开发者把编译好的端侧推理库封装成插件。你可以在小程序的主包里只放UI和业务逻辑,把推理引擎和模型文件放在插件包里。插件包有单独的体积限制和加载策略,比塞在主包里灵活得多。
做一个现实的设计决策:模型文件不放进初始包。小程序启动时要拉取的资源越少,用户流失率越低。正确的做法是首次打开时在后台静默下载模型包,下载完成后存到本地缓存目录。模型包的管理要做版本号,每次启动检查版本,有更新才重新下载。做过移动端开发的都知道,这个模式其实就是资源的"按需加载"策略,只是在小程序环境里你得更加小心,因为小程序缓存空间和下载策略在不同平台上不完全一致,安卓机型的文件系统访问权限和小程序沙箱的复杂度都比iOS高不少。
我曾在某款安卓机型上踩过一个坑:小程序把模型文件写进了系统临时目录,结果用户清理垃圾文件后模型被系统当成缓存清掉了,下次启动时模型文件缺失,整个推理链路直接崩溃。后来统一改成了应用专属目录,并在启动时做模型完整性和版本校验,这个问题才算彻底解决。
2.3 端侧AI小程序的整体模块划分
从工程角度,小程序可以拆成以下模块:
- UI层:负责拍照页、错题列表页、知识点图谱页、复习计划页。
- 图像处理模块:负责图片加载、压缩、旋转校正、灰度转换。这一层最好不要全部用Canvas API硬算,某些操作交给端侧推理框架自带的图像预处理函数会更高效。
- 推理调度模块:管理模型加载、推理请求排队、结果后处理,并向上层回调结构化数据。
- 模型管理模块:负责模型下载、版本检测、文件校验、缓存清理。
- 本地存储模块:用SQLite或类SQLite的本地数据库存储结构化错题数据,图片文件单独存文件系统,数据库只存路径索引。
- 复习算法模块:基于错题标签和复习记录计算复习队列。
模块间的通信都通过事件总线或状态管理库完成。这里的重点不是用什么状态管理库,而是明确"推理结果是非同步的、耗时的、可能失败的",UI层不能假设推理一定会成功。每一次拍照识别都必须设计失败分支,比如"识别失败请重拍""网络不佳但本地模型可用""模型版本过旧,正在更新"这类交互状态。
从架构上看,这个模块划分和常规云后端App的最大区别在于:没有服务端。所有数据都在本地,不需要设计登录鉴权、接口鉴权、数据同步协议。这会省下大量后端开发工作,但也意味着数据备份和多端同步需要单独设计。如果你的产品规划里有换机迁移数据的场景,建议在一开始就把错题数据导出为通用格式的JSON,图片单独打包,后续做同步或迁移时就不需要重新梳理数据结构。
3. 模型选型:从OCR到知识点分类,每一环都精确匹配需求
3.1 印刷体与手写体的OCR选型
OCR是整个AI链路里最成熟也最容易选型的一部分。错题场景里,OCR需要做的是两件事:识别印刷体题干和选项文字,以及在部分场景下识别手写解题过程。印刷体识别在移动端已经非常成熟,开源方案里有两种路线可以选:PaddleOCR的移动端模型和Tesseract。Tesseract的历史包袱较重,中文识别效果当年挺一般,虽然有新版本改进,但和针对中文场景专项优化的方案比差距依然明显。PaddleOCR的移动端模型将检测和识别拆成两个模型,检测模型负责找出文字行坐标,识别模型负责把文字行转换为字符串。两个模型都是轻量版本。
在手写体识别上,情况会麻烦一些。手写文字的自由度太大,同一个字不同人写法差别很明显,所以端侧手写OCR的上限相对有限。我的建议是,不要追求端侧模型直接识别整段手写解题过程,而是只识别手写答案区域里的关键词,比如判断题的"对/错"、填选题里的具体数字。识别结果作为错题标签的补充信息即可,如果识别置信度不高,就当作未识别处理,不能影响主流程。
OCR模型选型时有一个容易忽略的点:图像分辨率。端侧模型输入尺寸通常被限制在640x640或960x960,但一张手机拍出来的试卷原图可能是4000x3000。必须有一个预处理阶段将大图缩放并切割成分块,或者通过缩放保持长宽比后送入模型。直接暴力缩放到模型输入尺寸会导致文字糊成一团,识别率断崖式下降。我建议按比例缩放,让图片短边不小于1000像素,然后从上到下切分成多个有重叠的区域,分别送入OCR模型识别,最后合并结果。这样既能保证识别精度,又不会超过模型的输入限制。
3.2 版面分析模型:题目切分的技术核心
前文已经强调过,版面分析是错题切分的核心前置环节。实现版面分析有两个思路:目标检测和分割。目标检测模型输出矩形框,标注为"题干""选项""题号""图片"等类别;分割模型则输出像素级掩码。在移动端,目标检测的效率明显高于分割,所以建议用目标检测模型。
模型结构推荐选轻量的YOLO变种或PaddleDetection推出的移动端检测模型。输入尺寸不宜超过640x640,因为错题切分不需要像素级边缘,矩形框足够用。
训练版面分析模型需要标注数据。公开的试卷版面数据集虽然有一些,但每个学科的试卷版式差异很大。实际落地时,建议先用手头能拿到的公开数据训练一版,然后在真实用户授权过的脱敏图片上做二次标注和微调。这一步非常关键,因为公开数据集里的试卷清晰度普遍较高,而用户拍的照片常常有阴影、褶皱、歪斜,真实发布分布和训练分布一旦差异过大,线上效果会很差。
有一个训练细节值得注意:题号在版面分析中的作用。务必把"题号"单独标成一个类别,因为题号是切分题目的天然锚点。即使检测模型漏检了某道题的题干区域,只要题号区域检测到了,你仍然可以用题号的位置来推算题目区域的边界。这是我在调试过程中总结出来的一个相当实用的经验。
3.3 知识点分类:轻量文本分类与知识图谱的取舍
题目切成单题图并识别出文字后,下一步就是打知识点标签。比如"二次函数图像与系数关系""牛顿第二定律的应用""现在完成时态"。这个环节可以从两个技术路线里选一条。
第一条路线是训练一个轻量文本分类模型。把OCR识别出的题干文本作为输入,输出是知识点标签。这个方案的好处是端到端,维护成本低,新知识点只需要更新训练数据重新训练。缺点是它完全依赖OCR的文本质量,如果OCR把题干识别错了,分类结果也会跟着错。另一个问题是,题目里的公式和图表信息在OCR阶段就丢了,有些题只看文字无法确定准确知识点,比如一道几何题光看文字描述很难区分是考圆的性质还是考三角形相似。
第二条路线是规则加知识图谱。预先维护一个知识图谱,每个学科的知识点有上下位关系,比如"函数→二次函数→二次函数图像"。然后把OCR结果用一组关键词规则映射到图谱节点上。因为题干里通常会出现"已知抛物线""求面积最大""当x取何值时"这类特征明显的表述,用关键词规则去匹配,准确率在实际场景里可能比文本分类模型更高,而且完全可控。
我自己的经验是两条路线结合使用:先跑规则,命中率高且稳定性强;规则命中不了的时候,再交给文本分类模型。这种"规则兜底、模型补漏"的设计,在端侧场景里既保证了效果,又控制了计算量。
3.4 端侧推理框架横向对比:TFLite、ONNX Runtime、MNN、NCNN
模型训练好之后,要找到一个能在端侧跑推理的框架。目前移动端主流的开源推理框架有这些:
- TensorFlow Lite:生态成熟,和Keras/TensorFlow训练栈无缝衔接。对移动端的算子支持全面,文档和社区资源丰富。TensorFlow Lite的Delegate机制允许你利用GPU或NPU加速,但不同芯片的兼容性需要逐个测试。
- ONNX Runtime Mobile:从ONNX导出导入的模型都可以直接跑,跨框架迁移方便。如果你用PyTorch训练,导出ONNX后转成ORT格式基本无痛。对ARM CPU的优化做得很扎实。
- MNN:国内团队主导的推理框架,对ARM架构优化得非常狠,在主流安卓机型上性能表现出色。内存占用控制好,模型转换工具链完整,适合安卓和iOS双端部署。
- NCNN:老牌轻量推理框架,腾讯开源,在CPU端的优化很强,尤其是ARM Neon指令集的利用率做到极致。缺陷是算子覆盖面相对窄一些,新模型里的一些算子可能需要手工实现。
选框架不能用"哪个最有名"来决定,要从三个维度看:你的训练框架是什么,你的目标机型芯片以什么为主,你的模型里有哪些算子。我的建议是,训练用Python生态,无论PyTorch还是Paddle都可以,部署时统一转成ONNX,再用各框架的转换器转成目标格式。这样你可以在TFLite和MNN之间做AB测试,量化和推理速度对比取优。
做一个小程序时,还有一个隐性要求:推理库必须能编译成可以在小程序插件里运行的二进制。这就意味着你用NDK交叉编译时,目标ABI要覆盖arm64-v8a和armeabi-v7a两个主流平台。x86不用管,因为真机上几乎遇不到。
4. 实操落地:从模型转换到端侧推理链路的完整实现
4.1 端侧推理链路的最小可运行版本
先不铺开太多,给出一个最小可运行的端侧推理链路应该长什么样。它必须包含以下5个环节:
- 模型文件加载:从本地缓存目录读入模型文件,加载为推理框架的Interpreter或Session对象。
- 输入数据预处理:把图像Bitmap缩放、归一化到模型期望的输入格式。
- 推理执行:调用推理框架的run接口执行前向计算。
- 输出后处理:把模型输出张量转换为检测框坐标、分类结果或文本字符串。
- 结果回调:把结构化结果交给UI层展示或入库。
这里最常被忽视的是第2个环节的预处理要和训练时的预处理完全对齐。训练时如果用了ImageNet的均值和方差做归一化,端侧推理时也必须用同样的值,否则输入分布不一致会导致精度下降。很多模型转换工具在这一步不会帮你检查语义对齐,只做数值上的张量重排,所以你必须自己对这一层负责。
具体到OCR流程,预处理还有更细的讲究。模型训练时用的图像是扫描件或截图,几乎没有摩尔纹和暗角。真实拍照图像则有大量噪声,直接送入模型会引入不必要的误差。我的做法是在送入模型前先做一个轻量级图像增强:使用OpenCV或图像处理库做自适应阈值化和快速降噪。但这个操作会增加几毫秒到几十毫秒的处理时间,所以要做成可选开关,如果图像本身已经很清晰就跳过。
4.2 模型量化:从FP32到INT8的精打细算
端侧推理最大的性能瓶颈是内存带宽和计算量。一个FP32精度的4MB模型,在跑推理时每次前向计算都要搬运大量浮点数据。把模型从FP32量化到INT8,模型体积减少约75%,推理速度在多数CPU上提升2到4倍。
量化不是没有代价。对OCR这类对边界和细节敏感的模型,朴素的后训练量化可能会让检测框偏移几个像素。这时候有几个补救手段:
- 使用量化感知训练,在训练阶段就模拟量化误差,让模型学会对量化噪声鲁棒。
- 只量化部分层,比如把卷积层量化而全连接层保持FP16,在精度和速度之间取平衡。
- 使用混合精度量化,对不同敏感度的层采用不同量化位宽。
在实际项目里,OCR检测模型在直接后训练量化后,检测框会偶尔出现偏移,导致题目切分差出十几像素。这种偏差单道题看不出来,但连续切分多道题时,题目边界就会互相重叠或间隙过大。最后我用了两步解决:一是把检测模型换成感知量化训练版本,把量化误差纳入训练目标;二是在后处理时加了非极大值抑制的宽松阈值,允许低置信度检测框参与合并,让切分结果更平滑。
这里顺便说一个端侧推理的常见误解:很多人以为量化之后推理一定变快。实际上在部分支持INT8加速的芯片上确实快很多,但在一些中低端机型上,如果你用的推理框架没有针对INT8做SIMD优化,它可能先把INT8转回FP32再算,反而更慢。所以每一版量化模型都要在真机上跑一遍耗时测试,不要只看模拟器的数字。
4.3 推理速度的优化与预热策略
错题识别对推理速度的要求,用户感知上大概分为三种状态:1秒内响应,用户觉得丝滑;1到3秒,用户能接受但会微微皱眉;超过3秒,用户大概率以为程序卡死了。所以你在端侧做推理优化的目标,就是把单题识别的完整链路控制在2秒以内,其中模型推理部分要压缩到500毫秒以内。
除了量化,有三个优化手段是必须做的:
- 线程数调优。推理框架一般都有线程数设置,很多设备CPU是大中小核架构,线程开太多反而会因为调度开销降低效率。实测下来,在中高端安卓机上,TFLite开4线程、MNN开4线程能达到比较优的推理速度,但部分机型开8线程会退化。保险做法是做成可配置项,发布前在目标机型上做一轮枚举测试。
- 内存复用。每次推理都要重新分配输入输出张量的话,内存碎片会越来越严重。推理框架一般支持预分配输入输出张量的buffer,创建session时就把buffer留好,每次推理复用,可以显著减少内存抖动导致的卡顿。
- 推理预热。模型首次加载到运行时,往往还要做算子初始化、内存映射,第一次推理会明显慢于后续推理。可以在小程序启动后的空闲时间做一个预热推理,让模型完成初始化,用户真正拍照时推理速度就不会忽快忽慢。
这里还要提一个容易被忽略的问题:多模型串行与并行。前面提到过,错题识别至少涉及版面分析模型和OCR模型,可能还有文本分类模型。如果串行跑三个模型,时间会累加。比较理想的做法是能用一条流水线并行处理多张图时,用多线程让一个模型处理上一张图的同时,另一个模型处理下一张图。但模型A和模型B都可能占用全部CPU核心,并行起来反而互相抢资源。我的实践是:在多数设备上,串行跑更稳。并行只在测试过的大内存、高性能机型上开启,做一个动态开关来控制。
4.4 本地数据库:错题的结构化存储与检索
错题数据要用结构化方式存储。我用的表大概是这样:
- 错题表:主键ID、学科、知识点ID、题目图片路径、OCR文本、错误原因、创建时间、下次复习时间。
- 知识点表:知识点ID、名称、学科、父知识点ID、难度系数。
- 复习记录表:复习ID、错题ID、复习时间、复习结果(记住/遗忘)、连续掌握次数。
设计表结构时有几个地方要想清楚。知识点ID必须允许为空,因为OCR可能识别失败,题目入库时无法正确归类。错误原因字段建议用预定义枚举加自由文本的方式,用户可以在预置的几个选项外补充自己原因。下次复习时间字段不需要精确到秒,精确到日期即可,因为复习排程的最小粒度是天。
一个比较重要的设计决策是图片文件的存储方式。我不建议把图片压缩后转成Base64塞进数据库字段,这样会让数据库文件膨胀到几百MB,而且每次查询都要做字符串解码。正确做法是图片文件压缩后存到文件系统,命名规则用"错题ID_学科_时间戳.jpg",数据库里只存相对路径。做完整备份或迁移时,把文件目录和数据库一起打包就行。
端侧检索还有一个需求是模糊搜索,用户可能想按题干关键词搜索错题。SQLite的LIKE查询在几百条数据时很快,但如果错题积累到上万条,LIKE全表扫描会明显变慢。此时建议把OCR文本分词后建一个倒排索引表,或者借用一个嵌入式搜索引擎组件来做全文检索。对于一个小程序来说,如果预见到用户会有大量错题,我建议直接用全文检索组件,不要走LIKE硬扛。
4.5 图像后处理:透视校正与题目边界合并
拍照试卷照片难免有透视变形。用户可能斜着拍、俯拍角度不够正,或者试卷本身摆得歪。如果直接对这些照片做版面分析,检测框的精度会打折。所以图像预处理里的透视校正不是可选项,而是必选项。
实现校正需要四个端点。可以用边缘检测找到试卷外边框的四个角点,然后映射到标准矩形。OpenCV提供findContours加approxPolyDP可以搞定,但试卷图案复杂时外边框可能检测失败。备选方案是让用户在拍照后手动调整四个角点,虽然用户体验略降,但鲁棒性大幅提升。
我比较推荐的是"自动校正 + 手动微调兜底"的设计。先用边缘检测自动校正,如果校正后检测置信度低于阈值,允许用户进入手动调整模式。实际用户的操作意愿比你想象的低,大多数用户如果第一次自动校正没成功,会直接删掉重拍而不是手动调角。所以相机引导非常重要,在拍照界面就提示"请将试卷放正、确保四角完整",能显著降低后续处理的难度。
还有一道隐藏工序是去除手写批注。试卷上往往有老师批改的痕迹、红笔勾画、学生自己的草稿,这些手写区域会和印刷体混在一起,影响OCR和切分。有一些分割模型可以区分手写和印刷体,但这类模型在端侧跑的性价比一般。实际项目里,我建议只在OCR结果置信度低时才做二次识别,不要对所有图片都做手写分离,因为绝大多数照片的手写内容占比不高,对主流程的影响其实有限。
5. 常见问题与排查实录
5.1 模型文件加载失败或崩溃
现象:小程序在部分机型上启动后首次导入模型,直接白屏崩溃。日志显示模型加载时内存分配失败或文件校验失败。
排查步骤:
- 确认模型文件是否完整。用哈希值校验本地缓存文件和部署包的模型文件是否一致。如果用户在下载模型过程中杀掉小程序,很容易留下半截文件。
- 确认模型文件是否真的被加载进正确的路径。小程序沙箱的路径在不同平台上有变化,不要使用硬编码路径,要用API动态获取。
- 确认推理框架的ABI是否匹配。如果你的小程序插件只编译了arm64-v8a,在老的32位安卓机型上就会直接加载失败。
- 确认模型算子是否被当前推理框架的版本支持。把模型加载失败时的具体错误信息打出来,定位到具体算子名,再到框架文档里搜索是否支持。
我自己踩过最坑的一次,是模型转换时用了一种新的注意力算子,推理框架的移动端版本不支持,加载时静默失败,没有抛出任何有效错误信息,最后只能逐个算子排查,费了很大功夫。
5.2 推理结果精度与预期不符
现象:模型单独在电脑上验证时精度很好,到了端侧准确率明显下降。
可能原因:
- 图像预处理不一致。输入和训练数据的尺寸、归一化参数没有对齐。
- 图片压缩过度。小程序为了省空间把图片压缩得过于剧烈,文字边缘出现大量锯齿,OCR识别率因此下降。
- 推理数据格式错误。模型输入要求RGB顺序的像素数组,而Bitmap默认是ARGB顺序,没有转换就直接喂给模型,颜色通道错位会导致检测框完全乱掉。
- 量化误差积累。INT8量化对某些层的动态范围覆盖不好,虽然测试集指标变化不大,但真实场景里遇到在量化边界附近的输入就崩。
排查方法:先在电脑上加载端侧模型跑同一张测试图,和训练框架跑的结果做对比,确定是转换过程引入的误差还是端侧预处理引入的误差。然后再逐步调整,直到每一步的输出都和预期一致。
5.3 推理耗时波动大
现象:同一张图片在不同机型上的耗时差异非常大,有的机型上2秒完成,有的机型上要8秒。甚至在同一个机型上,连续跑三张图耗时也在1.5秒到5秒之间剧烈波动。
原因分析:不同机型的CPU调度策略、温控策略差异很大。小程序运行在容器里,CPU资源抢占比原生App更严重。还有一个常见的隐形杀手是后台进程和系统垃圾清理进程在跑,CPU主频被限制。
优化方向:
- 把推理放到一个独立的Worker线程里执行,避免阻塞UI渲染。
- 采用"用时才跑"策略,避免一次性把所有模型都加载到内存。
- 在低端机上降低输入图片分辨率。很多模型对输入尺寸并不敏感,缩到512x512对精度影响有限,但速度可能快一倍。
- 使用一个预热开关,用户在空闲时主动跑一次预热推理,把模型加载开销提前消耗掉。
5.4 内存占用持续上涨
现象:小程序连续使用一段时间后,内存占用不断上涨,明显感觉操作变得卡顿。
问题根源通常在于图像Bitmap没有及时回收。每次拍照、切图、压缩都会产生新的Bitmap对象,如果引用没有释放,GC又来不及回收,内存就会水涨船高。
归类一下容易造成内存泄漏的点:
- 高分辨率Bitmap长驻内存。处理完的图片应该立即缩略,释放原始大图。
- 推理输入输出张量每次重新分配。建议固定复用session的buffer。
- 模型加载后没有释放。如果不再使用某个模型,应该调用框架的释放接口,而不是依赖GC。
- 列表页长时间持有大量错题图片。错题列表要做图片懒加载和复用,滑动时只加载当前可见项的图片。
5.5 安卓与iOS的差异适配
如果你要把小程序发布到两大移动平台,有几个差异必须提前处理:
- 包体积限制:模型文件常常是体积大头,iOS和安卓对包内文件大小和下载时的流量限制不同,很可能要搞两套资源打包策略。
- 性能差异:相同价位的iOS和安卓机型,推理性能差距可以大到两倍。iOS的Metal GPU加速在部分框架中能显著提速,但安卓端的加速方案碎片化严重。
- 相机参数差异:不同机型返回的相机图像方向、EXIF旋转标记不一致,必须在拍照后统一做方向校正,否则图像会旋转90度导致检测全部失效。
我的经验是,做端侧AI小程序必须守住一个兼容性底线:在最差机型上能跑,不崩溃,用2到3秒时间给出可接受的结果。在最主流机型上,要做到1秒内出结果,且UI全程不卡顿。超出这个底线,用户就会流失。
6. 端侧推理之外的几个工程细节
6.1 模型包管理与版本迭代
模型一定会更新,因为你要修训练数据的错误,要新增学科和知识点。此时如果用户更新了小程序,但模型还在旧版本,新逻辑就用不上。所以在模型配置和推理代码之间,要设计一套清晰的版本映射机制。
我的做法是把模型版本号作为一个字段嵌入到推理结果的数据结构里,每次推理结果都附带模型版本。如果发现某道题使用的是旧模型识别,而新模型已经发布,可以在后台静默重新识别一次并更新标签。这个过程不会打扰用户,只影响数据的准确性。
模型更新推送策略也要设计好。不要一上来就强制全量下载新模型,要在用户连接到Wi-Fi且充电状态下才静默更新,否则用户流量会无谓消耗。用户可以手动触发检查更新,但是默认不主动打扰。
6.2 离线优先与数据安全
整个产品架构是离线优先的,这意味着95%的核心功能在断网状态下完全可用。这里要特别注意一点:小程序本身是运行在宿主App里的,宿主App的存储权限和生命周期你无法完全掌控。所以重要数据要在每次写入后做一次本地备份,备份到系统剪贴板或者导出一个JSON文件,用户自己可以通过云盘或文件传输工具保存。
数据安全还没有部门监管来管你,但你要自觉得对用户负责。OCR识别出的错题文本,包含大量个人信息和隐私,不应该明文存储在小的临时文件里。至少要做到数据库加密或者字段级加密。端侧没有服务端,防火墙这层防护根本不存在,所以客户端的数据加密比服务端场景更依赖开发者的自觉。
6.3 调试工具与测试方法
端侧模型调试过程中,有三个工具非常关键:
- 模型推理可视化工具:把推理结果和原图叠加展示,检测框、文本、置信度全部可视化,方便快速定位问题。
- 真机日志系统:在小程序里预留一个调试日志开关,开启后记录每次推理的耗时、模型版本、设备型号、图像分辨率。用户反馈问题时,这些日志能省去大量猜谜时间。
- 批量回放工具:把用户标记为"识别错误"的图片收集成测试集,每次模型更新都在测试集上回归跑一遍,看错误率是否有回退。
这三个工具不复杂,但相当有效。没有它们,你以为修好了一个问题,实际上可能只是换了一个坑。
7. 我这个项目走到最后的真实体会
说点不太好听的实话。做这样一个本地AI错题小程序,最难的不是模型调优,也不是框架适配,而是你要在"体验流畅"和"识别准确"之间做大量的取舍。模型精度稍高一点,体积大了,推理慢了,低端机跑不动,体验崩了。模型压到能带出门的规格,边缘案例又识别得稀烂,用户骂声一片。
我的建议是:第一版务必做减法。只支持印刷体试卷、固定学科范围,优先跑通"拍照-切题-分类-入库-复习"的主链路。不要一上来就追求手写识别、复杂表格、多学科知识图谱全覆盖。等你真正在真实用户那儿跑了一段时间,收集到足够多的失败案例,再逐步加功能。这条路才是端侧AI产品落地的正常路径。
第二个体会是,模型的失败提示比模型的准确率更重要。用户拍了一张识别效果很差的题,你需要先告诉他"这道题我没看太清楚",而不是把错的识别结果默默地存进去。一个好的失败提示,会显著降低用户对产品的不信任感。识别准确率99%的产品,遇到1%的失败如果提示得很糟糕,也会给用户留下坏印象。
最后一个建议给团队配置。做本地AI小程序,团队需要有训练模型的算法同学、做端侧推理框架集成的移动端同学,以及能打磨交互细节的产品同学。三个人各司其职,足以支撑一个这样规模的产品。如果缺了某一环,最好先找人补齐再动工,端侧AI最忌讳的就是把模型训练和端侧适配都压给同一个人,最后两边都做不深。
如果你正打算做或者已经在做类似的东西,希望这篇文章能帮你少踩几个坑。端侧AI这条路不难,但坑确实不少,早一点把这些工程上容易被忽略的细节想清楚,后面会顺很多。