第八篇:开发实战与问题解决——填过的坑比大胖老师的枸杞还多

——大胖老师:“架构讲完了,流水线跑通了,王大姐也点过赞了。是不是该写结题报告了?”
——二黑从抽屉里掏出一本厚厚的笔记本,封面写着《文渊慧典填坑日志》,往桌上一拍:“老师,这里面记着我们从第一行代码到昨天为止,大大小小一百七十多个坑。结题报告可以写,但填坑史必须单独成篇。”
——小菜凑过去翻了翻,只见里面密密麻麻记录着:2023年3月12日,内存泄漏,原因:循环内创建PyMuPDF对象未关闭;2023年5月7日,CPU满载,原因:并行线程数设成cpu_count(),忘记留一个核给系统;2023年8月20日,玄学崩溃,原因:某县馆扫描件DPI标称300实际72……
——大胖老师拿起保温杯,看了一眼,又放下了:“二黑,你念几条给小菜听听。让他知道,把代码跑通只是长征第一步。把代码在八百种稀奇古怪的扫描件和老爷机上跑稳,才是真正的西天取经。”
一、十大翻车现场之首:内存泄漏,一个变量名引发的血案
大胖老师点开二黑的填坑日志,翻到第一页:“就从最经典的这个开始说吧——2023年3月的内存泄漏事件。”

二黑回忆道:“那时候我们刚把PDF解析模块写完,用PyMuPDF批量处理一本三百页的地方志。在自己的开发机上跑,一点问题没有。但放到王大姐那台4G内存的老电脑上,跑到一百多页的时候,系统突然卡死,桌面都动不了。王大姐强行关机重启,结果再打开,系统提示内存不足。”
小菜问:“是PyMuPDF的问题?”
“不,是我们自己的问题。”二黑打开当年的代码片段,投到屏幕上,“我当时图省事,在一个循环里写了doc = fitz.open(pdf_path),但循环结束前忘记调用doc.close()。Python的垃圾回收在开发机上内存充裕,来得及自动清理。但王大姐的电脑内存只有4G,PDF一页页打开不释放,到一百多页就把内存撑爆了。”
“更坑的是,”大胖老师补充,“这个bug不是每次都出现。如果王大姐一次只处理五十页,系统正常;处理一百页以上才崩。查了两天才定位到那个漏掉的close。二黑从此养成了一个习惯:凡是打开文件、分配内存的操作,必须配一个with语句或显式的close,然后在注释里写清楚‘此处勿删,否则王大姐的电脑会炸’。这注释风格后来成了我们代码仓库的规范。”
二黑面无表情地补充:“我还写了个脚本,专门扫描代码里所有fitz.open,检查是否配了close。这个脚本现在就挂在CI流水线里,每次提交代码自动跑。从那以后,再没出过内存泄漏。”
二、CPU满载:不要把cpu_count()当线程数
第二个大坑发生在并行处理上。2023年5月,文渊慧典加入了多线程并行识别功能,理论上可以同时处理多页,大幅提升速度。

“当时我写了一句ThreadPoolExecutor(max_workers=cpu_count())。”二黑说,“在我自己的i7-12700上,16个线程全开,CPU占用90%左右,系统还能响应。但王大姐的电脑是i5-8500,6核6线程。全开之后CPU直接100%,连鼠标都动不了,王大姐以为电脑中毒了,拔了电源。”
小菜忍不住笑:“那后来怎么解决的?”
大胖老师接过话:“二黑把cpu_count()改成了cpu_count() - 1,至少给系统留一个核。但这样还是有风险——如果王大姐正在用电脑做别的事呢?所以又加了一个动态检测:先检测当前CPU占用率,如果已经超过50%,线程数就再减半。如果王大姐正在放视频摸鱼,系统自动降速,绝不跟她抢资源。”
“这个功能的灵感,”二黑推了推眼镜,“来自于2018年Windows更新时的‘活动时间’设计——系统更新会尽量选在你不用电脑的时候。我们的OCR也是,宁可慢一点,也不能把王大姐的电脑搞成暖手宝。后来王大姐反馈说‘这系统挺懂事的’,我们觉得这个评价比任何benchmark都值钱。”
三、DPI陷阱:标称300实际72的扫描件
第三个坑,是大胖老师至今提起来还摇头的“DPI欺诈事件”。

