设备厂商的 ISO 认证怎么被 AI 看见:hasCertification 与认证实体的结构化实战
去年年底,一家做包装机械的厂子遇到件怪事:德国采购方在 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 下面,走的是"一份可查证的文档"这条线,所以它天然带有 issuedBy、datePublished、expires 这些字段。它下面还有 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 节点跟知识库里已有的公司记录合并,合并的依据是 @id、url、sameAs 这些标识字段。合并成功之后,你写的 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,那个字段属于 Offer 和 Permit,放在 Certification 里属于未定义属性,写了也不报错,只是永远不会被读。
certificationStatus 的值是枚举 URI。 写 "certificationStatus": "有效" 或 "active" 都不算数,正确取值是 https://schema.org/CertificationActive 和 https://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、没有 url 和 sameAs,引擎没法确认这是哪家机构,答案里就把机构名省掉了。补上机构官网 URL 之后,机构名才开始出现在答案里。
证书页做成了 PDF 扫描件。 结构化数据写全了,但 url 指向的落地页是个扫描件 PDF,用户点进去体验很差,AI 给的来源也显得单薄。后来补了一页 HTML 的证书详情页,把编号、范围、发证机构、有效期都写成文字,PDF 只作为附件链接。
集团节点和厂区节点混用。 一开始把分厂的证书挂在集团 Organization 上,AI 回答"该公司在第二厂区有 ISO 9001 认证"时把范围搞反过一次。改成 parentOrganization / subOrganization 分层之后,范围问题消失了。
误区澄清:结构化不保证被引用
有同行问过:字段都写全了,为什么 AI 还是不提我们的认证。几种情况是结构化解不了的。
认证的发证机构本身在公开知识库里权重很低,引擎对这条记录的置信度就低,答案里倾向于不提。这类情况能做的是把 issuedBy 的 sameAs 指向机构的权威条目,让它有机会被对齐。
问答里没问到认证时,认证字段不会被主动展示。用户问"这家厂的交期多长",答案不会顺带讲 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|结构化数据