SYSTEM OK | ACTION CONTRACTS 247 DOMAINS 35 DEMO ENVIRONMENT 无需申请 | 服务热线 18926139835
首页/洞察/技术系列/铭牌识别与设备台账自动建立

基于 Qwen-VL 的铭牌识别与设备台账自动建立

「拍一张铭牌照片,系统自动提取字段并填入设备台账」是设备资产管理系统中被感知最强的一项 AI 能力, 也是工程复杂度最容易被低估的一项。本文逐段拆解这条管线的实现: 两阶段的职责划分、约 60 条字段映射表的构造方式、动态表单的三类字段来源与着色规则, 以及一处曾使单次识别延迟达到 130 秒的思维链配置问题及其修复过程。

作者 EAMX 产品团队 发布 2026-09-28 阅读 约 19 分钟 专题 技术系列

01铭牌是设备的身份凭证

设备铭牌是一台设备的身份凭证。品牌、型号、序列号、额定功率、制造日期等信息刻在铭牌上,并且只在铭牌上才是权威来源。 设备台账中这些字段正确与否,取决于录入时的取值是否来自铭牌本身,而非来自经手人的记忆或事后回忆。

传统工作流的做法是人工比对:设备管理员在设备现场读取铭牌,随后在系统中逐字段录入。一台设备耗时 3–10 分钟。 这段时间中风险最高的是序列号一类的高密度字符串——它不具备语义,无法凭常识校验,出错之后录入人员自身也难以发现。 台账中大量「型号相近、序列号错一位」的记录由此产生,并且在后续的维修、备件与折旧环节才暴露出来。

目标可以一句话定义:使用者在设备现场拍摄铭牌照片,系统自动提取结构化字段,返回一张已预填的设备台账表单,使用者只需核对确认。

把这一能力理解为在表单上加一个拍照按钮,会低估其中的工程约束。约束来自图像本身:铭牌的拍摄条件不受控制, 画面中可能出现反光、遮挡、倾斜、局部磨损与中英文混排;识别结果要写入的是有固定字段定义与权限规则的设备台账, 写错一个序列号的代价高于不写。以下各节说明这两端之间如何衔接。

02三项工程要求

对 AI 管线而言,铭牌建台账这一场景提出三项要求,并且三项要求之间存在张力:提高忠实性通常要牺牲速度,提高速度容易放宽对结果的约束。

表 1 · 铭牌识别管线的三项工程要求
要求含义与判定方式
忠实性看到什么提取什么,不推断、不编造、不遗漏。判定方式是字段取值能否在图像上逐字定位
性能设备建档属高频操作,端到端延迟须控制在 3–8 秒以内
可靠降级图片质量不足或画面中不含铭牌时,不展示空表单误导使用者

忠实性的难点在于视觉模型的默认行为是理解画面内容,其产物形态是一句结论,例如「这是一台螺杆式空压机」。 结论无法逐字段对应到图像上的字符,也就无法核对。管线因此把「理解」这一步完全排除在提取阶段之外, 要求模型只做字符级转录。

性能要求并非体验层面的偏好,而是这条管线能否被真正使用的前提。设备建档通常发生在设备旁边或收货现场, 操作者手中只有移动设备。延迟超出 3–8 秒这一区间后,操作者会退回纸质记录与事后补录的旧习惯,管线的价值随之消失。

可靠降级对应的是另一种损失:一张半空的表单比一张空表单更危险。使用者会默认系统给出的字段已经过校验,从而跳过核对; 未被提取的字段则被当作该设备本就没有此项。降级路径要做的,是在识别结果不足以支撑一张表单时明确说明原因,并给出下一步建议。

03整体架构:两个阶段与一次表单构造

管线分为两个阶段,末端接一次动态表单构造。两个阶段之间的边界,是整条管线最重要的设计决定。

