基于 Qwen-VL 的 OCR 铭牌自动建设备台账的实现方案

> EAMX 是一个 AI-Native 的设备资产管理系统。其中"拍一张铭牌照片,系统自动提取字段并填入设备台账"是用户感知最强的 AI 能力之一。本文从工程实现角度,完整拆解这一能力的技术架构、两阶段管线、动态表单构造,以及一个曾让管线延迟达到 130 秒的 Qwen3-VL 思维链陷阱及修复过程。

1. 问题定义

设备铭牌是一台设备的"身份证"——品牌、型号、序列号、额定功率、制造日期等关键信息都刻在上面。传统工作流中,设备管理员需要手动对照铭牌逐字段录入系统,一台设备耗时 3-10 分钟,且序列号这类高密度字符串极易录入错误。

目标:用户拍摄铭牌照片 → 系统自动提取结构化字段 → 返回预填好的设备台账表单,用户仅需核对确认。

对 AI 管线有三个核心要求:

| 要求 | 说明 |<br>|---|---|<br>| 忠实性 | 看到什么提取什么,不推断、不编造、不遗漏 |<br>| 性能 | 设备建档是高频操作,延迟必须控制在 3-8 秒以内 |<br>| 可靠降级 | 图片质量差或不含铭牌时,不展示空表单误导用户 |

2. 整体架构

管线分为两阶段 + 动态表单构造

``<br>用户拍照上传<br> │<br> ├─ [意图识别] IntentAgent 判断是否为"设备建档"意图<br> │<br> ├─ [Stage 1] Qwen-VL 视觉模型:忠实提取铭牌上所有键值对 → RawField[]<br> │ 模型:AI_VISION_MODEL (Qwen/Qwen3-VL-32B-Instruct)<br> │ Prompt:工业铭牌OCR专用提示词,要求输出标准JSON<br> │<br> ├─ [Stage 2] 纯代码查表:FIELD_MAP 将 RawField 分类为 A/C 两类<br> │ 精确匹配 → 模糊匹配 → 未命中进 technicalSpecs<br> │<br> └─ [Stage 3] prepare_equipment_form:构造 DynamicForm UI Card<br> A 类(铭牌来源)+ B 类(历史推断)+ C 类(技术参数)<br> 每字段带置信度、来源标签、视觉着色<br>``

两阶段分离是刻意设计——Stage 1 只管"读",完全不理解台账 Schema;Stage 2 只管"映射",完全不依赖 LLM。 这种"提取-映射分离"带来了三个好处:

1
Stage 1 的 Prompt 可以专注于 OCR 准确性,不涉及台账业务逻辑
2
Stage 2 是纯代码查表(<1ms),无网络 IO,无 token 消耗
3
两者独立优化:换更好的视觉模型不改 FIELD_MAP,新增字段类型不改 Prompt
3. Stage 1:Qwen-VL 忠实提取
3.1 模型选择

选用 Qwen/Qwen3-VL-32B-Instruct,通过 OpenAI 兼容 API 调用。关键优势:

3.2 核心实现

```typescript<br>// nameplate-extractor.ts — extractFromNameplate()<br>const VISION_MODEL = (process.env.AI_VISION_MODEL ?? AI_CHAT_MODEL).trim();

export async function extractFromNameplate(<br> imageBase64: string,<br> mimeType: string,<br> sourceName: string,<br>): Promise<RawField[]> {<br> const resp = await openai.chat.completions.create({<br> model: VISION_MODEL,<br> max_tokens: 1024,<br> temperature: 0,<br> messages: [{<br> role: "user",<br> content: [<br> {<br> type: "image_url",<br> image_url: {<br> url: data:${mimeType};base64,${imageBase64},<br> detail: "high",<br> },<br> },<br> {<br> type: "text",<br> text: `你是工业铭牌OCR助手。请忠实转录这张铭牌/标牌图片上每一个可见的键值对。

【规则】

1
看到什么写什么,不推断、不补全、不解释
2
每个字段一条记录(label=铭牌上的字段名原文,value=字段值原文含单位)
3
铭牌顶部/最大字体的设备名称若无明显键名,label填"设备名称"
4
遇到模糊字符如实填写并将confidence设为0.4-0.6
5
宁多勿少:电压、电流、频率、压力、转速、防护等级等每一个有数值的参数都要提取

