设备厂商的 ISO 认证怎么被 AI 看见:hasCertification 与认证实体的结构化实战

2026-09-23 01:21:30 0 次浏览
GEOschema.orgJSON-LDPython结构化数据AI搜索

去年年底,一家做包装机械的厂子遇到件怪事:德国采购方在 AI 搜索里问"这家公司有没有 ISO 9001 认证",得到的回答是"公开渠道未查询到该企业的体系认证信息"。可这家厂的资质页上,ISO 9001 证书照片挂了六年,扫描件清晰得连钢印都能看清。

问题不在证书真假,在于那张证书在页面上只是一张图。AI 抓到的是一个 <img> 标签,alt 写的是"证书",PDF 链接里的内容还得再解析一层。

适用读者:制造业 B2B 官网与独立站的运维、外贸建站开发、负责结构化数据的前端和 SEO 工程师。读完你能把 ISO、CE、API 这类资质从图片和散文变成 AI 能直接引用的实体字段,并知道哪些字段写了会被判无效。

采购方那一问,AI 卡在什么地方

销售张伟跟的这个德国客户,第三周才问出那句话。对方采购流程里有硬性要求:供应商必须有 ISO 9001,且证书在有效期内。客户没有翻官网,直接问了 AI,得到的回答是"未查询到",单子就停在资格审查那一关。

工厂机械与认证徽章经数据线相连

我们接手后把资质页翻了一遍,问题是三层的。证书是扫描件图片,页面上没有出现可抽取的文字;页面正文里确实有一句"公司已通过 ISO9001 质量管理体系认证和 ISO14001 环境管理体系认证",但两个认证挤在同一句话里,主体、有效期、发证机构全都没有;公司有两个厂区,这张证书只覆盖总厂,页面上完全没提。

改之前先测了一轮,把同类页面上的写法和 AI 的实际抽取结果对了一遍:

页面上的写法 页面示例 AI 抽取到的内容
证书扫描件图片 <img alt="证书" src="iso9001.jpg"> 几乎为空,偶尔得到"证书"二字
一句话散文 "已通过 ISO9001 与 ISO14001 认证" 能抽出认证名,但归属主体、有效期、发证机构缺失
PDF 下载链接 "点击查看证书 PDF" 视抓取器是否解析 PDF 而定,命中率不稳
结构化 JSON-LD hasCertification + Certification 实体 认证名、状态、有效期、发证机构、覆盖范围齐备

最后一行是我们改完之后的样子。差别不在写得多,在写成了能被引用的实体。

认证该挂在哪:三个容易混的字段

schema.org 里跟"资质"沾边的属性有好几个,混用是常事,混用之后 AI 读到的语义就跑偏了。

属性 挂在哪类实体上 值的类型 该用在哪
hasCertification Organization、Person、Place、Product、Service Certification ISO 体系认证、产品认证这类第三方审核结论
hasCredential Organization、Person Credential 个人或机构持有的执照、从业资格,偏教育职业类资质
companyRegistration Organization Certification 工商注册信息,也就是营业执照那一类登记
award Thing(含 Organization) Text 荣誉和奖项,不是认证,别拿它顶替

hasCertification 是这活儿的主力。Certification 这个类型挂在 CreativeWork 下面,走的是"一份可查证的文档"这条线,所以它天然带有 issuedBydatePublishedexpires 这些字段。它下面还有 DeclarationOfConformity(符合性声明)、DigitalProductPassport(数字产品护照)两个子类型,做欧盟出口的同行可以直接用子类型,语义比硬套 Certification 更准。

hasCredential 的语义是"某人或某机构持有的资格凭证",值类型是 Credential 或其子类型 EducationalOccupationalCredential。设备厂商如果要把"焊接工程师持 AWS 认证"这类人员资质挂上去,可以用它;把 ISO 9001 塞进 hasCredential 属于放错抽屉,AI 在生成答案时要么忽略,要么给出一句含糊的"具备相关资质"。

判断标准很简单:谁发的、什么时候过期、覆盖什么范围,这三个问题答得上来就用 Certification;答不上来说明你手里的东西可能只是个荣誉。

AI 引擎是怎么读到这段数据的(机制剖析)

知道机制,才知道字段写错了会掉在哪一环。

从抓页面到生成答案的四步