用户拍照上传
  │
  ├─ [意图识别] 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 只负责「读」,完全不理解台账的字段结构;Stage 2 只负责「映射」,完全不依赖模型;Stage 3 把映射结果组装为前端可渲染的表单结构。 三个环节的输入与产出如下表。

表 2 · 三个环节的输入、职责与产出
环节实现方式输入与产出
Stage 1 视觉提取 Qwen-VL 视觉模型 输入铭牌图片(base64);产出铭牌原文键值对 RawField[],每项含 label、value、confidence、source 四个属性
Stage 2 字段映射 静态查表,纯代码 输入 RawField[];产出台账标准字段、技术参数与来源映射
Stage 3 表单构造 动作契约 prepare_equipment_form 输入映射结果;产出 DynamicForm UI Card,含分组、置信度与来源标签

「提取—映射分离」带来三项可验证的好处:

  • 提示词只服务于字符识别。Stage 1 的提示词可以专注于识别准确性,不涉及台账业务逻辑;调整识别行为不会牵动字段定义。
  • Stage 2 没有运行时成本。它是纯代码查表,耗时低于 1 毫秒,无网络 IO,无 token 消耗,也不受模型服务可用性影响。
  • 两者可独立优化。更换识别能力更强的视觉模型,无需改动 FIELD_MAP;新增一类台账字段,无需改动提示词。
本文的核心判断

识别精度与台账字段的正确性属于两类问题。把两者压在同一个模型调用里,会同时失去两者的可优化性与可核对性; 管线在这两件事之间划出一条明确的边界:模型只负责「看见什么」,代码负责「写成什么」。

依据:nameplate-extractor.ts、field-classifier.ts、equipment.agent.ts 的实现代码。

04Stage 1:Qwen-VL 的忠实提取

模型选择

视觉提取选用 Qwen/Qwen3-VL-32B-Instruct,通过 OpenAI 兼容接口调用。选择依据有三项:

  • 中文 OCR 准确率。工业铭牌普遍存在中英文混合标注,例如「型号 / Model」「额定功率 / Rated Power」,该模型对这类混排字符的识别表现良好。
  • 参数规模与延迟的平衡。32B 规模在识别精度与响应延迟之间取得平衡;规模继续上调会推高单次调用延迟,与 3–8 秒的要求冲突。
  • 可关闭思维链。该模型支持 enable_thinking: false,关闭后首 token 延迟大幅降低。这一项在性能一节中还有进一步说明。

提取函数的实现

提取函数接收图片的 base64 编码、MIME 类型与来源名称,返回铭牌原文键值对数组。调用参数中有三处为固定值: temperature: 0 保证同一图片的输出稳定,max_tokens: 1024 限制输出长度, detail: "high" 要求按高精度解析图像细节。

// 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: PROMPT },
      ],
    }],
  });

  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,
  }));
}

PROMPT 即工业铭牌识别提示词,全文如下:

你是工业铭牌 OCR 助手。请忠实转录这张铭牌 / 标牌图片上每一个可见的键值对。

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

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

提示词的四项设计判断

该提示词经过多次迭代,其中四项判断直接决定管线行为。

其一,锁定转录而非理解。「看到什么写什么」是整条管线的根基。视觉模型天然倾向于对画面内容作理解与推理, 会输出「这是一台空压机」之类的结论;Stage 1 需要的只是原始键值对。提示词第一句就锁定了方向,把理解排除在输出之外。

其二,模糊字符要求诚实标注。提示词要求对模糊字符给出 0.4–0.6 的置信度,而非一律给高分。 这一约定直接影响 Stage 3 的着色:置信度落在该区间的字段在前端标记为黄色,提示操作者逐字核对。

其三,输出格式锁定为 JSON。Qwen-VL 具有输出思维链的倾向,不加约束时会先输出大段推理再给出结果。 提示词末句要求「只输出 JSON,不要任何解释文字」,并与输出长度上限配合,避免把响应时间消耗在解释上。