【输出格式】<br>只输出JSON,不要任何解释文字:<br>{ "fields": [<br> { "label": "字段名", "value": "字段值含单位", "confidence": 0.9 }<br>] }`,<br> },<br> ],<br> }],<br> });

const raw = resp.choices[0]?.message?.content ?? "";<br> const parsed = extractJsonObject(raw);<br> return parsed.fields.map(f => ({<br> label: String(f.label), value: String(f.value),<br> confidence: typeof f.confidence === "number" ? f.confidence : 0.7,<br> source: sourceName,<br> }));<br>}<br>```

3.3 Prompt 设计要点

这套 Prompt 经过多次迭代,几个关键设计决策:

① "看到什么写什么" — 这是整个管线的根基。Qwen-VL 作为视觉模型,天然倾向于对图片内容做理解和推理("这是一台空压机"),但 Stage 1 需要的是原始键值对转录,不做语义理解。Prompt 第一句就锁定了这个方向。

② 模糊字符的诚实处理confidence: 0.4-0.6 而非盲目给高置信度。这直接影响 Stage 3 动态表单的着色——模糊字段会被标记为黄色,提示用户核对。

③ 输出格式锁定为 JSON only — 要求"只输出 JSON,不要任何解释文字"。Qwen-VL 有思维链输出习惯,不加这句会输出大量推理过程。

④ "宁多勿少" — 工业铭牌常有 20+ 参数。LLM 做结构化提取时容易漏掉"看起来不重要"的参数(如防护等级 IP54、绝缘等级 F),但设备台账的 technical_specs JSONB 列专门存储这些。所以 Prompt 明确要求每个数值参数都要提取。

3.4 JSON 提取容错

不同版本的 Qwen-VL 对纯 JSON 输出的遵从度不完全一致。有的会在 JSON 前后附加解释文字,有的会输出 Markdown 代码块包裹。因此 Stage 1 不依赖 response_format: { type: "json_object" },而是用 extractJsonObject() 做容错解析:

``typescript<br>// llm-json-utils.ts<br>export function extractJsonObject(raw: string | null | undefined) {<br> if (!raw) return null;<br> const start = raw.indexOf("{");<br> const end = raw.lastIndexOf("}");<br> if (start === -1 || end === -1 || end <= start) return null;<br> try {<br> const obj = JSON.parse(raw.slice(start, end + 1));<br> if (typeof obj === "object" && obj !== null && !Array.isArray(obj))<br> return obj;<br> } catch { / ignore / }<br> return null;<br>}<br>``

思路:找到第一个 { 和最后一个 },裁剪中间内容尝试 JSON.parse。这样无论模型在 JSON 前后附加了什么文字,都能正确提取。

4. Stage 2:FIELD_MAP 纯代码分类

Stage 1 输出的 RawField[] 是"铭牌原文键值对",例如 { label: "额定功率", value: "37 kW", confidence: 0.88 }。Stage 2 的任务是把它们分类为 A 类(台账有专列)和 C 类(台账无专列)。

4.1 静态映射表

FIELD_MAP 是约 60 条字符串映射,覆盖工业铭牌全部常见标注:

```typescript<br>const FIELD_MAP: Record<string, StandardField> = {<br> // ── 设备名称 ──<br> "设备名称": "name", "产品名称": "name", "品名": "name",

// ── 型号/规格 ──<br> "型号": "model", "规格": "model", "Model": "model",<br> "Model No.": "model", "Spec": "model", "Type": "model",

// ── 序列号(别名最多,最容易漏识)──<br> "出厂编号": "serialNumber", "序列号": "serialNumber",<br> "S/N": "serialNumber", "SN": "serialNumber",<br> "Serial No.": "serialNumber", "Product No.": "serialNumber",

// ── 额定功率 ──<br> "额定功率": "ratedPower", "功率": "ratedPower",<br> "Power": "ratedPower", "Rated Power": "ratedPower",

// ── 制造商 ──<br> "制造商": "manufacturer", "生产厂家": "manufacturer",<br> "Manufacturer": "manufacturer", "Mfr": "manufacturer",

// ... 约 60 条,覆盖 16 个标准字段<br>};<br>```

