课程页补上 courseWorkload 之后:用 ISO 8601 时长把学习成本讲给 AI 听的 60 天数据
9 月 12 日 20:47,我在 Perplexity 里问「Python 数据分析入门课要学多久」,回答里引用了两个竞品站,我们站上同一门课的页面只被带了一句课程名,时长一栏明明写着「约 40 小时」,AI 没有采信。9 月 18 日给课程 JSON-LD 补上 courseWorkload 之后跟踪 60 天(9 月 18 日—11 月 16 日),212 次相关 AI 提问里我们课程页的引用占比从 17% 涨到 43%,落地页到试听表单的转化率从 3.1% 走到 5.8%。改动其实只是往一个 <script type="application/ld+json"> 里塞了三行字段。
一、问题:课程页的「学多久」在 AI 眼里是空白
先说现场。我们课程站的详情页模板做了两年,结构化数据一直只有 name、description、offers 三件套。传统 SEO 时代这没出过大问题——标题、描述、正文里的「约 40 小时」足够搜索引擎给出摘要。但 GEO 的语境下,生成式引擎回答问题时不是摘录一段文字,而是先在页面里找结构化的实体属性,找不到才退回到正文片段。

「这门课要学多久」「适合零基础吗」这类问题恰恰是退回正文片段的重灾区。原因有两个:正文里写「约 40 小时」,隔壁 FAQ 又写「两个月学完」,两处口径不一致;自然语言里的「40 小时」到底是视频总时长还是含练习的投入,模型自己也拿不准。9 月初我们抽样了 48 次 AI 提问(覆盖 Perplexity、Bing Copilot、Google AI Overviews 三个渠道),只有 8 次回答提到我们的课程,且没有一次把时长数据归到我们页面上。
结论很直接:正文里的人话是给用户看的,AI 引擎认的是 schema.org 里的机器话。
二、机制剖析:AI 回答课程问题时依赖的字段链路
这一节拆开讲生成式引擎是怎么把「XX 课程要学多久」变成一次结构化数据查询的。整条链路分四步:
- 实体识别:模型先从问题里抽出课程实体,判断问题在问哪个属性(时长、难度、价格);
- 页面检索与解析:抓取候选课程页,优先解析 JSON-LD 里的
Course类型,直接读取属性值; - 属性归一:
courseWorkload是 ISO 8601 Duration,PT40H可以无损换算成「40 小时」,不存在歧义; - 置信度打分:结构化字段 + 正文口径一致,引用置信度高;只有正文口径、字段缺失时置信度低,容易被竞品页顶掉。
flowchart LR
A[用户提问<br>这门课要学多久] --> B[生成式引擎<br>检索课程页]
B --> C{JSON-LD 里<br>有 courseWorkload?}
C -- 有 --> D[抽取 PT40H<br>换算成自然语言]
C -- 无 --> E[退回正文段落<br>和 FAQ 里捞时长]
E -- 捞到但口径不一 --> F[低置信度引用<br>容易被丢弃]
E -- 捞不到 --> G[改引竞品页<br>或给模糊答案]
D --> H[高置信度引用<br>附课程链接]
三个字段的分工要分清,混用是常见事故:
courseWorkload:预期学习投入总量,ISO 8601 Duration 格式,回答「要学多久」的核心字段;educationalLevel:难度分层,取Beginner、Intermediate、Advanced这类受控词表,回答「适合零基础吗」;numberOfCredits:学分,非学分制的课程不要硬凑,AI 会把「3 学分」错位解读成 3 周或 3 小时,这是我们在 B 组一门课的引用错误里真实观察到的。
courseWorkload 与 numberOfCredits 是两个量纲,前者是时间投入,后者是学业计量,AI 引擎按各自语义取用,页面侧把它们写混会导致引用错位。
三、改造前后:Course JSON-LD 代码
改造前的模板代码从线上构建产物里捞出来的(9 月 10 日),问题一目了然:
// 改造前:课程详情页 JSON-LD 模板(2026-09-10 自线上构建产物提取)
// 症结:只有名称、描述、价格三件套,AI 问「要学多久」时无字段可读
const courseJsonLd = {
"@context": "https://schema.org",
"@type": "Course",
// 课程名是实体锚点,AI 靠它把页面和提问里的「这门课」对上号
"name": "Python 数据分析零基础入门",
// 描述全在讲卖点,没有任何学习成本口径
"description": "从 Excel 思维迁移到 pandas,覆盖清洗、可视化与报表自动化",
"provider": { "@type": "Organization", "name": "示例学院" },
"offers": { "@type": "Offer", "price": "199", "priceCurrency": "CNY" }
// 缺 courseWorkload:时长问题无从引用
// 缺 educationalLevel:零基础适配问题无从引用
};
9 月 18 日 11:30 随全站构建上线了改造版,实际提交的代码如下:
// 改造后:补齐 courseWorkload / educationalLevel / numberOfCredits
// 上线时间:2026-09-18 11:30,随下一次全站构建发布
const courseJsonLd = {
"@context": "https://schema.org",
"@type": "Course",
"name": "Python 数据分析零基础入门",
// courseWorkload 用 ISO 8601 Duration,PT40H 即 40 小时
// 只算视频与直播总长,练习时间不混入,口径和正文保持一致
"courseWorkload": "PT40H",
// educationalLevel 取受控词表,不自造「小白可学」这类值
"educationalLevel": "Beginner",
// numberOfCredits 只给按学分制设计的进阶课,入门课留空
"inLanguage": "zh-CN",
"provider": { "@type": "Organization", "name": "示例学院" },
"offers": { "@type": "Offer", "price": "199", "priceCurrency": "CNY" }
};
上线前用 Google Rich Results Test 跑了一遍,改造版返回「Course 检测成功,courseWorkload: PT40H」。字段层面的改动汇总:
| 字段 | 改造前 | 改造后 | AI 由此可回答的问题 |
|---|---|---|---|
| courseWorkload | 缺失 | PT40H | 这门课要学多久 |
| educationalLevel | 缺失 | Beginner | 适合零基础吗 |
| numberOfCredits | 缺失 | 仅进阶课补充 3 学分 | 能否换学分或证书 |
| inLanguage | 缺失 | zh-CN | 有没有中文讲解 |
同时页面正文里原来散落的「约 40 小时」「两个月学完」统一改成了与 PT40H 对应的口径:「视频与直播共 40 小时」。结构化字段和正文口径一致,是拿到高置信度引用的前提。
四、ISO 8601 Duration 写法:五种典型错误
courseWorkload 的值必须是 ISO 8601 Duration(见 schema.org 的 Duration 定义),语法骨架是 P 开头,日期部分跟 P,时间部分跟 T。这是我们评审时拦下来的真实错误清单:
| 写法 | 解析结果 | 问题所在 | 正确写法 |
|---|---|---|---|
"40h" |
校验器报错 | 不是 ISO 8601 格式,AI 无法解析 | "PT40H" |
"40 小时" |
校验器报错 | 自然语言,机器不可读 | "PT40H" |
"P40H" |
校验器报错 | H 属于时间部分,缺 T |
"PT40H" |
"PT1M" |
被解析成 1 分钟 | 想写 1 个月,分钟和月份的 M 有位置歧义 |
1 个月写 "P1M" |
"PT2W" |
校验器报错 | W 只属于日期部分 |
"P2W" 或 "PT336H" |
M 的位置歧义是重灾区:PT10H30M 里 M 在 T 之后,是 30 分钟;P1M 里 M 在日期部分,是 1 个月。课程页里写月课、周课的同事最容易在这里翻车。我们最后在模板层加了一道校验,存库时直接拦截非法格式:
# 模板层校验:courseWorkload 必须匹配 ISO 8601 Duration
# 上线于 2026-09-16,三天内拦下 4 次运营误填
import re
DURATION_RE = re.compile(
# P 开头;日期部分 P\d+([YMDW]) 或时间部分 T\d+([HMS]),至少出现一段
r"^P(?=(?:\d+[YMDW])*(?:T\d+[HMS])?$)"
r"(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?"
r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
)
def check_workload(value):
# 拒绝 "40h"、"40 小时"、"P40H" 这类误填
if not DURATION_RE.match(value or ""):
raise ValueError(f"非法 ISO 8601 Duration: {value!r}")
# 再拦一个语义坑:纯日期部分不带 T,说明没有小时口径
if "T" not in value and "Y" not in value:
raise ValueError("时长含小时/分钟请补 T 前缀")
五、60 天 A/B 对照数据
改造不是全量推的。我们把 12 门定位相近的 Python 与数据分析类课程分成两组:A 组 6 门在 9 月 18 日补齐 courseWorkload 与 educationalLevel,B 组 6 门保持原样作为对照。监测方式是每周固定抓取三个 AI 渠道对课程类提问的回答,记录引用来源与落地页转化。
xychart-beta
title "AI 回答引用占比周度走势(A组 vs B组)"
x-axis [W1, W2, W3, W4, W5, W6, W7, W8]
y-axis "引用占比 %" 0 --> 50
line "A组" [15, 19, 27, 33, 38, 41, 43, 43]
line "B组" [16, 17, 17, 18, 17, 18, 17, 17]
60 天累计数据如下(第 8 周末盘点,10 月 30 日中检过一次):
| 指标 | A 组(补 workload) | B 组(未补) | 差值 |
|---|---|---|---|
| 相关 AI 提问中被引用次数 | 91 次 / 43% | 36 次 / 17% | +26 个百分点 |
| 引用时标注时长数据的比例 | 78% | 21% | +57 个百分点 |
| 落地页→试听表单转化率 | 3.1% → 5.8% | 3.0% → 3.3% | A 组提升 2.7 个百分点 |
| 「适合零基础吗」类回答中被引用 | 24 次 | 7 次 | +17 次 |
三点读数:其一,引用占比的提升发生在第 2 到第 4 周,说明生成式引擎重新抓取并更新索引需要两到四周,别指望上线次日见效;其二,B 组几乎横盘,排除了同期整体流量波动带来的干扰;其三,转化增量比引用增量的曲线晚约一周,引用先到、决策后到,符合 GEO 的典型传导节奏。
有一点要如实交代:A 组里有一门课在第 5 周引用不升反降,排查发现该课的 courseWorkload 填了 P3M(3 个月),而正文写「12 周」,3 个月约 13 周口径打架。把正文统一改成「约 3 个月,每周 3.5 小时」之后第 6 周恢复。字段与正文口径一致,比字段本身存在更重要。
六、上线踩坑记录
几条执行层面的坑,按时间顺序记:
- 9 月 16 日:运营批量回填时长,把周课填成
PT2W,校验脚本拦下,正确写法是P2W; - 9 月 19 日:Rich Results Test 对一门课报「courseWorkload 缺失」,实际是页面缓存层还在吐旧模板,清缓存后恢复;
- 10 月 8 日:发现 Perplexity 引用一门课时把
numberOfCredits: 3说成「约 3 小时」,随后我们把非学分课的该字段全部清空; - 10 月 22 日:补测了 Bing Copilot 渠道,A 组引用占比 39%,略低于 Perplexity 的 47%,两个引擎对结构化字段的采信度存在差异。
回头看,这次改造的代码量不到十行,真正的成本在校准口径:把页面正文、FAQ、客服话术里的时长表述全部对齐到 PT40H 这一个来源。GEO 做到后面拼的不是堆字段,而是全站数据口径的纪律。如果你的课程页也常被 AI 提问绕过,欢迎在评论区聊聊你遇到的引用错位案例。
参考与延伸
- schema.org Course 类型定义(含 courseWorkload 属性说明):https://schema.org/Course
- schema.org Duration(ISO 8601 Duration 子集)规格:https://schema.org/Duration
- Google Search Central 课程结构化数据文档:https://developers.google.com/search/docs/appearance/structured-data/course
- W3C ISO 8601 日期与时间格式说明页:https://www.w3.org/QA/Tips/iso-8601
关键词:GEO、courseWorkload、ISO 8601、Course、JSON-LD、结构化数据、课程被AI推荐、AI优化AIO