其四,参数提取宁多勿少。工业铭牌的参数数量常在二十项以上。模型在做结构化提取时倾向于丢弃「看起来不重要」的参数, 例如防护等级 IP54、绝缘等级 F。设备台账的 technical_specs JSONB 列专门用于存放这类参数, 因此提示词明确要求每一个数值参数都要提取。

JSON 解析的容错处理

不同版本的 Qwen-VL 对纯 JSON 输出的遵从程度并不一致:有的会在 JSON 前后附加说明文字,有的会用 Markdown 代码块包裹。 因此 Stage 1 不依赖 response_format 一类接口参数保证格式,而由 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 前后附加了什么文字,中间的对象都能被取出;解析失败时返回空值,不抛出异常,由调用方按无结果处理。

依据:nameplate-extractor.ts 的调用参数与提示词全文;llm-json-utils.ts 的容错解析实现。

05Stage 2:FIELD_MAP 的纯代码分类

Stage 1 的产出是铭牌原文键值对,例如 { label: "额定功率", value: "37 kW", confidence: 0.88 }。 Stage 2 的任务是把它们分为两类:A 类为台账中有对应列的字段,C 类为台账中没有独立列的字段。这一阶段不调用任何模型。

静态映射表

FIELD_MAP 是约 60 条字符串映射,覆盖工业铭牌上的常见标注,归入 16 个标准字段。以下是其中一部分:

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 个标准字段
};

该表的设计原则有两条:维护成本低——新增设备类型只需追加别名条目,提示词与前端代码都不需要改动; 中英混合覆盖——同一字段同时收录中文铭牌、英文铭牌与中英混排铭牌的写法。

表 3 · FIELD_MAP 覆盖的部分标准字段与常见别名(16 个标准字段中取 5 个示例)
标准字段典型别名
name 设备名称设备名称、产品名称、品名。铭牌顶部最大字体的名称若无键名,由提示词规则填入该字段
model 型号 / 规格型号、规格、Model、Model No.、Spec、Type
serialNumber 出厂序列号出厂编号、序列号、S/N、SN、Serial No.、Product No.。别名最多,也是最容易漏识的一项
ratedPower 额定功率额定功率、功率、Power、Rated Power。取值含单位
manufacturer 制造商制造商、生产厂家、Manufacturer、Mfr。可与合同来源交叉校验

两级匹配策略

铭牌字符在拍摄与识别过程中可能出现轻微偏差,因此匹配分两级进行:先去标点后做精确匹配,再退回子串匹配。

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;
}

归一化处理的字符集包含中英文冒号、句读、顿号、各类括号、连字符与空白,比较前统一转为大写。 经过归一化,「额定功率:」与「额定功率」被视为同一字符串。子串匹配用于处理识别误差或铭牌版式造成的轻微偏差, 例如标注中夹带附加说明文字的情形。两级匹配都不命中时,字段落入 C 类,而不是被丢弃。

分类逻辑与多源冲突处理

分类函数遍历键值对,对每一项先查静态表,再退回模糊匹配;命中标准字段的进入 A 类,未命中的进入技术参数集合。

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.5 毫秒,无任何网络 IO。这一阶段的稳定性不依赖外部服务, 也不随识别模型的更换而变化。

依据:field-classifier.ts 中 FIELD_MAP 与 classifyFields 的实现;耗时为本机实测口径。

06Stage 3:DynamicForm 的三类字段构造

分类结果交给动作契约中的 prepare_equipment_form,构造一张按来源分为三类的动态表单。 三类字段的差别在于取值来源与可信程度,前端据此采用不同的着色。

A 类字段:来自文档的直接提取

A 类字段由铭牌、合同、发票等文档直接提取,置信度直接沿用 Stage 1 的输出,不做事后调整。

// 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 },
// ...

置信度不低于 90% 的字段在前端渲染为绿色,操作者可以快速通过;必填字段与低置信字段渲染为红色,需要优先检查。

B 类字段:基于同型号历史设备的推断