设计原则:

4.2 两级匹配策略

```typescript<br>function findFuzzyMatch(label: string): StandardField | null {<br> const normalize = (s: string) =><br> s.replace(/[::。,,.、\s\-_()()[\]【】]/g, "").toUpperCase();

const normalizedLabel = normalize(label);

// 1. 去标点后精确匹配<br> for (const [key, field] of Object.entries(FIELD_MAP)) {<br> if (normalize(key) === normalizedLabel) return field;<br> }

// 2. 子串匹配<br> for (const [key, field] of Object.entries(FIELD_MAP)) {<br> const nk = normalize(key);<br> if (nk.length < 2) continue;<br> if (nk.includes(normalizedLabel) || normalizedLabel.includes(nk))<br> return field;<br> }<br> return null;<br>}<br>```

先精确匹配(去标点后完全一致),再子串匹配(处理 OCR 误差导致的轻微偏差)。

4.3 分类逻辑与多源冲突处理

```typescript<br>export function classifyFields(rawFields: RawField[]): ClassifiedFields {<br> const standard: ClassifiedFields["standard"] = {};<br> const technicalSpecs: Record<string, string> = {};

for (const field of rawFields) {<br> const val = field.value?.trim();<br> if (!val) continue;

const mappedKey = FIELD_MAP[field.label] ?? findFuzzyMatch(field.label);

if (mappedKey) {<br> // 多源冲突处理:同一字段取置信度更高的值<br> const existing = standard[mappedKey];<br> if (!existing || field.confidence > existing.confidence)<br> standard[mappedKey] = { value: val, source: field.source,<br> confidence: field.confidence };<br> } else {<br> // 未命中 → C 类技术参数<br> if (!(field.label in technicalSpecs))<br> technicalSpecs[field.label] = val;<br> }<br> }<br> return { standard, technicalSpecs, sourceMap: {} };<br>}<br>```

当同一字段有多个数据源(铭牌 + 合同)提供值时,取置信度更高的那个。例如铭牌上的"制造商"(置信度 0.95)优先于合同上的"制造商"(置信度 0.78)。

整个 Stage 2 纯内存计算,耗时 <0.5ms,无任何网络 IO。

5. Stage 3:DynamicForm 构造

classifyFields 的输出交给 prepare_equipment_form,构造一张分三类字段的动态表单:

5.1 A 类字段:铭牌/文档直接提取

``typescript<br>// equipment.agent.ts<br>{ key: "name", label: "设备名称", confidence: 0.95,<br> source: "铭牌", required: true },<br>{ key: "model", label: "型号/规格", confidence: 0.92,<br> source: "铭牌", required: false },<br>{ key: "serialNumber", label: "出厂序列号", confidence: 0.92,<br> source: "铭牌", required: false },<br>// ...<br>``

这些字段的置信度直接来自 Stage 1 的 Qwen-VL 输出。高置信度(≥90%)字段前端渲染为绿色,用户可以"闭眼过"。

5.2 B 类字段:AI 基于历史推断

``typescript<br>// 查询同型号历史设备,推断 category / department / location<br>const similar = await db.select({ category, department, location, grade })<br> .from(equipmentsTable)<br> .where(and(eq(tenantId), ilike(model, %${modelVal}%`)))<br> .limit(5);

// 取出现最频繁的值作为建议<br>bSuggestion = {<br> category: freq(similar.map(s => s.category)),<br> department: freq(similar.map(s => s.department)),<br> // ...<br>};<br>```

B 类字段的 source 显示 "AI建议 · 同租户历史设备",置信度通常 0.65-0.72,前端渲染为黄色。用户需要确认这些推断值。

5.3 C 类字段:技术参数 JSONB

铭牌上所有不属于台账列的参数(额定电压、工作压力、防护等级、主轴转速等)全部进入 technicalSpecsJson,存为 JSONB:

``json<br>{<br> "额定电压": "380V",<br> "额定电流": "72A",<br> "频率": "50Hz",<br> "工作压力": "0.7MPa",<br> "防护等级": "IP54",<br> "绝缘等级": "F"<br>}<br>``

