外贸站 60 天引用对照:aggregateOffer 让 AI 读对全系列报价,而不是只记住最便宜那档

2026-09-22 01:22:39 1 次浏览
GEOAI搜索AggregateOfferSchema.orgJSON-LD

适用读者:外贸独立站的建站与运维工程师、做海外询盘增长的运营、把 ERP 报价导到站点的人。你要看得懂 JSON-LD,能动手改模板,愿意为一个产品页的多档报价做一次结构调整。文中代码都不依赖框架,Node 与 Python 各一份,照抄可跑。

一个产品页挂着 5 个规格档,起订量从 100 件到 10000 件,单价从 1.85 美元一路降到 1.15 美元。页面顶部还有一条促销文案,写着 0.98 美元起。两个月里,海外客户在 AI 搜索里问这个产品,回来的答案几乎都是 0.98 美元这一档,而 0.98 是 5 万件起订才拿得到的引流价。改文案、加说明、把促销条往下挪,全试过,没用。真正起作用的改动只有一处:把 5 档报价从「页面上的一段文字」变成结构化数据里的 AggregateOffer。

一个报价把询盘全带偏了

这家站做的是出口工业配件,主营不锈钢卡箍和管件,SKU 大概 1400 个,其中 300 多个是同一款产品挂多档报价的。产品页的模板是 2023 年那版,价格区用一张表格渲染,表格上方有一块促销位,运营会按季度换文案。

主题配图

问题第一次被注意到,是 5 月底的一封询盘。客户要 500 件,开口就写「我看到你们的报价是 0.98 美元一件」。销售回过去解释半天,客户觉得被钓鱼,单子黄了。后面两周又来了 3 封类似的,都是照着 0.98 砍价,起订量却只有几百件。

我们把客户提问的原话拿去几个 AI 搜索里复现了一遍,答案里那条价格几乎总是 0.98。这就不是销售话术的问题了,是引擎读到的东西本身偏了。

关键结论:AI 复述的不是你的报价体系,是它从页面上抓到的那个最容易被抓住的数字。页面排版决定了哪个数字最容易被抓住。

抽取机制:AI 从页面里拿价格的两条路径

要讲清楚这件事,得先说 AI 引擎拿到一个网页之后怎么处理价格。大致分两条路。

第一条路是结构化数据优先通道。抓取端把页面解析成 DOM,顺手把 JSON-LD 里的节点抽出来,做实体对齐,确认这是 Product 不是 Article,然后把 offers 字段下的价格做归一化。这条路省事,字段是机器写好的,几乎不用猜。

第二条路是正文文本抽取。没有结构化数据,或者结构化数据里没写价格,才会走这条路。此时引擎要在正文里找「哪个数字是价格」,靠的是位置、邻近词和货币符号三样线索。位置权重最高,靠页面顶部的段落会被当成摘要性内容,权重往上叠。

我们的问题就出在这儿。促销位在页面上方,文案是自然语言,句子里既有货币符号又有单位,还带「起」字,对抽取器来说这是一句完美的价格描述。而真正的 5 档报价在下方表格里,表格是 JS 渲染的,抓取端拿到的 HTML 里那块区域经常是空的。

flowchart TD
    A[抓取产品页 HTML] --> B{页面里有 JSON-LD 吗}
    B -- 没有 --> C[退回正文文本抽取]
    C --> D[页面上方促销文案权重最高]
    D --> E[只拿到一个数字 0.98]
    B -- 有 --> F[解析 Product 节点]
    F --> G{offers 是 AggregateOffer 吗}
    G -- 是 --> H[读到 lowPrice 与 highPrice 区间]
    G -- 否 --> I[读到单个 price 标量]
    H --> J[答案里给出区间与分档]
    I --> E

这里还有一层,是生成阶段的取值偏好。检索回的上下文里如果只有一个标量价格,模型在生成答案时就只能复述它,这是上下文决定的,不是模型偷懒。反过来,如果上下文里同时存在 lowPrice、highPrice、offerCount 三个字段,模型就具备了表达区间的素材,答案会写成「1.15 到 1.85 美元,看起订量」。生成式引擎优化(Generative Engine Optimization, GEO)里相当一部分工作,本质就是给模型的上下文补齐这类素材。

结构化数据不是给排名用的加分项,它是直接写进模型上下文的原料。原料里缺字段,答案里就缺信息。

数据先理顺:5 档报价在 ERP 里长什么样

改结构之前,我们先把商品库里的原始报价摊开看了一遍。以 HC-1140 这款卡箍为例,ERP 导出来是 5 行,每行一个起订量阶梯。