台账需要的管理属性(资产分类、使用部门、安装位置、设备等级)通常不出现在铭牌上。管线以型号为键, 查询同一租户下的历史设备,取出现频率最高的取值作为建议值。

// 查询同型号历史设备,推断 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 类字段的来源标签显示为「AI 建议 · 同租户历史设备」,置信度通常落在 0.65–0.72 区间,前端渲染为黄色。 这类取值来自统计而非文档,操作者需要逐项确认后才写入台账。

C 类字段:技术参数写入 JSONB

铭牌上所有不属于台账独立列的参数,全部进入 technicalSpecsJson,以 JSONB 形式存储。典型内容如下:

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

前端以可编辑的技术参数区域展示这部分内容,每条带「铭牌」来源标签。这样处理有两点好处: 台账的列结构保持稳定,不为个别铭牌上的参数增设列;铭牌参数不因「台账没有对应列」而被丢弃。

UI Card 的返回结构

Stage 3 的返回值是一张 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 自动着色(绿 / 黄 / 红), 并附来源标签(「铭牌」「合同」「AI 建议」)。着色规则的作用是让操作者在数秒内判断哪些字段可以直接通过、哪些需要逐字核对。

表 4 · 三类字段的取值来源、置信度与前端表现
类别取值来源典型置信度前端表现
A 类 铭牌、合同、发票等文档直接提取 由模型输出,示例值为 0.92–0.95 不低于 90% 渲染为绿色,可快速通过;必填与低置信字段渲染为红色
B 类 同租户同型号历史设备的统计取值 通常 0.65–0.72 渲染为黄色,来源标注为「AI 建议 · 同租户历史设备」,需逐项确认
C 类 铭牌上的技术参数,写入 technicalSpecsJson 随提取结果,不单独设定 以可编辑的技术参数区域展示,每条带「铭牌」来源标签
这一节的着色规则可以当场核验

识别结果的可信度如何呈现、哪些字段需要人工确认,均可由演示环境核对工具清单与动作定义,无需申请。 查看核验方式 →

07与 AI 助手系统的集成

铭牌识别并非一个独立的页面功能,而是 AI 助手内部的一条专用管线。是否走这条管线,由意图判断决定: 命中建档关键词时走两阶段快速管线,未命中时交由主模型自行判断。

路径一:明确的建档意图

当使用者上传图片,且消息中包含「建档 / 新建设备 / 录入设备」等关键词时,协调器直接走两阶段快速管线, 绕过模型的工具调用环节。

// 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 };
}

该路径的判定条件是一条关键词表与图片存在性的合取。命中后依次执行视觉提取、字段分类、有效性检查与表单构造四步, 其中只有第一步调用模型,后三步均在服务端完成,不产生额外的模型往返。

路径二:交由模型判断

当使用者上传图片但未使用建档关键词(例如仅上传图片,或图片附带「帮我看看这个」这类含糊描述)时, 管线不走快速通道,而是先由视觉模型描述图片内容,再把描述并入文本交给主模型,由其自行选择动作。

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

两条路径的分工如下表。这一划分的目的是把高频且意图明确的场景从模型推理中剥离出来: 识别结果不因模型当次的判断而变化,延迟也不受提示词与工具清单长度的影响。

表 5 · 两条集成路径的判定条件与执行方式
路径触发条件执行方式
两阶段快速管线 图片存在,且消息命中建档关键词表 视觉提取 → 字段分类 → 有效性检查 → 表单构造,绕过工具调用环节
模型判断路径 图片存在,但消息未命中关键词表 视觉模型描述图片内容,描述并入文本后由主模型自行选择动作,可能落到报修、入库等其他动作

有效性检查与降级

若图片质量不足(模糊、反光、遮挡),或画面中不含铭牌,识别的返回结果可能为空或接近为空。判定条件是: 提取结果为空,或标准字段为零且技术参数少于两项。

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