前端以可编辑技术参数区域展示,每条带 [铭牌] 来源标签。

5.4 UI Card 构造

``typescript<br>return {<br> type: "UI_CARD",<br> cardType: "dynamic_form",<br> formTitle: "设备台账确认",<br> taskId: "create_equipment",<br> description: "请核对AI提取的设备信息,确认无误后点击「提交确认」写入台账。<br> 红色字段为必填或置信度低,建议优先检查。",<br> fieldGroups: [<br> { id: "classA", title: "A类 — 基本信息(从文档提取)",<br> description: "AI直接从铭牌/合同/发票中识别的字段,绿色表示高置信度" },<br> { id: "classB", title: "B类 — 管理属性(AI建议,请核对)",<br> description: "台账管理所需但文档未记录的字段" },<br> ],<br> fields: [...classAFields, ...classBFields],<br> technicalParams, // C 类<br>};<br>``

前端收到 UI_CARD 后,按 fieldGroups 分组渲染,每个字段根据 confidence 值自动着色(绿/黄/红),附上 source 标签("铭牌"/"合同"/"AI建议")。

6. 与 AI Chat 系统的集成
6.1 两阶段管道(明确建档意图)

当用户上传图片且消息中包含"建档/新建设备/录入设备"等关键词时,coordinator 走两阶段快速管道,绕过 LLM Tool Calling

```typescript<br>// coordinator.agent.ts<br>const EQUIPMENT_CREATE_KEYWORDS = [<br> "新建设备", "设备建档", "建档", "设备台账", "新增设备",<br> "录入设备", "添加设备", "登记设备", "创建设备"<br>];

const hasEquipmentCreateIntent =<br> imageBase64 && EQUIPMENT_CREATE_KEYWORDS.some(kw => userMessage.includes(kw));

if (hasEquipmentCreateIntent && imageBase64) {<br> // Stage 1: Qwen-VL OCR<br> const rawFields = await extractFromNameplate(imageBase64, mimeType, "铭牌");

// Stage 2: 字段分类<br> const classified = classifyFields(rawFields);

// 有效性检查:无有用数据时走 LLM 降级路径<br> if (rawFields.length === 0 || (standardCount === 0 && techCount < 2)) {<br> // → 用视觉模型描述图片 → 主LLM解释原因<br> }

// Stage 3: 直接 prepare_equipment_form,不走 LLM<br> const formParams = flattenToFormParams(classified);<br> const formResult = await executeEquipmentAction(<br> "prepare_equipment_form", formParams, ctx<br> );<br> return { fullContent: briefing, uiCard: formResult };<br>}<br>```

6.2 LLM 路径(无明确关键词)

当用户上传图片但未使用建档关键词(如纯图片上传、或图片附带"帮我看看这个"这类含糊描述),管线不走两阶段快速通道,而是交由主 LLM 自行判断:

``typescript<br>// 视觉模型描述图片后拼入文本,主 LLM 只接收纯文字<br>const imgDesc = await describeImageForCoordinator(imageBase64, mimeType);<br>llmUserText = ${userMessage}\n\n[图片内容摘要]\n${imgDesc};<br>// → 主 LLM 根据描述自行选择 Tool(prepare_equipment_form/create_fault_report/...)<br>``

6.3 有效性检查与降级

如果铭牌照片质量差(模糊、反光、遮挡),Stage 1 提取不到有效字段(标准字段 0 个且技术参数少于 2 个),不展示空表单

``typescript<br>const hasUsefulData = rawFields.length > 0 && (standardCount > 0 || techCount >= 2);<br>if (!hasUsefulData) {<br> // 走 LLM 降级:让 LLM 看图解释原因<br> // "图片不是设备铭牌" / "铭牌模糊请重拍" / "这是入库单,应该用入库功能"<br>}<br>``

这是对用户信任的保护——宁可说"我没识别出来"并给建议,也不展示一张半空的不可靠表单。

7. 性能优化:从 130s 到 3-8s
7.1 Qwen3-VL 思维链陷阱

最初实现时使用 Vercel AI SDK 的 generateText() 调用视觉模型:

``typescript<br>// ❌ 旧实现 — 延迟 130s+<br>const result = await generateText({<br> model: provider.chat(VISION_MODEL),<br> messages: [...],<br> providerOptions: { openai: { enable_thinking: false } }, // 意图禁用<br>});<br>``

问题:Vercel AI SDK 的 providerOptions.openai.enable_thinking 参数在转发给 SiliconFlow(Qwen3-VL 的运行平台)时未被正确传递。Qwen3-VL 以默认的全思维链模式运行——先推理数万 token 再输出结果,单次调用延迟达到 130 秒以上

7.2 修复:切换为 OpenAI 原生客户端

``typescript<br>// ✅ 新实现 — 延迟 3-8s<br>const resp = await openai.chat.completions.create({<br> model: VISION_MODEL,<br> max_tokens: 1024,<br> temperature: 0,<br> messages: [{<br> role: "user",<br> content: [<br> { type: "image_url", image_url: { url: data:${mimeType};base64,${imageBase64} } },<br> { type: "text", text: PROMPT },<br> ],<br> }],<br> // enable_thinking 通过 FAST_OPTS 的 extra_body 传递<br>});<br>``

关键改动:

1
放弃 Vercel AI SDK 的 generateText,改回 openai.chat.completions.create
2
通过 extra_body: { enable_thinking: false } 传递禁用思维链参数
3
temperature: 0 保证确定性输出
4
max_tokens: 1024 限制输出长度(铭牌字段通常在 500 tokens 以内)

修复后实测延迟降至 3-8 秒,满足了"设备建档"场景的用户体验要求。

7.3 FAST_OPTS 机制

``typescript<br>// client.ts<br>export const FAST_OPTS = {<br> ...(process.env.AI_DISABLE_THINKING !== "false"<br> ? { extra_body: { enable_thinking: false } } as any<br> : {}),<br>} as const;<br>``

FAST_OPTS 对 Qwen3 系列模型通过 extra_body 禁用思维链;对非思考型模型(GPT、豆包)该参数会被忽略,无副作用。

8. 多源扩展设计

extractFromNameplatesourceName 参数预留了多源扩展能力:

```typescript<br>// 铭牌<br>const nameplateFields = await extractFromNameplate(base64, "image/jpeg", "铭牌");<br>// 采购合同 PDF(将页面截图传入)<br>const contractFields = await extractFromNameplate(contractBase64, "image/png", "合同");<br>// 发票<br>const invoiceFields = await extractFromNameplate(invoiceBase64, "image/jpeg", "发票");

// classifyFields 自动处理多源冲突(取置信度更高的值)<br>const allFields = [...nameplateFields, ...contractFields, ...invoiceFields];<br>const classified = classifyFields(allFields);<br>```

classifyFields 的多源冲突处理逻辑(取高置信度值)使得铭牌字段(置信度 0.95)自动优先于合同字段(置信度 0.78)。前端 DynamicForm 的 source 标签也会显示每个字段来自哪个文档。

9. 总结

整个铭牌 OCR 建台账方案的技术关键点:

| 设计决策 | 价值 |<br>|---|---|<br>| 两阶段分离(提取/映射) | Stage 1 Prompt 不涉业务逻辑;Stage 2 纯代码零延迟 |<br>| Qwen3-VL 32B 视觉模型 | 中文 OCR 准确率高,工业铭牌中英混合适应好 |<br>| 静态 FIELD_MAP(约 60 条) | 新增字段只需追加入口,无需修改 Prompt |<br>| OpenAI 原生客户端 + enable_thinking: false | 延迟从 130s 降至 3-8s |<br>| JSON 容错解析 (extractJsonObject) | 不依赖模型输出格式一致性 |<br>| DynamicForm A/B/C 三分组 + 置信度着色 | 用户 3 秒判断哪些字段可信任 |<br>| 有效性检查 + LLM 降级 | 图片无效时不展示半空表单 |<br>| 多源支持(铭牌/合同/发票) | 同一设备可从多个文档提取,自动取高置信度值 |

这套管线已在 EAMX 项目中稳定运行,支撑了设备台账建档的核心 AI 体验。核心文件:

作者:白杨,十余年设备资产管理从业经验,EAMX 产品负责人。