设备 3D 展示页 AI 读不懂怎么办:3DModel 与 CAD 文件页的结构化改造实战

2026-09-19 09:59:16 10 次浏览
JSON-LDASP.NET CoreSEO制造业B2B3D模型GEO

适用读者:制造业 B2B 站点的前端与全栈工程师,负责产品页搜索可见性的 SEO 工程同学。需要会写 ASP.NET Core Razor 模板、看得懂 JSON-LD,不需要碰 WebGL 渲染管线。

场景还原:37 个型号页,60 天引用为零

接手这个站点的改版,是从一次例行的页面资产盘点开始的。客户是做数控折弯机与伺服送料机的机械厂,产品中心一共 37 个型号页,每个型号页顶部嵌了一块 WebGL 画布,鼠标拖拽能看设备外形,画布右下角挂着一个下载按钮,指向 /files/xxx.step 这样的静态地址。销售负责人说这批三维模型前后做了两个多月,是整站最贵的内容资产,指望它在外贸询盘里当敲门砖。

3D 线框设备模型与 CAD 图纸

我们把这 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/stepmodel/stlmodel/iges,而不是在一个节点上塞数组。

需要说明的是 STEP 的 MIME 在 IANA 里没有正式登记项,业界常见写法有 model/stepapplication/step 两种。我们的做法是统一写 model/step,同时在页面正文里用中文写明「STEP AP242,单位毫米」,让文本与结构化声明互相印证。

contentUrl:能不能真的拿到

contentUrl 必须是一个可公开 GET 的完整 URL。这一步是实打实的下载校验:不少管线在把节点放进候选集之前,会发起一次 HEAD 或 Range 请求,确认状态码是 200、Content-Type 对得上、体积在合理区间。三种写法会直接出局:

  1. 走登录态或带一次性 token 的下载地址;
  2. 由 JavaScript 点击事件触发的伪链接(href="#"javascript:void(0));
  3. 重定向到 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/stepmodel/stl 格式判定依据 application/octet-stream 或留空
contentSize 18.4 MB 体积提示,影响下载意愿 写成字节整数,与页面文案不一致
isAccessibleForFree true 声明免登录免费 实际需要注册却标 true,造成引用后被投诉
datePublished 2026-04-18 版本新鲜度信号 建模改版后不更新
license 授权页完整 URL 商用授权说明 指向首页,无法定位条款
subjectOf / about Product 的 @id 关联归属主体 只写字符串名称,未用 @id 引用

多格式发布的型号,按格式拆成多个 3DModel 节点,各自带独立 @idcontentUrl,再由 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" }
    }
  ]
}

这段结构里最容易被省掉的是 aboutsubjectOf 的双向关联。只写单向时,如果抓取器先命中 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 contentUrlencodingFormat 成对出现
豆包 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 取值,失败就阻断发布;把可达性自检任务从启动一次改成每日定时,异常直接进工单;模型文件每次改版,同步更新 datePublishedcontentSize,避免声明与实际文件漂移。

这三步加起来不到两百行代码,换来的却是那块昂贵画布第一次真正进入 AI 的视野。

如果你在实施过程中遇到了特殊的地址鉴权场景,或者发现某个入口对 encodingFormat 的取值有特殊偏好,欢迎在评论区把具体的字段值和抓取日志贴出来一起讨论。

参考与延伸

3DModel · JSON-LD · ASP.NET Core · 制造业B2B · GEO · AI优化AIO · 结构化数据 · CAD文件页

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