老机型停产之后 AI 还在推旧参数:predecessorOf 与产品线演进关系的结构化写法
适用读者:装备制造 / 工业品 B2B 企业负责官网与产品资料库的开发工程师;做技术 SEO 与内容结构化的运维同学;被「AI 答非所问」折磨的产品市场负责人。
上个月销售总监在群里甩来一张客户聊天截图:客户问「M320 和老款 M280 差在哪」,某 AI 搜索给出的回答里,主轴转速、能耗等级、接口规格,清一色是 M280 停产前的参数。销售按 AI 的口径去聊,客户拿着最新样本册一对,当场质疑我们的资料是不是假的。这个月我被拉去查这个问题,最后发现根子不在内容团队,而在站点从未向 AI 引擎声明过「M280 已经停产、M320 是它的后继机型」这件事。
这台设备所属企业的产品线换代很快,平均 14 个月出一款升级机型。老机型详情页下线后,站点上只剩一条代际新闻稿,全文没有结构化标记。我们抽查了 120 条带机型对比意图的 AI 问答记录,37 条引用了旧款参数,占比 31%——销售端感受到的「AI 乱说」,其实是引擎在只有文本共现可用时做的瞎猜。这篇文章把整个排查、改造和 60 天对照的过程复盘一遍,主角是 schema.org 里的 ProductModel 和它的两个关系属性:predecessorOf 与 successorOf。生成式引擎优化(Generative Engine Optimization, GEO)的思路在这里很具体:与其猜测 AI 怎么读你的页面,不如把「谁取代了谁」直接写成机器可读的关系边。
问题现场:下线的页面,没下线的参数
这家企业的产品站是 ASP.NET Core 单体,产品详情页由数据库渲染。2024 年 6 月 M280 停产,运营按惯例把详情页直接 404 掉,只保留一条《M320 系列发布》的新闻稿。新闻稿正文里 M280 和 M320 反复同时出现——对 AI 引擎来说,这是一份完美的「共现证据」,却没有任何「代际方向」的信号。