flowchart LR
  A[抓取器取回 HTML] --> B[抽取 script ld+json]
  B --> C[按 @id 归拢实体图]
  C --> D[与知识库里同名实体对齐]
  D --> E[检索时按字段取值作答]
  C -.@id 缺失则退化为匿名节点.- F[实体无法跨页合并]
  E --> G[答案里带出有效期与发证机构]

第一步是抓取。抓取器拿到 HTML 后,会专门找 <script type="application/ld+json"> 块,这块内容不参与渲染,也不受页面版式影响,抽取成本远低于解析表格或图片。

第二步是解析成节点。每个带 @id 的对象会被当成一个可被引用的节点,没有 @id 的对象是匿名节点,只在当前这段 JSON-LD 内部可见。

第三步是实体对齐。引擎会把页面里的 Organization 节点跟知识库里已有的公司记录合并,合并的依据是 @idurlsameAs 这些标识字段。合并成功之后,你写的 hasCertification 才真正挂到了"这家公司"头上,而不是挂在一个孤岛节点上。

第四步才是回答。用户问"有没有 ISO 9001",引擎做的是一次带字段过滤的查询:在这个公司的认证列表里找名字匹配 ISO 9001 的项,再看它的 certificationStatus 是不是 Active、expires 有没有过期。字段齐全,答案里就能带上"有效期至 2027 年 6 月";字段缺失,引擎只能回一句"未查询到"。

实体引用比嵌套内嵌更扛得住

