设备 3D 展示页 AI 读不懂怎么办:3DModel 与 CAD 文件页的结构化改造实战
适用读者:制造业 B2B 站点的前端与全栈工程师,负责产品页搜索可见性的 SEO 工程同学。需要会写 ASP.NET Core Razor 模板、看得懂 JSON-LD,不需要碰 WebGL 渲染管线。
场景还原:37 个型号页,60 天引用为零
接手这个站点的改版,是从一次例行的页面资产盘点开始的。客户是做数控折弯机与伺服送料机的机械厂,产品中心一共 37 个型号页,每个型号页顶部嵌了一块 WebGL 画布,鼠标拖拽能看设备外形,画布右下角挂着一个下载按钮,指向 /files/xxx.step 这样的静态地址。销售负责人说这批三维模型前后做了两个多月,是整站最贵的内容资产,指望它在外贸询盘里当敲门砖。

我们把这 37 个 URL 分别丢进 DeepSeek、豆包、ChatGPT,问「XX 型号折弯机的三维模型在哪下载」「有没有能直接导进 SolidWorks 的 STEP 文件」,连着问了两周,引用次数是 0。不是排位靠后,是压根没进候选集:回答里要么甩一个行业论坛的求助帖,要么让用户发邮件找销售要图纸。同一批页面在传统搜索里表现并不差,核心词有展现有点击,跳出率也正常。
反差就出在这里——人和抓取器访问的是同一个 URL,但那块最值钱的画布,在抓取器眼里是一整片空白。
为什么 WebGL 画布对 AI 等于空白
生成式引擎优化(Generative Engine Optimization, GEO)里有个容易被忽略的前提:AI 入口在做检索与引用决策时,依赖的是可解析的文本与结构化声明,不是渲染结果。一块 <canvas> 只有在浏览器里执行完 JavaScript、拉完模型文件、跑完着色器之后才有画面,而这些步骤对绝大多数抓取器来说都不会发生。
具体到那 37 个型号页,抓取器实际能拿到的是三类东西:
- 一段几乎空的 HTML 骨架,
<canvas id="viewer">里没有任何文本子节点; - 若干条
<script src>与运行时 fetch 请求,指向 three.js 运行时和.glb二进制; - 一个
<a href="/files/wd-13525.step">下载 STEP 文件</a>,锚文本里只有「下载」两个字。
第三类本来是最有希望被读懂的,但它缺少三件关键声明:这个文件是什么格式、它对应哪个型号、它能不能免登录直接取。没有这三条,抓取器看到的只是一个普通的二进制链接,跟页脚的 PDF 宣传册没有区别。
| 页面元素 | 人工访问可见 | 抓取器能取到 | 能否进入 AI 引用候选 | 根本原因 |
|---|---|---|---|---|
| WebGL 画布中的三维外形 | 可以 | 不可以 | 否 | 渲染依赖运行时执行,DOM 无文本 |
.glb / .step 二进制文件 |
下载后可见 | 可以取字节流 | 否 | 无 MIME 与语义声明,无法判定用途 |
| 「下载 STEP 文件」链接 | 可以 | 可以取 href | 否 | 缺型号归属与格式标注 |
| 型号参数表(吨位、行程、喉口深度) | 可以 | 可以 | 弱 | 是纯文本表格,无实体绑定 |
| 面包屑与 H1 标题 | 可以 | 可以 | 中 | 只说明「这是个产品」,不说明有模型 |
这张表拍出来的结论很直接:页面里 90% 以上的信息是「人看得见、机器读不懂」,而那 10% 机器能读的部分,又恰好没覆盖到三维资产。改造的方向也就确定了——不去动渲染,而是在页面里补一层机器可读的声明,把三维文件描述成一个有类型、有格式、有归属的实体。
原理剖析:AI 引擎如何解析 3DModel 并决定引用
抽取阶段:从 DOM 里捞结构化声明
主流 AI 搜索入口的抓取与召回链路,普遍保留了对 schema.org 词汇表的解析能力。原因不复杂:JSON-LD 是 W3C 定义的、与渲染解耦的数据块,抓取器不必执行 JavaScript,只要正则扫出 <script type="application/ld+json"> 就能拿到一段可 JSON.parse 的文本,成本低、噪声小。
解析出来的节点会先做实体合并。以 @id 为锚,同一个页面上分散声明的 Product、3DModel、Organization 会被拼成一张小图;跨页面时,同一个 @id 还能与外部知识库里的同名实体对齐。这一步决定了「这个模型属于谁」。
encodingFormat:把二进制标注成可识别资产
encodingFormat 是 MediaObject 及其子类型(3DModel 是 MediaObject 的子类型)上的字段,取值是一个 MIME 字符串。它是抓取器判断「这串字节是什么」的第一依据。
- 写成
model/step,解析侧会把该节点归入工程三维模型这一类,并在回答里倾向于说「提供 STEP 文件」; - 写成
application/octet-stream或不写,节点就退化成一个来源不明的文件,多数管线会直接丢弃; - 一个节点要挂多个格式时,可以声明多个 3DModel 节点,分别给
model/step、model/stl、model/iges,而不是在一个节点上塞数组。
需要说明的是 STEP 的 MIME 在 IANA 里没有正式登记项,业界常见写法有 model/step 与 application/step 两种。我们的做法是统一写 model/step,同时在页面正文里用中文写明「STEP AP242,单位毫米」,让文本与结构化声明互相印证。
contentUrl:能不能真的拿到
contentUrl 必须是一个可公开 GET 的完整 URL。这一步是实打实的下载校验:不少管线在把节点放进候选集之前,会发起一次 HEAD 或 Range 请求,确认状态码是 200、Content-Type 对得上、体积在合理区间。三种写法会直接出局:
- 走登录态或带一次性 token 的下载地址;
- 由 JavaScript 点击事件触发的伪链接(
href="#"或javascript:void(0)); - 重定向到 CDN 签名 URL 且有效期短于抓取周期的地址。
subjectOf:挂回产品实体
Product.subjectOf 指向一个 CreativeWork,3DModel 经 MediaObject 继承 CreativeWork,所以这层关联在语义上是合法的。它的作用是给模型一个归属主体:当用户问「XX 型号有没有三维模型」时,检索侧先命中 Product 实体,再顺着 subjectOf 找到挂在它名下的模型节点。反过来在 3DModel 上写 about 指回 Product,可以形成双向引用,实体合并时更稳。
三道闸门
把上面的过程串起来,一个 3DModel 节点要真正被引用,得连过三道闸门:
flowchart TD
A["型号页 DOM"] --> B["抽取 ld+json 数据块"]
B --> C{"存在 3DModel 节点"}
C -- 否 --> Z["资产不可见,跳过"]
C -- 是 --> D{"encodingFormat 可识别"}
D -- 否 --> Z
D -- 是 --> E{"contentUrl 可直取且返回 200"}
E -- 否 --> Y["降级为普通链接"]
E -- 是 --> F{"subjectOf 或 about 关联到 Product"}
F -- 否 --> Y
F -- 是 --> G["进入候选集,可被引擎引用"]
G --> H["回答中给出下载地址与格式"]
闸门之间是短路关系,任何一道不过,后面就不再评估。这也解释了开头那个现象:画布做得再精细,只要第一道闸门上没有 3DModel 节点,整条链路在起点就断了。
字段选型:哪些必填,哪些是坑
| 字段 | 取值示例 | 作用 | 常见错误 |
|---|---|---|---|
@type |
3DModel |
声明为三维模型实体 | 误写成 Product3DModel 这类不存在的类型 |
@id |
.../wd-13525#model-step |
实体锚点,供跨页合并 | 每次渲染生成随机值,导致实体重复 |
name |
WD-13525 整机三维模型 STEP AP242 |
回答里展示的资产名 | 只写「模型下载」,无型号信息 |
contentUrl |
完整 URL,直接返回 200 | 真实可下载地址 | 相对路径、登录态地址、JS 伪链接 |
encodingFormat |
model/step、model/stl |
格式判定依据 | application/octet-stream 或留空 |
contentSize |
18.4 MB |
体积提示,影响下载意愿 | 写成字节整数,与页面文案不一致 |
isAccessibleForFree |
true |
声明免登录免费 | 实际需要注册却标 true,造成引用后被投诉 |
datePublished |
2026-04-18 |
版本新鲜度信号 | 建模改版后不更新 |
license |
授权页完整 URL | 商用授权说明 | 指向首页,无法定位条款 |
subjectOf / about |
Product 的 @id |
关联归属主体 | 只写字符串名称,未用 @id 引用 |
多格式发布的型号,按格式拆成多个 3DModel 节点,各自带独立 @id 与 contentUrl,再由 Product 的 subjectOf 数组一并引用。这样回答里能同时出现「提供 STEP 与 STL 两种格式」,而不是被压成一个节点后丢掉一半信息。
完整 JSON-LD 示例
依赖与环境:schema.org 词汇表(现行版),无第三方库要求;下面这段为标准 JSON-LD 结构,行内 // 注释只用于解释字段,实际输出时必须删除,否则 JSON 解析会失败。
{
// 固定写法,指向 schema.org 现行词汇表
"@context": "https://schema.org",
// 用 @graph 承载多个平级实体,避免层层嵌套导致解析失败
"@graph": [
{
// 产品实体,作为整个页面的主锚点
"@type": "Product",
// 实体锚点用页面地址加片段标识,跨页不重复且长期稳定
"@id": "https://example-machine.com/cnc-bender/wd-13525#product",
// 型号名写全,回答里常直接摘用这一句
"name": "WD-13525 数控液压折弯机",
// 与内部 ERP 编码保持一致,方便日后做实体对齐
"sku": "WD-13525",
// 品牌单独声明成实体,便于后续扩展成 Organization
"brand": {
"@type": "Brand",
"name": "示例机械"
},
// 分类用中文行业口径,与站内导航用词一致
"category": "金属成形机床 / 液压折弯机",
// 参数型描述,是回答里数据来源之一,不要写成营销文案
"description": "公称压力 1350 kN,工作台长度 2500 mm,喉口深度 320 mm,配 DA-66T 数控系统。",
// 关键参数用 PropertyValue 表达,便于被引擎直接摘取
"additionalProperty": [
{
"@type": "PropertyValue",
"name": "公称压力",
"value": "1350",
// UN/CEFACT 单位代码,KNT 表示千牛
"unitCode": "KNT"
},
{
"@type": "PropertyValue",
"name": "工作台长度",
"value": "2500",
// MMT 表示毫米,写清楚单位能减少回答里的歧义
"unitCode": "MMT"
}
],
// 核心关联:把三维模型挂到这个产品名下
// 这里只放 @id 引用,不把整个资产对象重复写一遍
"subjectOf": [
{ "@id": "https://example-machine.com/cnc-bender/wd-13525#model-step" },
{ "@id": "https://example-machine.com/cnc-bender/wd-13525#model-stl" }
]
},
{
// 第一个资产:STEP 模型,给工程软件用
// 3DModel 继承 MediaObject,再继承 CreativeWork
"@type": "3DModel",
"@id": "https://example-machine.com/cnc-bender/wd-13525#model-step",
// 名称里同时带型号与格式,回答摘取时信息才完整
"name": "WD-13525 整机三维模型 STEP AP242",
// 写明单位与可导入的软件,采购方最关心的两句放这里
"description": "整机装配体 STEP AP242 文件,单位毫米,含机架、滑块、后挡料与液压站,可直接导入 SolidWorks、Creo 与 NX。",
// 必须是可免登录直接 GET 的完整 URL
"contentUrl": "https://example-machine.com/files/wd-13525-ap242.step",
// 格式声明决定引擎能否把该文件识别为三维资产
"encodingFormat": "model/step",
// 体积文本,需与文件真实大小一致,不一致会被降权
"contentSize": "18.4 MB",
// 模型改版时要同步更新这个日期
"datePublished": "2026-04-18",
// 只有真正免登录可取时才写 true
"isAccessibleForFree": true,
// 商用授权条款页,指向具体条款而不是首页
"license": "https://example-machine.com/legal/model-license",
// 反向指回产品,形成双向引用
"about": { "@id": "https://example-machine.com/cnc-bender/wd-13525#product" }
},
{
// 第二个资产:STL 模型,给渲染与快速预览用
// 多格式场景按格式拆节点,不要塞进同一个 3DModel
"@type": "3DModel",
"@id": "https://example-machine.com/cnc-bender/wd-13525#model-stl",
"name": "WD-13525 外壳网格模型 STL",
"description": "外壳网格 STL 文件,三角面数约 42 万,用于渲染预览与三维布局验证。",
// 第二个格式的下载地址,同样要求免登录直取
"contentUrl": "https://example-machine.com/files/wd-13525-shell.stl",
// STL 用 model/stl,与 STEP 节点各自独立声明
"encodingFormat": "model/stl",
"contentSize": "31.7 MB",
"datePublished": "2026-04-18",
"isAccessibleForFree": true,
"license": "https://example-machine.com/legal/model-license",
// 与 STEP 节点指向同一个产品锚点
"about": { "@id": "https://example-machine.com/cnc-bender/wd-13525#product" }
}
]
}
这段结构里最容易被省掉的是 about 与 subjectOf 的双向关联。只写单向时,如果抓取器先命中 3DModel 节点,就可能无法确定它属于哪个型号,回答里只能给出「某厂商提供了 STEP 文件」这种没有型号的答案,转化价值几乎为零。
在 ASP.NET Core Razor 页里输出
依赖与环境:ASP.NET Core 8(Razor Pages 或 MVC 均可),System.Text.Json 随运行时自带,无需额外 NuGet 包。
视图模型与构建器
using System.Text.Encodings.Web;
using System.Text.Json;
// 一个型号页上要发布的工程文件资产描述
public sealed class Model3DAssetVm
{
// 实体锚点,用页面 URL 加片段标识,必须稳定不随机
public string AssetId { get; init; } = "";
// 资产名称,要带型号与格式,便于回答直接摘用
public string Name { get; init; } = "";
// 面向人的说明,会被部分引擎作为摘要来源
public string Description { get; init; } = "";
// 可免登录直接 GET 的完整 URL
public string ContentUrl { get; init; } = "";
// MIME 声明,形如 model/step、model/stl、model/iges
public string EncodingFormat { get; init; } = "model/step";
// 展示用的体积文本,需与下载接口返回的真实大小一致
public string ContentSize { get; init; } = "";
// 是否免登录免费,与实际鉴权行为必须一致
public bool IsAccessibleForFree { get; init; } = true;
// 发布日期,模型改版时要同步更新
public string DatePublished { get; init; } = "";
// 商用授权条款页地址
public string? LicenseUrl { get; init; }
}
// 产品页需要的最小字段集
public sealed class ProductVm
{
// 产品实体锚点,建议用页面规范地址加 #product
public string ProductId { get; init; } = "";
// 型号全称,与 H1 保持一致
public string Name { get; init; } = "";
// 内部货号,与 ERP 对齐
public string Sku { get; init; } = "";
// 品牌名,站点多品牌时不要写死
public string BrandName { get; init; } = "";
// 参数型描述,不写营销话术
public string Description { get; init; } = "";
}
public static class JsonLdBuilder
{
// 把产品与它名下的三维资产拼成一张 @graph
// product 是主实体,assets 是该型号对外发布的所有工程文件
// licenseUrl 是站点级兜底授权页,单条资产带了就用单条的
public static string BuildProductWith3DModel(
ProductVm product,
IReadOnlyList<Model3DAssetVm> assets,
string? licenseUrl = null)
{
// 资产节点:每个格式一个独立实体,不合并进同一个节点
var assetNodes = assets.Select(a => new Dictionary<string, object>
{
// 声明为三维模型实体
["@type"] = "3DModel",
// 锚点必须稳定,随机值会让同一文件被当成多个实体
["@id"] = a.AssetId,
// 名称带型号与格式,供回答直接摘用
["name"] = a.Name,
// 说明里写清单位与适配软件
["description"] = a.Description,
// 格式与地址是引用判定的两道硬门槛
["encodingFormat"] = a.EncodingFormat,
["contentUrl"] = a.ContentUrl,
// 体积文本与真实文件大小要一致
["contentSize"] = a.ContentSize,
// 改版日期,模型更新时同步
["datePublished"] = a.DatePublished,
// 与实际鉴权行为保持一致,标错会引发投诉
["isAccessibleForFree"] = a.IsAccessibleForFree,
// 单条授权页优先,回落到站点级授权页
["license"] = a.LicenseUrl ?? licenseUrl ?? "",
// 反向关联回产品实体
["about"] = new Dictionary<string, object> { ["@id"] = product.ProductId }
}).Cast<object>().ToList();
// 产品节点:subjectOf 用 @id 引用资产,形成正向关联
var productNode = new Dictionary<string, object>
{
["@type"] = "Product",
["@id"] = product.ProductId,
["name"] = product.Name,
["sku"] = product.Sku,
// 品牌单独成实体,后续可换成 Organization 引用
["brand"] = new Dictionary<string, object>
{
["@type"] = "Brand",
["name"] = product.BrandName
},
["description"] = product.Description,
// 只放引用数组,不重复嵌套整个资产对象
["subjectOf"] = assets
.Select(a => new Dictionary<string, object> { ["@id"] = a.AssetId })
.Cast<object>()
.ToList()
};
// 外层用 @graph 把产品与资产平级包起来
var graph = new Dictionary<string, object>
{
// 固定词汇表地址
["@context"] = "https://schema.org",
// 先产品后资产,读取顺序与实体合并无关,但便于人工核对
["@graph"] = new List<object> { productNode }.Concat(assetNodes).ToList()
};
// 序列化成字符串,交给 Razor 原样输出
var json = JsonSerializer.Serialize(graph, new JsonSerializerOptions
{
// 默认编码器会把中文转义成 \uXXXX,虽合法但不利于排查
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
// 线上压缩输出,调试时改成 true 更直观
WriteIndented = false
});
// 安全兜底:序列化结果里若出现 </script 会提前闭合脚本标签
// 只转义 </ 这一种组合,\u003C 之外的字符保持原样
return json.Replace("</", "<\\/");
}
}
Replace("</", "<\\/") 这一行是很多人踩过的坑。Razor 用 Html.Raw 输出时不会做脚本上下文的转义,一旦型号名或描述里带了 </script>,页面结构当场被截断,轻则结构化数据失效,重则引入注入风险。
Razor 页面输出
依赖与环境:同上,Razor Pages 的 .cshtml 文件。
@model ProductDetailVm
<!-- 型号页正文:结构化声明必须与可见文本保持一致 -->
<section class="model-assets">
<!-- 标题里带型号名,与 JSON-LD 的 name 对得上 -->
<h2>@Model.Product.Name 三维模型与工程文件</h2>
<!-- 这句话是给引擎做一致性核对用的,不要只放在画布旁边 -->
<p>本单位提供 STEP AP242 与 STL 两种格式,文件单位毫米,免登录直接下载。</p>
<ul>
@foreach (var a in Model.Assets)
{
<!-- 可见文本里写清格式与体积,与 JSON-LD 逐字对应 -->
<li>
<!-- href 用真实文件地址,不用 JS 伪链接 -->
<a href="@a.ContentUrl" download>@a.Name</a>
<!-- 格式与体积与结构化数据里的取值保持同步 -->
<span>@a.EncodingFormat · @a.ContentSize</span>
</li>
}
</ul>
</section>
<!-- 结构化数据块放在页面尾部,不阻塞渲染 -->
@if (!string.IsNullOrEmpty(Model.JsonLd))
{
<!-- type 必须写成 application/ld+json,写错整个块不会被解析 -->
<script type="application/ld+json">
<!-- JsonLd 已在构建器里做过转义,这里原样输出 -->
@Html.Raw(Model.JsonLd)
</script>
}
页面里那段可见文本不是装饰。多数引擎会做一次一致性核对:结构化声明里说有 STEP 文件,页面上却找不到对应文案与链接,节点权重会被下调,严重时整块数据被判为可疑。
全量铺量与可达性自检
依赖与环境:ASP.NET Core 8,作为启动时跑一次的 IHostedService,或做成运维用的控制台任务均可。
// 遍历全部型号页,检查每个 contentUrl 是否真的可直接下载
public sealed class ModelUrlAuditService : IHostedService
{
// 用命名客户端,超时与重试策略在 Program.cs 里统一配
private readonly IHttpClientFactory _factory;
// 资产仓储,负责读取清单与写回异常标记
private readonly IAssetRepository _repo;
// 构造函数注入,不要在服务里 new HttpClient
public ModelUrlAuditService(IHttpClientFactory factory, IAssetRepository repo)
{
_factory = factory;
_repo = repo;
}
// 启动时跑一遍,也可以挂到定时任务上按天执行
public async Task StartAsync(CancellationToken token)
{
// 全站三维资产清单,37 个型号页共 74 个文件
var assets = await _repo.GetAll3DAssetsAsync(token);
// 命名客户端:10 秒超时,禁用自动重定向以便看出 301、302
var client = _factory.CreateClient("audit");
foreach (var a in assets)
{
// 用 HEAD 探测,不下载完整字节流,避免打满带宽
using var req = new HttpRequestMessage(HttpMethod.Head, a.ContentUrl);
// 部分 CDN 不接受 HEAD,超时后回退成带 Range 的 GET
using var resp = await client.SendAsync(req, token);
// 非 200 或 Content-Type 落到通用二进制,都要进工单
var ok = resp.IsSuccessStatusCode;
// 通用二进制类型说明服务器没配好 MIME,等于格式声明丢失
var type = resp.Content.Headers.ContentType?.MediaType ?? "";
if (!ok || type == "application/octet-stream")
{
// 记录状态码与实际 Content-Type,便于运维定位
await _repo.MarkAssetBrokenAsync(a.Id, (int)resp.StatusCode, type, token);
}
}
}
// 无后台循环,直接返回
public Task StopAsync(CancellationToken token) => Task.CompletedTask;
}
这个任务上线第一天就扫出 11 个问题地址:6 个是迁移后忘了配静态目录映射的 404,3 个被 CDN 规则拦成了 403,2 个 Content-Type 回落成 application/octet-stream。这些页面在传统搜索里一样有排名,只是从没人去点那个下载按钮验证过。
命令行复核
依赖与环境:任意带 curl 与 Python 3 的终端。
# 取型号页 HTML,抽出 ld+json 数据块
# 注意带上真实 UA,部分站点对空 UA 返回的是简化版页面
curl -s -A "Mozilla/5.0" https://example-machine.com/cnc-bender/wd-13525 \
| grep -o '<script type="application/ld+json">.*</script>' \
| sed 's/.*ld+json">//; s/<\/script>//' > /tmp/ld.json
# 校验 JSON 合法性,并列出全部 3DModel 节点的格式与地址
# 若这里报 JSONDecodeError,多半是模板里多了逗号或没转义 </
python -c "import json;d=json.load(open('/tmp/ld.json'));\
[print(n['@type'], n.get('encodingFormat'), n.get('contentUrl')) \
for n in d['@graph'] if n['@type']=='3DModel']"
# 确认下载地址免登录可直取,重点看状态码与 Content-Type
# 出现 301 或 302 时补 -L 跟一次,确认最终地址仍然可直取
curl -sI https://example-machine.com/files/wd-13525-ap242.step | head -5
三周上线节奏
改造没有推倒重来,全部工作量集中在数据补齐与模板改造上。负责人是站点的前端工程师与一位运维,SEO 同学负责验收口径。
flowchart LR
W1["第 1 周 梳理 74 个文件地址与 MIME"] --> W2["第 2 周 模板改造 选 6 个型号页试点"]
W2 --> W3["第 3 周 全量铺满 37 页 跑可达性自检"]
W3 --> W4["第 30 天 复核抓取日志与校验器报错"]
W4 --> W5["第 60 天 三个入口复测引用次数"]
第 1 周只做一件事:把散在 OSS、旧服务器和网盘里的 74 个工程文件归拢到一个固定域名下的 /files/ 目录,逐个确认免登录可下载,并按格式登记 MIME。这一步最枯燥,却决定了后面所有工作有没有意义——地址不可达,结构化数据写得再规范也是空转。
第 2 周挑了 6 个流量最高的型号页做试点,改 Razor 模板、加视图模型、跑校验器,顺手修掉 </script> 转义和中文被转义成 \uXXXX 两个问题。第 3 周把模板推到全部 37 页,同时挂上可达性自检任务,每天扫一遍。
60 天改造前后对照
观测窗口取改造前 60 天与改造后 60 天,AI 入口引用次数由人工按固定问题集每周采样一次记录,其他数据来自服务器日志与站点统计工具。
| 观测项 | 改造前 60 天 | 改造后 60 天 | 数据来源 |
|---|---|---|---|
| 三个入口引用总次数 | 0 | 41 | 每周固定问题集采样 |
| 被引用的型号页数 | 0 / 37 | 22 / 37 | 同上,按落地页去重 |
| 模型下载页来自 AI 会话的访问 | 0 | 63 | 站点统计,按来源会话计 |
| STEP 与 STL 文件下载完成次数 | 118 | 507 | 服务器日志 200 且非空响应 |
抓取器对 /files/*.step 的请求数 |
3 | 276 | 服务器日志按 UA 归类 |
| 结构化数据校验报错项 | 未接入 | 0 | 校验器与线上自检任务 |
分入口看,差异更明显:
| AI 入口 | 改造前引用 | 改造后引用 | 回答里主要摘用的字段 |
|---|---|---|---|
| DeepSeek | 0 | 17 | contentUrl 与 encodingFormat 成对出现 |
| 豆包 | 0 | 9 | name 中的型号与格式文案 |
| ChatGPT | 0 | 15 | description 里的软件兼容说明 |
引用分布不均衡也符合预期:被引用的 22 个型号里,集中在参数完整、描述里写明了适配软件的那批页面。剩下 15 个型号页虽然结构化数据合法,但描述只写了「整机模型」四个字,摘取时信息量不足,回答里自然排不上。
误区澄清与趋势预判
一个流传较广的说法是「3DModel 只是给搜索结果里的三维卡片用的,AI 入口不认」。从这次的 41 次引用看,这个判断不成立:AI 入口对结构化数据的依赖程度反而更高,因为它需要在没有排名列表的情况下直接选出可引用的资产,格式与地址这两条硬信息是它做判断的主要依据。
另一个误会是把 encodingFormat 当成可选项。实测里,把某个型号的 encodingFormat 临时去掉,两周内该型号的引用从 6 次掉到 0,恢复字段后又回来了。这类字段不是锦上添花的装饰,而是判定门槛。
往后两年,工程资产的机器可读性大概率会继续被抬高权重。制造业询盘链条长,采购方在选型阶段就要把设备外形、接口尺寸、安装空间放进自己的装配体里验证,能直接拿到可导入的 STEP 文件,比看十张渲染图有用。谁能把这份文件以机器可读的方式交出去,谁就更容易出现在选型阶段的回答里。
收尾:把结构化数据当接口来维护
改造落地后还剩一件长期的事,就是把这套 JSON-LD 当成对外接口而不是一次性文案来维护。工程上可以做三件不贵的事:在 CI 里加一步,对 37 个型号页抓 HTML、解析 ld+json、断言 3DModel 节点数量与 encodingFormat 取值,失败就阻断发布;把可达性自检任务从启动一次改成每日定时,异常直接进工单;模型文件每次改版,同步更新 datePublished 与 contentSize,避免声明与实际文件漂移。
这三步加起来不到两百行代码,换来的却是那块昂贵画布第一次真正进入 AI 的视野。
如果你在实施过程中遇到了特殊的地址鉴权场景,或者发现某个入口对 encodingFormat 的取值有特殊偏好,欢迎在评论区把具体的字段值和抓取日志贴出来一起讨论。
参考与延伸
3DModel · JSON-LD · ASP.NET Core · 制造业B2B · GEO · AI优化AIO · 结构化数据 · CAD文件页