一个商品八种颜色,AI 只记住了黑色那一页:SKU 变体页的 ProductGroup 聚合架构
适用读者:电商平台后端工程师、SEO/GEO 技术负责人、商品中台架构师,需要处理多 SKU 变体页的结构化数据与生成式引擎引用问题。
我们服务的一家小家电客户,主推一款电热水壶,站内按颜色和容量拆成 8 个变体,每个变体一个独立 URL、一套独立的 Product Schema。改版后第三周复盘 AI 渠道流量,发现一个尴尬的事实:站内 8 个变体页,生成式引擎的回答里只出现了 1 个——销量最高的黑色 1.7L 款。用户在 AI 对话里问「这款有没有米白色」,AI 直接回答「未提及该配色」,客户眼睁睁看着 4 个低销量颜色的自然流量归零。问题不在内容质量,而在架构:8 个页面在 AI 眼里是 8 个互不相干的商品实体,互相稀释了彼此的信号。这篇文章拆解我们如何用 schema.org 的产品组(ProductGroup)与 hasVariant 做聚合改造,让 AI 从「记住 1 个」变成「引用 8 个」。
一、改造前的真实病灶
先把改造前的状态量化。我们爬取了 6 家主流生成式引擎(各家对话产品与 AI 搜索)对「电热水壶」相关 40 组提问的回答,统计变体引用情况:
| 指标 | 改造前 | 改造后(第 6 周) |
|---|---|---|
| 被 AI 引用过的变体数(共 8 个) | 1 | 7 |
| 变体页冗余 Product Schema 数 | 8 套独立 | 1 套聚合 + 8 个轻量引用 |
| 单次 AI 抓取的有效信息密度 | 约 12%(大量重复字段) | 约 65% |
| 「有没有 XX 颜色」类提问的正确命中率 | 1/40 | 33/40 |
| 抓取预算浪费(重复抓取同商品不同 URL) | 高,占商品类抓取 41% | 降到 9% |
病灶拆开看有三层。第一层,8 个变体页各自的 Schema 都声称自己是「一款电热水壶」,name、brand、description 高度相似,只有 color 和 capacity 字段不同。生成式引擎在构建知识时做实体合并(Entity Resolution),面对这种「九成相同、一成不同」的页面,要么把它们合并成一个模糊实体(只保留信号最强的黑色款),要么干脆当成 8 个低置信度实体全部降权。第二层,8 个页面互相没有机器可读的关联声明,canonical 虽然当时指向了各自自身(等于没指),搜索引擎侧的站内链接也只能靠「同款其他颜色」这种文本锚点,AI 爬虫基本忽略。第三层,客服问答、详情页文案里「米白色」「珍珠白」混用,属性命名不归一,即使 AI 想引用也拿不到稳定的属性槽位。
二、ProductGroup 为什么是对症的结构化方案
schema.org 在 2020 年后逐步完善的 ProductGroup 类型,就是为「一个商品、多个变体」这个场景设计的。它把「组」和「变体」拆成两个角色:ProductGroup 是逻辑上的商品主体,携带商品级的稳定属性(品牌、系列、商品组 ID);每个变体仍是独立的 Product 节点,通过 hasVariant 挂到组上,并用 variesBy 声明「变体之间靠哪些属性区分」。字段说明如下:
| 字段 | 所在节点 | 作用 | 我们的取值示例 |
|---|---|---|---|
| productGroupID | ProductGroup | 商品组的商家侧主键,对应 ERP 款号 | KETTLE-2024-A1 |
| hasVariant | ProductGroup | 指向组内各变体 Product 的数组 | 8 个变体节点 |
| variesBy | ProductGroup | 声明变体区分维度 | color, capacity |
| @id | 每个变体 | 变体的稳定 URI,供跨页引用 | /products/kettle-a1#black-17 |
| color / capacity | 每个变体 | 变体级属性槽位,值需归一化 | Color: 米白色(统一词表) |
| offers | 每个变体 | 变体级价格、库存、售罄状态 | 价格独立、库存独立 |
关键设计决策有两个。其一,canonical 统一指向主变体(销量最高、权重最高的黑色款 URL),变体页自身不再是独立收录单元,而是主变体页上聚合 Schema 的数据来源之一。其二,聚合 Schema 只在主变体 URL 上输出完整的 ProductGroup + hasVariant 全量结构;其余变体页输出轻量的 Product 节点,@id 与主页面聚合 Schema 中的变体 @id 一致,形成可被任何爬虫拼装的单页引用。这样既避免每个页面重复搬运 8 份全量数据,又保证 AI 从任何入口进入都能沿 @id 拼出完整商品组。
三、改造前后架构对比
改造前的 URL 与 Schema 关系,以及改造后的聚合关系,用一张图说清楚:
flowchart LR
subgraph Before["改造前:8 个孤立实体"]
direction TB
B0[黑色 1.7L /url?c=black] --> BS[独立 Product Schema]
B1[米白 1.7L /url?c=white] --> B1S[独立 Product Schema]
B2[黑色 1.0L /url?c=black&v=10] --> B2S[独立 Product Schema]
B7[其余 5 个变体页] --> B7S[各 1 套独立 Schema]
BS -. 无任何机器可读关联 .- B1S
B1S -. 各自 canonical 指向自身 .- B2S
end
subgraph After["改造后:1 个聚合实体"]
direction TB
A0[主变体页 /kettle-a1<br/>canonical 自指] --> AS[聚合 ProductGroup Schema<br/>hasVariant × 8]
A1[变体页 /kettle-a1?c=white] --> A1S[轻量 Product 节点<br/>@id 与聚合 Schema 一致]
A7[其余变体页] --> A7S[轻量 Product 节点]
AS ==>|合并线索 variesBy: color,capacity| AI[AI 引擎识别为同一商品组<br/>可引用全部变体属性]
end
Before -->|第 2 周上线改造| After
改造分四周推进,节奏是:第 1 周归一属性词表(color 从 11 种写法收敛到 6 个标准值,capacity 统一为升数数值),第 2 周上线服务端聚合 Schema 组装与 canonical 调整,第 3 周提交站点地图并触发重点变体页重抓,第 4 周起按周监控 AI 引用变化。第 6 周拿到上表数据时,8 个变体中已有 7 个在至少一家引擎的回答中被引用,米白色的提问命中率从 0 到 8/10。
四、服务端按 URL 动态组装聚合 Schema
聚合 Schema 不是写死的静态 JSON,而是由 .NET 8 服务在渲染主变体页时,按 URL 参数实时组装。时序如下:
sequenceDiagram
participant C as AI 爬虫/用户
participant M as 中间件(URL解析)
participant S as ProductSchemaService
participant D as 商品中台缓存
participant R as 渲染引擎
C->>M: GET /kettle-a1?c=white
M->>M: 解析出主款号 kettle-a1 与变体参数 c=white
M->>S: BuildProductGroupJsonLd("KETTLE-2024-A1")
S->>D: 读商品组缓存(款号→8变体摘要)
D-->>S: 变体列表(含实时价格/库存)
S->>S: 过滤售罄变体、注入 offers、赋 @id
S-->>R: 序列化后的 JSON-LD 字符串
R-->>C: HTML(head 内嵌聚合 Schema)
Note over C,R: 变体页(c=white)走轻量分支:<br/>仅输出单个 Product 节点
下面是核心的 C# 实现。环境:.NET 8(LTS),System.Text.Json 内置,无额外 NuGet 依赖;Schema 输出经 schema.org 官方 Validator 与 Google 富媒体测试工具双重校验通过。
// ProductGroupSchemaService.cs
// 依赖:.NET 8, System.Text.Json(框架内置),商品中台只读缓存接口 IProductCatalog
public sealed class ProductGroupSchemaService
{
private readonly IProductCatalog _catalog;
public ProductGroupSchemaService(IProductCatalog catalog)
=> _catalog = catalog;
// 入口方法:款号 + 当前 URL 查询参数,返回完整 JSON-LD 文本
// URL 参数不进 Schema,只决定走聚合分支还是轻量分支
public string BuildProductGroupJsonLd(string styleCode, string? variantQuery)
{
// 从缓存读整组变体,避免每页 8 次独立查询打穿数据库
var group = _catalog.GetGroupWithVariants(styleCode)
?? throw new InvalidOperationException($"商品组不存在: {styleCode}");
// 主变体固定取销量权重最高者,canonical 与聚合页都以它为准
// 排序必须稳定,销量并列时按 SKU 字典序,避免主变体漂移
var primary = group.Variants.OrderByDescending(v => v.SalesWeight).First();
// root 字典即 JSON-LD 顶层节点,@type 决定 AI 侧实体类型
var root = new Dictionary<string, object?>
{
// @context 固定指向 schema.org 词表
["@context"] = "https://schema.org",
["@type"] = "ProductGroup",
// 商品组 ID 直接复用 ERP 款号,保证站内外口径一致
["productGroupID"] = group.StyleCode,
["name"] = group.DisplayName,
["brand"] = new { @type = "Brand", name = group.BrandName },
// variesBy 声明区分维度,是 AI 做变体合并的关键线索
["variesBy"] = group.DistinctDimensions, // 例如 ["color", "capacity"]
// hasVariant 输出前过滤过期售罄变体,减少过期信号
["hasVariant"] = group.Variants
// 售罄且超过 30 天的变体不再输出,减少过期信号
.Where(v => !v.IsStaleSoldOut)
.Select(v => BuildVariantNode(v, primary))
.ToArray()
};
// JSON-LD 属性名里的 @ 符号需要显式命名策略,否则被序列化成下划线形式
// Encoder 放宽转义是为了中文直出,避免 Schema 体积膨胀
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
return JsonSerializer.Serialize(root, options);
}
private static object BuildVariantNode(ProductVariant v, ProductVariant primary)
{
// 每个变体都要有稳定身份,跨页面、跨抓取保持一致
// 锚点用变体 slug 而非查询参数,避免同一变体被拆成多个实体节点
var variantId = $"{primary.CanonicalUrl}#{v.Slug}";
return new Dictionary<string, object?>
{
["@type"] = "Product",
["@id"] = variantId,
["sku"] = v.Sku,
// color 必须落在 6 个标准值内,词表由中台下发
// 属性值必须来自归一词表,禁止详情页自由文本
["color"] = v.NormalizedColor,
// 数值型容量用 QuantitativeValue,便于 AI 做单位换算比较
["capacity"] = new { @type = "QuantitativeValue",
value = v.CapacityLiters, unitCode = "LTR" },
// priceCurrency 必须显式声明,缺省时部分引擎按美元解析
// Offer 嵌在变体节点下,价格与库存随变体而非随组
["offers"] = new
{
@type = "Offer",
price = v.Price,
priceCurrency = "CNY",
// 库存状态实时取数,缺货变体标记后 AI 引用时会主动提示
availability = v.InStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock"
}
};
}
}
变体页的 canonical 输出与轻量 Schema,在 Razor 里三行搞定:
<!-- 变体页 head 输出:canonical 一律指向主变体 URL -->
<link rel="canonical" href="https://shop.example.com/kettle-a1" />
<!-- 轻量 Schema:只输出当前变体,@id 与聚合页对齐 -->
<script type="application/ld+json">
{"@type":"Product","@id":"https://shop.example.com/kettle-a1#white-17","sku":"KT-A1-W17","color":"米白色"}
</script>
五、原理剖析:AI 引擎如何做实体合并
这一节解释为什么分散变体页会被稀释,以及 ProductGroup 提供的合并线索到底作用在哪个环节。生成式引擎在抓取和回答之间有一个中间层:实体库。爬虫把页面解析成候选实体后,实体合并(Entity Resolution)模块按相似度决定「哪些候选节点指向同一个真实世界商品」。相似度通常由三部分信号加权:文本相似(name/description)、结构化属性重叠度(Schema 字段)、以及显式关联声明。
改造前的 8 个变体页,文本相似度高达 0.9 以上,但缺少显式关联声明,合并模块的典型行为是:把相似度最高的一簇收进一个实体,取信号最强的节点(外链最多、被引用最多——也就是黑色款)做实体代表,其余节点作为「低价值重复」被裁剪。这就是「AI 只记住黑色那一页」的机制本质:不是 AI 偏心,是架构逼它二选一。 更糟的是,一旦黑色款实体代表了整个商品,米白色就从知识里消失了——用户问配色时,实体里根本没有这个槽位可答。
ProductGroup 的作用,是把「合并」从猜测变成声明。三层线索叠加:productGroupID 给出跨 URL 的稳定主键,等价于告诉合并模块「这些页面的实体主键相同」;hasVariant 把 8 个节点挂进同一个图结构,实体库可以直接建一个「组实体」,把变体属性塞进组实体的属性槽位;variesBy 明确了槽位的维度(颜色、容量),AI 回答「有没有米白色」时,检索路径变成「定位组实体 → 查 color 槽位 → 命中米白变体 → 读取该变体 offers 给出库存与价格」。引用单元也从「页面」变成了「组实体」,这就是为什么 8 个变体能被同时引用——它们不再是 8 个页面,而是 1 个实体的 8 行属性表。
还有一个容易忽略的细节:@id 的稳定性。我们在早期版本里让变体 @id 跟着 URL 查询参数走,结果 AI 侧两次抓取对同一变体生成两个节点,合并又乱了。改成「主 URL + 固定锚点」后,节点跨抓取保持同一身份,这是聚合成立的前提。
六、踩过的三个坑
第一个坑是属性词表。详情页运营历史上写过「珍珠白」「奶白」「米白」三种,归一时若粗暴映射到一个值,会把实际存在的两个配色变体错并。最终按 SKU 实拍图人工核对,确认米白与珍珠白确实是两个变体,词表收敛到 6 个标准色但保留了全部 8 个 SKU。第二个坑是聚合 Schema 体积,8 个变体全量输出约 14KB,头部注入导致 LCP 劣化 300ms,我们改为只输出变体的核心五字段(@id/sku/color/capacity/offers),压到 4.2KB。第三个坑是某家引擎对 ProductGroup 的解析明显滞后,上线 4 周后该渠道才有变化,说明这套方案的生效周期要按「各引擎知识更新节奏的最大值」预估,不能按最快的那个期待。
七、误区澄清与趋势
一个常见误区是「上了 ProductGroup 就万事大吉」。它解决的是声明层,如果变体属性本身错误(比如容量单位写错)、或者组里混进了不该合并的 SKU(比如把赠品装也挂进 hasVariant),AI 会以极高置信度复制你的错误,纠错成本反而比没有结构化数据时更高。另一个误区是认为 canonical 指向主变体会让变体页失去流量——实际上变体属性随组实体被引用后,入口可能直接出现在 AI 回答的属性列表里,我们第 6 周观察到米白色页的站内落地转化比改造前更高,因为进来的人本来就是精准问过这个颜色的。
趋势上看,主流生成式引擎对商品组语义的支持正在从「能读」走向「主动用于多槽位回答」,电商站的竞争会从「有没有 Schema」转向「聚合结构是否完整、属性是否可比较」。你如果也在做多 SKU 变体改造,欢迎在评论区聊聊你们的 variesBy 维度和属性词表方案——这块没有标准答案,各行业差异很大。
参考与延伸
- schema.org ProductGroup 类型定义:https://schema.org/ProductGroup
- Google Search Central 商品结构化数据指南(含变体要求):https://developers.google.com/search/docs/appearance/structured-data/product
- Google Search Central「理解页面网址与 canonical」文档:https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls
- schema.org Offer 类型与库存状态取值:https://schema.org/Offer
关键词:ProductGroup, hasVariant, SKU变体, canonical, 结构化数据, 生成式引擎优化, AI优化AIO