设备页、案例页、公司页被 AI 搜索当成三个孤岛:用 @id 接成一个实体网络

2026-10-06 01:19:58 0 次浏览
GEOAI搜索JSON-LD实体对齐Schema.orgASP.NET Core

适用读者:负责制造企业官网或 B2B 产品站的后端工程师(示例用 ASP.NET Core,其他语言可对照思路)、正在推生成式引擎优化(Generative Engine Optimization, GEO)的技术负责人、同时维护设备型号页、行业案例页、公司介绍页三类页面模板的开发者。不需要 SEO 背景,但要能改服务端代码、能往页面 head 里注入 JSON-LD。

去年 8 月给一家做数控机床的客户做 GEO 诊断,翻 Perplexity 的引用记录时发现一件很别扭的事:AI 介绍那台主力立式加工中心,全程没提厂家是谁;介绍厂家,又只字未提那台投了三年推广费的主力机型。三类页面各自都能被 AI 搜索引用,但拼不成一个完整的实体。

内容没毛病,缺的是语义层的"引线"。这篇只讲一件事:用 Schema.org 的 @id——一个 IRI 形式的实体标识符——把三类页面接成跨页面实体网络。about+mentions、sameAs、isBasedOn 这些字段的语义之前已经写过多篇,这里不重复展开,聚焦 @id 引用机制本身:IRI 怎么命名、@graph 里骨架节点怎么摆、跨页面怎么串联。

先泼冷水:AI 引擎不会替你做实体合并

很多人的直觉是"AI 这么聪明,同一家公司总能猜出来吧"。实测恰恰相反。AI 爬虫拿到的是一页页孤立的 HTML,实体对齐依赖页面里显式的机器可读信号。没有 @id,引擎只能靠字符串匹配公司名;公司名在设备页是全称、在案例页是简称、在 About 页混着英文名,匹配链就断了。

@id 实体网络

6 月我们做过一个对照:同一个站,三份 JSON-LD 语法全部合规,但设备页用公司全称、案例页用简称。Perplexity 生成的回答里,这两个名字各说各的,一看就是两个实体。引擎的实体对齐不是语义猜测,是标识符对齐,标识符得你自己给。

原理剖析:@id 的跨页面归并机制

先说 JSON-LD 层的规则。@id 是节点的身份标识。同一份文档里,两个节点只要 @id 相同,解析器就把它们合并成一个节点,字段取并集,这是 JSON-LD 1.1 规范里写得明明白白的行为。跨文档时,因为 IRI 在全网范围内不重复,合规的解析器同样可以把不同 URL 上抓到的同 @id 节点归并到一起。

AI 引擎侧的处理链路大致四步:抓取页面、抽取 JSON-LD 里的实体与关系、按 @id 做对齐归并、生成回答时按实体取材而不是按页面取材。下面这条时序图就是符合各家公开文档描述的流程:

sequenceDiagram
    participant Bot as AI 爬虫
    participant Idx as 实体索引
    Bot->>Idx: 抓 /products/vmc850,登记 @id=…/products/vmc850/#device
    Bot->>Idx: 抓 /cases/c0231,读到 about 指向同一个设备 @id
    Idx->>Idx: 把案例的成交背景挂到设备实体上
    Bot->>Idx: 抓 /about,登记 @id=…/#organization
    Idx->>Idx: 由 brand 引用补上设备到公司的边
    Note over Idx: 三个 URL 归并成两个实体、一组关系边

还有一个容易忽略的点:骨架节点(stub)不是可有可无的。有些引擎的解析器只处理单页文档,不会为了补全引用再回源抓一遍。所以每个页面的 @graph 里,被引用的实体要放一个只带 @id、@type、name 的骨架节点,把图在本页内部先连通;跨页面的归并交给 IRI 本身完成。原则就一条:完整定义只出现一次,其他地方只留 @id。

@id 用 IRI 而不是普通字符串,还有一层含义:规范上它不要求可访问、不保证能解引用,但实践里我们一律用"真实页面 URL + 锚点"的形式。这样既让不认识 JSON-LD 的兜底解析器也能顺着 URL 找到页面,出问题时人也方便直接打开排查。

动手前,先把 IRI 命名规矩定死