graph TD
  O[Organization 总厂 @id #org] -->|hasCertification| C1[Certification ISO9001 @id #cert-9001]
  O -->|hasCertification| C2[Certification ISO14001 @id #cert-14001]
  O -->|subOrganization| P1[Organization 分厂 @id #plant-b]
  P1 -->|hasCertification| C1
  C1 -->|issuedBy| B1[Organization 认证机构]
  C1 -->|validIn| A1[AdministrativeArea 覆盖区域]
  C2 -->|issuedBy| B2[Organization 另一家认证机构]

上面这张图对应的就是推荐写法:每个认证单独一个节点、带独立 @id,组织节点用 hasCertification 指向它,覆盖范围用 validIn 单独说明。这样同一张证书被总厂和分厂共享时,只需要引用同一个 @id,而不是把整段证书复制两遍。复制两遍的后果是两边字段可能不一致,AI 会认为存在两张证书。

Organization 上挂认证的完整写法

依赖:无,静态站点直接内联;环境:放在 <head></body> 前的 <script type="application/ld+json"> 里。

// 说明用的 // 注释在实际粘贴时要删掉,JSON 本身不支持注释
{
  "@context": "https://schema.org",
  "@type": "Organization",
  // @id 是实体对齐的关键,别省
  "@id": "https://www.example-machine.com/#org",
  "name": "示例机械有限公司",
  // url 用来做同名实体的合并依据
  "url": "https://www.example-machine.com/",
  "hasCertification": [
    {
      "@type": "Certification",
      // 每个认证一个独立节点,方便跨页复用
      "@id": "https://www.example-machine.com/cert/iso9001#cert",
      // name 写标准全称,别只写 ISO9001
      "name": "ISO 9001:2015 质量管理体系认证",
      // alternateName 兜住采购方的各种写法
      "alternateName": ["ISO9001", "ISO 9001", "GB/T 19001-2016"],
      // 覆盖范围写进描述,答案里常被摘出来
      "description": "覆盖包装机械的设计、生产与售后服务",
      // 证书编号,便于人工复核
      "certificationIdentification": "CN21Q10087R0M",
      // 状态只接受 CertificationActive 或 CertificationInactive
      "certificationStatus": "https://schema.org/CertificationActive",
      // issuedBy 建议带上 url,方便对齐到机构实体
      "issuedBy": {
        "@type": "Organization",
        "name": "示例认证中心",
        "url": "https://www.cert-body.example/"
      },
      // 生效日期用 ISO 8601 的 YYYY-MM-DD
      "validFrom": "2021-06-18",
      // 到期字段叫 expires,schema.org 上没有 validUntil
      "expires": "2027-06-17",
      // 最近一次监督审核日期
      "auditDate": "2024-05-09",
      // validIn 说明地域范围,出口证书常用
      "validIn": {
        "@type": "AdministrativeArea",
        "name": "中国"
      },
      // url 指向证书详情 HTML 页,别只挂 PDF
      "url": "https://www.example-machine.com/cert/iso9001"
    }
  ]
}

几个容易写错的点:

到期字段是 expires,不是 validUntil 网上不少教程沿用了 validUntil,那个字段属于 OfferPermit,放在 Certification 里属于未定义属性,写了也不报错,只是永远不会被读。

certificationStatus 的值是枚举 URI。"certificationStatus": "有效""active" 都不算数,正确取值是 https://schema.org/CertificationActivehttps://schema.org/CertificationInactive

certificationIdentification 留给证书编号。 这个字段的定义就是"在独立认证机构登记的实例编号",采购方复核时用得上,也让 AI 在回答里能给出可核对的编号,而不是一句空话。

只覆盖某一个厂区的证书,validIn 写区域,同时把这条认证挂到厂区对应的 Organization 子节点上,别挂在集团节点上。

多厂区多证书:把认证实体拆出去引用

集团站点常见的情况是:总厂有 ISO 9001 和 ISO 14001,新建的分厂只拿到 ISO 9001,还有一份 CE 符合性声明只针对某几个型号。全塞进一个 Organization 节点会让覆盖范围说不清。

推荐做法是拆成两个文件——认证单独成页并各自带 @id,组织页只做引用。

// 组织页:只放引用,不重复证书字段
{
  "@context": "https://schema.org",
  "@type": "Organization",
  // 厂区节点也要有自己的 @id
  "@id": "https://www.example-machine.com/#plant-b",
  "name": "示例机械有限公司 第二厂区",
  // 用 parentOrganization 表达层级,别只写名字
  "parentOrganization": { "@id": "https://www.example-machine.com/#org" },
  // 只列这个厂区实际持有的认证
  "hasCertification": [
    // 直接引用总厂那条 ISO9001 记录,不复制字段
    { "@id": "https://www.example-machine.com/cert/iso9001#cert" }
  ]
}

认证页上再写完整节点。分厂没拿到的那份 ISO 14001,就不要出现在它的 hasCertification 里,留空比写错强。

上线前自检:一段 Python 把错挡在 CI 里

依赖:Python 3.9 及以上,只用标准库;环境:本地跑或挂到 CI 的静态检查步骤。

# 用法:python check_cert.py certs/*.jsonld
# 依赖:Python 3.9+ 标准库,无需安装第三方包
import json
import sys
import pathlib
import datetime

# schema.org 给 certificationStatus 定义的两个合法取值
ACTIVE = "https://schema.org/CertificationActive"
INACTIVE = "https://schema.org/CertificationInactive"


def iter_certs(node):
    # hasCertification 可能写成对象,也可能写成数组
    value = node.get("hasCertification", [])
    # 统一成列表,后面就不用分支了
    return value if isinstance(value, list) else [value]


def check_file(path):
    # 读文件,JSON 解析失败会抛异常,交给调用方兜住
    data = json.loads(pathlib.Path(path).read_text(encoding="utf-8"))
    problems = []
    # 支持一个文件里含 @graph 的写法
    nodes = data.get("@graph", [data])
    for node in nodes:
        # 只关心带 hasCertification 的节点
        for cert in iter_certs(node):
            # 引用式写法(只有 @id)交给认证页去校验
            if set(cert.keys()) == {"@id"}:
                continue
            # 类型检查:Certification 及其子类型都放行
            if cert.get("@type") != "Certification":
                problems.append(f"{path}: @type 应为 Certification")
            # 状态字段必须是枚举 URI,写"有效"不算
            if cert.get("certificationStatus") not in (ACTIVE, INACTIVE):
                problems.append(f"{path}: certificationStatus 取值非法")
            # 发证机构得有名字,否则答案里说不清谁发的
            if not cert.get("issuedBy", {}).get("name"):
                problems.append(f"{path}: issuedBy 缺机构名")
            # 有效期检查:字段存在、可解析、未过期
            exp = cert.get("expires")
            if not exp:
                problems.append(f"{path}: 缺 expires")
                continue
            try:
                # fromisoformat 只吃 YYYY-MM-DD,写错格式会抛异常
                exp_d = datetime.date.fromisoformat(exp)
            except ValueError:
                problems.append(f"{path}: expires 日期格式错误 {exp}")
                continue
            # 已过期但状态还标 Active,是最常见的数据腐坏
            if exp_d < datetime.date.today() and cert.get("certificationStatus") == ACTIVE:
                problems.append(f"{path}: 已过期却仍标 Active")
            # validFrom 早于 expires 才说得通
            vf = cert.get("validFrom")
            if vf and datetime.date.fromisoformat(vf) > exp_d:
                problems.append(f"{path}: validFrom 晚于 expires")
    return problems


# 入口:所有入参当文件路径,汇总后按退出码反馈给 CI
if __name__ == "__main__":
    all_problems = []
    # 逐个文件检查,异常不算通过
    for arg in sys.argv[1:]:
        try:
            all_problems += check_file(arg)
        except (ValueError, OSError) as exc:
            # 解析失败或文件读不到,都记为一条错误
            all_problems.append(f"{arg}: 无法解析 {exc}")
    # 有问题就打印并返回 1,CI 会拦住这次合并
    if all_problems:
        print("\n".join(all_problems))
        sys.exit(1)
    # 全部通过时退出码 0
    print("certification nodes ok")

这段脚本跑在我们的 GitLab CI 上,证书到期前 60 天会由另一个定时任务把 certificationStatus 改成 Inactive 并发工单。机器盯比人盯靠谱,我们吃过一次亏:2023 年那张 ISO 14001 过期三个月,页面上还写着"已通过认证"。

30 天之后:引用对照和踩到的坑

改完上线,我们挑了 12 条采购方常问的句式,每天在同一时段问一遍,记录 AI 是否回答出认证、是否给出有效期、是否给出来源页。数据是自己记的,样本不大,看趋势够用。

观察项 改造前(30 天均值) 改造后(30 天均值)
问"有没有 ISO 9001"答出认证 2 次 / 12 问 11 次 / 12 问
答案里带有效期 0 次 9 次
答案里带发证机构 0 次 7 次
答案标注来源为资质页 0 次 10 次
把分厂认证错记到总厂 3 次 0 次

中途踩到几个坑,值得单独说:

认证名写法不统一。 客户会写 ISO9001、ISO 9001、ISO-9001 三种,name 只写一种时匹配率明显低。补上 alternateName 数组后,命中次数从 7 次涨到 11 次。

认证机构名写成了中文简称。 issuedBy 里只有 name、没有 urlsameAs,引擎没法确认这是哪家机构,答案里就把机构名省掉了。补上机构官网 URL 之后,机构名才开始出现在答案里。

证书页做成了 PDF 扫描件。 结构化数据写全了,但 url 指向的落地页是个扫描件 PDF,用户点进去体验很差,AI 给的来源也显得单薄。后来补了一页 HTML 的证书详情页,把编号、范围、发证机构、有效期都写成文字,PDF 只作为附件链接。

集团节点和厂区节点混用。 一开始把分厂的证书挂在集团 Organization 上,AI 回答"该公司在第二厂区有 ISO 9001 认证"时把范围搞反过一次。改成 parentOrganization / subOrganization 分层之后,范围问题消失了。

误区澄清:结构化不保证被引用

有同行问过:字段都写全了,为什么 AI 还是不提我们的认证。几种情况是结构化解不了的。

认证的发证机构本身在公开知识库里权重很低,引擎对这条记录的置信度就低,答案里倾向于不提。这类情况能做的是把 issuedBysameAs 指向机构的权威条目,让它有机会被对齐。

问答里没问到认证时,认证字段不会被主动展示。用户问"这家厂的交期多长",答案不会顺带讲 ISO 9001,这是正常的。

还有一种是抓取还没更新。改完页面之后,重新抓取到实体合并完成要一段时间,我们这边观察到的是一到三周不等,取决于站点整体更新频率。这期间拿旧结果去判断改造失败,就是白费劲。

参考与延伸

  • Certification 类型定义与全部字段:https://schema.org/Certification
  • hasCertification 属性说明与可用类型:https://schema.org/hasCertification
  • hasCredential 与 Credential 的适用范围:https://schema.org/Credential
  • Google 结构化数据通用规范与校验建议:https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
  • schema.org 官方校验器:https://validator.schema.org/

认证这块的做法,各家站点的层级都不一样,尤其是集团多厂区、出口多认证体系的场景。你们在落地时卡在哪一步,或者踩过哪些字段的坑,评论区聊聊,我这边有实测过的写法会补进来。

GEO|hasCertification|Certification 实体|JSON-LD|AI 搜索优化|制造业 B2B|结构化数据

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。