档位 起订量 MOQ 单价(美元) 币别来源 报价有效期 备注
T1 100 1.85 ERP 写 RMB,导出后换算 2026-10-31 样品与小批量
T2 500 1.62 ERP 写 RMB,导出后换算 2026-10-31 常规小单
T3 1000 1.41 ERP 写 USD 2026-12-15 主力档
T4 3000 1.28 ERP 写 USD 2026-12-15 整柜档
T5 10000 1.15 ERP 写 USD 2027-03-31 年度框架价

导出表里有三处脏数据要提前处理。一是币别不统一,ERP 里老数据写 RMB,新数据写 USD,混在一列里;二是价格带千分位逗号,直接塞进 JSON 会让解析器读不出来;三是报价有效期有的档位留空,留空的档在引擎那边等于没有约束,容易被引申成长期有效。

还有一条容易被忽略:页面顶部那条 0.98 的促销价,在 ERP 里根本不是一个档位,它是市场部单独维护的活动价,起订量 5 万件,只有两个大客户拿过。它在商品库里没有对应记录,这也是它一开始没进结构化数据的原因。我们最后的处理是把它也做成一档 Offer,标上 eligibleQuantity 的 minValue 为 50000,让引擎自己看懂这个价的前提条件,而不是让它消失。

JSON-LD 的写法:AggregateOffer 加逐档 Offer

Schema.org 里表达多档报价的类型是 AggregateOffer,它是 Offer 的子类,字段上多了 lowPrice、highPrice、offerCount,同时保留 offers 数组用来挂逐档的 Offer。这个组合正好对应「先给区间,再给明细」的读法。

下面这段是模板层拼 JSON-LD 的函数,Node.js 环境,无第三方依赖。

// 环境:Node.js 18+,纯 ES Module,无第三方依赖
// 输入:product 为商品主信息,tiers 为按 MOQ 升序排好的档位数组
// 输出:可直接序列化进 ld+json 脚本块的普通对象

function buildProductJsonLd(product, tiers, siteUrl) {
  // 统一取两位小数,避免浮点尾巴把 1.85 写成 1.8500000001
  const norm = (n) => Number(Number(n).toFixed(2));
  // 先把所有档位的价格抽成数组,供后面算区间用
  const prices = tiers.map((t) => norm(t.price));
  // 区间取自全部档位,含促销档,不要在这里做人工筛选
  // 筛选逻辑统一放在上一节的 Python 聚合脚本里

  return {
    "@context": "https://schema.org",
    "@type": "Product",
    // 用真实 URL 加片段标识符做实体锚点,全站不重复
    // 同页出现两个 Product 节点时,引擎会挑错,锚点能帮它认出来
    "@id": `${siteUrl}/product/${product.sku}#product`,
    name: product.name,
    sku: product.sku,
    brand: { "@type": "Brand", name: product.brand },
    // 聚合报价挂在 Product 主节点下,别藏在别的类型里
    offers: {
      "@type": "AggregateOffer",
      priceCurrency: product.currency,
      // lowPrice 与 highPrice 必须是纯数字字符串,不能带千分位逗号
      lowPrice: String(Math.min(...prices)),
      highPrice: String(Math.max(...prices)),
      // offerCount 必须等于下面 offers 数组的真实长度,写错会被判为不一致
      offerCount: tiers.length,
      offers: tiers.map((t) => ({
        "@type": "Offer",
        price: String(norm(t.price)),
        priceCurrency: product.currency,
        // 有效期写成 ISO 日期字符串,过期档位会被判为失效报价
        priceValidUntil: t.validUntil,
        // 库存状态别省,缺这个字段引擎会补一个默认值
        availability: "https://schema.org/InStock",
        // 起订量用 QuantitativeValue 表达,不要写进 name 当文案
        // unitCode C62 在 UN/CEFACT 里表示「件」
        eligibleQuantity: {
          "@type": "QuantitativeValue",
          minValue: t.moq,
          unitCode: "C62",
        },
        // 单价规格:billingIncrement 说明按几件计价,这里是按 1 件
        priceSpecification: {
          "@type": "UnitPriceSpecification",
          price: String(norm(t.price)),
          priceCurrency: product.currency,
          billingIncrement: 1,
        },
        // 每档给一个可定位的 URL,方便引擎回链到具体档位
        url: `${siteUrl}/product/${product.sku}?moq=${t.moq}`,
      })),
    },
  };
}

写的时候有三个地方最容易出错。lowPrice 和 highPrice 要写成字符串形式的数字,写成数字类型有些解析器也认,但写字符串兼容性更稳。offerCount 必须和 offers 数组长度对得上,我们第一版漏算了促销档,写了 5 但实际挂了 6 个 Offer,校验器直接给了警告。priceCurrency 全站只写一个币种,混币种会让区间失去意义。

生成逻辑:从商品库导出聚合报价