@id 一旦被引擎收录,改一次就是一次实体分裂:老 IRI 和新 IRI 会被当成两个实体,各自在索引里漂一阵子。所以命名规矩要当成接口契约来定,我们站上收敛成三条:

  1. 路径段用系统主键(产品编号、案例编号),不用 SEO 友好的中文别名,URL 改版不影响 @id;
  2. 锚点后缀标明实体类型(#device、#case、#organization),一个页面将来挂多个实体时不打架;
  3. 全站 @id 只允许 https 一个 scheme,www 与非 www 只留一种写法,301 重定向必须先于 JSON-LD 变更上线。

三类页面在实体网络里各自扮演什么角色,一张表说清楚:

页面 主节点(完整定义现场) 引用的实体(只留 @id)
设备型号页 /products/{型号} Product 设备本体,字段写全 公司实体(brand 引用 + Organization 骨架)
行业案例页 /cases/{编号} Article 案例正文,字段写全 设备实体、公司实体(about/author 引用 + 骨架)
公司 About 页 /about Organization 公司本体,字段写全 不引用,是全网的引用终点

改造后的实体网络长这样:

flowchart LR
    P1["设备型号页 /products/vmc850"] -->|主节点 @id| D(("设备实体<br/>…/products/vmc850/#device"))
    P2["行业案例页 /cases/c0231"] -->|about 引用 @id| D
    P2 -->|author 引用 @id| O(("公司实体<br/>…/#organization"))
    P3["公司 About 页 /about"] -->|主节点 @id| O
    D -->|brand 引用 @id| O
    style D fill:#eef7ee
    style O fill:#eef7ee

对应到代码,就是把所有 IRI 收进一个静态类,任何页面不允许手写 @id 字符串。下面是完整的注册表写法。

// 依赖:.NET 8(System.Text.Json 为框架自带,无需第三方包)
// 环境:ASP.NET Core 8 MVC,Razor 布局页统一注入 <script type="application/ld+json">
namespace EquipmentSite.Schema;

// 全站实体 IRI 注册表:所有 @id 只在这里定义,别的文件不许出现裸字符串
public static class EntityIri
{
    // 主域做前缀:@id 是全局 IRI,要求全网不重复且长期稳定
    public const string Base = "https://www.example-equipment.com/";

    // 公司实体锚点:全站只有这一处定义,三类页面统一引用
    public const string Organization = Base + "#organization";

    // 设备实体:路径段用产品编号,页面 URL 改版时 @id 不跟着漂移
    public static string Device(string modelNo) => $"{Base}products/{modelNo}/#device";

    // 案例实体:编号与后台主键一致,排查时方便和数据库对账
    public static string Case(string caseNo) => $"{Base}cases/{caseNo}/#case";

    // 规矩:新增实体先来这里登记,再写页面;注销实体要保留 IRI 并做 301
}

设备型号页:主节点写全,引用只留 @id

设备页是设备实体的"定义现场",主节点把字段写完整,公司作为 brand 引用只留 @id。JSON-LD 输出收敛到一个基类加几个节点类:

// JSON-LD 节点基类:@ 开头的字段名用转义标识符或特性映射都可以,
// 团队约定统一走 JsonPropertyName,全文检索 "@id" 时不用考虑转义写法
public abstract class LdNode
{
    // 实体 IRI:值一律来自 EntityIri,禁止手写字符串
    [JsonPropertyName("@id")]
    public string Id { get; set; } = "";

    // 实体类型:Product / Organization / Article
    [JsonPropertyName("@type")]
    public string Type { get; set; } = "";
}

// 只留引用的节点:不带任何描述字段,语义上等于一个指针
public sealed class IdRef
{
    // 只有 @id 一个字段,AI 引擎沿 IRI 找完整定义
    [JsonPropertyName("@id")]
    public string Id { get; set; } = "";
}

// 骨架节点:被引用实体在本页的最小登记,只有身份没有细节
public sealed class StubNode : LdNode
{
    // name 必须与定义现场一致,否则同一 @id 出现冲突字段
    [JsonPropertyName("name")] public string Name { get; set; } = "";
}

public sealed class DeviceNode : LdNode
{
    // 正式型号名,是设备实体在本站的权威叫法
    [JsonPropertyName("name")] public string Name { get; set; } = "";

    // 产品编号,方便引擎与 sku 类检索对上
    [JsonPropertyName("sku")] public string Sku { get; set; } = "";

    // 一段式摘要,控制在一百字左右
    [JsonPropertyName("description")] public string Description { get; set; } = "";

    // brand 指回公司实体,靠 @id 完成跨实体连线
    [JsonPropertyName("brand")] public IdRef Brand { get; set; } = new();
}

// 文档根:每个页面输出一个 @graph
public sealed class GraphDoc
{
    // 固定值,JSON-LD 的版本锚点
    [JsonPropertyName("@context")] public string Context { get; set; } = "https://schema.org";

    // 装主节点与骨架节点的数组
    [JsonPropertyName("@graph")] public List<object> Graph { get; set; } = new();
}

// 控制器:设备型号页的实体图输出
public IActionResult DeviceDetail(string modelNo)
{
    // 序列化选项:默认 Encoder 会把中文转成 \uXXXX,日志里没法直接读,换成宽松模式
    var opt = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // 型号不存在直接 404,避免给爬虫喂空 @id 的残缺图
    var device = _repo.GetByModelNo(modelNo)
        ?? throw new HttpRequestException($"型号 {modelNo} 不存在");

    // 新建文档:@context 固定 schema.org,@graph 装本页实体
    var doc = new GraphDoc();

    // 主节点:字段在这里写完整,本页就是设备实体的定义现场
    doc.Graph.Add(new DeviceNode
    {
        Id = EntityIri.Device(modelNo),   // 主节点的正式 IRI
        Type = "Product",
        Name = device.ModelName,
        Sku = device.ModelNo,
        Description = device.Summary,
    });

    // 公司骨架节点:保证单页解析也能看到 brand 的落点
    doc.Graph.Add(new StubNode
    {
        Id = EntityIri.Organization,
        Type = "Organization",
        Name = device.VendorName,   // 必须与 About 页注册名保持一致
    });

    // Razor 视图里一行输出即可
    ViewBag.JsonLd = JsonSerializer.Serialize(doc, opt);
    return View(device);
}

案例页与公司页:引用靠 @id 串起来

案例页的主节点是案例本身。案例在 schema.org 里没有官方类型,我们用 Article 承载,让 about 与 author 两个属性各自指向设备和公司的 @id(这两个属性的取舍之前文章讲过,这里只看 @id 怎么接)。写法上和设备页共用同一套类:

// 案例页:主节点是案例,另外两个是纯骨架,全靠 @id 连图
// ArticleNode 的 About/Author 属性类型与 IdRef 同构,定义从略
public IActionResult CaseDetail(string caseNo)
{
    // 与设备页共用同一套序列化选项
    var opt = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

    // 案例不存在同样直接 404
    var c = _repo.GetCase(caseNo) ?? throw new HttpRequestException("案例不存在");

    var doc = new GraphDoc();

    // 主节点:案例本体,字段在这里写完整
    doc.Graph.Add(new ArticleNode
    {
        Id = EntityIri.Case(caseNo),   // 案例实体的正式 IRI
        Type = "Article",
        Headline = c.Title,
        DatePublished = c.PublishedAt.ToString("yyyy-MM-dd"),
        // about 与 author 都只塞 @id,AI 引擎沿 IRI 去找完整定义
        About = new IdRef { Id = EntityIri.Device(c.ModelNo) },
        Author = new IdRef { Id = EntityIri.Organization },
    });

    // 设备骨架:name 必须与设备页主节点的正式型号名一致
    doc.Graph.Add(new StubNode
    {
        Id = EntityIri.Device(c.ModelNo),
        Type = "Product",
        Name = c.DeviceName,
    });

    // 公司骨架:与 About 页注册名一致
    doc.Graph.Add(new StubNode
    {
        Id = EntityIri.Organization,
        Type = "Organization",
        Name = c.VendorName,
    });

    ViewBag.JsonLd = JsonSerializer.Serialize(doc, opt);
    return View(c);
}

公司 About 页反过来:Organization 是主节点,logo、contactPoint、地址这些字段在这里写完整;设备和案例不出现在它的 @graph 里——公司实体的定义现场只有这一处,别处只留引用。三类页面各管各的定义现场,谁也不替谁复制数据,这也是后面不出现字段冲突的前提。

上线前,先用脚本把图走一遍

发布到 staging 后,我们用一个小脚本逐页自检,两个断言就能拦掉大部分事故:不允许出现注册表之外的野 @id;被引用的实体必须在本页 @graph 里有落点。

# 依赖:pip install pyld requests
# 环境:本地 Python 3.11,对 staging 环境逐页抓取自检,URL 列表放 txt 里逐行传入
import json, re, sys, requests
from pyld import jsonld

BASE = "https://www.example-equipment.com/"

def check(url):
    html = requests.get(url, timeout=10).text
    blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)
    assert blocks, f"{url} 没有 JSON-LD"
    for raw in blocks:
        doc = json.loads(raw)
        # flatten 会按 @id 归并节点,归并结果里能看到所有实体和引用边
        flat = jsonld.flatten(doc)
        ids = {n["@id"] for n in flat if "@id" in n}
        # 校验 1:不允许出现注册表之外的野 @id
        wild = [i for i in ids if not i.startswith(BASE)]
        assert not wild, f"野 @id:{wild}"
        # 校验 2:公司实体要么完整定义、要么以骨架出现在本页,
        # 防止只有引用没有落点,单页解析器连不成图
        assert any(i.endswith("#organization") for i in ids), f"{url} 缺公司实体节点"

