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

Nameplate OCR → Equipment Ledger

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


1. 问题定义

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

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

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

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

2. 整体架构

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

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

两阶段分离是刻意设计——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 调用。关键优势:

  • 中文 OCR 准确率高,对工业铭牌上的中英文混合标注("型号/Model"、"额定功率/Rated Power")识别良好
  • 32B 参数规模在精度与延迟之间取得平衡
  • 支持 enable_thinking: false 禁用思维链,大幅降低首 token 延迟

3.2 核心实现

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

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

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

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

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

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() 做容错解析:

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

思路:找到第一个 { 和最后一个 },裁剪中间内容尝试 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 条字符串映射,覆盖工业铭牌全部常见标注:

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

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

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

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

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

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

设计原则:

  • 维护成本极低 — 新增设备类型只需追加别名条目,无需修改任何 Prompt
  • 中英混合覆盖 — 同时支持中文铭牌("额定功率")、英文铭牌("Rated Power")、混合铭牌

4.2 两级匹配策略

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

  const normalizedLabel = normalize(label);

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

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

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

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

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

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

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

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

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

性能亮点

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


5. Stage 3:DynamicForm 构造

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

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

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

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

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

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

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

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

5.3 C 类字段:技术参数 JSONB

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

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

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

5.4 UI Card 构造

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

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


6. 与 AI Chat 系统的集成

6.1 两阶段管道(明确建档意图)

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

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

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

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

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

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

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

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

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

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

6.3 有效性检查与降级

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

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

设计哲学

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


7. 性能优化:从 130s 到 3-8s

7.1 Qwen3-VL 思维链陷阱

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

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

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

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

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

关键改动:

  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 机制

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

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


8. 多源扩展设计

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

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

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

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


9. 总结

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

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

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

  • artifacts/api-server/src/agents/nameplate-extractor.ts — Stage 1 视觉提取(171 行)
  • artifacts/api-server/src/agents/field-classifier.ts — Stage 2 字段分类(232 行)
  • artifacts/api-server/src/agents/equipment.agent.ts — Stage 3 DynamicForm 构造(prepare_equipment_form 约 230 行)
  • artifacts/api-server/src/agents/coordinator.agent.ts — 两阶段管道集成与降级
  • artifacts/api-server/src/lib/llm-json-utils.ts — JSON 容错解析
  • lib/integrations-openai-ai-server/src/client.ts — OpenAI 客户端与 FAST_OPTS

准备好让AI管理你的设备资产了吗?

EAMX提供永久免费版,无需信用卡,2周内完成部署。加入1000+工厂已在使用的AI原生EAM系统。