模板函数有了,剩下的是数据从哪来。我们的做法是在服务端渲染前跑一次聚合脚本,把 ERP 导出的 CSV 变成每个 SKU 一份 JSON,模板直接读这份 JSON 拼 ld+json。这样价格变动不用改代码,重跑一次导出就行。

# 环境:Python 3.10+,只用标准库,无需 pip 安装
# 用途:把 ERP 导出的报价阶梯 CSV 聚合成逐 SKU 的 JSON-LD 输入文件
import csv, json
from decimal import Decimal
from datetime import date, timedelta

# ERP 导出表列:sku,moq,price,currency,valid_days
# 一行一个阶梯档,同一个 sku 会出现多行
SRC = "erp_export/price_tiers.csv"
OUT_DIR = "site_data/jsonld/"

def load_tiers(path):
    # 按 sku 分组,key 是 sku,value 是该 sku 的全部档位列表
    grouped = {}
    with open(path, encoding="utf-8-sig") as f:
        for row in csv.DictReader(f):
            # ERP 里的价格带千分位逗号,去逗号后再转 Decimal 保精度
            price = Decimal(row["price"].replace(",", ""))
            # 币别归一:对外站点统一出 USD,RMB 与 CNY 走同一分支
            cur = "USD" if row["currency"] in ("USD", "RMB", "CNY") else row["currency"]
            grouped.setdefault(row["sku"], []).append({
                "moq": int(row["moq"]),
                "price": float(round(price, 2)),
                "currency": cur,
                # 有效期用导出日加有效天数,落成 ISO 字符串
                # 留空的按 90 天兜底,避免引擎把报价当长期有效
                "validUntil": (
                    date.today() + timedelta(days=int(row["valid_days"] or 90))
                ).isoformat(),
            })
    return grouped

def clean(tiers):
    # 按起订量升序排,保证引擎读到的档位顺序稳定
    tiers = sorted(tiers, key=lambda t: t["moq"])
    # 丢掉价格非正的脏档位,这类行多是 ERP 里的占位数据
    tiers = [t for t in tiers if t["price"] > 0]
    # 起订量重复的档只留价格更低的那条,防止同档两个价
    picked = {}
    for t in tiers:
        if t["moq"] not in picked or t["price"] < picked[t["moq"]]["price"]:
            picked[t["moq"]] = t
    return list(picked.values())