for line in open(sys.argv[1], encoding="utf-8"):
    # 逐行读 URL 清单,一行一个页面,建议覆盖三类模板各几条
    check(line.strip())

改造前后,我们观察到什么

改造 7 月中旬上线,观测到 9 月初,约 7 周。样本只有一个站,数据仅供定性参考:

观察项 改造前(5-6 月) 改造后(8-9 月)
Perplexity 回答里"设备+公司"同框出现 8 周里 1 次 7 周里 11 次
引用设备时给出的公司名 全称/简称/英文名随机出现 统一为 About 页注册名
GPTBot 抓设备页后 48 小时内跟进抓 About 页 偶发 每周 3-5 组连续抓取
Bing 索引里 About 页快照的更新间隔 20 天以上 约 5-7 天
问"XX 加工中心是哪家做的"能否答对 答不出或答错 第 3 周起稳定答对

第 9 天才在 Perplexity 的一次回答里第一次看到引用链生效,比我们预想的慢。实体索引的更新周期明显以周计,改完头几天看不到变化很正常,别急着回滚。

三个我亲手踩过的坑

坑一:http/https 双写导致实体分裂。 7 月底 sitemap 里混进了两条 http 开头的老链接,@id 基于配置项拼接,跟着输出了 http 版。一周内 Bing 的实体面板里同时存在两个名字相同的公司实体,互相抢事实。修复是把 EntityIri.Base 收敛成单一来源,CI 里加了一条 grep:@id 拼接结果里出现 http:// 直接让构建失败。