“2023年夏天,我们收到某县馆一批扫描件,文件属性里写着DPI=300。但OCR出来的字小得跟蚂蚁一样,准确率惨不忍睹。我们检查了所有参数,都没发现问题。最后二黑用图像的实际像素和纸张物理尺寸反推,发现真实DPI只有72。原来是那台老扫描仪的驱动bug,保存的时候在文件头里写了个300,实际扫描是72。”
二黑补充:“这种问题如果发现不了,后面的处理全错。我们的图像预处理会根据DPI自动调整参数——300DPI下用11x11的二值化窗口,72DPI下就得换成更小的窗口,不然一个字会被切成好几块。后来我们加了一个‘DPI真实性校验’模块:读取图片的实际像素,再根据页面尺寸(A4、B5等)反推真实DPI,如果跟文件头里标的不一致,就以反推值为准,并弹个提示‘此扫描件DPI标称与实际不符,已自动修正’。王大姐看不懂这个提示,但她知道,弹这个框的时候,识别结果更准了。”
四、模型更新的甜蜜陷阱:新版本为什么反而更差了?
2023年11月,PaddleOCR发布了一个新模型,在通用场景下准确率提升了2个百分点。二黑兴奋地更新了依赖,替换了模型文件。结果第二天,王大姐打来电话:“你们是不是改了什么?昨天还能认对的字,今天全错了!”

二黑排查了一整天,发现问题出在“夹注检测”上。新模型的文本检测部分对行间距的敏感度更高,导致古籍的双行夹注被频繁误切成三行,阅读顺序全乱。虽然通用准确率上升了,但古籍的专项指标暴跌。
“这就是‘总体进步,局部退步’的典型。”大胖老师说,“从此我们定了一条铁律:任何模型升级,必须先在‘古籍金标测试集’上跑一遍,把古籍专项指标——尤其是夹注检测召回率、竖排阅读顺序准确率——作为卡控条件。如果专项指标下降超过0.5%,拒绝升级,哪怕通用指标涨了10%也不行。我们的用户不是通用场景,是古籍场景,不能为了追新而牺牲稳定性。”
二黑补充:“后来我们干脆建了一个‘模型回归测试流水线’。每次PaddleOCR发布新版本,自动拉取、自动部署到测试环境、自动跑两千页测试样本、自动生成对比报告。如果古籍指标下降,自动发邮件告警,并锁定当前版本。这套自动化测试体系,后来被PaddleOCR官方注意到了,还邀请我们去做过一次经验分享。”
五、玄学崩溃:换一张图就好,再换回来又不行
“这是我最不想回忆的一个坑。”二黑难得地露出了烦躁的表情,“2024年1月,一个用户报告说,某页扫描件识别到一半就闪退。我们拿到那页图片,在自己电脑上跑,正常。用用户发来的原始文件跑,正常。但用户说,就那一页,在他电脑上必崩。我们远程连过去,果然崩了。同样的代码,同样的模型,同样的图片,开发机没事,他的电脑崩溃。这不就是玄学吗?”

小菜问:“那后来找到原因了吗?”
“找到了。但不是一天找到的,是三天。”二黑说,“最后发现,那张图片的EXIF信息里嵌了一段异常长的元数据——是那台扫描仪自动写入的厂商广告信息,长达几KB。PyMuPDF在解析这张图的时候,会尝试读取EXIF,在某些特定版本的依赖库组合下,会触发一个缓冲区溢出bug,概率性崩溃。我们的开发机装的是新版依赖,没这个问题。用户电脑的依赖库版本稍旧,刚好踩中了这个坑。”
大胖老师总结:“这就是环境一致性的重要性。从那以后,我们把所有依赖库的版本精确锁定到小版本号,写在requirements.txt里,并且用Docker打包了标准运行环境。虽然我们主打轻量化、不需要Docker,但至少提供给用户一个‘如果遇到玄学问题,可以切到Docker模式’的选项。玄学不可怕,可怕的是玄学找不到原因。一旦找到,它就不再是玄学,只是条件苛刻的bug。”
六、老爷机的尊严:在2014年联想扬天上的性能之战
2024年3月,文渊慧典迎来了一次真正的“极限挑战”。某偏远县馆唯一的一台电脑,是一台2014年的联想扬天,AMD A8处理器,4G内存,500G机械硬盘,操作系统还是Windows 7。

