资讯动态

Dify+多模态大模型:智能铭牌识别工作流实践

发布时间:2026/10/26 14:07:46 来源:尧图企业网站定制
1. 先说清楚这个项目到底在解决什么问题1.1 一次被骂出来的需求我第一次接触“铭牌识别”这个场景是在一个设备台账数字化的项目里。现场运维的人抱着一台机器对着铭牌拍了张照然后要在系统里手工敲进去十来个字段型号、额定功率、电压、电流、出厂编号、生产日期、厂商、防护等级……一台机器两三分钟一个厂区几千台设备光录入就够一个小组干上两个月。更麻烦的是手抄很容易出错字母 O 和数字 0、字母 l 和数字 1“3kW”写成“30kW”这种事我都见过。于是很自然地想到了用技术解决。第一反应是上传统的 OCR拍图、识别文字、正则抽取字段。实际跑下来问题很集中——铭牌上的文字排版千奇百怪有的是表格有的是竖排有的字段名和值挤在一行还有大量中英混排、符号缩写比如“IP55”“Y2-132M-4”这类型号编码。纯 OCR 只能把一堆字吐出来谁是谁完全分不清正则写到最后变成了一堆 if-else 的屎山稍微换个厂商的铭牌就全线崩。第二反应是直接调多模态大模型把图丢进去让它按 JSON 输出字段。效果确实好了一大截模型能理解“额定功率”和“3kW”之间的对应关系也能猜出被锈斑遮住半个字的字段名。但裸调模型也有它的麻烦提示词散落在代码里、改一次要发一次版、不同场景想换模型要改代码、没有可视化调试、多个客户要用不同字段模板时管理混乱。最后落地的方案是**用 Dify 做应用编排把图像预处理、多模态大模型抽取、规则后处理、字段校验串成一条可视化工作流外层用一段轻量 Python 代码负责接入业务系统。**这就是标题里说的“基于 Dify 开发的多模态大模型应用-智能铭牌识别”。这套东西解决的核心问题其实就三个把非结构化的现场照片变成结构化字段把提示词和模型配置从代码里抽出来让非算法同学也能调把识别结果的可信度量化出来让人工只复核有疑问的那部分。适合谁参考我觉得是三类人做设备管理/资产管理数字化的开发想入门多模态应用但不知道从哪下手的工程师以及手里已经有一堆现场照片、苦于没法批量结构化的人。1.2 为什么是“Dify 多模态”这个组合拆开来讲Dify 在这条链路里承担的角色是编排层。它把“输入图片 → 调模型 → 拿结果 → 跑一段自定义代码 → 输出”这件事变成了可视化的节点流好处是很实在的提示词在界面上改保存即生效不用重新部署模型可以在配置里切换今天用云端多模态接口明天换成本地部署的开源视觉模型业务代码一行不动每个节点的输入输出都能在日志里看到出问题的时候能立刻定位是“图片没传对”还是“模型抽错了”还是“后处理把正确的值改坏了”。多模态大模型承担的是语义抽取层。它跟 OCR 的根本区别在于OCR 给的是“像素到字符”的映射多模态模型给的是“图像到语义”的映射。铭牌上一行写着“额定功率 3 kW”OCR 吐出来是四个孤立的文本块多模态模型能直接告诉你“rated_power 3kW”。这个差别在字段结构化任务上是决定性的。中间我还加了一层轻量图像预处理也就是在图片进模型之前做方向纠正、尺寸压缩、对比度增强。这一步看着不起眼但对识别率的影响比我预想的大得多——同一批照片光是做了 EXIF 方向纠正竖拍照片的字段完整率就从七成出头拉到了九成以上。2. 整体链路设计与关键决策2.1 从一张现场照片到一条台账记录的完整链路整条链路由四段构成我在设计时特意把每一段的边界划清楚因为这类项目最容易出问题的地方就是“不知道错误是哪一段引入的”。第一段是采集端。手机或手持终端拍照图片通过接口上传。这里有个容易被忽略的点上传前就要做一次尺寸约束长边压到 1280 像素左右质量 85~88。原因是现场照片动辄 4000×3000、三四兆一张直接送进模型既慢又贵而对铭牌这种主体占画面比例较大的场景1280 长边已经足够模型看清每一个字符。实测下来压缩前后的字段抽取准确率差异在 1 个百分点以内但单张耗时能从七八秒降到三秒左右。第二段是Dify 工作流。核心节点只有四个开始节点接收图片变量一个视觉大模型节点负责“看图说话”按预设的 JSON Schema 输出字段一个代码节点做规则清洗和数值归一化结束节点把结构化结果吐出来。中间我还挂了一个条件分支用来处理“一张图里有多个铭牌”的情况这个后面细说。第三段是业务侧后处理。Dify 返回的 JSON 并不是直接入库的还要经过一层校验必填字段有没有缺、数值有没有超出合理区间、型号编码的字符集对不对、跟历史台账比对有没有明显冲突。校验不通过的记录打上标记进入人工复核队列。第四段是反馈闭环。人工复核的结果会被记录下来定期回灌成提示词里的 few-shot 示例。这一步是整个系统能越用越准的关键也是很多人做完就忘掉的一步。2.2 为什么不走“纯 OCR 后接规则”的老路我认真试过这条路结论是在铭牌这个特定场景下纯 OCR 加规则的维护成本会随着厂商数量线性上升而且是那种“越修越脆”的上升。举个例子。某个厂商的铭牌是表格式排版字段名在左列、值在右列OCR 出来是“型号 | 额定功率 | 电压 | 电流”一行下面一行是“Y2-132M-4 | 3kW | 380V | 6.8A”。这种情况下你需要靠坐标位置去配对也就是得同时拿到 OCR 的文本框坐标再写一套基于行列关系的匹配算法。换一个厂商排版变成“字段名: 值”的横向列表你的坐标算法就得重写。再换一个竖排的铭牌坐标算法彻底失效。多模态模型的好处是它天然理解“布局语义”你不需要告诉它“左边是键右边是值”它自己看得出来。代价是它可能看错所以要用后处理去兜。用规则兜底模型而不是用规则替代模型这个思路上的转变是我在这个项目里最大的收获。2.3 为什么选择在 Dify 里做编排而不是自己写脚本坦白说如果只是自己一个人用、只服务一个客户、字段模板永远不变写个 Python 脚本调 API 是最快的半天就能跑通。但我遇到的实际情况是字段模板有四五套不同设备类型字段不一样、提示词在调试期一天改十几遍、后面还想换更便宜的模型试试效果。这三件事在裸脚本里的成本分别是字段模板靠参数传入越传越乱提示词改一次要重启服务换模型要改 SDK 调用方式和返回解析逻辑。而在 Dify 里这三件事分别对应“建多个应用/用变量控制”“界面上改完保存”“模型下拉框里换一个”。省下来的时间足够我把后处理规则打磨得更细。还有一个隐性好处工作流的运行日志是可视化的。给客户演示、给同事交接、自己排查问题都能直接看到“这张图进来的时候是什么样、模型返回了原始什么、代码节点改了什么”。这比在日志文件里 grep 一堆 JSON 舒服太多。3. 动手实现从零搭一条可用的识别流水线3.1 环境准备与 Dify 端的应用配置Dify 的部署方式按官方文档来就行本地用容器方式起一套注意把存储卷挂出来不然文件上传的记录重启就没了。起好之后进控制台创建一个工作流类型的应用不要建聊天助手类型——这两者的区别在于工作流是单次输入单次输出更适合这种“一张图进、一条 JSON 出”的场景也更好做批量。工作流建好之后按下面的顺序搭节点第一个节点是开始节点定义一个变量image类型选单文件file限定只接受图片。这个变量的作用是把业务侧上传的文件 ID 传进来。第二个节点是视觉大模型节点。模型选择上云端的多模态接口和本地部署的开源视觉语言模型都能用区别主要是成本和数据出域的问题。如果铭牌图片涉及敏感的设备信息建议走本地部署如果只是通用设备台账云端接口的开发效率更高。这个节点里最关键的是提示词和输出格式约束下一节展开。第三个节点是代码节点语言选 Python负责清洗、归一化、补默认值。这个节点的入参就是上一个节点的文本输出。第四个节点是结束节点把代码节点的输出映射成最终的结构化字段。配完之后在界面上用“运行”按钮试几张图看看中间每个节点的输入输出这一步别跳过很多问题在这里就能看出来。比如我就遇到过一次模型节点输出的 JSON 外面包了一层 markdown 代码块标记代码节点解析直接报错在日志里一眼就能看到。3.2 提示词与输出结构的设计要点提示词这块我踩过的坑最多总结下来有几条是必须写进去的第一只输出 JSON不要任何解释性文字。这一点必须明确写否则模型很爱在 JSON 前面加一句“根据图片内容识别结果如下”。虽然可以在后处理里用正则把 json 剥掉但从源头掐掉更省事。第二字段缺失时填 null绝对不允许编造。这是最重要的一条。多模态模型有个坏习惯看到“额定功率”这四个字但值看不清它会根据型号猜一个填上去。对台账系统来说猜错比空着危险得多。所以提示词里要反复强调“无法确认的字段一律输出 null不要推测”。第三单位和格式的约定写在提示词里。比如功率统一输出瓦特数值3kW 输出 3000电压统一输出伏特整数日期统一YYYY-MM-DD型号保留原始大小写和连字符。这些约定如果只写在代码里模型返回的还是带单位的字符串你就得在代码里再写一堆解析逻辑不如让它一步到位。第四给两三个示例。铭牌的字段抽取是个典型的格式敏感任务给两三个不同排版的示例一个是表格排版、一个是列表排版准确率提升非常明显。示例不用多多了反而占 token。输出结构我用的是一份固定字段的 JSON大致长这样{ model: 设备型号字符串看不清填 null, rated_power_w: 额定功率单位为瓦特的数字看不清填 null, rated_voltage_v: 额定电压单位为伏特的数字, rated_current_a: 额定电流单位为安培的数字, serial_no: 出厂编号字符串保留原始大小写与连字符, manufacture_date: 生产日期格式 YYYY-MM-DD只有年月则补 01, protection_level: 防护等级如 IP55, manufacturer: 厂商名称字符串, confidence: 0 到 1 之间的数字表示整体识别把握程度 }最后那个confidence字段看起来有点玄学但它确实有用——让模型自己给一个把握程度配合后处理的规则可以粗略地把“这张图大概率要人工看一眼”的记录筛出来。它的绝对值不准但相对排序是有参考价值的我一般会把低于 0.6 的丢进复核队列。3.3 图像预处理那几行容易被跳过的代码预处理这块的代码不多但每一行都有它的理由。先说结论方向纠正、尺寸压缩、对比度与锐度轻微增强这三件事做完识别率能有一个明显提升而且耗时和成本都下降。from PIL import Image, ImageOps, ImageEnhance def preprocess(src_path, dst_path, max_side1280, quality88): img Image.open(src_path) # 关键一步按 EXIF 信息把照片转正手机竖拍的图不带这步会横过来 img ImageOps.exif_transpose(img) w, h img.size scale max_side / max(w, h) if scale 1: img img.resize((int(w * scale), int(h * scale)), Image.LANCZOS) # 轻微增强参数不要贪大过犹不及 img ImageEnhance.Contrast(img).enhance(1.15) img ImageEnhance.Sharpness(img).enhance(1.20) img.convert(RGB).save(dst_path, JPEG, qualityquality, optimizeTrue) return dst_path方向纠正为什么要单独强调因为手机拍的照片里实际像素方向和显示方向经常不一致靠 EXIF 里的 Orientation 标记来纠正。如果不处理竖着拍的铭牌到了模型眼里就是躺倒的横排文字变成竖排识别率掉得厉害。这个问题在本地看图片时完全发现不了看图软件会自动转正只有传到服务端才会暴露。对比度和锐度的增强系数我一直控制在 1.1~1.2 之间。试过 1.5结果噪点也被放大了模型反而更容易把锈斑看成字符。参数这东西够用就行。如果你的场景里铭牌经常是斜着拍的、有明显的透视变形可以再加一步透视矫正先用边缘检测找到铭牌的四个角点做一次四点透视变换把它拉正。这一步对纯 OCR 帮助很大对多模态模型来说收益没那么明显因为它对轻微变形本身就有一定鲁棒性。所以我一般把它作为可选项只有在某些特定批次的照片上效果不好时才启用。3.4 调用 Dify 工作流的完整代码业务侧要做的就两件事把图传上去拿到文件 ID然后带着这个 ID 调工作流。下面这段是能直接跑的import os import requests DIFY_BASE os.environ.get(DIFY_BASE, http://127.0.0.1/v1) API_KEY os.environ.get(DIFY_APP_KEY, app-xxxxxxxxxxxxxxxx) USER_ID plate-recognizer def upload_file(path, timeout30): 上传本地图片返回 Dify 侧的文件 ID name os.path.basename(path) with open(path, rb) as f: resp requests.post( f{DIFY_BASE}/files/upload, headers{Authorization: fBearer {API_KEY}}, files{file: (name, f, image/jpeg)}, data{user: USER_ID}, timeouttimeout, ) resp.raise_for_status() return resp.json()[id] def run_workflow(file_id, timeout90): 阻塞模式跑一次工作流返回 outputs 字典 payload { inputs: { image: { transfer_method: local_file, upload_file_id: file_id, type: image, } }, response_mode: blocking, user: USER_ID, } resp requests.post( f{DIFY_BASE}/workflows/run, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json, }, jsonpayload, timeouttimeout, ) resp.raise_for_status() data resp.json() if data.get(data, {}).get(status) ! succeeded: raise RuntimeError(fworkflow failed: {data}) return data[data][outputs] def recognize(path, workdir/tmp/plate): os.makedirs(workdir, exist_okTrue) fixed os.path.join(workdir, os.path.basename(path).rsplit(., 1)[0] _pre.jpg) preprocess(path, fixed) fid upload_file(fixed) return run_workflow(fid)几个实操上的注意点。response_mode我用的blocking也就是同步等结果适合单张交互式场景。如果是批量跑几千张建议改成streaming或者干脆用批处理接口同步模式下并发一高就容易超时。超时时间给到 90 秒是留了余量的正常情况下三五秒就返回了但遇到模型侧排队或者网络抖动太短的超时会导致大量无谓的重试。文件上传接口里那个user字段不是可选的Dify 用它来区分会话和统计用量随便传个固定字符串就行但别传空。另外上传的文件是有生命周期的别指望传一次能反复用每次识别都老老实实传一遍最稳妥。3.5 代码节点里的清洗与归一化Dify 工作流中间那个代码节点是整个链路的“除错层”。模型返回的东西看着规整实际用起来你会发现它总能在意料之外的地方给你惊喜功率给了个“3 kW”带空格的字符串、日期给了“2023年5月”、型号里的连字符被识别成了破折号、该填 null 的地方给了空字符串。import json import re NULL_TOKENS {, null, none, n/a, na, -, --, 未知, 不详} def _to_null(v): if v is None: return None s str(v).strip() return None if s.lower() in NULL_TOKENS else s def _to_number(v, unit_scaleNone): 从字符串里抠出数字支持 3kW / 3,000W / 3000 这几种写法 s _to_null(v) if s is None: return None m re.search(r[-]?\d[\d,\.]*, s) if not m: return None num float(m.group(0).replace(,, )) low s.lower() if unit_scale: for unit, factor in unit_scale.items(): if unit in low: num * factor break return num def _norm_date(v): s _to_null(v) if s is None: return None m re.search(r(\d{4})\D{0,2}(\d{1,2})(?:\D{0,2}(\d{1,2}))?, s) if not m: return None y, mo, d m.group(1), int(m.group(2)), int(m.group(3) or 1) if not (1 mo 12): return None if not (1 d 31): d 1 return f{y}-{mo:02d}-{d:02d} def _norm_model(v): 型号里的常见误识别破折号统一、去空格、全角转半角 s _to_null(v) if s is None: return None trans str.maketrans({–: -, —: -, : -, : , : O, : 0}) return re.sub(r\s, , s.translate(trans)).upper() def main(image_result: str) - dict: try: raw json.loads(image_result) except json.JSONDecodeError: # 模型偶尔会包一层 json 围栏兜一下 cleaned re.sub(r^(?:json)?|$, , image_result.strip(), flagsre.M).strip() raw json.loads(cleaned) power _to_number(raw.get(rated_power_w), {kw: 1000, w: 1}) voltage _to_number(raw.get(rated_voltage_v), {kv: 1000, v: 1}) result { model: _norm_model(raw.get(model)), rated_power_w: power, rated_voltage_v: voltage, rated_current_a: _to_number(raw.get(rated_current_a), {a: 1}), serial_no: _to_null(raw.get(serial_no)), manufacture_date: _norm_date(raw.get(manufacture_date)), protection_level: (_to_null(raw.get(protection_level)) or ).upper() or None, manufacturer: _to_null(raw.get(manufacturer)), confidence: _to_number(raw.get(confidence)), } missing [k for k, v in result.items() if v is None and k ! confidence] result[_missing_fields] missing return result这段代码里我最看重的两处一是_to_null把各种“看起来是空但又不是 null”的东西统一收口避免空字符串和 null 混在数据库里二是_missing_fields这个附加字段它让下游一眼就能知道这条记录缺了啥缺三个以上直接进复核队列缺一两个的可以先入库再补。这个小设计在后面的运营阶段省了很多沟通成本——业务方看到_missing_fields就知道该查什么不用来问我“为什么这条没记录”。3.6 批量场景下的并发与限流单张跑通之后下一个需求一定是批量。这里我给个能用的骨架核心是控制并发数别把模型接口打崩from concurrent.futures import ThreadPoolExecutor, as_completed def batch_recognize(paths, max_workers4): results, errors [], [] with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {pool.submit(recognize, p): p for p in paths} for fut in as_completed(future_map): p future_map[fut] try: results.append({path: p, data: fut.result()}) except Exception as e: errors.append({path: p, error: str(e)}) return results, errorsmax_workers设 4 是我这边实测下来比较稳的值。设到 8 的时候开始出现接口限流错误重试虽然能兜住但整体耗时反而更长。这个值跟你的模型侧配额强相关没有通用答案建议从 2 开始往上试盯着错误率看一旦错误率上去了就往回收。另一个建议是给批量任务加个本地缓存以图片内容的哈希值为键。现场采集经常有重复照片同一个人拍了两遍、或者同一个设备被不同人拍了缓存能省掉这部分开销在几千张的批次里能砍掉一两成的调用量。4. 踩坑记录那些文档里不会写的问题4.1 识别不准时的排查顺序遇到识别不准最忌讳上来就改提示词。我总结了一个从外往里的排查顺序按这个走八成问题十分钟内能定位。**第一步看图片本身。**把预处理之后的图打开看一眼是不是糊了、是不是太暗、是不是铭牌只占了画面十分之一。很多时候问题根本不在模型是采集端的问题。我遇到过一次批量识别率暴跌最后发现是采集的兄弟为了省事用广角模式拍铭牌在画面角落里只有几百像素模型当然看不清。**第二步看模型原始输出。**在 Dify 的运行日志里看模型节点吐了什么。如果它把字段名和值都识别出来了只是配对错了那是提示词的问题如果它整个字段都没提那是提示词里没定义这个字段或者图片里确实看不到。**第三步看代码节点。**清洗逻辑是有可能把正确的值改坏的。比如我那个_norm_model里做了大写转换结果某个厂商的型号本身就带小写且大小写有含义被我改坏了。这种问题很隐蔽因为报错不会有只是数据悄悄错了。**第四步看校验规则。**有些“识别错误”其实是校验规则写得太严。比如防护等级我一开始限定了必须是IP加两位数字结果遇到IP5XX 表示未指定防尘等级就被判为无效。4.2 常见问题速查表下面这张表是我这两年里实际碰到过的问题按出现频率排的现象大概率原因处理办法竖拍照片识别率明显低于横拍EXIF 方向未纠正预处理里加exif_transpose字段名识别出来了但值全是 null图片分辨率太低或压缩过度检查长边是否低于 1000px提高 JPEG 质量型号里的 0 全变成 O1 变成 l字符形近模型视觉混淆型号字段做字符集白名单校正结合历史型号库模糊匹配功率出现 30000 这类离谱值单位误读kW 当成 W提示词强制要求换算成瓦特代码里加区间校验一张图返回了两个设备的字段画面里有多台设备提示词明确“只识别画面中最大的铭牌”或在采集端加裁剪返回内容不是合法 JSON模型自作主张加了说明文字提示词强约束 代码里做围栏剥离兜底同一张图两次识别结果不一致模型采样随机性温度参数调到 0 或 0.1关键字段做双次比对单张耗时超过 15 秒图片过大或模型侧排队压缩尺寸、检查配额、改异步模式4.3 关于“双次比对”的一个小技巧上面表格里提到的那条值得单独展开。多模态模型即使在温度设为 0 的情况下返回结果也不保证完全确定尤其是在字段边界模糊的时候。我用的办法是对关键字段型号、额定功率、电压、出厂编号跑两次两次结果一致就直接采信不一致就取置信度高的那次并把记录标记为“需复核”。这个做法会增加一倍的调用成本所以我只在关键字段上做而且只对置信度低于 0.8 的记录做。实际算下来大概只有两成多的记录会触发二次调用成本增加不到三成但关键字段的准确率能提升好几个百分点。这笔账在台账类系统里是划算的因为错一条数据带来的返工成本远高于多调一次接口。还有一个小细节二次调用时我会在提示词里加一句“请特别注意校验型号和功率数字”这个“定向提示”对提升第二次的准确率有点作用虽然不严谨但实测有效。这算是提示词工程里比较玄学但确实管用的东西。5. 上线之后的一些体会和还能继续做的事这套东西上线跑了几个月最大的感受是**识别准确率不是这个项目的核心指标人工复核率才是。**一开始我死盯着准确率从 82% 调到 91%觉得很有成就感。但业务方关心的是“我要看一眼的比例有多高”这个指标从最初的六成降到了两成多他们的工作量才真正降下来。而推动人工复核率下降的不完全是模型变准了更多是后处理规则和校验逻辑越来越贴合实际业务——很多错误被规则提前拦住了根本走不到人工那一步。如果让我重做一遍有两件事我会更早做。一是更早地建立型号库。设备型号这个字段看着简单但形近字太多纯靠模型识别效果有天花板。后来我拿历史台账里已有的几千个型号做了一份白名单识别结果先跟白名单做模糊匹配匹配度高的直接采用白名单里的标准写法准确率一下就上去了。这件事应该在做模型之前就做。二是更早地把提示词纳入版本管理。Dify 界面上改提示词确实方便但方便的另一面是容易改乱改了以后效果变差还回不去。后来我养成习惯每次改提示词都在应用描述里记一笔改了什么、为什么改、改完在固定测试集上的表现变化。这个习惯听起来很笨但它救过我两次。后面还能扩展的方向也挺多。最直接的是把识别结果直接对接设备台账系统做成“拍照即入库”稍远一点的是把这套工作流复用到其他类似的场景比如仪表盘读数识别、产品标签识别、证书信息抽取这些场景的链路结构几乎是一样的只是提示词和字段表不一样。我试过把同一个工作流改个提示词就去做压力表的读数识别一天之内就能出个能演示的版本这正是把编排和业务代码分开的价值所在。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