坑二:骨架节点带了矛盾字段。 案例页的设备骨架图省事,name 用了案例标题里的旧款名,和设备页主节点的正式型号名对不上。引擎对同一 @id 的冲突字段怎么处理是黑盒,稳妥做法是骨架节点只放 @id、@type、name,且 name 必须和定义现场一致。我们把冗余字段的取值改成实时查产品表,宁可多一次查询。

坑三:@id 跟着页面 URL 一起改版。 8 月市场部想给设备页换 SEO 友好的中文路径,幸好 @id 里的路径段是产品编号,只改了页面路由,实体标识一根汗毛没动。如果当初 @id 直接用页面 URL,这次改版就是一次全站实体迁移,前面 7 周的索引积累要清零重来。

误区澄清与趋势判断

两个常被问到的问题。一是"@id 必须是能打开的网页吗"——规范上不要求,IRI 是标识不是地址;但可解引用的 IRI 对引擎更友好,排查也更方便,没有理由不用。二是"写了 @id 引用,引用马上就涨吗"——不会,实体索引按周更新,看引用率的变化趋势,别盯单日数据。

趋势上,AI 搜索正在从页面级索引走向实体级索引,回答引用的单位是实体和它关系网上的事实,而不是某篇孤立的页面。对设备厂商这种多类页面、长决策链的 B2B 场景,@id 实体网络是投入产出比很划算的一类 GEO 改造:不新增页面、不大改内容,只把语义层的引用关系补齐。改造期间遇到归并异常,或者引擎侧行为和预期对不上的,欢迎评论区贴日志交流。

参考与延伸

  • JSON-LD 1.1 规范(W3C):节点与 @id 的合并语义 — https://www.w3.org/TR/json-ld11/
  • Schema.org Thing 属性定义(@id 所依附的根类型)— https://schema.org/Thing
  • Schema.org Product 类型 — https://schema.org/Product
  • Google 搜索中心:结构化数据工作原理 — https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data

关键词:GEO、AI 搜索、@id 实体对齐、JSON-LD、Schema.org、ASP.NET Core、实体网络

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