“王大姐打电话来说,安装包双击没反应。”小菜回忆,“二黑远程一看,不是没反应,是反应太慢——双击之后,安装程序正在解压,进度条走了二十分钟。这台机器的USB接口还是2.0的,读取速度感人。”
二黑说:“我当时想,光装上还不够,必须让它跑起来。我们做了三个针对性优化。第一,安装包瘦身,把模型文件从安装包里分离出来,做成可选的离线下载包,这样安装包本身只有几十MB。第二,推理时使用内存映射文件加载模型,而不是一次性全读入内存,这样4G内存也能跑动。第三,用固态硬盘的价格给馆里打了个报告,建议花两百块加装一个128G的SSD做系统盘。馆长批了。换上之后,识别速度从每页45秒降到了12秒。虽然比不上新电脑的7秒,但至少茶还没凉就出一页。”
大胖老师感慨:“这就是我们做文渊慧典的底线思维——不能因为用户穷、设备旧,就放弃他们。恰恰相反,越是这样条件艰苦的馆,越需要数字化。我们做性能优化,不是在炫耀技术,是在给那些买不起新电脑的图书馆,送去一张数字化时代的入场券。”
七、当王大姐成为“测试工程师”
“其实我们最怕的不是bug,而是王大姐发现bug的方式。”小菜分享了一个让他哭笑不得的事。

“有一次,王大姐在群里发了一张截图,说‘你们这个系统是不是有反清复明的情结?’我们吓一跳,赶紧点开看。原来是一页明代方志,里面有个‘大明’二字,系统输出的文本变成了‘反清复明’。二黑排查了半天,发现是语言模型在自动纠错的时候,碰到了一个特别诡异的上下文组合,触发了模型内部的一个训练数据污染——可能是训练语料里夹杂了一些网络小说,出现了‘反清复明’这个词组。模型看到‘大明’前面有个‘反’字(实际是‘反大明’的‘反’被误识别了),然后自动联想补全了‘反清复明’。”
大胖老师当场下令:“所有自动纠错,如果原始文本在古籍语料库里有匹配,就不纠;只有现代语料里高频、古籍语料里极低频的替换,才允许纠。而且,凡是涉及朝代名称的,一律不纠。这是红线。”
小菜补充:“后来我们在后处理里加了一个‘敏感词词典’——不是屏蔽,而是标记为‘必须人工校对’。凡是输出中包含这类词,自动标红加粗,提醒王大姐多看两眼。这个功能后来被王大姐表扬‘有良心’,因为‘有些错字我一眼就能看出来,但朝代弄错了,我看不出来’。”
八、二黑的“填坑智慧”
讲完这些坑,大胖老师让二黑总结一下,从这些填坑经历里,到底学到了什么。

二黑站起来,在白板上写了三行字:
-
永远在最低配的环境里测试——开发机跑得顺不算顺,王大姐的电脑跑得顺才算。
-
永远用真实数据测试——你永远想不到用户会用什么样的扫描件、什么样的文件名、什么样的操作顺序。
-
永远给用户留一条“还原”的路——每一个自动操作,都要有手动开关;每一个自动纠错,都要能一键还原到原始输出。机器可以聪明,但不能替用户做不可逆的决定。
大胖老师满意地点头:“这三点,比任何技术论文都值钱。论文是写给同行看的,这三条是写给用户用的。”
九、下期预告
小菜合上笔记本,提问:“这些坑都是开发阶段的。那开发完了,怎么证明系统真的能用了呢?总不能发给王大姐说‘你试试,有bug再告诉我’吧?”

大胖老师摇头:“当然不能。我们有一整套测试体系——魔鬼测试集、回归测试流水线、真实场景压力测试、以及全国各地图书馆的试用反馈。下一期,就讲项目测试。”
二黑打开一个文件夹,里面全是测试报告:“我用两万页古籍把文渊慧典往死里测过。有虫蛀的、透背的、手抄的、手机拍的、朱墨套印的、甚至还有水泡过的。每一项测试都有数据,有对比,有翻车记录。”
大胖老师拍板:“那就下期,主题就叫——”
《我们用两万页古籍把文渊慧典往死里测,结果……它活下来了》
“小菜,把王大姐上次误操作的那个case准备好——她把整个文件夹拖进去,里面不仅有扫描件,还混了一张她孙子的满月照。系统居然没崩,还认出了照片上的‘百日留念’四个字。这个故事,下期一定要讲。”
窗外,王大姐又传来一批新扫描的族谱。这一次,文件名整整齐齐,没有混杂孙子的照片。
本文为注水技术版,您看看即可,不必当真,写此文字就是图一乐:-)

1万+

被折叠的 条评论
为什么被折叠?