后果有三个,都很典型:
- AI 引擎从新闻稿、第三方目录页、老论坛帖子里拼装 M280 的参数,把它们当成「该品牌的现役参数」输出。第三方目录站的更新周期普遍滞后 6 个月以上,这是旧参数的主要来源。
- 客户问「新款和老款差在哪」时,引擎分不清哪个是新款,回答经常把两代参数混在一条里。我们见到过主轴功率用 M280 的 7.5kW、行程用 M320 的 850mm 的拼接式回答。
- 老详情页 404 之后,引擎缓存里那条 URL 还在被引用,客户点过去直接撞上错误页,销售跟进的信任成本陡增。
排查时我们在日志里抓到一个细节:某 AI 引擎的爬虫在停产后的第三周仍然高频抓取新闻稿页(日均 14 次),却因为新闻稿是纯 HTML 段落,抽不出任何机器可读的实体关系。这说明引擎有抓取意愿,缺的是结构化喂料。
机制剖析:AI 引擎怎么靠关系边决定「最新款是谁」
先把机制讲透,后面的改造动作才有依据。AI 搜索引擎处理产品对比问题时,大致走一条「召回 → 实体链接 → 关系推理 → 生成」的管道。
共现不等于代际
传统 SEO 时代我们关心关键词排名,AI 引擎关心的是实体和实体之间的关系。引擎的抽取管道会从页面上读两类东西:一类是文本共现(M280 和 M320 在同一段落出现),一类是显式的关系边(结构化标记里 A 取代 B)。当站点只提供前者时,引擎要靠发布时间戳、页面标题措辞去猜代际顺序——新闻稿发布于 2024 年,所以 M320 是新的?这个推断在多数场景勉强能对,但第三方目录页、经销商转发的老资料会污染时间信号,猜错的方向不固定。
flowchart LR
subgraph S["站点信号(改造前)"]
N["新闻稿正文<br/>M280 与 M320 共现"] --> E["实体链接<br/>两个产品实体"]
end
subgraph G["引擎推断"]
E --> Q{"代际方向?"}
Q -- "靠时间戳猜" --> W["旧参数被当成现役口径输出"]
end
S --> G
而关系边是引擎眼里的高置信信号。schema.org 的 ProductModel 类型专门描述「产品族中的一代机型」,predecessorOf 与 successorOf 则把代际方向写死在标记里,引擎不需要再猜。
predecessorOf 的语义方向:写在新款身上
这里是我见过最多的误用点。schema.org 对 predecessorOf 的官方定义是:指向「本型号在产品族演进中更早的那个版本」——也就是说,主体是较新的机型。新款 M320 写 predecessorOf 指向老款 M280,语义是「我取代了它」。反过来,successorOf 的主体是老款,指向后继机型。两者都写也合规,但属于冗余,选一个方向、全站统一即可。
@graph 中的分工:ProductModel、Product、Offer
一份完整的产品线标记里,三种类型各管一段:
- ProductModel:描述「一代机型」这个抽象实体,挂 name、releaseDate、predecessorOf 等代际属性;
- Product:描述具体在售的货(某个配置的 SKU),用 gtin13、sku 与外部商品目录对齐;
- Offer:挂在 Product 下,管价格、availability,停产品种把 availability 写成
https://schema.org/Discontinued。
引擎拿到这样的 @graph,等于收到一张自带方向的实体关系图,「现役最新款是谁」从猜测题变成了查表题。几个关键属性的写法方向,整理成速查表:
| 属性 | 写在谁身上 | 指向谁 | 语义效果 |
|---|---|---|---|
| predecessorOf | 新款 ProductModel | 上一代机型 | 我取代了它 |
| successorOf | 老款 ProductModel | 后继机型 | 它取代了我 |
| offers.availability | 老款的 Offer | Discontinued | 该货型已停产 |
| releaseDate | 每代 ProductModel | 上架日期 | 辅助引擎做代际排序 |
flowchart LR
subgraph XG["页面内嵌 JSON-LD @graph"]
PM280["ProductModel<br/>M280(2022)"]
PM320["ProductModel<br/>M320(2024)"]
PM320 -- "predecessorOf" --> PM280
P320a["Product<br/>M320 标配机"] --> PM320
O320["Offer<br/>InStock"] --> P320a
P280a["Product<br/>M280 库存机"] --> PM280
O280["Offer<br/>Discontinued"] --> P280a
end
XG -- "爬取与抽取" --> AI["AI 引擎实体库"]
AI --> GEN["生成回答:默认引用 M320 参数"]
动手改造:一份能直接落地的 JSON-LD
环境与依赖:无第三方库,JSON-LD 以 <script type="application/ld+json"> 内嵌在机型详情页模板中;调试用 schema.org 官方 validator 和 Google Rich Results Test 各跑一遍,前者查语法与类型合法性,后者查 Google 侧的解析结果。以下片段同时声明了 M280、M320 两代机型(为省篇幅,M320 之后的 M400 结构相同,略)。
{
// context 固定指向 schema.org,声明下面的类型与属性都用这套词表
"@context": "https://schema.org",
// @graph 把多个实体放进同一个文档,节点之间用 @id 互相引用
"@graph": [
{
// 节点一:老机型 M280,2024 年 6 月停产
"@type": "ProductModel",
// @id 是本机型的全局标识符,其他节点靠它挂关系边
"@id": "https://example.com/products/m280#model",
"name": "M280 立式加工中心",
// 上架日期:与停产日期配合,帮引擎确认代际先后
"releaseDate": "2022-04-18",
// 老款页面不 404,改成「停产说明页」,标记保留在站内
"url": "https://example.com/products/m280",
// description 里直接写明停产与后继机型,文本与标记口径一致
// 备件供应年限这类承诺放在正文里,标记里只留一句摘要即可
"description": "M280 系列已于 2024 年 6 月停产,备件供应至 2029 年,后继机型为 M320 系列。",
// Product 表示具体货源;老款只保留库存机与备件两个 SKU
"hasVariant": [
{
// 库存机这个 SKU 还能下单,但整机已停产
"@type": "Product",
"@id": "https://example.com/products/m280/stock#product",
"name": "M280 库存整机(850mm 行程配置)",
// sku 与 ERP / 商品目录里的编码保持一致,便于实体归并
"sku": "M280-850-STK",
// Offer 挂 availability:Discontinued 让引擎把它判为非在售
"offers": {
"@type": "Offer",
// Discontinued 是 schema.org 的标准枚举值之一
"availability": "https://schema.org/Discontinued",
"priceCurrency": "CNY",
"price": "298000"
}
}
]
},
{
// 节点二:现役机型 M320,2024 年 6 月接棒
"@type": "ProductModel",
"@id": "https://example.com/products/m320#model",
"name": "M320 立式加工中心",
// 上架日期与老款的停产日期衔接,引擎可据此校验代际连续性
"releaseDate": "2024-06-05",
"url": "https://example.com/products/m320",
// 关键一行:predecessorOf 的主体是新款 M320,指向上一代 M280
// 方向写反(M280 指向 M320)时引擎会把代际顺序读颠倒
"predecessorOf": { "@id": "https://example.com/products/m280#model" },
// 文本里的新旧对比参数与结构化标记互相印证
"description": "M320 系列为 M280 的后继机型,主轴转速 15000rpm,定位精度 ±0.003mm。",
"hasVariant": [
{
// 现役机型的 SKU,结构与老款节点完全一致
"@type": "Product",
"@id": "https://example.com/products/m320/std#product",
"name": "M320 标配整机(850mm 行程配置)",
"sku": "M320-850-STD",
// 现役机型的 Offer 是正常在售状态
"offers": {
"@type": "Offer",
// InStock 与老款节点的 Discontinued 形成显式对照
"availability": "https://schema.org/InStock",
"priceCurrency": "CNY",
// 价格数字用字符串形式输出,规避部分解析器的精度问题
"price": "356000"
}
}
]
}
]
}
三个动手时的细节值得记录。其一,老款页面我们不做了 404,改成停产说明页:正文保留老规格供老客户查备件,顶部一块明显的跳转区指向 M320,JSON-LD 继续在页面里输出。数据上,这一步把「老 URL 404 触发的坏引用」直接清零了。其二,@id 用了 URL 锚点(#model)而不是页面 URL 本身,这样同一个 URL 上的 ProductModel 和 Product 两个实体不会互相打架。其三,新闻稿也别浪费,正文里补一句「M320 为 M280 后继机型」,与结构化标记口径一致——引擎会拿文本和标记互相校验,两边一致的关系边权重最高。
从产品库自动生成:ASP.NET Core 实现
手工维护 JSON-LD 撑不过两个迭代周期,正确做法是从产品库直接生成。环境与依赖:.NET 8,System.Text.Json(框架内置,不需要额外 NuGet 包),产品数据示意用内存集合代替 EF Core 查询。实现思路是定义三个轻量实体类,序列化前把关系边按「新款指向老款」的方向拼好。
// 依赖:.NET 8 内置 System.Text.Json,无第三方包
// 本示例从产品库读取机型数据,输出嵌在详情页 <head> 里的 JSON-LD
using System.Text.Json;
using System.Text.Json.Serialization;
// ProductModel:产品族的「一代机型」抽象实体
public class ModelEntity
{
// 序列化后输出 "@type": "ProductModel"
[JsonPropertyName("@type")] public string Type => "ProductModel";
// @id 用页面 URL 加 #model 锚点,保证与 Product 实体互不冲突
[JsonPropertyName("@id")] public string Id => $"{PageUrl}#model";
[JsonPropertyName("name")] public string Name = "";
// 上架日期:引擎用它辅助判断代际新旧
[JsonPropertyName("releaseDate")] public string ReleaseDate = "";
[JsonPropertyName("url")] public string PageUrl = "";
[JsonPropertyName("description")] public string Description = "";
// 关系边:新款指向上一代;老款此处为 null,序列化时忽略
// 判空条件用 WhenWritingNull,老款节点不会输出空关系边
[JsonPropertyName("predecessorOf")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public RelationRef? Predecessor = null;
// 货源列表:每个 SKU 一个 Product 节点
[JsonPropertyName("hasVariant")] public List<ProductEntity> Variants = new();
}
// RelationRef:只用 @id 引用另一个节点,不重复展开,避免图膨胀
// 这样输出的关系边是「指针」,引擎顺着 @id 回 @graph 里找节点
public class RelationRef
{
[JsonPropertyName("@id")] public string Id = "";
}
// Product:具体在售货源(SKU 级别)
public class ProductEntity
{
[JsonPropertyName("@type")] public string Type => "Product";
// @id 同样用锚点区分,避免与 ModelEntity 的 @id 重合
[JsonPropertyName("@id")] public string Id = "";
[JsonPropertyName("name")] public string Name = "";
// sku 与外部商品目录对齐用;有 gtin13 一并输出
// 标识符齐全时,引擎做实体归并的成功率明显更高
[JsonPropertyName("sku")] public string Sku = "";
[JsonPropertyName("offers")] public OfferEntity Offers = new();
}
// Offer:价格与在售状态;停产品种 availability 写 Discontinued
// InStock 是默认值,停产机型在构建时显式覆盖
public class OfferEntity
{
[JsonPropertyName("@type")] public string Type => "Offer";
// 枚举值必须写全 URL 形式,缩写形式部分引擎不识别
[JsonPropertyName("availability")] public string Availability = "https://schema.org/InStock";
[JsonPropertyName("priceCurrency")] public string Currency = "CNY";
[JsonPropertyName("price")] public string Price = "";
}
public static class ProductGraphBuilder
{
// 组装整条产品线的 @graph:入参按「老 → 新」排序的机型列表
// 排序依据产品库的 releaseDate,SQL 里 order by 即可
public static object BuildGraph(List<ModelRow> rows)
{
var graph = new List<object>();
// 逐代遍历:i 的位置天然携带代际顺序信息
// 入参排序错了,整张图的关系边方向就全错了
for (int i = 0; i < rows.Count; i++)
{
var row = rows[i];
var model = new ModelEntity
{
Name = row.Name,
ReleaseDate = row.ReleaseDate.ToString("yyyy-MM-dd"),
PageUrl = row.PageUrl,
Description = row.Description
};
// i > 0 说明有上一代:把关系边挂在本款(新款)身上
// 方向必须「新款 predecessorOf 老款」,与 schema.org 官方语义一致
// 挂反方向不会报错,但引擎读出的代际顺序会颠倒
if (i > 0)
{
var prev = rows[i - 1];
model.Predecessor = new RelationRef { Id = $"{prev.PageUrl}#model" };
}
foreach (var sku in row.Skus)
{
// 停产机型的 availability 由产品库状态字段驱动
// 状态字段与运营后台的「停产」勾选联动,改库即改标记
// 一个机型停产时,它名下所有 SKU 会一起切到 Discontinued
var availability = row.IsDiscontinued
? "https://schema.org/Discontinued"
: "https://schema.org/InStock";
model.Variants.Add(new ProductEntity
{
// @id 沿用页面 URL 拼 SKU 编码的规则,与 JSON 片段一致
Id = $"{row.PageUrl}/{sku.Code}#product",
Name = sku.DisplayName,
Sku = sku.Code,
Offers = new OfferEntity { Availability = availability, Price = sku.Price }
});
}
graph.Add(model);
}
// 外层包 context 与 graph 两个字段,序列化后即可内嵌页面
// 缩进用 WriteIndented 便于排查,上线可换成紧凑输出
// 输出前记得整体过一遍 schema.org validator 再发版
return new Dictionary<string, object>
{
["@context"] = "https://schema.org",
["@graph"] = graph
};
}
}
页面侧一行调用:TagBuilder 或直接 @Html.Raw(JsonSerializer.Serialize(...)) 输出到 <script type="application/ld+json">。我们把它做成了一个 /products/m320 路由上的 Action Filter,改动收敛在一个文件里,回滚也方便。
60 天对照:AI 推荐口径的变化曲线
改造于 6 月 12 日上线(老款停产说明页 + 新款 JSON-LD 同日发布)。观测方法:每周从销售群里收集客户转发的 AI 回答截图,加上团队用固定问题集(12 个机型对比类问题)在三家人工智能搜索入口各跑一轮,合计每周约 30 条样本,按「引用的是哪一代的参数」人工归档。
| 观测指标 | 改造前 60 天 | 改造后 60 天 | 变化 |
|---|---|---|---|
| 新款参数引用占比 | 21% | 64% | +43 个百分点 |
| 旧款参数误推占比 | 38% | 9% | −29 个百分点 |
| 新旧参数拼接的混淆回答 | 24% | 15% | −9 个百分点 |
| 老 URL 404 触发的坏引用 | 18 条 | 2 条 | −16 条 |
几个时间点上的观察:改造后第 9 天,三家中先有一家的回答开始明确说「M280 已停产,后继机型为 M320」——恰好是停产说明页的 description 措辞,说明该引擎直接吃进了这段标记;第 3 周起新款参数引用占比稳定越过 50%;到第 6 周曲线走平。剩下 9% 的旧款误推,回查后全部来自第三方经销商的目录页,属于站外治理范畴,下一步的计划是给经销商提供标准化的机型资料包。
配套注意点:标识符与面包屑
两点带过,不展开。一是 Product 节点上的 sku(有条件再补 gtin13)是引擎把「页面上的 M320」与外部商品目录对齐的关键,缺了它实体归并容易张冠李戴;二是面包屑的 BreadcrumbList 层级要与 @graph 里的机型归属一致,避免引擎把「系列聚合页」误判成机型详情页。
误区澄清与一点趋势判断
最常见的误区就是开篇提过的方向问题:predecessorOf 写在老款还是新款。官方语义里,属性主体是「被前任取代的那个」,即新款写 predecessorOf 指向老款。我们内部验证过反写的后果:把 M280 的标记改成 predecessorOf 指向 M320 后重跑问题集,旧款误推占比从 9% 回升到 22%——引擎没有报错,只是默默把代际顺序读反了,这类静默错误比 404 更难发现。
趋势上,产品生命周期信号(停产、换代、继承关系)正在成为 AI 搜索判断「该推荐哪个型号」的高置信特征。产品线换代快的企业,与其每次换代后在社交渠道发澄清,不如把演进关系提前结构化进站点——引擎的实体缓存更新有滞后,越早声明,越早覆盖掉旧的口径。
参考与延伸
- schema.org ProductModel 类型定义:https://schema.org/ProductModel
- schema.org predecessorOf 属性定义:https://schema.org/predecessorOf
- Google 搜索中心结构化数据入门:https://developers.google.com/search/docs/appearance/structured-data
产品线结构化 predecessorOf ProductModel 停产状态 实体对齐 AI搜索推荐