1. 为什么偏偏选 Dify 来做铭牌识别先说场景。做过设备台账、资产盘点、工厂巡检这类活的人都知道最烦的从来不是识别这件事本身而是那堆贴在电机、水泵、配电柜、空压机上的金属铭牌。巴掌大一块铝板密密麻麻印着型号、额定电压、额定电流、功率、频率、防护等级、出厂编号、生产日期、执行标准还有一堆认证标志。现场人员举着手机拍一张照片回去之后要么手打录入到 Excel 或者资产系统里要么就堆在相册里没人管。一个中型厂区上千台设备一个人一天能录两百条已经算手快了还特别容易把0打成O、把380V写成38OV。基于 Dify 开发的多模态大模型应用-智能铭牌识别做的就是把这件拍照—录入—校验的重复劳动压缩成一次上传。核心思路很直接借助 Dify 这个可视化的大模型应用编排平台把图像理解和结构化输出两件事串成一条工作流前端传一张铭牌照片后端直接吐出干净的 JSON 字段能对上台账就直接入库对不上的打回人工复核。它解决的是非结构化图像到结构化数据的最后一公里问题适合三类人参考一是想给内部业务系统加个拍照录入能力的后端开发二是手上有一堆图片数据要批量处理的实施或数据岗三是刚开始接触大模型应用、想找一个完整小项目练手的同学。我前后在这套链路上折腾了小半年从最早的直接把图片丢给模型让它读的粗暴做法一路迭代到现在带预处理、带校验、带人工回流的三段式流程。中间踩的坑比想象中多模型读得出来但格式每次都不一样、单位五花八门、反光铭牌直接瞎编、批量跑起来把额度烧光。下面把我这套东西完整拆开讲包括代码和那些文档里不会写的细节。2. 需求拆解与技术选型为什么不是传统 OCR动手之前得先想清楚一件事铭牌识别到底难在哪里。想明白这个选型基本就定了不用纠结。2.1 铭牌识别真正的难点不在看清字很多人第一反应是这不就是 OCR 吗。如果铭牌都是标准印刷体、正面平拍、光照均匀那确实传统 OCR 一骑绝尘又快又便宜。但真实现场是这样铭牌是金属拉丝面闪光灯一打一片白设备用了七八年字迹被油污和锈蚀吃掉一半安装位置刁钻只能斜着仰拍透视变形严重同一个参数区有的是表格排版有的是参数名值竖排有的干脆印成一行小字单位混用功率有的写 kW 有的写 HP电压有的标 220~240V 有的标单相 230V。更要命的是语义层面的混乱。传统 OCR 给你的是一个文本框列表它认得A和1但它不知道A才是型号的一部分也不知道1.5后面跟着 kW 才是功率。要把这些散落的文本重新拼成型号XXX、功率1.5kW这种带语义的键值对得再写一堆规则和模板每来一个新品牌的铭牌就得加一条正则维护成本是个无底洞。这就是我认为多模态大模型在这个场景里真正有价值的地方它输出的不是文字而是理解后的结构。你告诉它提取型号字段它能在一堆噪声文字里挑出型号甚至能根据上下文补全被遮挡的半个字符——当然这个能力也是双刃剑后面会讲它瞎编的时候怎么防。2.2 传统 OCR 多模态大模型的分工我最后的方案不是二选一是分工。判断逻辑其实简单维度传统 OCR多模态大模型我的用法字符级准确率清晰图极高较高清晰图优先 OCR版面理解与字段归属无强靠模型做遮挡、污损补全无有但有幻觉风险仅做参考不落库单位换算与归一化需硬编码可用提示词约束模型给原始值代码做归一化单张成本极低有成本大批量先过滤多语言混排一般好靠模型实际的链路是这样的先用图像质量评分做一道筛子太糊的直接打回重拍不浪费模型调用清晰图走 OCR 拿原始文本同时把图片和 OCR 文本一起喂给多模态模型让它基于图像 文本双通道做字段抽取。双通道的好处是互相印证——模型看错了但 OCR 认对了文本能兜住OCR 被反光干扰了模型还能凭图像语义猜个大概。实测下来比单走任何一路都稳。2.3 Dify 在这个方案里到底承担什么角色有人会问这套东西我直接用模型 API 写个脚本不就完了为什么要套一层 Dify。我的理由是三条都很实际。第一流程编排可视化。铭牌识别这条链路天然是有分支的图像预处理、质量判定、抽取、校验、兜底重试、人工复核。用纯代码写这些 if-else 很快就把函数写成面条在 Dify 里就是画几个节点连起来条件分支用现成的 IF/ELSE 节点后面调整顺序、加一个低置信度走二次抽取的分支改两下连线就行不用重构代码。第二提示词和模型解耦。多模态模型迭代很快今天这个便宜明天那个准业务上经常要换。如果提示词硬编码在代码里换一次模型就得改一遍代码、发一次版。Dify 里应用配置和提示词是配置项换模型只改一个下拉框提示词还能在界面上做版本对比A/B 试哪个效果好这个对非算法岗的同事特别友好。第三天然的 API 出口。工作流发布之后自动给你一个 HTTP 接口前端传图片拿 JSON不需要自己写服务、部署、搞鉴权。对内部工具类需求这一条直接省掉一周工作量。当然边界也要说清楚Dify 不是万能的。它的代码节点运行环境有限制依赖不能随便装超时时间也不长复杂的图像预处理比如透视矫正、去反光我是在外部先做掉再上传的不指望在节点里跑 OpenCV 全套。另外它的并发和排队策略是平台侧的真要跑几十万张的批量任务得自己在外面做队列和限流不能一把梭往里塞。3. 整体架构一条四段式流水线架构定下来之后再写代码会省很多返工。3.1 四个环节的职责划分我把整条链路切成四段每段职责单一出问题好定位接入层负责图片接收、格式校验jpeg/png/webp单张不超过限制、大小压缩、EXIF 方向纠正、生成任务 ID。这一层在业务系统侧不进 Dify。预处理层图像质量打分清晰度、亮度、倾斜角低分直接给出请重拍的提示。清晰图可选做自动裁边和对比度增强。抽取层Dify 工作流主体。上传文件 → 多模态模型抽取 → 代码节点做字段归一化与合法性校验 → 条件分支决定直出还是二次抽取。落地层结果写库置信度低于阈值的进人工复核队列人工改完的样本回流成评估集。这个切法最大的好处是成本可控。质量分这道筛子能挡掉大概两成的废片二次抽取只对疑难图触发真正烧额度的只有中间那一小撮。我算过账比起无脑全量调模型这套下来调用量降了三分之一还多。3.2 输出字段设计先定 Schema 再写提示词这一点是我最早的教训。第一版我图省事直接让模型把铭牌信息整理成 JSON结果它每次给的字段名都不一样有时叫model有时叫型号有时嵌套一层spec下游解析代码天天改。后来我把字段表定死提示词里强制要求代码节点再做一次 schema 校验才算稳定。核心字段清单如下具体业务可以按需裁剪{ device_name: 设备名称如三相异步电动机, model: 型号原样保留不做翻译, serial_no: 出厂编号/序列号去空格, rated_voltage: 额定电压统一为 V多值时保留原文, rated_current: 额定电流统一为 A, rated_power: 额定功率统一为 kW, frequency: 频率统一为 Hz, protection_level: 防护等级形如 IP55, manufacturer: 制造厂商, manufacture_date: 生产日期归一化为 YYYY-MM, standard: 执行标准号, confidence: 整体置信度0~1, uncertain_fields: [不确定的字段名列表], raw_text: 铭牌上的全部可见文字按阅读顺序 }两个设计细节值得单独说。一是uncertain_fields让模型主动标出它没把握的字段比只给一个总置信度有用得多——总置信度 0.9 但型号那项是猜的这种图落库就是个雷。二是raw_text一定要留这是后面人工复核和问题排查的唯一依据也是迭代提示词时最有价值的素材。3.3 工作流还是对话流Dify 里两种应用形态都能做但铭牌识别我强烈建议用工作流Workflow不要用对话助手Chatflow。原因很实在这个任务是无状态的、一次性的、输入输出格式固定的不需要多轮上下文。用对话流的话历史消息会带上模型容易受上一张图干扰而且每次调用都要传会话 ID批量处理时管理麻烦。工作流模式输入即输出接口干净做批量脚本时直接循环调用就行。4. 在 Dify 里把工作流搭起来这部分是实操重点我按节点顺序一个个说包括每一处为什么这么配。4.1 前置准备模型选择与变量定义先建应用类型选工作流。进去之后第一件事是定义开始节点的输入变量这里我定义了三个plate_image类型 File单文件限制 image必填。这是主输入。ocr_text类型 文本选填。如果外部已经跑过 OCR把文本带进来模型判断会更准没有就留空。strict_mode类型 文本选填默认off。开启后模型只输出铭牌上明确存在的字段拒绝任何推理补全。选模型这块原则是能看清小字 输出结构稳定。我试过几款不同量级的多模态模型体感是参数量大一些的模型在密集小字上明显更稳尤其是那种参数区一行挤七八项的铭牌小模型速度快、便宜但容易把相邻两行的值串到一起。建议正式跑之前自己造 30 张有代表性的测试图清晰、反光、锈蚀、斜拍、英文铭牌各几张把候选模型都跑一遍看字段级准确率而不是整体观感。我当初就是被几张看起来很准的样例骗了全量跑起来才发现型号字段错得离谱。提示模型选择不要只看单次效果一定要看输出格式的稳定性。有些模型内容对得很但 JSON 偶尔漏个引号或者多句解释性文字你的解析代码就崩了。4.2 图像预处理节点的替代方案前面说过Dify 代码节点的运行环境不适合跑重型图像处理。我的做法是把预处理放在外部工作流内只做一个轻量的质量判定如果业务系统侧已经把图片处理好压缩到长边 1600px 左右、纠正方向、必要时裁掉背景工作流里其实可以直接进抽取节点。非要在这里做判定的话可以用一个 LLM 节点做视觉质量评估提示词大致是判断这张铭牌照片是否适合识别文字是否清晰可辨、是否严重反光或遮挡、是否倾斜过大。仅输出 PASS 或 FAIL 加简短理由。这个判定不追求精确作用是挡掉明显废片。代价是多一次模型调用所以如果你的图源质量本来就不错比如都用固定工装拍摄这一步可以省掉我后来的版本就砍了。4.3 抽取节点的提示词怎么写才不飘这是整个应用的核心我前后改了七八版。先给完整版再逐条解释为什么这么写。你是工业设备铭牌信息抽取工具。你的唯一任务是从给定图片以及可能提供的 OCR 文本中抽取结构化字段并严格按指定 JSON 格式输出。 【输入】 图片铭牌照片 OCR 文本可能为空{{#start.ocr_text#}} 严格模式{{#start.strict_mode#}} 【抽取规则】 1. 只提取铭牌上真实存在的文字信息不要根据常识推断、补全或翻译。 2. 型号、出厂编号、执行标准号必须逐字符原样保留包括大小写、连字符、斜杠、小数点。不要规范化不要纠正看似笔误的字符。 3. 数值字段保留数字与单位若铭牌标注多个值如 220~240V、50/60Hz原样保留该表达不做换算。 4. 无法辨认的字符用 ? 占位例如 SN-202?0345不要猜。 5. 铭牌上不存在的字段值填空字符串 并在 uncertain_fields 中加入该字段名。 6. 若图片模糊、反光或遮挡导致某个字段只能靠推测即使推测值合理也必须把字段名加入 uncertain_fields。 7. 严格模式为 on 时任何需要推理才能得出的值一律留空并标记 uncertain。 【输出要求】 仅输出一个 JSON 对象不要输出任何解释文字、不要使用 Markdown 代码块包裹、不要在 JSON 前后添加任何字符。 JSON 结构必须严格为 { device_name: , model: , serial_no: , rated_voltage: , rated_current: , rated_power: , frequency: , protection_level: , manufacturer: , manufacture_date: , standard: , confidence: 0.0, uncertain_fields: [], raw_text: } 字段说明 - confidence对本次抽取整体准确性的自评0 到 1 的小数。 - uncertain_fields字符串数组列出你不确定的字段名没有则给空数组。 - raw_text把铭牌上所有可见文字按从上到下、从左到右的顺序拼接用换行分隔辨认不清的字符用 ? 代替。几个关键设计点都是踩坑换来的规则 2 里的不要规范化。最开始我写的是型号请规范化为大写结果模型把MTR-3a/2改成MTR-3A/2台账里跟历史数据对不上了。工业场景里型号就是主键一个字符都不能动。规则 4 的?占位。这一条效果出乎意料地好。以前模型遇到半个字被锈吃掉会硬猜一个猜错了你完全看不出现在它会输出SN-202?0345代码节点一看到?就知道要转人工成本很低但拦住了大量脏数据。规则 7 的严格模式。这个开关是给不同业务线用的。资产入账要求零差错开严格模式巡检初筛只看个大概关掉能提高召回。一个应用覆盖两种诉求比维护两套提示词省事。关于仅输出 JSON的反复强调。多模态模型特别喜欢在 JSON 前面加一句好的以下是提取结果后面还跟个代码块标记。加了不要使用 Markdown 代码块包裹这一句之后干净了很多但没根治。所以后面代码节点里我还加了一层容错解析属于双保险。4.4 代码节点字段归一化与合法性校验模型给的是原文落到业务系统要的是规范值。这一步交给代码节点用 Python 写主要干四件事JSON 容错解析、单位归一化、格式合法性校验、置信度重算。import json import re def main(extract_result: str) - dict: # ---------- 1. 容错解析 ---------- text (extract_result or ).strip() # 去掉可能的代码块包裹 if text.startswith(): text re.sub(r^[a-zA-Z]*\s*, , text) text re.sub(r\s*$, , text) # 截取第一个 { 到最后一个 } 之间的内容 start, end text.find({), text.rfind(}) if start -1 or end -1: return { ok: False, reason: no_json_found, payload: {}, need_review: True } try: data json.loads(text[start:end 1]) except Exception as e: return { ok: False, reason: fjson_parse_error: {e}, payload: {}, need_review: True } # ---------- 2. 缺字段补空 ---------- fields [ device_name, model, serial_no, rated_voltage, rated_current, rated_power, frequency, protection_level, manufacturer, manufacture_date, standard, raw_text ] for f in fields: if f not in data or data[f] is None: data[f] # ---------- 3. 数值与单位归一化 ---------- def pick_number(s): m re.search(r\d(?:\.\d)?, str(s)) return float(m.group()) if m else None # 功率统一 kW p_raw str(data[rated_power]).upper().replace( , ) p_kw None n pick_number(p_raw) if n is not None: if HP in p_raw or 马力 in p_raw: p_kw round(n * 0.7355, 3) elif MW in p_raw: p_kw n * 1000 elif W in p_raw and KW not in p_raw: p_kw round(n / 1000, 4) else: p_kw n data[rated_power_kw] p_kw # 电压统一 V v_raw str(data[rated_voltage]).upper().replace( , ) n pick_number(v_raw) data[rated_voltage_v] n # 电流统一 A i_raw str(data[rated_current]).upper().replace( , ) data[rated_current_a] pick_number(i_raw) # 频率统一 Hz f_raw str(data[frequency]).upper().replace( , ) data[frequency_hz] pick_number(f_raw) # ---------- 4. 格式合法性校验 ---------- problems [] # 序列号不允许出现 ? 占位 if ? in str(data[serial_no]): problems.append(serial_no_incomplete) # 防护等级必须形如 IP 两位数字 可选字母 ip str(data[protection_level]).upper().replace( , ) if ip and not re.fullmatch(rIP\d{2}[A-Z]{0,2}, ip): problems.append(ip_format_invalid) else: data[protection_level] ip # 生产日期归一到 YYYY-MM d str(data[manufacture_date]) m re.search(r(19|20)\d{2}, d) if m: year m.group() mm re.search(r(?!\d)(0[1-9]|1[0-2])(?!\d), d.replace(year, )) data[manufacture_date] f{year}-{mm.group()} if mm else year elif d: problems.append(date_unrecognized) # 型号不允许空 if not str(data[model]).strip(): problems.append(model_missing) # ---------- 5. 置信度重算 ---------- try: conf float(data.get(confidence, 0)) except Exception: conf 0.0 uncertain data.get(uncertain_fields) or [] if not isinstance(uncertain, list): uncertain [] # 每出现一个不确定字段扣 0.08每个格式问题扣 0.1 conf max(0.0, conf - 0.08 * len(uncertain) - 0.10 * len(problems)) # 关键字段缺失直接判定需复核 critical [model, rated_power_kw] need_review bool(problems) or conf 0.7 or any( data.get(k) in (None, ) for k in critical ) or len(uncertain) 0 return { ok: True, payload: data, confidence: round(conf, 3), problems: problems, need_review: need_review }这段代码有几个地方值得单独强调。rated_power_kw这类派生字段单独存不要覆盖原值。原始值rated_power保留着归一化结果另开字段原因是万一换算规则错了比如某些特殊设备的 HP 定义不同你还能回溯原始数据重新算。我吃过这个亏第一版直接覆盖后来发现一批国外设备的马力换算系数用错了原始值没了只能重新调模型识别一遍。need_review的判定逻辑要写死不要只信模型。模型自评置信度普遍偏高尤其是它觉得自己看对了的时候。所以我在代码里加了硬规则关键字段缺失、格式校验不过、有不确定字段任意一条命中就进复核。宁可多复核不可错入账。关于返回值结构。代码节点的输出会成为后续节点的变量所以我在外面包了一层ok和reason出问题时下游分支能直接拿到原因字符串日志里一眼看懂卡在哪一步。4.5 条件分支与二次抽取抽取节点之后接一个 IF/ELSE 分支判断依据是代码节点输出的need_review。走需要复核那一支可以再接一个二次抽取的 LLM 节点这次把strict_mode强制设为 on同时把第一次的raw_text一起塞进提示词让它基于原文逐字比对只保留确凿存在的内容。实测二次抽取能把一部分第一次因为联想而错的字段纠正回来尤其是型号和编号这类字符级错误的修正效果不错。二次抽取之后还要再挂一个代码节点做同样的校验然后进结束节点。这种重试一次的设计比一上来就把提示词写得无比严格更实用——第一次宽一点保召回第二次严一点保精度两次结果一致的可信度最高。5. 用 API 把能力接进业务系统工作流在界面上跑通只是第一步真正的落地一定是代码调用。5.1 接口形态与鉴权工作流发布后Dify 会给一个接口地址路径通常是/v1/workflows/run鉴权用应用密钥放在Authorization: Bearer app-xxxx这个头里。调用有两种响应模式blocking同步等结果适合单张、交互式streaming流式拿适合前端要显示处理中的过程感。铭牌识别是短任务一般几秒到十几秒我全程用blocking简单。图片的传法有两种一是先调/v1/files/upload上传拿到upload_file_id再在 inputs 里引用二是直接传公网可访问的 URL。前者适合内部系统图片在内网后者适合图片本来就在对象存储里的场景。我选上传因为可控而且能复用文件。5.2 Python 调用完整示例import time import requests BASE_URL https://your-dify-host/v1 API_KEY app-xxxxxxxxxxxxxxxx HEADERS {Authorization: fBearer {API_KEY}} def upload_image(path: str, user: str plate-bot) - str: 上传本地图片返回 upload_file_id with open(path, rb) as f: resp requests.post( f{BASE_URL}/files/upload, headersHEADERS, files{file: (path.split(/)[-1], f, image/jpeg)}, data{user: user}, timeout60, ) resp.raise_for_status() return resp.json()[id] def recognize_plate(path: str, strict: str off, ocr_text: str ) - dict: 识别单张铭牌返回结构化结果 file_id upload_image(path) payload { inputs: { plate_image: { type: image, transfer_method: local_file, upload_file_id: file_id, }, ocr_text: ocr_text, strict_mode: strict, }, response_mode: blocking, user: plate-bot, } resp requests.post( f{BASE_URL}/workflows/run, headers{**HEADERS, Content-Type: application/json}, jsonpayload, timeout180, ) resp.raise_for_status() body resp.json() # 工作流输出在 data.outputs 里字段名取决于结束节点的配置 outputs body.get(data, {}).get(outputs, {}) return { task_id: body.get(workflow_run_id), status: body.get(data, {}).get(status), result: outputs.get(result, {}), confidence: outputs.get(confidence, 0), need_review: outputs.get(need_review, True), } if __name__ __main__: t0 time.time() r recognize_plate(./samples/plate_001.jpg) print(f耗时 {time.time() - t0:.1f}s) print(r)有几处容易出问题提前说user字段必须传而且不要全用同一个值。这个字段在平台侧用来区分调用方做统计和限流。我最早图省事全写死一个字符串后来想看具体是哪个业务线在猛调傻眼了。建议按业务线或终端 ID 生成比如plant-a-terminal-07。inputs里的变量名必须和开始节点定义的一模一样大小写敏感。这个坑我踩过变量名写错了不报错模型收到空输入输出一堆空字段排查了半小时才发现是拼写问题。超时时间要给够。多模态推理本来就慢遇到大图或者二次抽取二三十秒很正常。我一开始设 30 秒跑批的时候大量超时改到 180 秒才稳。5.3 批量处理并发与限流要自己管批量跑几千张图几个原则并发别开太大。我实测下来并发 4~6 是比较舒服的区间再往上收益就不明显了反而超时率飙升。用线程池控制就行from concurrent.futures import ThreadPoolExecutor, as_completed def batch_run(paths, max_workers4): results [] with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {pool.submit(recognize_plate, p): p for p in paths} for fut in as_completed(future_map): p future_map[fut] try: results.append({path: p, **fut.result()}) except Exception as e: results.append({path: p, status: error, error: str(e)}) return results一定要断点续跑。批量任务跑到一半挂了不能从头再来。我的做法是每处理完一张就往 SQLite 或者 CSV 里追加一条重跑时先读已完成清单跳过已有的。这个看起来是小事但真到跑一万张的时候能救命。另外失败要分类。网络超时、平台限流、图片格式错误这三种的重试策略完全不一样超时可以退避重试限流要降并发格式错误重试一万次也没用。我在外层加了个简单分类只有超时类才做最多三次的指数退避重试。5.4 结果落库与人审回流落库这块没什么花哨的关键是字段对齐和留痕。我建了两张表一张主表存结构化结果含task_id、原始识别值、归一化值、置信度、复核状态一张明细表存每一版的raw_text和模型返回的完整 JSON。后者看起来冗余但复盘时候特别有用——想搞清楚某类铭牌为什么总识别错直接翻历史原始返回比重新跑一遍快得多。人审环节我用的是最简单的做法一个网页左边显示原图右边是可编辑的表单字段预填识别结果不确定字段标黄。审完提交把这个人工修正后的值和模型原始值一起存下来形成标注对。积累到几百条就有了自己的评估集和微调素材比什么都值钱。6. 实测效果与常见问题排查聊点实在的数据和坑。6.1 各环节耗时与效果分布我拿一批约 600 张真实铭牌照片做过统计包括手机随手拍、巡检工装拍、翻拍纸质记录的扫描件指标结果一次抽取成功率返回合法 JSON约 96%关键字段型号、功率全对率约 82%触发二次抽取的比例约 18%最终需人工复核比例约 11%单张平均耗时含上传8~15 秒单张成本相对人工录入降到原耗时的 1/10 左右关键字段全对率 82% 这个数字说实话一开始我不满意但配合人审之后端到端效率提升还是很明显的因为人只需要复核那 11%剩下的直接过。而且这个数字随着提示词迭代和样本积累在往上走第二个月就到了 88% 左右。6.2 常见问题速查这张表是我自己攒的基本覆盖了跑这套流程能遇到的大多数问题现象大概率原因处理办法模型在前面加好的以下是...提示词约束不够强提示词末加强制指令 代码节点做截取容错型号被自动纠错大小写、连字符变了模型默认做了规范化规则里明确写逐字符保留不纠正功率数值对但单位乱HP/kW/W 混模型直接搬运原文归一化放代码里做别指望模型算出厂编号变成一串?图片过曝或划痕转人工不要用模型猜的值同时在前端提示重拍同一张图两次调用结果不一致模型采样随机性业务上要求确定性时温度设 0关键字段做二次比对大批量跑到一半全超时并发过高触发限流降到 4 并发加指数退避重试学习过的品牌铭牌突然识别变差提示词改版引入了副作用保留评估集每次改提示词全量回归一次上传成功但输入为空inputs 变量名拼写不一致对照开始节点变量名逐字检查日期识别成有效期限的年份铭牌上多个日期模型串了提示词中明确生产日期字段通常紧跟 MFD 或制造年月字样6.3 一个典型的排查过程有段时间我发现某几类配电柜铭牌的生产日期错误率异常高。翻了几十条原始数据才看明白这些铭牌上有三个日期——生产日期、出厂检验日期、合格证有效期格式完全一样都在同一片区域。模型有时候会抓错。改法分两步。第一步在提示词里加一句定位线索生产日期通常标注为 MFD、MANUFACTURED、制造年月、生产日期 等字样紧邻该字样的日期为生产日期若无任何字样标注该字段留空并标记 uncertain。第二步在代码节点加了个日期合理性校验如果识别出的日期晚于当前日期或者早于 1980 年直接判为异常转人工。改完之后这类错误基本消失。这个案例给我的启发是模型的错误往往不是看不清而是没说清该看哪里。补充空间位置和标识词的线索比反复强调要准确有用得多。7. 几条只有真跑过才明白的经验最后分享几个不太适合放进系统设计、但确实影响落地效果的心得。一定要先做 30 张图的黄金测试集再做别的。选题材的时候要有意识覆盖正面清晰、强反光、锈蚀严重、斜拍、中英混排、纯英文、参数表排版、竖排。这 30 张图是你后面所有提示词改动的回归基准没有它你根本不知道自己是改好了还是改坏了。我最初的版本就是因为没有基准集改到第七版反而比第三版差白白绕了弯路。图片质量决定上限提示词只能逼近上限。这个话说得有点绝对但在铭牌场景是实话。同一张糊到看不清的图你换十个模型、写二十版提示词也救不回来。所以我在接入层加了一道硬性约束长边低于 800px 的直接拒模糊度超阈值的提示重拍。前端加一句请正对铭牌、避免闪光灯直射、尽量填满画面能省掉后面一大半的麻烦。别迷信置信度这个数字要信规则。模型自评的 confidence 只能当参考真要靠它做分流迟早出错。我现在的做法是规则校验关键字段非空、格式合法、无?占位为主置信度只作为辅助排序——同样的图里先复核置信度低的那些。留着 raw_text它是你未来的资产。很多人只存结构化字段把原始文本扔了。等到你想分析为什么型号总错或者想训练一个专用的小模型才发现没有原始语料。这个字段几乎不占成本价值却随时间是复利增长的。换模型的时候别只看准确率还要看稳定性。我换过一次模型字段准确率涨了 3 个点但输出格式的抖动明显增加JSON 缺引号的比例从 1% 涨到 5%结果代码节点的容错逻辑被反复触发整体人工复核率反而上升了。后来的评估标准就变成了准确率 × 格式合格率这个组合指标单看任何一个都会误导。这套东西现在的形态依然不算完美人工复核那 10% 左右短期内估计消不掉涉及锈蚀和遮挡的图模型确实没辙。接下来我打算做两件事一是把人工复核的修正数据攒起来做一个轻量的字段纠错模型专门处理型号和编号这两类字符级错误二是给前端加上实时取景框拍照时就提示对齐和光照从源头减少废片。这两条哪条收益大还得跑一段时间数据才知道。