命中降级条件时不展示表单,改由模型说明原因并给出建议。典型输出包括「图片中未识别到设备铭牌」 「铭牌模糊,建议重拍」「这是入库单,应使用入库功能」三类。 这一处理是对使用者信任的保护:宁可明确说明未识别出结果并给出下一步建议,也不展示一张半空且不可靠的表单。

依据:coordinator.agent.ts 中两条路径的分支条件、有效性判定与降级实现。

08性能:单次识别从 130 秒到 3–8 秒

这一节的数字来自开发过程中的一次性能故障。它同时说明一件事:调用参数的传递链路与模型本身的配置同样重要, 配置正确但未送达服务端,效果等同于未配置。

故障现象与成因

初始实现使用 Vercel AI SDK 的 generateText() 调用视觉模型,并在调用选项中设置禁用思维链:

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

该参数在经 SDK 转发到 Qwen3-VL 的运行平台(SiliconFlow)时未被正确传递。Qwen3-VL 因此以默认的全思维链模式运行—— 先生成数万 token 的推理内容,再输出最终结果。单次调用延迟由此达到 130 秒以上。 这一延迟在开发环境中一度被误读为模型本身的速度上限。

修复方式

修复的做法是放弃 generateText(),改回 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 传递
});

关键改动有四项:

  • 调用方式改回 openai.chat.completions.create,避开参数转发链路中的丢失点。
  • 禁用思维链的参数通过 extra_body 传递,直达模型服务端。
  • temperature: 0 保证同一图片的输出稳定,便于核对与回归比对。
  • max_tokens: 1024 限制输出长度。铭牌字段通常在 500 token 以内,该上限足以容纳完整结果,同时防止异常输出拖长响应。

修复后实测延迟降至 3–8 秒,满足设备建档场景对响应时间的要求。

表 6 · 两种调用方式的对照
对照项修复前修复后
调用方式Vercel AI SDK 的 generateText()OpenAI 原生客户端 chat.completions.create
思维链参数的传递经 providerOptions 转发,未送达服务端经 extra_body 直达服务端,生效
模型运行模式全思维链,先生成数万 token 推理内容禁用思维链,直接输出结果
单次调用延迟130 秒以上3–8 秒
输出的确定性受思维链内容影响temperature 设为 0,同一图片输出稳定

FAST_OPTS:调用选项的单一出口

禁用思维链的参数集中在 FAST_OPTS 一处定义,由客户端统一附加到每次调用,避免在各调用点重复书写、漏写或写法不一:

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

该选项对 Qwen3 系列模型通过 extra_body 生效;对非思考型模型(如 GPT、豆包)该参数会被忽略,无副作用。 因此同一个出口可以服务全部模型,不需要在调用处逐个判断模型类型。该开关默认开启,可通过环境变量 AI_DISABLE_THINKING=false 关闭。

这一节的数字可以核对

130 秒与 3–8 秒为同一管线的实测值,差异来自识别路径的调用方式重构,而非更换模型。 工具清单与动作定义可由演示环境核对,无需申请。 查看核验方式 →

09多源扩展:铭牌、合同与发票

提取函数的第三个参数是来源名称,这一设计预留了多文档提取的能力。同一台设备的信息可能分散在铭牌、采购合同与发票三份材料中, 三者共用同一个提取函数与同一套分类逻辑:

// 铭牌
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);

三份材料的提取结果合并后交给同一个分类函数。分类函数按置信度取舍,铭牌字段(置信度 0.95)自动优先于合同字段(置信度 0.78); 前端表单的来源标签显示每个字段实际来自哪一份文档,便于在核对时回到原始材料。

表 7 · 多源提取的调用方式与来源标签
来源调用方式说明
铭牌extractFromNameplate(base64, "image/jpeg", "铭牌")现场拍摄的铭牌照片,管线的默认来源
合同extractFromNameplate(contractBase64, "image/png", "合同")采购合同 PDF 先转为页面截图,随后按图片处理
发票extractFromNameplate(invoiceBase64, "image/jpeg", "发票")与铭牌共用同一提取函数与分类逻辑