def dump(grouped):
    # 每个 sku 输出一份文件,文件名就是 sku,模板按 sku 读取
    for sku, tiers in grouped.items():
        tiers = clean(tiers)
        # 区间值在这里就算好,模板层只做序列化,不再做业务判断
        payload = {
            "sku": sku,
            "currency": tiers[0]["currency"],
            "lowPrice": min(t["price"] for t in tiers),
            "highPrice": max(t["price"] for t in tiers),
            "offerCount": len(tiers),
            "tiers": tiers,
        }
        with open(f"{OUT_DIR}{sku}.json", "w", encoding="utf-8") as f:
            # ensure_ascii 关掉,避免非 ASCII 字符被转义
            json.dump(payload, f, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    # 单次全量跑,1400 个 sku 大概十几秒,跑完覆盖旧文件
    dump(load_tiers(SRC))

脚本跑完还有一步验证,用命令行确认渲染出来的页面里确实有且只有一个 Product 节点,字段也对得上。

# 环境:Linux 服务器,curl 7.68+,python3 3.10+
# 用途:上线后确认页面里的 ld+json 块数量与关键字段

# 数一下结构化数据块有几个,期望只有一个 Product,多了说明模板重复注入
curl -s "https://example.com/product/HC-1140" | grep -c "application/ld+json"

# 上面输出大于 1 时,先去查评论组件有没有自己注入 Product 节点
# 再看 head 与 body 里是否各渲染了一份同样的 ld+json
# 重复的块会让引擎挑到字段不全的那一份,价格就是在这种情况下读空的

# 抽出 JSON-LD 并打印区间三件套,确认 offerCount 与档位数一致
# 四个输出依次是 lowPrice、highPrice、offerCount、实际档位数
curl -s "https://example.com/product/HC-1140" \
  | python3 -c "import sys,re,json;t=sys.stdin.read();b=re.findall(r'application/ld\+json\">(.*?)</script>',t,re.S);d=json.loads(b[0]);o=d['offers'];print(o['lowPrice'],o['highPrice'],o['offerCount'],len(o['offers']))"

上线第 2 周掉链子:六个坑

改完上线是 6 月 18 日,第 2 周开始陆续出问题,我按时间顺序记一下,都是当天在服务器上真实看到的现象。

第一个是模板转义。JSON-LD 里的 & 符号被模板引擎转成了 &,解析器读到一半就断。改成不转义输出解决。

第二个是重复节点。商品详情页除了主模板,评论组件自己也注入了一段 Product 节点,导致同页两个 Product。引擎挑了评论那段,里面价格是空的。给主节点加 @id,把评论那段降级成只有 Review,问题解决。

第三个是 offerCount 对不上,前面说过,漏算了促销档。

第四个是币别。脚本第一版判断写反了,把 USD 的档位按 RMB 处理,区间变成 8 块到 12 块,客户以为涨价了。

第五个是 priceValidUntil 写了 2026-06-01 这种过去日期,校验器提示报价已过期,我们改成了导出日加有效天数。

第六个比较隐蔽。部分产品页的 JSON-LD 被放在 body 中间,被 JS 异步渲染覆盖了一次,最终 DOM 里出现两份,一份旧一份新。把输出位置挪到 head 里,异步渲染不再碰它,才稳定下来。

sequenceDiagram
    participant U as 海外买家
    participant E as AI 引擎
    participant S as 外贸独立站
    U->>E: 卡箍 HC-1140 买 500 件什么价
    E->>S: 抓取产品页 HTML
    S-->>E: JSON-LD 含 AggregateOffer 共 6 档
    E->>E: 实体对齐与价格归一化
    E->>E: 匹配起订量 500 命中 T2 档
    E-->>U: 区间 1.15 到 1.85 美元,500 件为 1.62 美元

60 天对照结果

我们把上线前 30 天和稳定后 30 天各抓了一轮引用,口径是:AI 答案里出现价格时,是否说清了这是分档报价,或者给出的数字与提问的采购量对得上。对得上就算准确,只报 0.98 一律算不准。

对照项 改造前 30 天 改造后 30 天 变化
抓到的 AI 引用条数 46 52 增加 6 条
价格口径准确条数 7 39 增加 32 条
只报最低档 0.98 的条数 34 6 减少 28 条
答案里出现价格区间的条数 3 31 增加 28 条
同期询盘封数 21 24 增加 3 封
询盘里自带采购量与预算的封数 4 19 增加 15 封

最后一行的变化是销售那边最看重的。改造前 21 封询盘里只有 4 封会写清楚要多少件,其余都是问「最低多少钱」。改造后 24 封里有 19 封会主动写「要 500 件,看到你们写 1.62 美元」。销售不用再花半小时解释价格体系,报价来回的轮次从平均 4.7 轮降到 2.1 轮。

需要说清楚的是,引用条数只多了 6 条,说明这次改动没有带来流量层面的暴涨,它改的是答案的准确程度。想靠结构化数据直接拉流量,那方向不对。

哪些做法其实是白费劲

回看这两个月,有几件事做完发现没用,写下来省得别人再踩。

把促销条从页面顶部挪到底部,没用。位置权重确实降了,但正文里还有别的低价数字,引擎照样能挑出一个。真正解决问题的是让结构化数据里有完整区间,引擎就不用去正文里挑了。

在页面正文里加一段「以上价格视采购量而定」的说明,没用。自然语言对模型来说是模糊表述,它没法从这句话里推出区间,区间得是数字。

给每个档位单独开一个子页面,成本很高且效果一般。子页面之间会被判为内容重复,且引擎引用时更倾向引用主页面,子页面的结构化数据反而散了。同一页里挂 AggregateOffer 更省事。

只写 lowPrice 和 highPrice 不挂 offers 数组,效果打折。区间能让答案报出范围,但没有逐档明细,客户问到具体数量时引擎还是给不出对应的那个数。两个都要写。

后面会怎么走

从这两个月的观察看,AI 引擎对结构化数据的依赖还在往深处走,尤其价格、库存、交期这几类会直接影响采购决策的字段。谁把这些字段写得完整且自洽,谁的答案口径就更稳。下一步我们打算把交期和起订量的联动也做成结构化字段,让引擎回答「500 件多久能到」时也有依据可引。

技术收尾提示一句:结构化数据要当成产品数据的一部分来维护,不是上线时贴一段代码就完事。价格改了要同步重跑导出,档位变了要同步改 offerCount,这套东西一旦和商品库脱节,AI 引用回来的错误比没有结构化数据时更难被发现。

你在自己的站点上做过多档报价的结构化吗,遇到过哪些字段对不上的情况,评论区说说。

参考与延伸

  • AggregateOffer 类型定义:https://schema.org/AggregateOffer
  • Offer 类型定义:https://schema.org/Offer
  • 结构化数据校验工具:https://validator.schema.org/
  • 产品结构化数据说明:https://developers.google.com/search/docs/appearance/structured-data/product

关键词:GEO、AI优化AIO、外贸独立站 AI 流量、AggregateOffer、Schema.org、JSON-LD、多档报价结构化

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