口径:多源合并的取舍规则为取置信度更高的取值;来源标签随字段一并返回前端,不单独存储。

10设计决策汇总

上述各节的实现细节可以收敛为八项设计决策:

表 8 · 八项设计决策与其作用
设计决策作用
两阶段分离(提取 / 映射)提取提示词不涉业务逻辑;映射为纯代码,无附加延迟
Qwen3-VL 32B 视觉模型中文 OCR 准确率较高,对工业铭牌的中英混排适应良好
静态 FIELD_MAP(约 60 条)新增字段只需追加入口,提示词无需改动
OpenAI 原生客户端 + enable_thinking: false单次调用延迟由 130 秒降至 3–8 秒
JSON 容错解析(extractJsonObject)不依赖模型输出格式的一致性
DynamicForm 三类分组 + 置信度着色操作者在数秒内判断哪些字段可直接通过
有效性检查 + 模型降级图片无效时不展示半空表单
多源支持(铭牌 / 合同 / 发票)同一设备可从多份文档提取,自动取置信度更高的取值

该管线已在 EAMX 中稳定运行,核心文件与职责如下表。

表 9 · 核心文件与职责
文件职责
artifacts/api-server/src/agents/nameplate-extractor.tsStage 1 视觉提取(171 行)
artifacts/api-server/src/agents/field-classifier.tsStage 2 字段分类(232 行)
artifacts/api-server/src/agents/equipment.agent.tsStage 3 动态表单构造(prepare_equipment_form 约 230 行)
artifacts/api-server/src/agents/coordinator.agent.ts两阶段管道集成与降级
artifacts/api-server/src/lib/llm-json-utils.tsJSON 容错解析
lib/integrations-openai-ai-server/src/client.tsOpenAI 客户端与 FAST_OPTS
这一能力对应的落地环节

识别管线的产出是一张预填表单,写入台账的动作由使用者确认后触发。卡片字段定义与其后的折旧、维修、处置各阶段的衔接, 见 资产购置之后,一次把账建对 →

11边界:本文不承诺的三件事

本文各项数字均带测试口径。就这条管线而言,以下三项不在承诺范围内。

表 10 · 不在承诺范围内的三项
事项说明
识别准确率不承诺 100% 置信度着色与人工核对环节的存在即为此。绿色字段表示模型的置信度较高,不表示该字段已由系统校验
不承诺完全无人值守 B 类推断字段与 C 类技术参数均需操作者确认;管线产出的是预填表单,写入台账由使用者确认后触发
图片无效时不做猜测 铭牌模糊、反光或画面中不含铭牌时,管线说明原因并给出建议,不生成半空表单

口径:延迟数字为开发环境实测记录;识别准确率未在本文中列示。

12作者与依据

作者
EAMX 产品团队 · 产品与解决方案
原始文稿
白杨,十余年设备资产管理从业经验,EAMX 产品负责人
依据
铭牌识别管线各阶段的实现代码:nameplate-extractor.ts、field-classifier.ts、equipment.agent.ts、coordinator.agent.ts、llm-json-utils.ts、client.ts
数据来源
开发环境的管线延迟实测记录;演示环境(无需申请公开)的工具清单
引用标准
认证的对象是组织,而非软件——任何声称「某系统通过 ISO 55001 认证」的表述,均与 ISO 55001 所界定的认证对象不符。
更新日期
2026-09-28
01相关产品能力

本文所述的问题,系统如何解决

02相关文章

同专题与跨专题的延伸

这条管线的每一项数字均可核对

130 秒到 3–8 秒的延迟优化、约 60 条字段映射、三类字段的置信度区间、六个核心文件与职责—— 上述内容均可由演示环境核对工具清单与动作定义,无需申请。

信息中心 / 技术评估
关心实现方式与接入细节
设备管理 / 运维
关心建档环节如何落地